fix(pl): 证书齐备按计划日期正序,并默认未齐备与近期日期。

Web 与小程序共用列表接口,打开时只看未齐备,计划日期从昨天起共 8 天;同时让移动端筛选下拉从筛选条向下展开。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-05 12:07:26 +08:00
1 parent 13cf2e216d
commit da8499bc92
8 files changed
+202 -7

No files matched your search

+1 -1
View File
@@ -248,7 +248,7 @@ def list_ready_plans(db, user, keyword="", waived=None, page=1, page_size=20, pl
query = query.filter(PlPlan.quota_cert_waived == int(waived))
total = query.count()
rows = (
query.order_by(PlPlan.create_time.desc(), PlPlan.id)
query.order_by(PlPlan.plan_date.is_(None), PlPlan.plan_date.asc(), PlPlan.id.asc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
+34
View File
@@ -4,6 +4,40 @@ from types import SimpleNamespace
from fastapi import HTTPException
def test_ready_list_orders_by_plan_date_ascending(monkeypatch):
from app.services import pl_quota_waiver as svc
monkeypatch.setattr(svc, "_toxic_exists", lambda *args, **kwargs: True)
monkeypatch.setattr(svc, "apply_pl_scope", lambda query, *args, **kwargs: query)
monkeypatch.setattr(svc, "_product_names", lambda *args, **kwargs: {})
orders = []
class Query:
def filter(self, *args, **kwargs):
return self
def order_by(self, *args):
orders.extend(args)
return self
def count(self):
return 0
def offset(self, _n):
return self
def limit(self, _n):
return self
def all(self):
return []
svc.list_ready_plans(SimpleNamespace(query=lambda *_args, **_kwargs: Query()), SimpleNamespace(tenant_no="t"))
text = " ".join(str(item).lower() for item in orders)
assert "plan_date asc" in text
assert "create_time desc" not in text
def test_only_external_toxic_stops_need_both_certs():
from app.services.pl_quota_waiver import needs_purchase_transport_cert
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { effectScope } from 'vue'
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
import { certReadyDefaultPlanDates } from '../src/utils/cert-ready-plan-date'
test('incomplete certificate filter is sent as quotaCertWaivedIn=0', () => {
const scope = effectScope()
const params = scope.run(() =>
useServerColumnFilters(
[
{
prop: 'quotaCertWaived',
label: '证书齐备',
type: 'enum',
queryKey: 'quotaCertWaived'
}
],
{ initialFilters: { quotaCertWaived: { text: '', selected: ['0'] } } }
).toQueryParams()
)
scope.stop()
assert.deepEqual(params, { quotaCertWaivedIn: '0' })
})
test('web funnel and miniprogram status dropdown default to incomplete', () => {
const web = readFileSync(new URL('../src/views/pl/cert-ready/index.vue', import.meta.url), 'utf8')
const mobile = readFileSync(
new URL('../src/views/m/pl/plan-cert/index.vue', import.meta.url),
'utf8'
)
assert.match(web, /quotaCertWaived:\s*\{\s*text:\s*'',\s*selected:\s*\['0'\]\s*\}/)
assert.match(mobile, /const readyState = ref\('0'\)/)
assert.match(mobile, /text:\s*'未齐备',\s*value:\s*'0'/)
assert.match(
web,
/planDate:\s*\{\s*from:\s*defaultPlanDates\.from,\s*to:\s*defaultPlanDates\.to\s*\}/
)
assert.match(mobile, /certReadyDefaultPlanDates\(\)/)
assert.match(mobile, /:plan-date-from="readyDateFrom"/)
})
test('default plan dates run from yesterday through seven days later', () => {
const range = certReadyDefaultPlanDates(new Date(2026, 10, 5))
assert.deepEqual(range, { from: '2026-11-04', to: '2026-11-11' })
const yearTurn = certReadyDefaultPlanDates(new Date(2026, 0, 1))
assert.deepEqual(yearTurn, { from: '2025-12-31', to: '2026-01-07' })
})
+24
View File
@@ -1969,3 +1969,27 @@ body:has(.mp-wv-root--crm, .mp-wv-root--transport-plan) :is(
) {
border-radius: 8px !important;
}
/*
* 筛选下拉必须从筛选条向下展开。
* van-popup 样式若晚于 dropdown-item 注入,.van-popup{position:fixed}
* 和 .van-popup--top{top:0} 会盖过 .van-dropdown-item__content{position:absolute},
* 选项层就被钉在页面顶部。
*/
.van-dropdown-item--down .van-dropdown-item__content.van-popup {
position: absolute !important;
top: 0 !important;
right: 0;
bottom: auto !important;
left: 0;
width: 100%;
}
.van-dropdown-item--up .van-dropdown-item__content.van-popup {
position: absolute !important;
top: auto !important;
right: 0;
bottom: 0 !important;
left: 0;
width: 100%;
}
@@ -0,0 +1,12 @@
function formatYmd(date: Date) {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${date.getFullYear()}-${month}-${day}`
}
/** 昨天到昨天之后第 7 天,含首尾。今天是 11 月 5 日时为 11 月 4 日~11 月 11 日。 */
export function certReadyDefaultPlanDates(today = new Date()) {
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 7)
return { from: formatYmd(start), to: formatYmd(end) }
}
@@ -52,7 +52,12 @@
import { certReadyErrorText, formatCertReadyTime } from '@/utils/pl-quota-cert-waiver'
import { getPlPlanFollowStatusLabel } from '@/utils/pl-display'
const props = defineProps<{ keyword: string; waived: string }>()
const props = defineProps<{
keyword: string
waived: string
planDateFrom: string
planDateTo: string
}>()
const rows = ref<CertReadyPlan[]>([])
const loading = ref(false)
const finished = ref(false)
@@ -79,7 +84,9 @@
page: page.value,
pageSize: 20,
keyword: props.keyword || undefined,
waived: props.waived || undefined
waived: props.waived || undefined,
planDateFrom: props.planDateFrom || undefined,
planDateTo: props.planDateTo || undefined
})
if (current !== epoch) return
const list = data?.list || []
@@ -119,7 +126,7 @@
}
watch(
() => [props.keyword, props.waived].join('\0'),
() => [props.keyword, props.waived, props.planDateFrom, props.planDateTo].join('\0'),
() => reload()
)
</script>
+63 -2
View File
@@ -47,6 +47,26 @@
</van-dropdown-menu>
<van-dropdown-menu v-else-if="tab === 'ready'" class="mp-customer-dd">
<van-dropdown-item v-model="readyState" :options="readyStateOptions" />
<van-dropdown-item
ref="readyDateFilter"
:title="readyDateTitle"
@open="syncReadyDateDraft"
>
<div class="mp-plan-quota-filter">
<PlanDateField v-model="readyDateFromDraft" label="开始日期" clearable />
<PlanDateField v-model="readyDateToDraft" label="结束日期" clearable />
<div class="mp-plan-quota-filter__actions">
<button class="mp-wv-btn" type="button" @click="clearReadyDate">清除</button>
<button
class="mp-wv-btn mp-wv-btn--primary"
type="button"
@click="applyReadyDate"
>
确定
</button>
</div>
</div>
</van-dropdown-item>
</van-dropdown-menu>
<van-dropdown-menu v-else-if="tab === 'plan'" class="mp-customer-dd">
<van-dropdown-item v-model="planScope" :options="planScopeOptions" />
@@ -93,7 +113,14 @@
:plan-date-to="planDateTo"
:show-all="showAllPlans"
/>
<ReadyTab v-else :key="session + committed" :keyword="committed" :waived="readyState" />
<ReadyTab
v-else
:key="session + committed"
:keyword="committed"
:waived="readyState"
:plan-date-from="readyDateFrom"
:plan-date-to="readyDateTo"
/>
</div>
</MpWebviewShell>
</template>
@@ -102,6 +129,7 @@
import MpBusinessHeader from '@/components/mp/MpBusinessHeader.vue'
import { useUserStore } from '@/store/modules/user'
import { goToMiniWorkbench } from '@/utils/mp-webview'
import { certReadyDefaultPlanDates } from '@/utils/cert-ready-plan-date'
import PlanDateField from '../plan/PlanDateField.vue'
import ArchiveTab from './ArchiveTab.vue'
import ContractTab from './ContractTab.vue'
@@ -134,7 +162,40 @@
const planStatus = ref('')
const planScope = ref('')
const showAllPlans = computed(() => planScope.value === '1')
const readyState = ref('')
const defaultReadyDates = certReadyDefaultPlanDates()
const readyState = ref('0')
const readyDateFrom = ref(defaultReadyDates.from)
const readyDateTo = ref(defaultReadyDates.to)
const readyDateFromDraft = ref(defaultReadyDates.from)
const readyDateToDraft = ref(defaultReadyDates.to)
const readyDateFilter = ref<{ toggle: (show?: boolean) => void }>()
const readyDateTitle = computed(() => {
if (!readyDateFrom.value && !readyDateTo.value) return '计划日期'
const short = (value: string) => (value ? value.slice(5) : '')
return `${short(readyDateFrom.value)} ~ ${short(readyDateTo.value)}`
})
function syncReadyDateDraft() {
readyDateFromDraft.value = readyDateFrom.value
readyDateToDraft.value = readyDateTo.value
}
function clearReadyDate() {
readyDateFromDraft.value = ''
readyDateToDraft.value = ''
readyDateFrom.value = ''
readyDateTo.value = ''
readyDateFilter.value?.toggle(false)
}
function applyReadyDate() {
const from = readyDateFromDraft.value
const to = readyDateToDraft.value
if (from && to && from > to) {
ElMessage.warning('计划日期起始不能晚于结束')
return
}
readyDateFrom.value = from
readyDateTo.value = to
readyDateFilter.value?.toggle(false)
}
const readyStateOptions = [
{ text: '全部齐备状态', value: '' },
{ text: '未齐备', value: '0' },
+9 -1
View File
@@ -109,6 +109,7 @@
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
import { certReadyDefaultPlanDates } from '@/utils/cert-ready-plan-date'
import { certReadyErrorText, formatCertReadyTime } from '@/utils/pl-quota-cert-waiver'
import {
PL_PLAN_FOLLOW_STATUS_OPTIONS,
@@ -124,6 +125,7 @@
const markingId = ref('')
const rows = ref<CertReadyPlan[]>([])
let epoch = 0
const defaultPlanDates = certReadyDefaultPlanDates()
const columns: (ColumnFilterDef & { minWidth: number })[] = [
{ prop: 'planNo', label: '计划编号', type: 'text', queryKey: 'planNo', minWidth: 160 },
{
@@ -188,7 +190,13 @@
clearAllFilters,
isFilterActive,
toQueryParams
} = useServerColumnFilters(columns, { onChange: search })
} = useServerColumnFilters(columns, {
initialFilters: {
quotaCertWaived: { text: '', selected: ['0'] },
planDate: { from: defaultPlanDates.from, to: defaultPlanDates.to }
},
onChange: search
})
async function load() {
const current = ++epoch