功能:完善用户服务关系展示与组织联动
统一平台、气站和配送点后台的服务关系业务名称展示。 新增气站、配送点、服务人员三级联动及暂未分配语义。 后端补充组织归属、人员角色和启用状态的严格校验。 补充前后端测试、项目文档、操作日志及本机日志忽略规则。
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ scripts/fixtures/platform-demo-seed-result.json
|
|||||||
# 后端本机运行日志与上传文件不得进入版本库。
|
# 后端本机运行日志与上传文件不得进入版本库。
|
||||||
backend/api/logs/
|
backend/api/logs/
|
||||||
backend/api/runtime/
|
backend/api/runtime/
|
||||||
|
|
||||||
|
# 前端本机调试日志不得进入版本库。
|
||||||
|
frontend/*/logs/
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ type staffListFilters struct {
|
|||||||
WorkStatus string
|
WorkStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type staffOrganizationFilters struct {
|
||||||
|
GasIdentity string
|
||||||
|
DeliveryIdentity string
|
||||||
|
GasUnassigned bool
|
||||||
|
DeliveryUnassigned bool
|
||||||
|
}
|
||||||
|
|
||||||
// parseStaffListFilters 将工作人员列表查询参数收敛为闭集条件,拒绝含糊或冲突值。
|
// parseStaffListFilters 将工作人员列表查询参数收敛为闭集条件,拒绝含糊或冲突值。
|
||||||
func parseStaffListFilters(roleCode, roleCodes, status, workStatus string) (staffListFilters, bool) {
|
func parseStaffListFilters(roleCode, roleCodes, status, workStatus string) (staffListFilters, bool) {
|
||||||
filters := staffListFilters{}
|
filters := staffListFilters{}
|
||||||
@@ -61,6 +68,34 @@ func parseStaffListFilters(roleCode, roleCodes, status, workStatus string) (staf
|
|||||||
return filters, true
|
return filters, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseStaffOrganizationFilters 解析服务关系下拉使用的精确组织范围,并拒绝相互冲突的条件。
|
||||||
|
func parseStaffOrganizationFilters(gasIdentity, deliveryIdentity, gasUnassigned, deliveryUnassigned string) (staffOrganizationFilters, bool) {
|
||||||
|
filters := staffOrganizationFilters{
|
||||||
|
GasIdentity: strings.TrimSpace(gasIdentity),
|
||||||
|
DeliveryIdentity: strings.TrimSpace(deliveryIdentity),
|
||||||
|
}
|
||||||
|
if strings.Contains(filters.GasIdentity, ",") || strings.Contains(filters.DeliveryIdentity, ",") {
|
||||||
|
return staffOrganizationFilters{}, false
|
||||||
|
}
|
||||||
|
parseUnassigned := func(value string) (bool, bool) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
return value == "1", value == "1"
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
filters.GasUnassigned, ok = parseUnassigned(gasUnassigned)
|
||||||
|
if !ok {
|
||||||
|
return staffOrganizationFilters{}, false
|
||||||
|
}
|
||||||
|
filters.DeliveryUnassigned, ok = parseUnassigned(deliveryUnassigned)
|
||||||
|
if !ok || filters.GasIdentity != "" && filters.GasUnassigned || filters.DeliveryIdentity != "" && filters.DeliveryUnassigned {
|
||||||
|
return staffOrganizationFilters{}, false
|
||||||
|
}
|
||||||
|
return filters, true
|
||||||
|
}
|
||||||
|
|
||||||
// ListStaff 查询服务人员分页列表,并应用调用方显式声明的角色与在岗状态条件。
|
// ListStaff 查询服务人员分页列表,并应用调用方显式声明的角色与在岗状态条件。
|
||||||
func ListStaff(ctx *gin.Context) {
|
func ListStaff(ctx *gin.Context) {
|
||||||
filters, ok := parseStaffListFilters(
|
filters, ok := parseStaffListFilters(
|
||||||
@@ -73,6 +108,16 @@ func ListStaff(ctx *gin.Context) {
|
|||||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
organizationFilters, ok := parseStaffOrganizationFilters(
|
||||||
|
ctx.Query("gas_basic_identities"),
|
||||||
|
ctx.Query("delivery_basic_identities"),
|
||||||
|
ctx.Query("gas_basic_unassigned"),
|
||||||
|
ctx.Query("delivery_basic_unassigned"),
|
||||||
|
)
|
||||||
|
if !ok {
|
||||||
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||||
|
return
|
||||||
|
}
|
||||||
page, size := common.PageSize(ctx)
|
page, size := common.PageSize(ctx)
|
||||||
var list []models.StaffAccount
|
var list []models.StaffAccount
|
||||||
var total int64
|
var total int64
|
||||||
@@ -86,6 +131,24 @@ func ListStaff(ctx *gin.Context) {
|
|||||||
if filters.WorkStatus != "" {
|
if filters.WorkStatus != "" {
|
||||||
query = query.Where("work_status = ?", filters.WorkStatus)
|
query = query.Where("work_status = ?", filters.WorkStatus)
|
||||||
}
|
}
|
||||||
|
if organizationFilters.GasIdentity != "" {
|
||||||
|
query = query.Where(
|
||||||
|
"gas_basic_id IN (SELECT id FROM gas_basic WHERE identity = ? AND status <> ?)",
|
||||||
|
organizationFilters.GasIdentity,
|
||||||
|
common.StatusArchived,
|
||||||
|
)
|
||||||
|
} else if organizationFilters.GasUnassigned {
|
||||||
|
query = query.Where("gas_basic_id = 0")
|
||||||
|
}
|
||||||
|
if organizationFilters.DeliveryIdentity != "" {
|
||||||
|
query = query.Where(
|
||||||
|
"delivery_basic_id IN (SELECT id FROM delivery_basic WHERE identity = ? AND status <> ?)",
|
||||||
|
organizationFilters.DeliveryIdentity,
|
||||||
|
common.StatusArchived,
|
||||||
|
)
|
||||||
|
} else if organizationFilters.DeliveryUnassigned {
|
||||||
|
query = query.Where("delivery_basic_id = 0")
|
||||||
|
}
|
||||||
query = common.ApplyKeywordFilter(ctx, query, &models.StaffAccount{})
|
query = common.ApplyKeywordFilter(ctx, query, &models.StaffAccount{})
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
infra.Response.Error(ctx, err)
|
infra.Response.Error(ctx, err)
|
||||||
|
|||||||
@@ -53,3 +53,25 @@ func TestStaffListFiltersRejectAmbiguousOrUnknownValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStaffOrganizationFilters 验证服务人员可以按已分配组织或未分配状态精确过滤。
|
||||||
|
func TestStaffOrganizationFilters(t *testing.T) {
|
||||||
|
filters, ok := parseStaffOrganizationFilters("gas-a", "delivery-a", "", "")
|
||||||
|
if !ok || filters.GasIdentity != "gas-a" || filters.DeliveryIdentity != "delivery-a" {
|
||||||
|
t.Fatalf("组织标识过滤解析失败:%#v", filters)
|
||||||
|
}
|
||||||
|
filters, ok = parseStaffOrganizationFilters("", "", "1", "1")
|
||||||
|
if !ok || !filters.GasUnassigned || !filters.DeliveryUnassigned {
|
||||||
|
t.Fatalf("未分配过滤解析失败:%#v", filters)
|
||||||
|
}
|
||||||
|
for _, test := range [][4]string{
|
||||||
|
{"gas-a", "", "1", ""},
|
||||||
|
{"", "delivery-a", "", "1"},
|
||||||
|
{"gas-a,gas-b", "", "", ""},
|
||||||
|
{"", "", "true", ""},
|
||||||
|
} {
|
||||||
|
if _, valid := parseStaffOrganizationFilters(test[0], test[1], test[2], test[3]); valid {
|
||||||
|
t.Fatalf("冲突或非法组织过滤被接受:%#v", test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,10 +226,38 @@ func resolveServiceRelation(ctx *gin.Context, request serviceRelationRequest) (u
|
|||||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||||
return 0, 0, 0, 0, false
|
return 0, 0, 0, 0, false
|
||||||
}
|
}
|
||||||
if gasBasicID == 0 && deliveryBasicID == 0 && staffAccountID == 0 ||
|
if !validateServiceRelationOrganization(gasBasicID, deliveryBasicID, staffAccountID) {
|
||||||
!common.ValidateOrganizationIDs(gasBasicID, deliveryBasicID, staffAccountID) {
|
|
||||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||||
return 0, 0, 0, 0, false
|
return 0, 0, 0, 0, false
|
||||||
}
|
}
|
||||||
return userAccountID, gasBasicID, deliveryBasicID, staffAccountID, true
|
return userAccountID, gasBasicID, deliveryBasicID, staffAccountID, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateServiceRelationOrganization 读取组织记录并执行服务关系专用的严格归属校验。
|
||||||
|
func validateServiceRelationOrganization(gasBasicID, deliveryBasicID, staffAccountID uint64) bool {
|
||||||
|
var delivery models.DeliveryBasic
|
||||||
|
if deliveryBasicID != 0 {
|
||||||
|
if err := common.ActiveRecords(impl.DBService).First(&delivery, deliveryBasicID).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var staff models.StaffAccount
|
||||||
|
if staffAccountID != 0 {
|
||||||
|
if err := common.ActiveRecords(impl.DBService).First(&staff, staffAccountID).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceRelationOrganizationMatches(gasBasicID, deliveryBasicID, staffAccountID, delivery, staff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceRelationOrganizationMatches 要求配送点、服务人员与当前选择完全一致;全空表示暂未分配。
|
||||||
|
func serviceRelationOrganizationMatches(gasBasicID, deliveryBasicID, staffAccountID uint64, delivery models.DeliveryBasic, staff models.StaffAccount) bool {
|
||||||
|
if deliveryBasicID != 0 && delivery.GasBasicID != gasBasicID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if staffAccountID == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
validRole := staff.RoleCode == "installer" || staff.RoleCode == "delivery" || staff.RoleCode == "operations"
|
||||||
|
return validRole && staff.Status == common.StatusEnable && staff.GasBasicID == gasBasicID && staff.DeliveryBasicID == deliveryBasicID
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,3 +65,30 @@ func TestUserAddressUpdateValuesUsesSupportedMap(t *testing.T) {
|
|||||||
t.Fatalf("update field count = %d, want 5", len(values))
|
t.Fatalf("update field count = %d, want 5", len(values))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServiceRelationOrganizationMatches 验证暂未分配与严格组织一致性规则。
|
||||||
|
func TestServiceRelationOrganizationMatches(t *testing.T) {
|
||||||
|
if !serviceRelationOrganizationMatches(0, 0, 0, models.DeliveryBasic{}, models.StaffAccount{}) {
|
||||||
|
t.Fatal("全空服务关系应允许保存为暂未分配")
|
||||||
|
}
|
||||||
|
delivery := models.DeliveryBasic{GasBasicID: 10}
|
||||||
|
staff := models.StaffAccount{
|
||||||
|
Entity: models.Entity{Status: 1},
|
||||||
|
RoleCode: "delivery",
|
||||||
|
GasBasicID: 10,
|
||||||
|
DeliveryBasicID: 20,
|
||||||
|
}
|
||||||
|
if !serviceRelationOrganizationMatches(10, 20, 30, delivery, staff) {
|
||||||
|
t.Fatal("完全一致的服务关系被拒绝")
|
||||||
|
}
|
||||||
|
if serviceRelationOrganizationMatches(11, 20, 30, delivery, staff) {
|
||||||
|
t.Fatal("配送点或服务人员气站不一致时不应通过")
|
||||||
|
}
|
||||||
|
if serviceRelationOrganizationMatches(10, 0, 30, models.DeliveryBasic{}, staff) {
|
||||||
|
t.Fatal("服务人员配送点与空选择不一致时不应通过")
|
||||||
|
}
|
||||||
|
staff.Status = 2
|
||||||
|
if serviceRelationOrganizationMatches(10, 20, 30, delivery, staff) {
|
||||||
|
t.Fatal("停用服务人员不应写入服务关系")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
70
docs/操作日志_服务关系名称展示_20260811.md
Normal file
70
docs/操作日志_服务关系名称展示_20260811.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# 服务关系名称展示操作日志
|
||||||
|
|
||||||
|
操作时间:2026-08-11
|
||||||
|
|
||||||
|
操作类型:扩展
|
||||||
|
|
||||||
|
影响模块:平台总后台、气站后台、配送点后台的服务关系列表
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
服务关系列表的用户、气站、配送点和工作人员字段仅显示唯一标识,普通用户难以直接识别关系归属;三个后台未统一采用既有的用户地址名称展示标准。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
1. 保持菜单、页面标题和面包屑中的“服务关系”名称不变。
|
||||||
|
2. 四列表头统一改为“用户账户、所属气站、所属配送点、服务人员”。
|
||||||
|
3. 列表主内容改为关联业务名称,悬停显示完整唯一标识,并提供复制按钮。
|
||||||
|
4. 名称缺失或关系加载失败时降级显示完整唯一标识。
|
||||||
|
5. 将相同规则同步到平台总后台、气站后台和配送点后台。
|
||||||
|
|
||||||
|
## 操作后状态
|
||||||
|
|
||||||
|
管理人员可以直接按业务名称识别服务归属,同时仍可查看和复制底层唯一标识。详情页和编辑流程保持原行为。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 三个后台的 `pnpm.cmd type:check` 均通过。
|
||||||
|
- 三个后台的 `pnpm.cmd build` 均通过。
|
||||||
|
- 未修改后端接口和数据库结构。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
影响范围限于列表渲染和字段文案。唯一标识保留为降级内容,可缓解关联数据暂未加载或名称缺失造成的识别问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
操作时间:2026-08-12
|
||||||
|
|
||||||
|
操作类型:扩展
|
||||||
|
|
||||||
|
影响模块:平台总后台服务关系记录页、服务人员筛选接口、服务关系写入校验
|
||||||
|
|
||||||
|
## 操作前状态
|
||||||
|
|
||||||
|
详情和编辑页仍显示“唯一标识”文案;气站、配送点、服务人员可以独立选择;后端不允许三项全部为空,且组织一致性规则比页面确认的规则宽松。
|
||||||
|
|
||||||
|
## 具体操作
|
||||||
|
|
||||||
|
1. 新建、详情、编辑页面统一使用“用户账户、所属气站、所属配送点、服务人员”。
|
||||||
|
2. 用户账户编辑时改为只读业务名称,并保留唯一标识复制能力。
|
||||||
|
3. 空组织统一显示“暂未分配”,允许三项全部为空。
|
||||||
|
4. 配送点按气站筛选并自动回填气站;服务人员按气站和配送点精确筛选。
|
||||||
|
5. 组织变化时清空不匹配的下级选项并提示。
|
||||||
|
6. 后端保存前严格校验配送点、服务人员归属、角色和启用状态。
|
||||||
|
7. 保持接口地址、请求字段、数据库结构及后台权限边界不变。
|
||||||
|
|
||||||
|
## 操作后状态
|
||||||
|
|
||||||
|
平台总后台的列表、详情、新建和编辑流程已统一采用业务名称。历史异常关系允许查看,但必须修正后才能保存;“暂未分配”关系可以正常创建和更新。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 平台、气站、配送点后台 TypeScript 类型检查通过。
|
||||||
|
- 平台资源页、工作人员策略和组织联动静态契约检查通过。
|
||||||
|
- 后端工作人员过滤与用户服务关系定向测试通过。
|
||||||
|
- 三套 Web 后台生产构建通过。
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
严格校验可能阻止历史异常记录原样保存,这是确认后的预期行为。工作人员列表新增过滤参数均为可选参数,旧调用保持原行为。
|
||||||
60
docs/项目文档_服务关系名称展示_v1.0.md
Normal file
60
docs/项目文档_服务关系名称展示_v1.0.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 服务关系管理项目文档 v1.1
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
- 项目名称:服务关系名称展示统一。
|
||||||
|
- 主要功能:服务关系在列表、详情、新建和编辑页面统一展示业务名称,并对气站、配送点、服务人员执行前后端组织一致性校验。
|
||||||
|
- 实施范围:平台总后台、气站后台、配送点后台。
|
||||||
|
- 技术栈:Vue 3、TypeScript、Arco Design。
|
||||||
|
|
||||||
|
## 2. 核心实现
|
||||||
|
|
||||||
|
三个后台的 `resources.ts` 已统一服务关系字段名称和空值语义。平台总后台是当前唯一暴露服务关系管理菜单和写接口的后台,因此新建、详情、编辑和联动交互在平台总后台实现;气站、配送点后台仅同步共享资源定义,不扩大原有权限范围。
|
||||||
|
|
||||||
|
平台总后台共享列表会补载当前页实际引用的关系记录,以业务名称渲染并保留唯一标识的悬停查看与复制能力。详情页展示业务名称和可复制标识;编辑页的用户账户以只读名称显示。
|
||||||
|
|
||||||
|
列表名称“服务关系”保持不变,四列表头统一为:
|
||||||
|
|
||||||
|
- 用户账户
|
||||||
|
- 所属气站
|
||||||
|
- 所属配送点
|
||||||
|
- 服务人员
|
||||||
|
|
||||||
|
新建和编辑页面的组织规则如下:
|
||||||
|
|
||||||
|
- 用户账户必选,编辑时不可变更。
|
||||||
|
- 气站、配送点、服务人员允许同时为空,表示“暂未分配”。
|
||||||
|
- 选择配送点后自动回填所属气站;切换气站会清理不匹配的配送点。
|
||||||
|
- 服务人员按当前气站、配送点、启用状态和业务角色精确过滤。
|
||||||
|
- 历史异常关系允许查看,但再次保存前必须修正。
|
||||||
|
|
||||||
|
接口地址、请求字段和数据库结构保持不变。工作人员列表接口新增可选组织过滤参数,旧调用不传参数时行为不变。
|
||||||
|
|
||||||
|
## 3. 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
frontend/
|
||||||
|
├── platform_admin/ # 平台总后台
|
||||||
|
│ └── src/views/shared/ # 通用列表与名称展示组件
|
||||||
|
├── gas_admin/ # 气站后台
|
||||||
|
│ └── src/views/shared/ # 通用列表与名称展示组件
|
||||||
|
└── delivery_admin/ # 配送点后台
|
||||||
|
└── src/views/shared/ # 通用列表与名称展示组件
|
||||||
|
backend/api/internal/logic/platform/
|
||||||
|
├── staff/ # 服务人员组织范围过滤
|
||||||
|
└── user/ # 服务关系写入一致性校验
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 维护指南
|
||||||
|
|
||||||
|
新增类似关联名称列时,应通过字段级配置启用,不得按资源名称硬编码。新增组织关联必须同时提供前端联动和后端最终校验,不得只依赖下拉框过滤。
|
||||||
|
|
||||||
|
## 5. 验证记录
|
||||||
|
|
||||||
|
- 平台总后台:类型检查、资源页契约、组织联动契约和生产构建通过。
|
||||||
|
- 气站后台、配送点后台:类型检查及生产构建通过。
|
||||||
|
- 后端:平台工作人员与用户服务关系定向测试通过。
|
||||||
|
|
||||||
|
## 6. 风险评估
|
||||||
|
|
||||||
|
风险集中在历史异常数据:详情仍可查看,编辑保存会被严格校验阻止。未新增菜单、角色或写接口,现有权限边界不变;接口字段和数据库结构保持兼容。
|
||||||
@@ -21,9 +21,15 @@ export type ResourceFieldType =
|
|||||||
export type ResourceField = {
|
export type ResourceField = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
listLabel?: string;
|
||||||
type?: ResourceFieldType;
|
type?: ResourceFieldType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
relation?: string;
|
relation?: string;
|
||||||
|
listRelationNameOnly?: boolean;
|
||||||
|
emptyText?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
showIdentityCopy?: boolean;
|
||||||
|
readonlyRelationText?: boolean;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
defaultValue?: string | number | boolean;
|
defaultValue?: string | number | boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -271,8 +277,13 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
|
|||||||
return { key, label, type, ...options };
|
return { key, label, type, ...options };
|
||||||
}
|
}
|
||||||
|
|
||||||
function relation(key: string, resource: string, required = false): ResourceField {
|
function relation(
|
||||||
return f(key, { type: 'identity', relation: resource, required });
|
key: string,
|
||||||
|
resource: string,
|
||||||
|
required = false,
|
||||||
|
options: Partial<ResourceField> = {},
|
||||||
|
): ResourceField {
|
||||||
|
return f(key, { ...options, type: 'identity', relation: resource, required });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
||||||
@@ -332,7 +343,12 @@ const platformResources: ResourceUiDefinition[] = [
|
|||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
||||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
|
define('user_service_relation', '用户服务关系', 'writable', [
|
||||||
|
relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }),
|
||||||
|
relation('gas_basic_identity', '/gas_basic', false, { label: '所属气站', listLabel: '所属气站', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属气站', showIdentityCopy: true }),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属配送点', showIdentityCopy: true }),
|
||||||
|
relation('staff_account_identity', '/staff_account', false, { label: '服务人员', listLabel: '服务人员', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择服务人员', showIdentityCopy: true }),
|
||||||
|
]),
|
||||||
|
|
||||||
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
||||||
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
||||||
|
|||||||
@@ -34,9 +34,10 @@
|
|||||||
<a-table-column title="唯一标识" :width="150">
|
<a-table-column title="唯一标识" :width="150">
|
||||||
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
|
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.listLabel ?? field.label" :width="columnWidth(field)" :ellipsis="!field.listRelationNameOnly" :tooltip="!field.listRelationNameOnly">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<IdentityText v-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
|
<RelationNameText v-if="field.listRelationNameOnly && identityFieldValue(field, record)" :name="relationListName(field, record)" :identity="identityFieldValue(field, record)" :identity-label="field.label" />
|
||||||
|
<IdentityText v-else-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
|
||||||
<template v-else>{{ displayFieldValue(field, record) }}</template>
|
<template v-else>{{ displayFieldValue(field, record) }}</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
@@ -269,6 +270,7 @@ import type {
|
|||||||
} from '@/api/resources';
|
} from '@/api/resources';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import IdentityText from '@/components/IdentityText.vue';
|
import IdentityText from '@/components/IdentityText.vue';
|
||||||
|
import RelationNameText from './RelationNameText.vue';
|
||||||
|
|
||||||
type Row = Record<string, unknown>;
|
type Row = Record<string, unknown>;
|
||||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||||
@@ -1153,6 +1155,15 @@ function identityFieldValue(field: ResourceField, row: Row) {
|
|||||||
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取关联记录的业务名称;关系尚未加载时降级显示完整唯一标识。 */
|
||||||
|
function relationListName(field: ResourceField, row: Row) {
|
||||||
|
const identity = identityFieldValue(field, row);
|
||||||
|
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
||||||
|
(option) => String(option.identity) === identity,
|
||||||
|
);
|
||||||
|
return match ? optionLabel(match) : identity;
|
||||||
|
}
|
||||||
|
|
||||||
function columnWidth(field: ResourceField) {
|
function columnWidth(field: ResourceField) {
|
||||||
if (field.key === 'identity') return 280;
|
if (field.key === 'identity') return 280;
|
||||||
if (field.type === 'datetime' || field.type === 'date') return 180;
|
if (field.type === 'datetime' || field.type === 'date') return 180;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!-- 功能描述:以业务名称展示关联记录,并保留唯一标识的查看与复制能力。版本:v1.0.0。 -->
|
||||||
|
<template>
|
||||||
|
<div class="relation-name-text">
|
||||||
|
<a-tooltip :content="`${identityLabel}:${identity}`">
|
||||||
|
<span class="relation-name">{{ displayName }}</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :content="`复制${identityLabel}`">
|
||||||
|
<button class="copy-button" type="button" :aria-label="`复制${identityLabel} ${identity}`" @click.stop="copyIdentity">
|
||||||
|
<icon-copy />
|
||||||
|
</button>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ name: string; identity: string; identityLabel: string }>();
|
||||||
|
const displayName = computed(() => props.name || props.identity);
|
||||||
|
|
||||||
|
/** 复制完整唯一标识,并给出明确的操作反馈。 */
|
||||||
|
async function copyIdentity() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.identity);
|
||||||
|
Message.success(`${props.identityLabel}已复制`);
|
||||||
|
} catch {
|
||||||
|
Message.error('复制失败,请从悬浮提示中复制');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.relation-name-text { display: flex; gap: 6px; align-items: center; min-width: 0; }
|
||||||
|
.relation-name { min-width: 0; overflow: hidden; color: var(--color-text-1); text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.copy-button { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; color: rgb(var(--primary-6)); font-size: 14px; background: transparent; border: 0; border-radius: 4px; cursor: pointer; }
|
||||||
|
.copy-button:hover, .copy-button:focus-visible { background: var(--color-fill-2); outline: none; }
|
||||||
|
</style>
|
||||||
@@ -21,9 +21,15 @@ export type ResourceFieldType =
|
|||||||
export type ResourceField = {
|
export type ResourceField = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
listLabel?: string;
|
||||||
type?: ResourceFieldType;
|
type?: ResourceFieldType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
relation?: string;
|
relation?: string;
|
||||||
|
listRelationNameOnly?: boolean;
|
||||||
|
emptyText?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
showIdentityCopy?: boolean;
|
||||||
|
readonlyRelationText?: boolean;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
defaultValue?: string | number | boolean;
|
defaultValue?: string | number | boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -271,8 +277,13 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
|
|||||||
return { key, label, type, ...options };
|
return { key, label, type, ...options };
|
||||||
}
|
}
|
||||||
|
|
||||||
function relation(key: string, resource: string, required = false): ResourceField {
|
function relation(
|
||||||
return f(key, { type: 'identity', relation: resource, required });
|
key: string,
|
||||||
|
resource: string,
|
||||||
|
required = false,
|
||||||
|
options: Partial<ResourceField> = {},
|
||||||
|
): ResourceField {
|
||||||
|
return f(key, { ...options, type: 'identity', relation: resource, required });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
/** 创建固定管理员角色字段,确保页面仅展示后端支持的角色。 */
|
||||||
@@ -332,7 +343,12 @@ const platformResources: ResourceUiDefinition[] = [
|
|||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
||||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
|
define('user_service_relation', '用户服务关系', 'writable', [
|
||||||
|
relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }),
|
||||||
|
relation('gas_basic_identity', '/gas_basic', false, { label: '所属气站', listLabel: '所属气站', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属气站', showIdentityCopy: true }),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择所属配送点', showIdentityCopy: true }),
|
||||||
|
relation('staff_account_identity', '/staff_account', false, { label: '服务人员', listLabel: '服务人员', listRelationNameOnly: true, emptyText: '暂未分配', placeholder: '请选择服务人员', showIdentityCopy: true }),
|
||||||
|
]),
|
||||||
|
|
||||||
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
||||||
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
||||||
|
|||||||
@@ -34,9 +34,10 @@
|
|||||||
<a-table-column title="唯一标识" :width="150">
|
<a-table-column title="唯一标识" :width="150">
|
||||||
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
|
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.listLabel ?? field.label" :width="columnWidth(field)" :ellipsis="!field.listRelationNameOnly" :tooltip="!field.listRelationNameOnly">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<IdentityText v-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
|
<RelationNameText v-if="field.listRelationNameOnly && identityFieldValue(field, record)" :name="relationListName(field, record)" :identity="identityFieldValue(field, record)" :identity-label="field.label" />
|
||||||
|
<IdentityText v-else-if="field.type === 'identity' && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" />
|
||||||
<template v-else>{{ displayFieldValue(field, record) }}</template>
|
<template v-else>{{ displayFieldValue(field, record) }}</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
@@ -269,6 +270,7 @@ import type {
|
|||||||
} from '@/api/resources';
|
} from '@/api/resources';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import IdentityText from '@/components/IdentityText.vue';
|
import IdentityText from '@/components/IdentityText.vue';
|
||||||
|
import RelationNameText from './RelationNameText.vue';
|
||||||
|
|
||||||
type Row = Record<string, unknown>;
|
type Row = Record<string, unknown>;
|
||||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||||
@@ -1153,6 +1155,15 @@ function identityFieldValue(field: ResourceField, row: Row) {
|
|||||||
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取关联记录的业务名称;关系尚未加载时降级显示完整唯一标识。 */
|
||||||
|
function relationListName(field: ResourceField, row: Row) {
|
||||||
|
const identity = identityFieldValue(field, row);
|
||||||
|
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
||||||
|
(option) => String(option.identity) === identity,
|
||||||
|
);
|
||||||
|
return match ? optionLabel(match) : identity;
|
||||||
|
}
|
||||||
|
|
||||||
function columnWidth(field: ResourceField) {
|
function columnWidth(field: ResourceField) {
|
||||||
if (field.key === 'identity') return 280;
|
if (field.key === 'identity') return 280;
|
||||||
if (field.type === 'datetime' || field.type === 'date') return 180;
|
if (field.type === 'datetime' || field.type === 'date') return 180;
|
||||||
|
|||||||
39
frontend/gas_admin/src/views/shared/RelationNameText.vue
Normal file
39
frontend/gas_admin/src/views/shared/RelationNameText.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!-- 功能描述:以业务名称展示关联记录,并保留唯一标识的查看与复制能力。版本:v1.0.0。 -->
|
||||||
|
<template>
|
||||||
|
<div class="relation-name-text">
|
||||||
|
<a-tooltip :content="`${identityLabel}:${identity}`">
|
||||||
|
<span class="relation-name">{{ displayName }}</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :content="`复制${identityLabel}`">
|
||||||
|
<button class="copy-button" type="button" :aria-label="`复制${identityLabel} ${identity}`" @click.stop="copyIdentity">
|
||||||
|
<icon-copy />
|
||||||
|
</button>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ name: string; identity: string; identityLabel: string }>();
|
||||||
|
const displayName = computed(() => props.name || props.identity);
|
||||||
|
|
||||||
|
/** 复制完整唯一标识,并给出明确的操作反馈。 */
|
||||||
|
async function copyIdentity() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.identity);
|
||||||
|
Message.success(`${props.identityLabel}已复制`);
|
||||||
|
} catch {
|
||||||
|
Message.error('复制失败,请从悬浮提示中复制');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.relation-name-text { display: flex; gap: 6px; align-items: center; min-width: 0; }
|
||||||
|
.relation-name { min-width: 0; overflow: hidden; color: var(--color-text-1); text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.copy-button { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; color: rgb(var(--primary-6)); font-size: 14px; background: transparent; border: 0; border-radius: 4px; cursor: pointer; }
|
||||||
|
.copy-button:hover, .copy-button:focus-visible { background: var(--color-fill-2); outline: none; }
|
||||||
|
</style>
|
||||||
@@ -19,6 +19,8 @@ const {
|
|||||||
linkedRelationValidationMessage,
|
linkedRelationValidationMessage,
|
||||||
optionParentIdentity,
|
optionParentIdentity,
|
||||||
shouldClearLinkedOption,
|
shouldClearLinkedOption,
|
||||||
|
staffOrganizationFilters,
|
||||||
|
staffOrganizationValidationMessage,
|
||||||
} = await import(moduleURL);
|
} = await import(moduleURL);
|
||||||
|
|
||||||
const gasA = 'gas-a';
|
const gasA = 'gas-a';
|
||||||
@@ -34,6 +36,36 @@ assert.equal(
|
|||||||
gasA,
|
gasA,
|
||||||
'普通配送点必须能回填所属气站',
|
'普通配送点必须能回填所属气站',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(staffOrganizationFilters(gasA, 'delivery-a'), {
|
||||||
|
gas_basic_identities: gasA,
|
||||||
|
delivery_basic_identities: 'delivery-a',
|
||||||
|
});
|
||||||
|
assert.deepEqual(staffOrganizationFilters('', ''), {
|
||||||
|
gas_basic_unassigned: '1',
|
||||||
|
delivery_basic_unassigned: '1',
|
||||||
|
});
|
||||||
|
const staffA = {
|
||||||
|
identity: 'staff-a',
|
||||||
|
gas_basic_identity: gasA,
|
||||||
|
delivery_basic_identity: 'delivery-a',
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
staffOrganizationValidationMessage(gasA, 'delivery-a', 'staff-a', staffA),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
staffOrganizationValidationMessage(gasB, 'delivery-a', 'staff-a', staffA),
|
||||||
|
/不属于当前气站/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
staffOrganizationValidationMessage(gasA, '', 'staff-a', staffA),
|
||||||
|
/不属于当前配送点/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
staffOrganizationValidationMessage(gasA, 'delivery-a', 'staff-a', undefined),
|
||||||
|
/无法确认/,
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
optionParentIdentity(platformDelivery, 'gas_basic_identity'),
|
optionParentIdentity(platformDelivery, 'gas_basic_identity'),
|
||||||
'',
|
'',
|
||||||
@@ -128,10 +160,11 @@ const resourcesSource = await readFile(
|
|||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(resourcesSource.match(/relationLinkage:/g) ?? []).length,
|
(resourcesSource.match(/relationLinkage:/g) ?? []).length,
|
||||||
1,
|
2,
|
||||||
'联动配置只能显式启用于工作人员配送点字段',
|
'联动配置必须显式启用于工作人员和用户服务关系的配送点字段',
|
||||||
);
|
);
|
||||||
assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/);
|
assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/);
|
||||||
|
assert.match(resourcesSource, /organizationScoped: true/);
|
||||||
assert.match(resourcesSource, /label: '所属气站'/);
|
assert.match(resourcesSource, /label: '所属气站'/);
|
||||||
assert.match(resourcesSource, /label: '所属配送点'/);
|
assert.match(resourcesSource, /label: '所属配送点'/);
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ 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.match(resourcesSource, /organizationScoped: true/);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(resourcesSource.match(/staffRelation:/g) ?? []).length,
|
(resourcesSource.match(/staffRelation:/g) ?? []).length,
|
||||||
3,
|
3,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type StaffRelationPolicy = {
|
|||||||
workStatus?: 'on_duty' | 'off_duty';
|
workStatus?: 'on_duty' | 'off_duty';
|
||||||
lockPrefilled?: boolean;
|
lockPrefilled?: boolean;
|
||||||
showIdentityCopy?: boolean;
|
showIdentityCopy?: boolean;
|
||||||
|
organizationScoped?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StaffRelationContext = {
|
export type StaffRelationContext = {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export type ResourceField = {
|
|||||||
displayPrecision?: number;
|
displayPrecision?: number;
|
||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
showIdentityCopy?: boolean;
|
||||||
|
readonlyRelationText?: boolean;
|
||||||
options?: Array<{ label: string; value: string | number }>;
|
options?: Array<{ label: string; value: string | number }>;
|
||||||
defaultValue?: string | number | boolean;
|
defaultValue?: string | number | boolean;
|
||||||
readonlyOnCreate?: boolean;
|
readonlyOnCreate?: boolean;
|
||||||
@@ -368,7 +370,54 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true, { listLabel: '用户账户', listRelationNameOnly: true }), f('address', { required: true, emptyText: '未填写', placeholder: '未填写' }), f('longitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('latitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('is_default')]),
|
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true, { listLabel: '用户账户', listRelationNameOnly: true }), f('address', { required: true, emptyText: '未填写', placeholder: '未填写' }), f('longitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('latitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('is_default')]),
|
||||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account', false, { staffRelation: { roles: ['installer', 'delivery', 'operations'], enabledOnly: true } })]),
|
define('user_service_relation', '用户服务关系', 'writable', [
|
||||||
|
relation('user_account_identity', '/user_account', true, {
|
||||||
|
label: '用户账户',
|
||||||
|
listLabel: '用户账户',
|
||||||
|
listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true,
|
||||||
|
showIdentityCopy: true,
|
||||||
|
readonlyRelationText: true,
|
||||||
|
}),
|
||||||
|
relation('gas_basic_identity', '/gas_basic', false, {
|
||||||
|
label: '所属气站',
|
||||||
|
listLabel: '所属气站',
|
||||||
|
listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true,
|
||||||
|
emptyText: '暂未分配',
|
||||||
|
placeholder: '请选择所属气站',
|
||||||
|
showIdentityCopy: true,
|
||||||
|
}),
|
||||||
|
relation('delivery_basic_identity', '/delivery_basic', false, {
|
||||||
|
label: '所属配送点',
|
||||||
|
listLabel: '所属配送点',
|
||||||
|
listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true,
|
||||||
|
emptyText: '暂未分配',
|
||||||
|
placeholder: '请选择所属配送点',
|
||||||
|
showIdentityCopy: true,
|
||||||
|
relationLinkage: {
|
||||||
|
parentKey: 'gas_basic_identity',
|
||||||
|
optionParentKey: 'gas_basic_identity',
|
||||||
|
filterKey: 'gas_basic_identities',
|
||||||
|
backfillParent: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
relation('staff_account_identity', '/staff_account', false, {
|
||||||
|
label: '服务人员',
|
||||||
|
listLabel: '服务人员',
|
||||||
|
listRelationNameOnly: true,
|
||||||
|
displayRelationLabel: true,
|
||||||
|
emptyText: '暂未分配',
|
||||||
|
placeholder: '请选择服务人员',
|
||||||
|
showIdentityCopy: true,
|
||||||
|
staffRelation: {
|
||||||
|
roles: ['installer', 'delivery', 'operations'],
|
||||||
|
enabledOnly: true,
|
||||||
|
organizationScoped: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
|
||||||
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address'), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]),
|
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address'), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]),
|
||||||
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ const entries = computed<DetailEntry[]>(() => {
|
|||||||
value: display,
|
value: display,
|
||||||
objectValue,
|
objectValue,
|
||||||
identityValue:
|
identityValue:
|
||||||
field?.staffRelation?.showIdentityCopy && value ? String(value) : '',
|
(field?.showIdentityCopy || field?.staffRelation?.showIdentityCopy) && value
|
||||||
|
? String(value)
|
||||||
|
: '',
|
||||||
wide:
|
wide:
|
||||||
objectValue ||
|
objectValue ||
|
||||||
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
|
/(address|terms|content|body|remark|reason|params|args)$/.test(key),
|
||||||
|
|||||||
@@ -68,7 +68,18 @@
|
|||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<div v-else-if="field.type === 'identity' || field.type === 'identity-list'" class="relation-control">
|
<div v-else-if="field.type === 'identity' || field.type === 'identity-list'" class="relation-control">
|
||||||
|
<div
|
||||||
|
v-if="disabledSet.has(field.key) && field.readonlyRelationText"
|
||||||
|
class="readonly-relation"
|
||||||
|
>
|
||||||
|
<span class="readonly-relation-name">{{ readonlyRelationLabel(field) }}</span>
|
||||||
|
<IdentityText
|
||||||
|
v-if="(field.showIdentityCopy || field.staffRelation?.showIdentityCopy) && model[field.key]"
|
||||||
|
:value="String(model[field.key])"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<a-select
|
<a-select
|
||||||
|
v-else
|
||||||
v-model="model[field.key]"
|
v-model="model[field.key]"
|
||||||
:disabled="disabledSet.has(field.key)"
|
:disabled="disabledSet.has(field.key)"
|
||||||
:multiple="field.type === 'identity-list'"
|
:multiple="field.type === 'identity-list'"
|
||||||
@@ -88,7 +99,7 @@
|
|||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<div
|
<div
|
||||||
v-if="disabledSet.has(field.key) && field.staffRelation?.showIdentityCopy && model[field.key]"
|
v-if="disabledSet.has(field.key) && (field.showIdentityCopy || field.staffRelation?.showIdentityCopy) && !field.readonlyRelationText && model[field.key]"
|
||||||
class="relation-identity"
|
class="relation-identity"
|
||||||
>
|
>
|
||||||
<span>人员标识</span>
|
<span>人员标识</span>
|
||||||
@@ -146,6 +157,16 @@ function isWide(field: ResourceField) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回只读关系字段的业务名称,未加载到历史记录时保留唯一标识以避免误导。 */
|
||||||
|
function readonlyRelationLabel(field: ResourceField) {
|
||||||
|
const identity = String(model.value[field.key] ?? '');
|
||||||
|
if (!identity) return field.emptyText ?? '暂未分配';
|
||||||
|
const option = (props.relationOptions[field.relation ?? ''] ?? []).find(
|
||||||
|
(item) => String(item.identity ?? '') === identity,
|
||||||
|
);
|
||||||
|
return option ? relationOptionLabel(field, option) : identity;
|
||||||
|
}
|
||||||
|
|
||||||
/** 为历史未知编码补充只读选项,避免下拉框退回显示原始值。 */
|
/** 为历史未知编码补充只读选项,避免下拉框退回显示原始值。 */
|
||||||
function selectOptions(field: ResourceField) {
|
function selectOptions(field: ResourceField) {
|
||||||
const options = [...(field.options ?? [])];
|
const options = [...(field.options ?? [])];
|
||||||
@@ -195,6 +216,19 @@ function selectOptions(field: ResourceField) {
|
|||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.readonly-relation {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
.readonly-relation-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.field-grid {
|
.field-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -305,7 +305,11 @@ const visibleActions = computed(() =>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const modeLabel = computed(() => recordPageModeLabel(mode.value));
|
const modeLabel = computed(() =>
|
||||||
|
definition.value.name === 'user_service_relation'
|
||||||
|
? `${recordPageModeLabel(mode.value)}${definition.value.title}`
|
||||||
|
: recordPageModeLabel(mode.value),
|
||||||
|
);
|
||||||
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
|
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
|
||||||
|
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
|
|||||||
@@ -60,6 +60,42 @@ export function linkedRelationFilters(
|
|||||||
return parentIdentity ? { [filterKey]: parentIdentity } : undefined;
|
return parentIdentity ? { [filterKey]: parentIdentity } : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 为服务人员关系生成精确组织范围;空组织使用显式未分配条件,避免返回其他组织人员。 */
|
||||||
|
export function staffOrganizationFilters(
|
||||||
|
gasIdentity: string,
|
||||||
|
deliveryIdentity: string,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...(gasIdentity
|
||||||
|
? { gas_basic_identities: gasIdentity }
|
||||||
|
: { gas_basic_unassigned: '1' }),
|
||||||
|
...(deliveryIdentity
|
||||||
|
? { delivery_basic_identities: deliveryIdentity }
|
||||||
|
: { delivery_basic_unassigned: '1' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验服务人员记录与当前气站、配送点完全一致,空字符串代表可以提交。 */
|
||||||
|
export function staffOrganizationValidationMessage(
|
||||||
|
gasIdentity: string,
|
||||||
|
deliveryIdentity: string,
|
||||||
|
staffIdentity: string,
|
||||||
|
option: ResourceRow | undefined,
|
||||||
|
) {
|
||||||
|
if (!staffIdentity) return '';
|
||||||
|
if (!option) return '无法确认所选服务人员的组织归属,请重新选择';
|
||||||
|
if (optionParentIdentity(option, 'gas_basic_identity') !== gasIdentity) {
|
||||||
|
return '所选服务人员不属于当前气站,请重新选择';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
optionParentIdentity(option, 'delivery_basic_identity') !==
|
||||||
|
deliveryIdentity
|
||||||
|
) {
|
||||||
|
return '所选服务人员不属于当前配送点,请重新选择';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/** 为同一关联资源生成递增版本号,用于识别并丢弃过期响应。 */
|
/** 为同一关联资源生成递增版本号,用于识别并丢弃过期响应。 */
|
||||||
export function createRelationRequestVersionGuard() {
|
export function createRelationRequestVersionGuard() {
|
||||||
const versions = new Map<string, number>();
|
const versions = new Map<string, number>();
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
optionParentIdentity,
|
optionParentIdentity,
|
||||||
relationIdentity,
|
relationIdentity,
|
||||||
shouldClearLinkedOption,
|
shouldClearLinkedOption,
|
||||||
|
staffOrganizationFilters,
|
||||||
|
staffOrganizationValidationMessage,
|
||||||
} from './resource-relation-linkage-policy';
|
} from './resource-relation-linkage-policy';
|
||||||
import type { ResourceRelations } from './use-resource-relations';
|
import type { ResourceRelations } from './use-resource-relations';
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ export function useResourceRelationLinkage(
|
|||||||
relations: ResourceRelations,
|
relations: ResourceRelations,
|
||||||
) {
|
) {
|
||||||
let active: ActiveLinkage | undefined;
|
let active: ActiveLinkage | undefined;
|
||||||
|
let organizationStaffField: ResourceField | undefined;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
function findOption(resource: string, identity: string) {
|
function findOption(resource: string, identity: string) {
|
||||||
@@ -33,6 +36,9 @@ export function useResourceRelationLinkage(
|
|||||||
|
|
||||||
function configure(fields: ResourceField[]) {
|
function configure(fields: ResourceField[]) {
|
||||||
const childField = fields.find((field) => field.relationLinkage);
|
const childField = fields.find((field) => field.relationLinkage);
|
||||||
|
organizationStaffField = fields.find(
|
||||||
|
(field) => field.staffRelation?.organizationScoped,
|
||||||
|
);
|
||||||
if (!childField?.relationLinkage) {
|
if (!childField?.relationLinkage) {
|
||||||
active = undefined;
|
active = undefined;
|
||||||
return;
|
return;
|
||||||
@@ -45,6 +51,49 @@ export function useResourceRelationLinkage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function staffRequest() {
|
||||||
|
const staffIdentity = relationIdentity(
|
||||||
|
organizationStaffField ? form[organizationStaffField.key] : '',
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
filters: staffOrganizationFilters(
|
||||||
|
relationIdentity(form.gas_basic_identity),
|
||||||
|
relationIdentity(form.delivery_basic_identity),
|
||||||
|
),
|
||||||
|
preserveIdentities: staffIdentity ? [staffIdentity] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadStaffOptions() {
|
||||||
|
const field = organizationStaffField;
|
||||||
|
if (!field?.relation) return;
|
||||||
|
relations.cancelSearch(field.relation);
|
||||||
|
await relations.loadField(field, '', staffRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组织变化后清理不再匹配的服务人员,并刷新精确范围选项。 */
|
||||||
|
async function synchronizeStaff() {
|
||||||
|
const field = organizationStaffField;
|
||||||
|
if (!field?.relation) return;
|
||||||
|
const staffIdentity = relationIdentity(form[field.key]);
|
||||||
|
if (staffIdentity) {
|
||||||
|
const option =
|
||||||
|
findOption(field.relation, staffIdentity) ??
|
||||||
|
(await relations.ensure(field.relation, staffIdentity));
|
||||||
|
const warning = staffOrganizationValidationMessage(
|
||||||
|
relationIdentity(form.gas_basic_identity),
|
||||||
|
relationIdentity(form.delivery_basic_identity),
|
||||||
|
staffIdentity,
|
||||||
|
option,
|
||||||
|
);
|
||||||
|
if (warning && relationIdentity(form[field.key]) === staffIdentity) {
|
||||||
|
form[field.key] = '';
|
||||||
|
Message.info(`${warning},已清空原服务人员`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await reloadStaffOptions();
|
||||||
|
}
|
||||||
|
|
||||||
function childRequest() {
|
function childRequest() {
|
||||||
if (!active?.childField.relationLinkage) return {};
|
if (!active?.childField.relationLinkage) return {};
|
||||||
const linkage = active.childField.relationLinkage;
|
const linkage = active.childField.relationLinkage;
|
||||||
@@ -69,14 +118,21 @@ export function useResourceRelationLinkage(
|
|||||||
initialized = false;
|
initialized = false;
|
||||||
configure(fields);
|
configure(fields);
|
||||||
if (!active?.childField.relationLinkage || !active.childField.relation) {
|
if (!active?.childField.relationLinkage || !active.childField.relation) {
|
||||||
await relations.preload(fields);
|
await relations.preload(
|
||||||
|
fields.filter((field) => field.key !== organizationStaffField?.key),
|
||||||
|
);
|
||||||
await relations.ensureValues(fields, form);
|
await relations.ensureValues(fields, form);
|
||||||
|
await reloadStaffOptions();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await relations.preload(
|
await relations.preload(
|
||||||
fields.filter((field) => field.key !== active?.childField.key),
|
fields.filter(
|
||||||
|
(field) =>
|
||||||
|
field.key !== active?.childField.key &&
|
||||||
|
field.key !== organizationStaffField?.key,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const linkage = active.childField.relationLinkage;
|
const linkage = active.childField.relationLinkage;
|
||||||
const parentIdentity = relationIdentity(form[linkage.parentKey]);
|
const parentIdentity = relationIdentity(form[linkage.parentKey]);
|
||||||
@@ -87,7 +143,14 @@ export function useResourceRelationLinkage(
|
|||||||
if (childIdentity) {
|
if (childIdentity) {
|
||||||
await relations.ensure(active.childField.relation, childIdentity);
|
await relations.ensure(active.childField.relation, childIdentity);
|
||||||
}
|
}
|
||||||
|
const staffIdentity = relationIdentity(
|
||||||
|
organizationStaffField ? form[organizationStaffField.key] : '',
|
||||||
|
);
|
||||||
|
if (staffIdentity && organizationStaffField?.relation) {
|
||||||
|
await relations.ensure(organizationStaffField.relation, staffIdentity);
|
||||||
|
}
|
||||||
await reloadChildOptions();
|
await reloadChildOptions();
|
||||||
|
await reloadStaffOptions();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
const warning = validationMessage();
|
const warning = validationMessage();
|
||||||
@@ -158,8 +221,12 @@ export function useResourceRelationLinkage(
|
|||||||
const identity = relationIdentity(value);
|
const identity = relationIdentity(value);
|
||||||
if (field.key === active.childField.relationLinkage.parentKey) {
|
if (field.key === active.childField.relationLinkage.parentKey) {
|
||||||
await handleParentChange(identity);
|
await handleParentChange(identity);
|
||||||
|
await synchronizeStaff();
|
||||||
} else if (field.key === active.childField.key) {
|
} else if (field.key === active.childField.key) {
|
||||||
await handleChildChange(identity);
|
await handleChildChange(identity);
|
||||||
|
await synchronizeStaff();
|
||||||
|
} else if (field.key === organizationStaffField?.key) {
|
||||||
|
await synchronizeStaff();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +237,10 @@ export function useResourceRelationLinkage(
|
|||||||
relations.searchField(field, keyword, childRequest());
|
relations.searchField(field, keyword, childRequest());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (field.key === organizationStaffField?.key) {
|
||||||
|
relations.searchField(field, keyword, staffRequest());
|
||||||
|
return;
|
||||||
|
}
|
||||||
relations.searchField(field, keyword);
|
relations.searchField(field, keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +252,22 @@ export function useResourceRelationLinkage(
|
|||||||
const parentIdentity = relationIdentity(form[linkage.parentKey]);
|
const parentIdentity = relationIdentity(form[linkage.parentKey]);
|
||||||
const childIdentity = relationIdentity(form[active.childField.key]);
|
const childIdentity = relationIdentity(form[active.childField.key]);
|
||||||
const option = findOption(active.childField.relation, childIdentity);
|
const option = findOption(active.childField.relation, childIdentity);
|
||||||
return linkedRelationValidationMessage(
|
const linkedMessage = linkedRelationValidationMessage(
|
||||||
parentIdentity,
|
parentIdentity,
|
||||||
childIdentity,
|
childIdentity,
|
||||||
option,
|
option,
|
||||||
linkage.optionParentKey,
|
linkage.optionParentKey,
|
||||||
);
|
);
|
||||||
|
if (linkedMessage) return linkedMessage;
|
||||||
|
const staffField = organizationStaffField;
|
||||||
|
if (!staffField?.relation) return '';
|
||||||
|
const staffIdentity = relationIdentity(form[staffField.key]);
|
||||||
|
return staffOrganizationValidationMessage(
|
||||||
|
parentIdentity,
|
||||||
|
childIdentity,
|
||||||
|
staffIdentity,
|
||||||
|
findOption(staffField.relation, staffIdentity),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { preload, change, search, validationMessage };
|
return { preload, change, search, validationMessage };
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
v-else-if="field.listRelationNameOnly && identityFieldValue(field, record)"
|
v-else-if="field.listRelationNameOnly && identityFieldValue(field, record)"
|
||||||
:name="relationListName(field, record, relations.options)"
|
:name="relationListName(field, record, relations.options)"
|
||||||
:identity="identityFieldValue(field, record)"
|
:identity="identityFieldValue(field, record)"
|
||||||
|
:identity-label="field.label"
|
||||||
/>
|
/>
|
||||||
<IdentityText
|
<IdentityText
|
||||||
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)"
|
||||||
@@ -284,6 +285,10 @@ async function load() {
|
|||||||
);
|
);
|
||||||
list.value = result.list;
|
list.value = result.list;
|
||||||
total.value = result.total;
|
total.value = result.total;
|
||||||
|
// 补载当前页实际引用的关系,避免关联记录超过首批选项时退回显示裸标识。
|
||||||
|
await Promise.all(
|
||||||
|
result.list.map((row) => relations.ensureValues(displayFields.value, row)),
|
||||||
|
);
|
||||||
await extras.loadExtras();
|
await extras.loadExtras();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error((error as Error).message);
|
Message.error((error as Error).message);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<!-- 功能描述:列表以关系名称为主展示,并提供完整唯一标识的悬停查看与复制。版本:v1.0.0。 -->
|
<!-- 功能描述:列表以关系名称为主展示,并提供完整唯一标识的悬停查看与复制。版本:v1.0.0。 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="relation-name-text">
|
<div class="relation-name-text">
|
||||||
<a-tooltip :content="`用户唯一标识:${identity}`">
|
<a-tooltip :content="`${identityLabel}:${identity}`">
|
||||||
<span class="relation-name">{{ displayName }}</span>
|
<span class="relation-name">{{ displayName }}</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip content="复制用户唯一标识">
|
<a-tooltip :content="`复制${identityLabel}`">
|
||||||
<button
|
<button
|
||||||
class="copy-button"
|
class="copy-button"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="`复制用户唯一标识 ${identity}`"
|
:aria-label="`复制${identityLabel} ${identity}`"
|
||||||
@click.stop="copyIdentity"
|
@click.stop="copyIdentity"
|
||||||
>
|
>
|
||||||
<icon-copy />
|
<icon-copy />
|
||||||
@@ -22,14 +22,18 @@ import { Message } from '@arco-design/web-vue';
|
|||||||
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{ name: string; identity: string }>();
|
const props = defineProps<{
|
||||||
|
name: string;
|
||||||
|
identity: string;
|
||||||
|
identityLabel?: string;
|
||||||
|
}>();
|
||||||
const displayName = computed(() => props.name || props.identity);
|
const displayName = computed(() => props.name || props.identity);
|
||||||
|
|
||||||
/** 复制完整用户唯一标识,并给出明确操作反馈。 */
|
/** 复制当前关联记录的完整唯一标识,并给出明确操作反馈。 */
|
||||||
async function copyIdentity() {
|
async function copyIdentity() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(props.identity);
|
await navigator.clipboard.writeText(props.identity);
|
||||||
Message.success('用户唯一标识已复制');
|
Message.success(`${props.identityLabel ?? '唯一标识'}已复制`);
|
||||||
} catch {
|
} catch {
|
||||||
Message.error('复制失败,请从悬浮提示中复制');
|
Message.error('复制失败,请从悬浮提示中复制');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user