From 17edd7016f88263cc555e68e622b0448cca33062 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Mon, 14 Sep 2026 16:25:09 +0800 Subject: [PATCH] feat(web-admin): add mobile card list for transport plans in webview. Co-authored-by: Cursor --- web-admin/src/styles/mp-webview.scss | 140 ++++++++++++++ web-admin/src/utils/mp-pl-plan-card.ts | 34 ++++ web-admin/src/views/m/pl/plan/index.vue | 236 +++++++++++++++++++++++- 3 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 web-admin/src/styles/mp-webview.scss create mode 100644 web-admin/src/utils/mp-pl-plan-card.ts diff --git a/web-admin/src/styles/mp-webview.scss b/web-admin/src/styles/mp-webview.scss new file mode 100644 index 000000000..5d1419317 --- /dev/null +++ b/web-admin/src/styles/mp-webview.scss @@ -0,0 +1,140 @@ +.mp-wv-header { + padding: 8px 0 12px; + color: #fff; + background: linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%); + margin: -12px -16px 12px; + padding: 12px 16px 16px; + border-radius: 0 0 20px 20px; +} + +.mp-wv-header__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.mp-wv-header__back { + font-size: 13px; + opacity: 0.95; + background: none; + border: none; + color: inherit; + cursor: pointer; +} + +.mp-wv-header h1 { + margin: 8px 0 0; + font-size: 20px; + font-weight: 700; +} + +.mp-wv-search { + margin-top: 12px; + padding: 10px 14px; + font-size: 13px; + color: #1f2a26; + background: #fff; + border: none; + border-radius: 999px; + width: 100%; +} + +.mp-wv-chips, +.mp-wv-tabs { + display: flex; + gap: 8px; + overflow-x: auto; + padding: 0 0 10px; + -webkit-overflow-scrolling: touch; +} + +.mp-wv-chip, +.mp-wv-tab { + flex: 0 0 auto; + height: 32px; + padding: 0 12px; + font-size: 12px; + color: #6b7b74; + background: #fff; + border: 1px solid #dcebe4; + border-radius: 999px; + cursor: pointer; +} + +.mp-wv-chip.is-active, +.mp-wv-tab.is-active { + color: #fff; + background: #0a9f6d; + border-color: #0a9f6d; + font-weight: 600; +} + +.mp-wv-card { + padding: 12px; + margin-bottom: 10px; + background: #fff; + border: 1px solid #dcebe4; + border-radius: 14px; + box-shadow: 0 6px 16px rgba(10, 159, 109, 0.08); + cursor: pointer; +} + +.mp-wv-card__head { + display: flex; + justify-content: space-between; + gap: 8px; +} + +.mp-wv-card__no { + font-size: 15px; + font-weight: 700; + color: #087f57; +} + +.mp-wv-card__sub, +.mp-wv-card__meta { + margin-top: 4px; + font-size: 12px; + color: #6b7b74; +} + +.mp-wv-card__product { + margin-top: 8px; + padding: 8px 10px; + font-size: 12px; + color: #087f57; + background: #e9f9f2; + border-radius: 10px; +} + +.mp-wv-empty { + padding: 32px 8px; + font-size: 14px; + color: #6b7b74; + text-align: center; +} + +.mp-wv-toolbar { + display: flex; + gap: 8px; + margin: 0 0 12px; +} + +.mp-wv-btn { + height: 36px; + padding: 0 12px; + font-size: 13px; + color: #1f2a26; + background: #fff; + border: 1px solid #dcebe4; + border-radius: 8px; + cursor: pointer; +} + +.mp-wv-btn--primary { + color: #fff; + background: #0a9f6d; + border-color: #0a9f6d; + font-weight: 600; +} diff --git a/web-admin/src/utils/mp-pl-plan-card.ts b/web-admin/src/utils/mp-pl-plan-card.ts new file mode 100644 index 000000000..e33ed3086 --- /dev/null +++ b/web-admin/src/utils/mp-pl-plan-card.ts @@ -0,0 +1,34 @@ +import type { Plan } from '@/api/pl' +import { + formatPlVehiclePlateDisplay, + getPlPlanFollowStatusLabel, + getPlPlanFollowStatusTagType, + getPlPlanTypeLabel +} from '@/utils/pl-display' + +function productSummary(plan: Plan): string { + const extra = (plan as Plan & { productSummary?: string }).productSummary + if (extra) return extra + const names = (plan.products || []) + .map((item) => item.productName) + .filter(Boolean) + if (names.length) return names.join(' · ') + const parties = [plan.supplierName, plan.customerName || plan.destinationName] + .filter(Boolean) + .join(' → ') + return parties || '—' +} + +export function buildPlanCard(plan: Plan) { + return { + id: plan.id, + planNo: plan.planNo || '—', + typeLabel: getPlPlanTypeLabel(plan.planType || ''), + planDate: plan.planDate || '—', + statusLabel: getPlPlanFollowStatusLabel(plan.followStatus), + statusTagType: getPlPlanFollowStatusTagType(plan.followStatus), + plate: formatPlVehiclePlateDisplay(plan.licensePlate, plan.trailerNo) || '—', + people: plan.personnelSummary || '—', + product: productSummary(plan) + } +} diff --git a/web-admin/src/views/m/pl/plan/index.vue b/web-admin/src/views/m/pl/plan/index.vue index a0e299aa5..b63808757 100644 --- a/web-admin/src/views/m/pl/plan/index.vue +++ b/web-admin/src/views/m/pl/plan/index.vue @@ -1,18 +1,236 @@ + import { planApi, type Plan } from '@/api/pl' + import { buildPlanCard } from '@/utils/mp-pl-plan-card' + import { getWxMiniProgram } from '@/utils/mp-webview' + import { + PL_PLAN_LIST_TABS, + plPlanListTabToFilters, + type PlPlanListTab + } from '@/utils/pl-display' + import { HttpError } from '@/utils/http/error' + import { ApiStatus } from '@/utils/http/status' + import '@/styles/mp-webview.scss' - + + function rangeFor(type: DateChip): string[] { + const today = new Date() + if (type === 'today') { + const d = formatYmd(today) + return [d, d] + } + if (type === 'tomorrow') { + const t = new Date(today) + t.setDate(t.getDate() + 1) + const d = formatYmd(t) + return [d, d] + } + if (type === 'week') { + const start = new Date(today) + const day = start.getDay() || 7 + start.setDate(start.getDate() - day + 1) + const end = new Date(start) + end.setDate(start.getDate() + 6) + return [formatYmd(start), formatYmd(end)] + } + const start = new Date(today.getFullYear(), today.getMonth(), 1) + const end = new Date(today.getFullYear(), today.getMonth() + 1, 0) + return [formatYmd(start), formatYmd(end)] + } + + function buildParams() { + const tabFilters = plPlanListTabToFilters(activeTab.value) + const params: Record = { + page: page.value, + pageSize + } + const q = keyword.value.trim() + if (q) params.q = q + if (tabFilters.followStatus) params.followStatus = tabFilters.followStatus + if (tabFilters.planType) params.planType = tabFilters.planType + if (dateRange.value[0]) params.planDateFrom = dateRange.value[0] + if (dateRange.value[1]) params.planDateTo = dateRange.value[1] + return params + } + + async function fetchPage(reset: boolean) { + if (reset) { + page.value = 1 + plans.value = [] + } + loading.value = true + try { + const res = await planApi.list(buildParams()) + const data = (res as { data?: { list?: Plan[]; total?: number }; list?: Plan[]; total?: number }) + .data ?? res + const list = (data as { list?: Plan[] }).list || [] + total.value = (data as { total?: number }).total || 0 + plans.value = reset ? list : plans.value.concat(list) + forbidden.value = false + } catch (error) { + if (error instanceof HttpError && error.code === ApiStatus.forbidden) { + forbidden.value = true + plans.value = [] + return + } + throw error + } finally { + loading.value = false + } + } + + function reload() { + void fetchPage(true) + } + + function applyDate(type: DateChip) { + dateShortcut.value = type + dateRange.value = rangeFor(type) + reload() + } + + function applyTab(tab: PlPlanListTab) { + activeTab.value = tab + reload() + } + + function loadMore() { + page.value += 1 + void fetchPage(false) + } + + function goBack() { + const mini = getWxMiniProgram() + if (mini?.navigateBack) { + mini.navigateBack({ delta: 1 }) + return + } + router.back() + } + + function goNew() { + void router.push('/m/pl/plan/new') + } + + function goPaste() { + void router.push('/m/pl/plan/paste') + } + + function goDetail(id: string) { + void router.push(`/m/pl/plan/${id}`) + } + + onMounted(() => { + dateRange.value = rangeFor('today') + reload() + }) +