fix(pl): 证书齐备按计划日期正序,并默认未齐备与近期日期。
Web 与小程序共用列表接口,打开时只看未齐备,计划日期从昨天起共 8 天;同时让移动端筛选下拉从筛选条向下展开。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
13cf2e216d
commit
da8499bc92
8 files changed
+202
-7
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user