Compare commits

..
Author SHA1 Message Date
cykj e97f47b06a fix(mobile): 协议单列表项目筛选下移并补齐科研捐赠筛选 (#59)
fix(mobile): 协议单列表项目筛选下移并补齐科研捐赠筛选 (#59)
2026-11-05 09:14:29 +00:00
zlwuandCursor a4defb2c94 fix(mobile): 协议单列表项目筛选下移并补齐科研捐赠筛选
将项目筛选并入搜索框下方的 van-dropdown 行,与协议状态样式一致;科研捐赠 Tab 同步展示两项筛选,并修复捐赠列表按 protocolStatus 过滤 donation_status。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-05 17:13:15 +08:00
cykj 8717702b58 fix(personal-dev): 结束 Gitea 短页评论翻页 (#57)
Merge pull request #58 from cursor/issue-57-gitea-pagination

#57
2026-11-05 09:00:54 +00:00
7 changed files with 108 additions and 15 deletions

No files matched your search

+10 -5
View File
@@ -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:
+23
View File
@@ -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
+2 -1
View File
@@ -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
+11 -3
View File
@@ -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>