From da8499bc924a0f30b1456e55687b2dc0df3d9e1c Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Thu, 5 Nov 2026 12:07:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E8=AF=81=E4=B9=A6=E9=BD=90=E5=A4=87?= =?UTF-8?q?=E6=8C=89=E8=AE=A1=E5=88=92=E6=97=A5=E6=9C=9F=E6=AD=A3=E5=BA=8F?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E9=BB=98=E8=AE=A4=E6=9C=AA=E9=BD=90=E5=A4=87?= =?UTF-8?q?=E4=B8=8E=E8=BF=91=E6=9C=9F=E6=97=A5=E6=9C=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web 与小程序共用列表接口,打开时只看未齐备,计划日期从昨天起共 8 天;同时让移动端筛选下拉从筛选条向下展开。 Co-authored-by: Cursor --- backend/app/services/pl_quota_waiver.py | 2 +- backend/tests/test_pl_quota_waiver.py | 34 ++++++++++ web-admin/scripts/cert-ready-default.test.ts | 49 ++++++++++++++ web-admin/src/styles/mp-webview.scss | 24 +++++++ web-admin/src/utils/cert-ready-plan-date.ts | 12 ++++ .../src/views/m/pl/plan-cert/ReadyTab.vue | 13 +++- web-admin/src/views/m/pl/plan-cert/index.vue | 65 ++++++++++++++++++- web-admin/src/views/pl/cert-ready/index.vue | 10 ++- 8 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 web-admin/scripts/cert-ready-default.test.ts create mode 100644 web-admin/src/utils/cert-ready-plan-date.ts diff --git a/backend/app/services/pl_quota_waiver.py b/backend/app/services/pl_quota_waiver.py index 0b2e74f4e..a1cbad58b 100644 --- a/backend/app/services/pl_quota_waiver.py +++ b/backend/app/services/pl_quota_waiver.py @@ -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() diff --git a/backend/tests/test_pl_quota_waiver.py b/backend/tests/test_pl_quota_waiver.py index 9c70a3654..78f9d3e76 100644 --- a/backend/tests/test_pl_quota_waiver.py +++ b/backend/tests/test_pl_quota_waiver.py @@ -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 diff --git a/web-admin/scripts/cert-ready-default.test.ts b/web-admin/scripts/cert-ready-default.test.ts new file mode 100644 index 000000000..97b960887 --- /dev/null +++ b/web-admin/scripts/cert-ready-default.test.ts @@ -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' }) +}) diff --git a/web-admin/src/styles/mp-webview.scss b/web-admin/src/styles/mp-webview.scss index 283518953..c2d0df60c 100644 --- a/web-admin/src/styles/mp-webview.scss +++ b/web-admin/src/styles/mp-webview.scss @@ -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%; +} diff --git a/web-admin/src/utils/cert-ready-plan-date.ts b/web-admin/src/utils/cert-ready-plan-date.ts new file mode 100644 index 000000000..eb9fc2b7f --- /dev/null +++ b/web-admin/src/utils/cert-ready-plan-date.ts @@ -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) } +} diff --git a/web-admin/src/views/m/pl/plan-cert/ReadyTab.vue b/web-admin/src/views/m/pl/plan-cert/ReadyTab.vue index fd8c26413..d61ff0882 100644 --- a/web-admin/src/views/m/pl/plan-cert/ReadyTab.vue +++ b/web-admin/src/views/m/pl/plan-cert/ReadyTab.vue @@ -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([]) 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() ) diff --git a/web-admin/src/views/m/pl/plan-cert/index.vue b/web-admin/src/views/m/pl/plan-cert/index.vue index a9bbc974c..6c48f912f 100644 --- a/web-admin/src/views/m/pl/plan-cert/index.vue +++ b/web-admin/src/views/m/pl/plan-cert/index.vue @@ -47,6 +47,26 @@ + +
+ + +
+ + +
+
+
@@ -93,7 +113,14 @@ :plan-date-to="planDateTo" :show-all="showAllPlans" /> - + @@ -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' }, diff --git a/web-admin/src/views/pl/cert-ready/index.vue b/web-admin/src/views/pl/cert-ready/index.vue index 9d814d966..41b6dcdfa 100644 --- a/web-admin/src/views/pl/cert-ready/index.vue +++ b/web-admin/src/views/pl/cert-ready/index.vue @@ -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([]) 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