修复配送合同候选范围与失效联动
This commit is contained in:
@@ -36,8 +36,32 @@ var (
|
|||||||
errBindingProductDuplicate = errors.New("所选气瓶已绑定到该合同,请勿重复绑定")
|
errBindingProductDuplicate = errors.New("所选气瓶已绑定到该合同,请勿重复绑定")
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListGasorderContract(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderContract{}) }
|
// ListGasorderContract 按明确用途收窄合同候选;合同管理列表不带用途参数时保持全量查询。
|
||||||
func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) }
|
func ListGasorderContract(ctx *gin.Context) {
|
||||||
|
candidate := strings.TrimSpace(ctx.Query("candidate"))
|
||||||
|
if candidate != "order" && candidate != "binding" {
|
||||||
|
common.ListResource(ctx, &models.GasorderContract{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
common.ListPageFiltered[models.GasorderContract](ctx, func(query *gorm.DB) *gorm.DB {
|
||||||
|
return filterGasorderContractCandidates(query, candidate, now)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterGasorderContractCandidates 应用订单履约或草稿绑定场景的合同候选条件。
|
||||||
|
func filterGasorderContractCandidates(query *gorm.DB, candidate string, now time.Time) *gorm.DB {
|
||||||
|
if candidate == "binding" {
|
||||||
|
return query.Where("contract_status = ?", common.StatusDraft)
|
||||||
|
}
|
||||||
|
return query.Where(
|
||||||
|
"contract_status = ? AND effective_at <= ? AND expired_at IS NOT NULL AND expired_at > ?",
|
||||||
|
common.StatusActive,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) }
|
||||||
func ListGasorderContractProduct(ctx *gin.Context) {
|
func ListGasorderContractProduct(ctx *gin.Context) {
|
||||||
common.ListResource(ctx, &models.GasorderContractProduct{})
|
common.ListResource(ctx, &models.GasorderContractProduct{})
|
||||||
}
|
}
|
||||||
@@ -538,7 +562,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if contract.ContractStatus != common.StatusActive || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
|
if !contractEligibleForOrder(contract, now) {
|
||||||
return errors.New("contract is not active")
|
return errors.New("contract is not active")
|
||||||
}
|
}
|
||||||
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
|
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
|
||||||
@@ -618,6 +642,14 @@ func CreateGasorderBasic(ctx *gin.Context) {
|
|||||||
common.RespondCreatedResource(ctx, order)
|
common.RespondCreatedResource(ctx, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contractEligibleForOrder 校验合同当前确实处于可履约时间窗口。
|
||||||
|
func contractEligibleForOrder(contract models.GasorderContract, now time.Time) bool {
|
||||||
|
return contract.ContractStatus == common.StatusActive &&
|
||||||
|
!contract.EffectiveAt.After(now) &&
|
||||||
|
contract.ExpiredAt != nil &&
|
||||||
|
contract.ExpiredAt.After(now)
|
||||||
|
}
|
||||||
|
|
||||||
func AssignGasorderBasic(ctx *gin.Context) {
|
func AssignGasorderBasic(ctx *gin.Context) {
|
||||||
var request struct {
|
var request struct {
|
||||||
DeliveryIdentity string `json:"delivery_basic_identity" binding:"required"`
|
DeliveryIdentity string `json:"delivery_basic_identity" binding:"required"`
|
||||||
|
|||||||
@@ -8,8 +8,69 @@ import (
|
|||||||
|
|
||||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestContractEligibleForOrder 覆盖草稿、未生效、有效、到期、终止和缺少到期时间的合同。
|
||||||
|
func TestContractEligibleForOrder(t *testing.T) {
|
||||||
|
now := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC)
|
||||||
|
past := now.Add(-time.Hour)
|
||||||
|
future := now.Add(time.Hour)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
contract models.GasorderContract
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"草稿", models.GasorderContract{ContractStatus: common.StatusDraft, EffectiveAt: past, ExpiredAt: &future}, false},
|
||||||
|
{"未到生效时间", models.GasorderContract{ContractStatus: common.StatusActive, EffectiveAt: future, ExpiredAt: &future}, false},
|
||||||
|
{"当前有效", models.GasorderContract{ContractStatus: common.StatusActive, EffectiveAt: past, ExpiredAt: &future}, true},
|
||||||
|
{"已经到期", models.GasorderContract{ContractStatus: common.StatusActive, EffectiveAt: past, ExpiredAt: &now}, false},
|
||||||
|
{"已经终止", models.GasorderContract{ContractStatus: common.StatusTerminated, EffectiveAt: past, ExpiredAt: &future}, false},
|
||||||
|
{"缺少到期时间", models.GasorderContract{ContractStatus: common.StatusActive, EffectiveAt: past}, false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := contractEligibleForOrder(test.contract, now); got != test.want {
|
||||||
|
t.Fatalf("contractEligibleForOrder() = %v, want %v", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilterGasorderContractCandidates 验证订单候选和绑定候选使用不同的服务端过滤条件。
|
||||||
|
func TestFilterGasorderContractCandidates(t *testing.T) {
|
||||||
|
sqlDatabase, _, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("创建模拟数据库失败:%v", err)
|
||||||
|
}
|
||||||
|
defer sqlDatabase.Close()
|
||||||
|
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("创建 GORM 数据库失败:%v", err)
|
||||||
|
}
|
||||||
|
now := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC)
|
||||||
|
statement := func(candidate string) string {
|
||||||
|
return database.ToSQL(func(tx *gorm.DB) *gorm.DB {
|
||||||
|
var contracts []models.GasorderContract
|
||||||
|
return filterGasorderContractCandidates(
|
||||||
|
tx.Model(&models.GasorderContract{}), candidate, now,
|
||||||
|
).Find(&contracts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
orderSQL := statement("order")
|
||||||
|
for _, fragment := range []string{"contract_status = 11", "effective_at <=", "expired_at IS NOT NULL", "expired_at >"} {
|
||||||
|
if !strings.Contains(orderSQL, fragment) {
|
||||||
|
t.Fatalf("订单合同候选缺少 %q:%s", fragment, orderSQL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bindingSQL := statement("binding")
|
||||||
|
if !strings.Contains(bindingSQL, "contract_status = 0") {
|
||||||
|
t.Fatalf("合同气瓶绑定候选未限定草稿:%s", bindingSQL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) {
|
func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) {
|
||||||
for _, creatorType := range []string{"user", "staff", "delivery", "gas"} {
|
for _, creatorType := range []string{"user", "staff", "delivery", "gas"} {
|
||||||
if gasorderCreatorModels[creatorType] == nil {
|
if gasorderCreatorModels[creatorType] == nil {
|
||||||
|
|||||||
@@ -185,6 +185,8 @@
|
|||||||
|
|
||||||
合同气瓶必须属于合同用户、处于可用状态且未报废;订单只能使用有效合同中仍绑定的气瓶。
|
合同气瓶必须属于合同用户、处于可用状态且未报废;订单只能使用有效合同中仍绑定的气瓶。
|
||||||
|
|
||||||
|
合同气瓶绑定页面只允许选择草稿合同;已生效、已过期或已终止合同不进入绑定候选。合同管理列表仍展示全部生命周期状态。
|
||||||
|
|
||||||
### 6.6 气体配送订单
|
### 6.6 气体配送订单
|
||||||
|
|
||||||
| 资源 | 路径 | 模式 | 已实现能力 |
|
| 资源 | 路径 | 模式 | 已实现能力 |
|
||||||
@@ -202,6 +204,8 @@
|
|||||||
|
|
||||||
创建订单页面必须先选择配送合同,收货地址下拉仅展示该合同签约用户的地址。地址选项以完整地址为主文案并标记默认地址;存在默认地址时自动选中,切换合同时清空原地址。若合同用户没有可用地址,页面应明确引导先维护用户地址并禁止提交。
|
创建订单页面必须先选择配送合同,收货地址下拉仅展示该合同签约用户的地址。地址选项以完整地址为主文案并标记默认地址;存在默认地址时自动选中,切换合同时清空原地址。若合同用户没有可用地址,页面应明确引导先维护用户地址并禁止提交。
|
||||||
|
|
||||||
|
订单合同候选必须同时满足“生效中、已到生效时间、尚未到期且到期时间有效”。草稿、未到生效时间、已到期和已终止合同不得进入订单候选;没有可履约合同时应引导管理员先启用或续签合同并禁止提交。
|
||||||
|
|
||||||
订单状态机如下:
|
订单状态机如下:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
64
docs/项目文档_配送合同候选范围修复_v1.0.md
Normal file
64
docs/项目文档_配送合同候选范围修复_v1.0.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 项目文档_配送合同候选范围修复_v1.0
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
本次修复平台总后台业务表单直接加载全部配送合同的问题。合同管理列表需要展示完整生命周期,但创建订单和绑定合同气瓶属于受状态约束的业务操作,候选范围必须与服务端最终校验一致。
|
||||||
|
|
||||||
|
## 2. 业务规则
|
||||||
|
|
||||||
|
- 创建配送订单只显示当前可履约合同:状态为“生效中”、当前时间不早于生效时间、到期时间存在且当前时间早于到期时间。
|
||||||
|
- 草稿、未到生效时间、已过期、已终止和缺少到期时间的合同不进入订单候选。
|
||||||
|
- 绑定合同气瓶只显示草稿合同。
|
||||||
|
- 合同管理列表不携带候选用途参数,继续展示全部合同。
|
||||||
|
- 没有可用候选时,页面显示对应业务引导并禁止保存。
|
||||||
|
- 页面重新加载后,严格筛选结果中不存在的旧合同选择会被清空。
|
||||||
|
- 服务端创建订单和绑定气瓶时继续执行最终校验,前端筛选不构成安全控制。
|
||||||
|
|
||||||
|
## 3. 接口兼容
|
||||||
|
|
||||||
|
`GET /gasorder_contract` 新增可选查询参数 `candidate`:
|
||||||
|
|
||||||
|
- 不传:合同管理全量列表,保持原行为。
|
||||||
|
- `candidate=order`:仅返回当前可履约合同。
|
||||||
|
- `candidate=binding`:仅返回草稿合同。
|
||||||
|
|
||||||
|
未新增数据库字段、表或迁移,也未修改合同数据。
|
||||||
|
|
||||||
|
## 4. 关键文件
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/api/internal/logic/platform/gasorder/gasorder.go # 合同候选过滤与订单最终校验
|
||||||
|
backend/api/internal/logic/platform/gasorder/gasorder_test.go # 五类合同状态与查询条件测试
|
||||||
|
frontend/platform_admin/src/api/resources.ts # 两类合同候选配置与中文提示
|
||||||
|
frontend/platform_admin/src/views/resource/use-resource-relations.ts # 字段级筛选参数
|
||||||
|
frontend/platform_admin/src/views/resource/use-resource-relation-linkage.ts # 失效选择清理和空候选阻断
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 变更记录
|
||||||
|
|
||||||
|
操作时间:2026-08-13
|
||||||
|
|
||||||
|
操作类型:修复
|
||||||
|
|
||||||
|
影响模块:配送合同、配送订单、合同气瓶
|
||||||
|
|
||||||
|
操作前状态:订单和合同气瓶表单均展示全部合同,用户选中后才由服务端拒绝。
|
||||||
|
|
||||||
|
操作后状态:不同业务表单只展示当前允许操作的合同,并继续保留服务端最终校验。
|
||||||
|
|
||||||
|
风险评估:管理列表和既有无参数调用保持兼容;候选接口只收窄可选数据,不修改数据事实。
|
||||||
|
|
||||||
|
## 6. 验证方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/api
|
||||||
|
go test ./internal/logic/platform/gasorder
|
||||||
|
go test ./...
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
cd frontend/platform_admin
|
||||||
|
npm run gasorder-contract-candidates:check
|
||||||
|
npm run type:check
|
||||||
|
npm run contract:check
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"staff-relations:check": "node scripts/check-staff-relation-policy.mjs",
|
"staff-relations:check": "node scripts/check-staff-relation-policy.mjs",
|
||||||
"user-address-display:check": "node scripts/check-user-address-relation-display.mjs",
|
"user-address-display:check": "node scripts/check-user-address-relation-display.mjs",
|
||||||
"gasorder-address:check": "node scripts/check-gasorder-address-linkage.mjs",
|
"gasorder-address:check": "node scripts/check-gasorder-address-linkage.mjs",
|
||||||
|
"gasorder-contract-candidates:check": "node scripts/check-gasorder-contract-candidates.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",
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ const linkage = readFileSync(new URL('../src/views/resource/use-resource-relatio
|
|||||||
|
|
||||||
assert.match(resources, /filterKey: 'contract_identity', filterOnly: true/, '气瓶字段必须按当前合同筛选');
|
assert.match(resources, /filterKey: 'contract_identity', filterOnly: true/, '气瓶字段必须按当前合同筛选');
|
||||||
assert.match(resources, /请先选择合同,再选择该合同用户可绑定的气瓶/, '气瓶字段必须解释筛选范围');
|
assert.match(resources, /请先选择合同,再选择该合同用户可绑定的气瓶/, '气瓶字段必须解释筛选范围');
|
||||||
assert.match(linkage, /if \(linkage\.filterOnly\) return ''/, '纯筛选联动不得执行父标识校验');
|
assert.match(linkage, /linkage\.filterOnly\) continue/, '纯筛选联动不得执行父标识校验');
|
||||||
|
|
||||||
console.log('合同气瓶候选过滤检查通过:气瓶下拉按当前合同加载可绑定项。');
|
console.log('合同气瓶候选过滤检查通过:气瓶下拉按当前合同加载可绑定项。');
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 功能:静态检查订单创建与合同气瓶绑定使用正确的合同候选范围。
|
||||||
|
* 版本:v1.0.0
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const resources = readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8');
|
||||||
|
const relations = readFileSync(new URL('../src/views/resource/use-resource-relations.ts', import.meta.url), 'utf8');
|
||||||
|
const linkage = readFileSync(new URL('../src/views/resource/use-resource-relation-linkage.ts', import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
assert.match(resources, /relationFilters: \{ candidate: 'order' \}/, '订单创建必须只加载可履约合同');
|
||||||
|
assert.match(resources, /relationFilters: \{ candidate: 'binding' \}/, '合同气瓶绑定必须只加载草稿合同');
|
||||||
|
assert.match(resources, /暂无可下单的生效合同,请先启用或续签配送合同/, '订单页缺少无可用合同时的引导');
|
||||||
|
assert.match(resources, /暂无可绑定气瓶的草稿合同,请先新建配送合同/, '合同气瓶页缺少无草稿合同时的引导');
|
||||||
|
assert.match(resources, /relationStrictFilter: true/, '合同候选必须启用严格筛选');
|
||||||
|
assert.match(relations, /\.\.\.\(field\.relationFilters \?\? \{\}\)/, '关联请求未携带字段级筛选参数');
|
||||||
|
assert.match(linkage, /clearInvalidStrictSelections/, '刷新后未清理失效合同选择');
|
||||||
|
|
||||||
|
console.log('配送合同候选检查通过:订单仅显示可履约合同,气瓶绑定仅显示草稿合同。');
|
||||||
@@ -68,6 +68,12 @@ export type ResourceField = {
|
|||||||
relationAutoSelectKey?: string;
|
relationAutoSelectKey?: string;
|
||||||
/** 关联候选为空时展示的业务引导。 */
|
/** 关联候选为空时展示的业务引导。 */
|
||||||
relationEmptyText?: string;
|
relationEmptyText?: string;
|
||||||
|
/** 该关联字段每次加载与搜索都必须携带的精确筛选参数。 */
|
||||||
|
relationFilters?: Record<string, string>;
|
||||||
|
/** 筛选结果中不存在当前值时立即清空,禁止通过详情补载绕过候选范围。 */
|
||||||
|
relationStrictFilter?: boolean;
|
||||||
|
/** 严格筛选清空失效值时展示的提示。 */
|
||||||
|
relationInvalidMessage?: string;
|
||||||
staffRelation?: import('./resource-staff-relation').StaffRelationPolicy;
|
staffRelation?: import('./resource-staff-relation').StaffRelationPolicy;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -504,6 +510,9 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
], { canCreate: true, canEdit: true }),
|
], { canCreate: true, canEdit: true }),
|
||||||
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true, {
|
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true, {
|
||||||
readonlyRelationText: true, displayRelationLabel: true,
|
readonlyRelationText: true, displayRelationLabel: true,
|
||||||
|
placeholder: '请选择草稿合同', relationFilters: { candidate: 'binding' }, relationStrictFilter: true,
|
||||||
|
relationInvalidMessage: '所选合同已不是草稿状态,已清空,请重新选择',
|
||||||
|
relationEmptyText: '暂无可绑定气瓶的草稿合同,请先新建配送合同',
|
||||||
}), relation('product_info_identity', '/product_info', true, {
|
}), relation('product_info_identity', '/product_info', true, {
|
||||||
placeholder: '请先选择合同,再选择该合同用户可绑定的气瓶',
|
placeholder: '请先选择合同,再选择该合同用户可绑定的气瓶',
|
||||||
relationLinkage: {
|
relationLinkage: {
|
||||||
@@ -514,7 +523,7 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
{ 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', []),
|
||||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', placeholder: '请选择配送合同' }), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true, {
|
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', placeholder: '请选择当前可履约的生效合同', relationFilters: { candidate: 'order' }, relationStrictFilter: true, relationInvalidMessage: '所选配送合同已不可履约,已清空,请重新选择', relationEmptyText: '暂无可下单的生效合同,请先启用或续签配送合同' }), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true, {
|
||||||
label: '收货地址', placeholder: '请先选择配送合同,再选择该合同用户的收货地址',
|
label: '收货地址', placeholder: '请先选择配送合同,再选择该合同用户的收货地址',
|
||||||
relationOptionLabelKey: 'address', relationOptionDefaultKey: 'is_default',
|
relationOptionLabelKey: 'address', relationOptionDefaultKey: 'is_default',
|
||||||
relationAutoSelectKey: 'is_default', relationEmptyText: '该合同用户暂无收货地址,请先维护用户地址',
|
relationAutoSelectKey: 'is_default', relationEmptyText: '该合同用户暂无收货地址,请先维护用户地址',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* 版本:v1.1.0
|
* 版本:v1.1.0
|
||||||
*/
|
*/
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
import type { ResourceField } from '@/api/resources';
|
import type { ResourceField } from '@/api/resources';
|
||||||
import {
|
import {
|
||||||
linkedRelationFilters,
|
linkedRelationFilters,
|
||||||
@@ -25,6 +26,7 @@ export function useResourceRelationLinkage(
|
|||||||
relations: ResourceRelations,
|
relations: ResourceRelations,
|
||||||
) {
|
) {
|
||||||
let active: ActiveLinkage[] = [];
|
let active: ActiveLinkage[] = [];
|
||||||
|
const configuredFields = ref<ResourceField[]>([]);
|
||||||
let organizationStaffField: ResourceField | undefined;
|
let organizationStaffField: ResourceField | undefined;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ export function useResourceRelationLinkage(
|
|||||||
|
|
||||||
/** 收集页面内全部显式联动,允许一个父字段同时驱动多个业务子项。 */
|
/** 收集页面内全部显式联动,允许一个父字段同时驱动多个业务子项。 */
|
||||||
function configure(fields: ResourceField[]) {
|
function configure(fields: ResourceField[]) {
|
||||||
|
configuredFields.value = fields;
|
||||||
active = fields
|
active = fields
|
||||||
.filter((field) => field.relationLinkage)
|
.filter((field) => field.relationLinkage)
|
||||||
.map((childField) => ({
|
.map((childField) => ({
|
||||||
@@ -147,15 +150,49 @@ export function useResourceRelationLinkage(
|
|||||||
field.key !== organizationStaffField?.key,
|
field.key !== organizationStaffField?.key,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await relations.ensureValues(fields, form);
|
await relations.ensureValues(
|
||||||
|
fields.filter((field) => !field.relationStrictFilter),
|
||||||
|
form,
|
||||||
|
);
|
||||||
await Promise.all(active.map((item) => reloadChildOptions(item)));
|
await Promise.all(active.map((item) => reloadChildOptions(item)));
|
||||||
await reloadStaffOptions();
|
await reloadStaffOptions();
|
||||||
|
clearInvalidStrictSelections();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
const warning = validationMessage();
|
const warning = validationMessage();
|
||||||
if (warning) Message.warning(`当前组织归属需要确认:${warning}`);
|
if (warning) Message.warning(`当前组织归属需要确认:${warning}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 严格候选中不存在当前值时清空,避免补载详情绕过业务筛选。 */
|
||||||
|
function clearInvalidStrictSelections() {
|
||||||
|
for (const field of configuredFields.value) {
|
||||||
|
if (!field.relationStrictFilter || !field.relation) continue;
|
||||||
|
const rawValue = form[field.key];
|
||||||
|
const identities = (Array.isArray(rawValue) ? rawValue : [rawValue])
|
||||||
|
.map(relationIdentity)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (identities.length === 0) continue;
|
||||||
|
const valid = new Set(
|
||||||
|
(relations.options[field.relation] ?? []).map((option) =>
|
||||||
|
relationIdentity(option.identity),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (identities.some((identity) => !valid.has(identity))) {
|
||||||
|
form[field.key] = field.type === 'identity-list' ? [] : '';
|
||||||
|
for (const linkageItem of active.filter(
|
||||||
|
(item) => item.childField.relationLinkage?.parentKey === field.key,
|
||||||
|
)) {
|
||||||
|
const childField = linkageItem.childField;
|
||||||
|
form[childField.key] = childField.type === 'identity-list' ? [] : '';
|
||||||
|
relations.clear(childField.relation);
|
||||||
|
}
|
||||||
|
Message.info(
|
||||||
|
field.relationInvalidMessage ?? '所选关联数据已失效,已清空',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleParentChange(
|
async function handleParentChange(
|
||||||
linkageItem: ActiveLinkage,
|
linkageItem: ActiveLinkage,
|
||||||
parentIdentity: string,
|
parentIdentity: string,
|
||||||
@@ -292,19 +329,22 @@ export function useResourceRelationLinkage(
|
|||||||
|
|
||||||
/** 必填联动候选为空时返回业务引导,用于阻止提交无效表单。 */
|
/** 必填联动候选为空时返回业务引导,用于阻止提交无效表单。 */
|
||||||
function unavailableMessage() {
|
function unavailableMessage() {
|
||||||
for (const linkageItem of active) {
|
for (const field of configuredFields.value) {
|
||||||
const field = linkageItem.childField;
|
|
||||||
const linkage = field.relationLinkage;
|
const linkage = field.relationLinkage;
|
||||||
if (
|
if (
|
||||||
!field.required ||
|
!field.required ||
|
||||||
!field.relation ||
|
!field.relation ||
|
||||||
!field.relationEmptyText ||
|
!field.relationEmptyText ||
|
||||||
!linkage?.requiresParent ||
|
|
||||||
!relationIdentity(form[linkage.parentKey]) ||
|
|
||||||
relations.loading[field.relation]
|
relations.loading[field.relation]
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
linkage?.requiresParent &&
|
||||||
|
!relationIdentity(form[linkage.parentKey])
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ((relations.options[field.relation] ?? []).length === 0) {
|
if ((relations.options[field.relation] ?? []).length === 0) {
|
||||||
return field.relationEmptyText;
|
return field.relationEmptyText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function useResourceRelations(
|
|||||||
return load(field.relation, keyword, {
|
return load(field.relation, keyword, {
|
||||||
...request,
|
...request,
|
||||||
filters: {
|
filters: {
|
||||||
|
...(field.relationFilters ?? {}),
|
||||||
...staffRelationFilters(field, context()),
|
...staffRelationFilters(field, context()),
|
||||||
...(request.filters ?? {}),
|
...(request.filters ?? {}),
|
||||||
},
|
},
|
||||||
@@ -170,6 +171,7 @@ export function useResourceRelations(
|
|||||||
search(field.relation, keyword, {
|
search(field.relation, keyword, {
|
||||||
...request,
|
...request,
|
||||||
filters: {
|
filters: {
|
||||||
|
...(field.relationFilters ?? {}),
|
||||||
...staffRelationFilters(field, context()),
|
...staffRelationFilters(field, context()),
|
||||||
...(request.filters ?? {}),
|
...(request.filters ?? {}),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user