Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e97f47b06a | ||
|
|
a4defb2c94 | ||
|
|
8717702b58 |
No files matched your search
@@ -984,11 +984,14 @@ def list_protocols(
|
||||
for row in cell_rows
|
||||
)
|
||||
if source in (None, SOURCE_DONATION):
|
||||
if status or collected_from or collected_until:
|
||||
donation_enabled = False
|
||||
else:
|
||||
donation_enabled = True
|
||||
if not donation_enabled:
|
||||
# 混合列表(source=None)下状态/采集时间只覆盖细胞存储,捐赠侧跳过。
|
||||
# 科研捐赠单源列表支持 protocolStatus→donation_status;采集时间筛仍不支持捐赠。
|
||||
skip_donation = False
|
||||
if source is None and (status or collected_from or collected_until):
|
||||
skip_donation = True
|
||||
elif source == SOURCE_DONATION and (collected_from or collected_until):
|
||||
skip_donation = True
|
||||
if skip_donation:
|
||||
query = None
|
||||
else:
|
||||
query = apply_donation_scope(
|
||||
@@ -1001,6 +1004,8 @@ def list_protocols(
|
||||
if query is None:
|
||||
donation_rows = []
|
||||
else:
|
||||
if status and source == SOURCE_DONATION:
|
||||
query = query.filter(BizResearchDonation.donation_status == status)
|
||||
if created_from:
|
||||
query = query.filter(BizResearchDonation.create_time >= created_from)
|
||||
if created_until:
|
||||
|
||||
@@ -3733,6 +3733,29 @@ def test_protocol_list_filters_status_create_time_and_sample_collect_time():
|
||||
assert [item['id'] for item in items] == ['hit']
|
||||
|
||||
|
||||
def test_donation_list_filters_by_protocol_status_alias():
|
||||
from app.services.market_protocol import list_protocols
|
||||
|
||||
db = _session()
|
||||
_masters(db)
|
||||
hit = _donation(db, 'don-hit', created=datetime(2026, 10, 8, 9, 0))
|
||||
hit.donation_status = 'sxz'
|
||||
miss = _donation(db, 'don-miss', created=datetime(2026, 10, 8, 9, 0))
|
||||
miss.donation_status = 'dsx'
|
||||
db.commit()
|
||||
|
||||
items, total = list_protocols(
|
||||
db,
|
||||
ctx=_ctx('specialist'),
|
||||
user_info=_user(),
|
||||
source='research_donation',
|
||||
protocol_status='sxz',
|
||||
)
|
||||
|
||||
assert total == 1
|
||||
assert [item['id'] for item in items] == ['don-hit']
|
||||
|
||||
|
||||
def test_cell_create_rolls_back_main_customer_and_children_on_child_failure():
|
||||
from app.services.market_protocol import create_protocol
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ export function createListFilters(module, source = '') {
|
||||
result.source = SOURCES.has(text(source)) ? text(source) : 'cell_storage'
|
||||
return {
|
||||
source: result.source,
|
||||
protocolStatus: result.protocolStatus
|
||||
protocolStatus: result.protocolStatus,
|
||||
projectId: ''
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -15,11 +15,13 @@ test('creates isolated defaults for every market module', () => {
|
||||
|
||||
assert.deepEqual(first, {
|
||||
source: 'cell_storage',
|
||||
protocolStatus: 'sxz'
|
||||
protocolStatus: 'sxz',
|
||||
projectId: ''
|
||||
})
|
||||
assert.deepEqual(second, {
|
||||
source: 'research_donation',
|
||||
protocolStatus: ''
|
||||
protocolStatus: '',
|
||||
projectId: ''
|
||||
})
|
||||
assert.deepEqual(createListFilters('presign'), { orderStatus: '' })
|
||||
assert.deepEqual(createListFilters('payment-chase'), { chaseType: '', payMode: '' })
|
||||
@@ -66,8 +68,14 @@ test('drops incomplete ranges and counts only user-selectable filters', () => {
|
||||
}), { progressStatus: 'yjq' })
|
||||
assert.equal(activeFilterCount('protocol', {
|
||||
source: 'cell_storage',
|
||||
protocolStatus: 'sxz'
|
||||
protocolStatus: 'sxz',
|
||||
projectId: ''
|
||||
}), 1)
|
||||
assert.equal(activeFilterCount('protocol', {
|
||||
source: 'research_donation',
|
||||
protocolStatus: 'sxz',
|
||||
projectId: 'p1'
|
||||
}), 2)
|
||||
assert.equal(activeFilterCount('freight', {
|
||||
transportMode: '', sampleSource: 'research_donation'
|
||||
}), 1)
|
||||
|
||||
@@ -313,6 +313,7 @@ test('protocol list uses inline protocol-status dropdown like presign instead of
|
||||
|
||||
assert.match(protocol, /inlineDropdownFilters/)
|
||||
assert.match(protocol, /全部协议状态/)
|
||||
assert.match(protocol, /source:\s*'research_donation'/)
|
||||
assert.match(protocol, /filterFields:\s*\[\]/)
|
||||
assert.doesNotMatch(protocol, /createTimeRange/)
|
||||
assert.doesNotMatch(protocol, /sampleCollectTimeRange/)
|
||||
@@ -320,6 +321,9 @@ test('protocol list uses inline protocol-status dropdown like presign instead of
|
||||
assert.doesNotMatch(filters, /createTimeRange/)
|
||||
assert.match(genericList, /inlineDropdownFilters/)
|
||||
assert.match(genericList, /market-list__filter-row/)
|
||||
assert.match(genericList, /showProtocolProjectFilter/)
|
||||
assert.match(genericList, /全部项目/)
|
||||
assert.doesNotMatch(genericList, /ProjectAssignmentField/)
|
||||
})
|
||||
|
||||
test('protocol list exposes mobile batch selection and sequential authorized deletion', () => {
|
||||
|
||||
@@ -478,6 +478,12 @@ export default {
|
||||
allText: '全部协议状态',
|
||||
source: 'cell_storage',
|
||||
options: CELL_PROTOCOL_STATUS_OPTIONS.map((item) => ({ text: item.label, value: item.value }))
|
||||
},
|
||||
{
|
||||
key: 'protocolStatus',
|
||||
allText: '全部协议状态',
|
||||
source: 'research_donation',
|
||||
options: DONATION_PROTOCOL_STATUS_OPTIONS.map((item) => ({ text: item.label, value: item.value }))
|
||||
}
|
||||
],
|
||||
statusLabel(detail) {
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
:show-header="false"
|
||||
>
|
||||
<div class="market-list__filters">
|
||||
<ProjectAssignmentField v-if="context.module === 'protocol'" v-model="listFilters.projectId"
|
||||
:biz-type="listStatus" filter :context="context" @update:model-value="search" />
|
||||
<div
|
||||
v-if="listFilterTabs.length"
|
||||
class="market-source-tabs"
|
||||
@@ -59,15 +57,24 @@
|
||||
<span v-if="activeFilters" class="market-list__filter-count">{{ activeFilters }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="inlineDropdownFilters.length" class="market-list__filter-row">
|
||||
<div
|
||||
v-if="inlineDropdownFilters.length || showProtocolProjectFilter"
|
||||
class="market-list__filter-row"
|
||||
>
|
||||
<van-dropdown-menu active-color="#0a9f6d">
|
||||
<van-dropdown-item
|
||||
v-for="field in inlineDropdownFilters"
|
||||
:key="field.key"
|
||||
:key="`${field.source || 'all'}:${field.key}`"
|
||||
:model-value="listFilters[field.key]"
|
||||
:options="inlineFilterOptionsByKey[field.key] || []"
|
||||
@change="(value) => onInlineFilterChange(field.key, value)"
|
||||
/>
|
||||
<van-dropdown-item
|
||||
v-if="showProtocolProjectFilter"
|
||||
:model-value="listFilters.projectId || ''"
|
||||
:options="protocolProjectFilterOptions"
|
||||
@change="(value) => onInlineFilterChange('projectId', value)"
|
||||
/>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,11 +161,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProjectAssignmentField from '../../market/components/ProjectAssignmentField.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { fetchMarketFormOptions, fetchMarketList, performMarketAction } from '../../api/market.js'
|
||||
import {
|
||||
fetchMarketFormOptions,
|
||||
fetchMarketList,
|
||||
fetchProjectOptions,
|
||||
performMarketAction
|
||||
} from '../../api/market.js'
|
||||
import { intersectAllowedActions } from '../../market/action-policy.js'
|
||||
import { deleteMarketItemsSequentially } from '../../market/batch-delete.js'
|
||||
import {
|
||||
@@ -169,6 +180,7 @@ import {
|
||||
import { marketSearchPlaceholder } from '../../market/list-presenter.js'
|
||||
import { marketErrorMessage } from '../../market/market-error.js'
|
||||
import { getMarketModule } from '../../market/module-registry.js'
|
||||
import { projectOptions } from '../../market/project-assignment.js'
|
||||
import {
|
||||
getMarketRouteContext,
|
||||
MARKET_MODULE_TITLES,
|
||||
@@ -247,6 +259,12 @@ const inlineDropdownFilters = computed(() => {
|
||||
const source = listFilters.value.source || listStatus.value
|
||||
return rows.filter((field) => !field.source || field.source === source)
|
||||
})
|
||||
const showProtocolProjectFilter = computed(() => context.value.module === 'protocol')
|
||||
const protocolProjectOptions = ref([])
|
||||
const protocolProjectFilterOptions = computed(() => [
|
||||
{ text: '全部项目', value: '' },
|
||||
...protocolProjectOptions.value
|
||||
])
|
||||
const activeFilters = computed(() =>
|
||||
activeFilterCount(context.value.module, listFilters.value)
|
||||
)
|
||||
@@ -299,6 +317,7 @@ const allDeletableSelected = computed(() =>
|
||||
)
|
||||
let requestSequence = 0
|
||||
let abortController = null
|
||||
let projectOptionsController = null
|
||||
let searchTimer = null
|
||||
|
||||
function normalizeList(result) {
|
||||
@@ -360,7 +379,32 @@ function queueSearch() {
|
||||
searchTimer = window.setTimeout(search, 350)
|
||||
}
|
||||
|
||||
async function loadProtocolProjectOptions() {
|
||||
projectOptionsController?.abort()
|
||||
if (!showProtocolProjectFilter.value) {
|
||||
protocolProjectOptions.value = []
|
||||
return
|
||||
}
|
||||
const current = new AbortController()
|
||||
projectOptionsController = current
|
||||
const bizType = listFilters.value.source || listStatus.value || 'cell_storage'
|
||||
try {
|
||||
const data = await fetchProjectOptions(bizType, {
|
||||
signal: current.signal,
|
||||
filter: true,
|
||||
context: context.value
|
||||
})
|
||||
if (projectOptionsController !== current) return
|
||||
protocolProjectOptions.value = projectOptions(data)
|
||||
} catch (error) {
|
||||
if (projectOptionsController === current && error?.name !== 'AbortError') {
|
||||
protocolProjectOptions.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadListFilterOptions() {
|
||||
await loadProtocolProjectOptions()
|
||||
const optionKeys = [
|
||||
...sheetFilterFields.value.map((field) => field.optionKey),
|
||||
...inlineDropdownFilters.value.map((field) => field.optionKey)
|
||||
@@ -596,6 +640,7 @@ function switchListStatus(value) {
|
||||
tab: next
|
||||
}
|
||||
router.replace({ query: nextQuery }).catch(() => {})
|
||||
loadProtocolProjectOptions()
|
||||
loadPage(true)
|
||||
}
|
||||
|
||||
@@ -644,6 +689,7 @@ onBeforeUnmount(() => {
|
||||
const requestId = ++requestSequence
|
||||
listState.value = invalidateMarketRequestState(listState.value, requestId)
|
||||
abortController?.abort()
|
||||
projectOptionsController?.abort()
|
||||
window.clearTimeout(searchTimer)
|
||||
})
|
||||
</script>
|
||||
Reference in new issue
Block a user