fix(mobile): 协议单表单项目归属移入协议信息 #62

Merged
cykj merged 1 commits from codex/issue-61-protocol-proj-order-b048f51e07f1 into V2.0 2026-11-05 09:59:46 +00:00
5 changed files with 158 additions and 53 deletions

No files matched your search

@@ -28,25 +28,27 @@
class="market-form-fields"
:class="{ 'market-form-fields--grid-2': section.fieldsLayout === 'grid-2' }"
>
<MarketFormField
v-for="field in visibleFields"
:key="field.key"
:field="field"
:model-value="value?.[field.key]"
:options="fieldOptions(field)"
:uploading="uploading"
:disabled="fieldDisabled(field)"
:search-customers="searchCustomers"
@update:model-value="setValue(field, $event)"
@upload="$emit('upload', { section, field, file: $event })"
@customer-select="$emit('customer-select', { section, ...$event })"
/>
<template v-for="item in fieldsWithInsert" :key="item.field.key">
<slot v-if="item.insertBefore" name="insert" />
<MarketFormField
:field="item.field"
:model-value="value?.[item.field.key]"
:options="fieldOptions(item.field)"
:uploading="uploading"
:disabled="fieldDisabled(item.field)"
:search-customers="searchCustomers"
@update:model-value="setValue(item.field, $event)"
@upload="$emit('upload', { section, field: item.field, file: $event })"
@customer-select="$emit('customer-select', { section, ...$event })"
/>
</template>
<slot v-if="appendInsert" name="insert" />
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
import { computed, useSlots } from 'vue'
import MarketFormField from './MarketFormField.vue'
import { sampleCodeOptionsForRow } from '../reference-options.js'
@@ -58,9 +60,12 @@ const props = defineProps({
mode: { type: String, default: 'create' },
uploading: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
searchCustomers: { type: Function, default: null }
searchCustomers: { type: Function, default: null },
/** 在首个匹配可见字段前插入 #insert 插槽(如协议状态下的项目归属) */
insertBeforeKeys: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'upload', 'field-change', 'customer-select'])
const slots = useSlots()
const value = computed(() => props.modelValue || {})
const rows = computed(() => Array.isArray(props.modelValue) ? props.modelValue : [])
const visibleFields = computed(() => (props.section.fields || []).filter(
@@ -71,6 +76,19 @@ const visibleFields = computed(() => (props.section.fields || []).filter(
!(typeof field.showWhen === 'function' && field.showWhen(value.value) === false) &&
!(field.createOnly && props.mode !== 'create')
))
const fieldsWithInsert = computed(() => {
const keys = props.insertBeforeKeys || []
let inserted = false
return visibleFields.value.map((field) => {
const insertBefore = Boolean(slots.insert) && !inserted && keys.includes(field.key)
if (insertBefore) inserted = true
return { field, insertBefore }
})
})
const appendInsert = computed(() => {
if (!slots.insert || !(props.insertBeforeKeys || []).length) return false
return !fieldsWithInsert.value.some((item) => item.insertBefore)
})
function fieldOptions(field, rowIndex = -1) {
const base = field.options || props.options[field.optionKey] || (field.optionKey === 'users' ? props.options.staff : []) || []
@@ -1,27 +1,49 @@
<template>
<section class="market-form-section">
<h2>{{ filter ? '项目筛选' : '项目归属' }}<span v-if="required && !editing && !filter"> *</span></h2>
<component :is="embedded ? 'div' : 'section'" :class="rootClass">
<h2 v-if="!embedded">{{ filter ? '项目筛选' : '项目归属' }}<span v-if="required && !editing && !filter"> *</span></h2>
<label v-else :id="labelId">所属项目<b v-if="required && !editing && !filter">*</b></label>
<template v-if="editing">
<van-cell title="所属项目" :value="projectLabel(record)" />
<van-cell title="项目组" :value="record.teamName || '—'" />
<van-cell label="归属调整请使用 Web 项目归属维护" />
<template v-if="embedded">
<div class="market-form-field__control market-form-field__control--text" :aria-labelledby="labelId">
{{ projectLabel(record) }}
</div>
<p class="market-form-field__tip">项目组:{{ record.teamName || '—' }};归属调整请使用 Web 项目归属维护</p>
</template>
<template v-else>
<van-cell title="所属项目" :value="projectLabel(record)" />
<van-cell title="项目组" :value="record.teamName || '—'" />
<van-cell label="归属调整请使用 Web 项目归属维护" />
</template>
</template>
<template v-else>
<PresignSelectField
:model-value="modelValue || ''"
:options="selectOptions"
:disabled="disabled || loading || Boolean(error)"
:loading="loading"
searchable
title="所属项目"
:placeholder="filter ? '全部项目' : '请选择所属项目'"
@update:model-value="emit('update:modelValue', $event)"
/>
<van-cell v-if="loading" title="正在加载项目…" />
<van-cell v-else-if="error" :label="error" title="项目加载失败,点击重试" is-link @click="load" />
<van-cell v-else-if="!options.length" :label="required && !filter ? '请联系管理员配置可选项目后再保存' : '当前没有可选项目'" />
<div :class="embedded ? 'market-form-field__control' : undefined">
<PresignSelectField
:model-value="modelValue || ''"
:options="selectOptions"
:disabled="disabled || loading || Boolean(error)"
:loading="loading"
searchable
title="所属项目"
:placeholder="filter ? '全部项目' : '请选择所属项目'"
@update:model-value="emit('update:modelValue', $event)"
/>
</div>
<template v-if="embedded">
<p v-if="loading" class="market-form-field__tip">正在加载项目…</p>
<button v-else-if="error" type="button" class="market-form-field__tip market-form-field__tip--action" @click="load">
项目加载失败,点击重试({{ error }})
</button>
<p v-else-if="!options.length" class="market-form-field__tip">
{{ required && !filter ? '请联系管理员配置可选项目后再保存' : '当前没有可选项目' }}
</p>
</template>
<template v-else>
<van-cell v-if="loading" title="正在加载项目…" />
<van-cell v-else-if="error" :label="error" title="项目加载失败,点击重试" is-link @click="load" />
<van-cell v-else-if="!options.length" :label="required && !filter ? '请联系管理员配置可选项目后再保存' : '当前没有可选项目'" />
</template>
</template>
</section>
</component>
</template>
<script setup>
@@ -35,6 +57,7 @@ const props = defineProps({
bizType: { type: String, required: true },
editing: { type: Boolean, default: false },
filter: { type: Boolean, default: false },
embedded: { type: Boolean, default: false },
context: { type: Object, default: null },
disabled: { type: Boolean, default: false },
record: { type: Object, default: () => ({}) }
@@ -44,7 +67,9 @@ const options = ref([])
const required = ref(false)
const loading = ref(true)
const error = ref('')
const labelId = `project-assignment-${Math.random().toString(36).slice(2, 9)}`
let controller
const rootClass = computed(() => (props.embedded ? 'market-form-field' : 'market-form-section'))
const selectOptions = computed(() => [
...(!required.value || props.filter ? [{ text: props.filter ? '全部项目' : '暂不归属', value: '' }] : []),
...options.value
@@ -326,6 +326,22 @@ test('protocol list uses inline protocol-status dropdown like presign instead of
assert.doesNotMatch(genericList, /ProjectAssignmentField/)
})
test('protocol form embeds project assignment inside protocol info before status fields', () => {
const form = source('../views/market/MarketModuleForm.vue')
const formSection = source('./components/MarketFormSection.vue')
const projectField = source('./components/ProjectAssignmentField.vue')
assert.match(form, /insert-before-keys="\['protocolStatus', 'donationStatus'\]"/)
assert.match(form, /#insert/)
assert.match(form, /embedded/)
assert.match(form, /<ProjectAssignmentField[\s\S]*?embedded/)
assert.doesNotMatch(form, /ProjectAssignmentField v-if="context\.module === 'protocol'"/)
assert.match(formSection, /insertBeforeKeys/)
assert.match(formSection, /name="insert"/)
assert.match(projectField, /embedded/)
assert.match(projectField, /market-form-field/)
})
test('protocol list exposes mobile batch selection and sequential authorized deletion', () => {
const genericList = source('../views/market/MarketModuleList.vue')
const styles = source('../styles/market.css')
+19
View File
@@ -1247,6 +1247,25 @@ body.van-toast--unclickable .presign-order-page * {
font-weight: 500;
}
.market-form-field > .market-form-field__tip,
.market-form-field .market-form-field__control--text .market-form-field__tip {
display: block;
margin: 0;
color: #64748b;
font-size: 11px;
font-weight: 500;
line-height: 1.4;
}
.market-form-field > .market-form-field__tip--action {
padding: 0;
border: 0;
background: transparent;
color: #0a9f6d;
text-align: left;
cursor: pointer;
}
.market-form-field > .van-field,
.market-form-field__control {
box-sizing: border-box;
@@ -12,25 +12,52 @@
</van-empty>
</div>
<template v-else>
<ProjectAssignmentField v-if="context.module === 'protocol'" ref="projectAssignment"
v-model="formData.projectId" :biz-type="formData.source" :editing="mode === 'edit'"
:record="formData" :disabled="fieldsLocked" @update:model-value="dirty = true" />
<MarketFormSection
v-for="section in leadingSections"
:key="section.key"
:section="section"
:model-value="sectionValue(section)"
:options="formOptions"
:source="formData.source"
:mode="mode"
:uploading="false"
:disabled="fieldsLocked"
:search-customers="searchProtocolCustomers"
@update:model-value="setSectionValue(section, $event)"
@upload="uploadField"
@field-change="handleFieldChange"
@customer-select="handleCustomerSelect"
/>
<template v-for="section in leadingSections" :key="section.key">
<MarketFormSection
v-if="!(context.module === 'protocol' && section.key === 'protocol')"
:section="section"
:model-value="sectionValue(section)"
:options="formOptions"
:source="formData.source"
:mode="mode"
:uploading="false"
:disabled="fieldsLocked"
:search-customers="searchProtocolCustomers"
@update:model-value="setSectionValue(section, $event)"
@upload="uploadField"
@field-change="handleFieldChange"
@customer-select="handleCustomerSelect"
/>
<MarketFormSection
v-else
:section="section"
:model-value="sectionValue(section)"
:options="formOptions"
:source="formData.source"
:mode="mode"
:uploading="false"
:disabled="fieldsLocked"
:search-customers="searchProtocolCustomers"
:insert-before-keys="['protocolStatus', 'donationStatus']"
@update:model-value="setSectionValue(section, $event)"
@upload="uploadField"
@field-change="handleFieldChange"
@customer-select="handleCustomerSelect"
>
<template #insert>
<ProjectAssignmentField
ref="projectAssignment"
embedded
v-model="formData.projectId"
:biz-type="formData.source"
:editing="mode === 'edit'"
:record="formData"
:disabled="fieldsLocked"
@update:model-value="dirty = true"
/>
</template>
</MarketFormSection>
</template>
<section
v-if="context.module === 'protocol' && formData.source === 'cell_storage' && showDepositSection"