feat(web-admin): add mobile card list for transport plans in webview.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
5220637639
commit
17edd7016f
3 files changed
+401
-9
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,236 @@
|
||||
<template>
|
||||
<MpWebviewShell>
|
||||
<p class="mp-wv-placeholder">运输计划加载中…</p>
|
||||
<div v-if="forbidden" class="mp-wv-empty">无权限</div>
|
||||
<template v-else>
|
||||
<header class="mp-wv-header">
|
||||
<div class="mp-wv-header__row">
|
||||
<button class="mp-wv-header__back" type="button" @click="goBack">← 工作台</button>
|
||||
<button class="mp-wv-btn mp-wv-btn--primary" type="button" @click="goNew">新增</button>
|
||||
</div>
|
||||
<h1>运输计划</h1>
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="mp-wv-search"
|
||||
type="search"
|
||||
placeholder="计划编号 / 供方 / 需方 / 产品 / 车牌号"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div class="mp-wv-chips">
|
||||
<button
|
||||
v-for="item in dateChips"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="mp-wv-chip"
|
||||
:class="{ 'is-active': dateShortcut === item.value }"
|
||||
@click="applyDate(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mp-wv-tabs">
|
||||
<button
|
||||
v-for="tab in PL_PLAN_LIST_TABS"
|
||||
:key="tab.name"
|
||||
type="button"
|
||||
class="mp-wv-tab"
|
||||
:class="{ 'is-active': activeTab === tab.name }"
|
||||
@click="applyTab(tab.name)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mp-wv-toolbar">
|
||||
<button class="mp-wv-btn" type="button" @click="goPaste">三方人车</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !cards.length" class="mp-wv-empty">加载中…</div>
|
||||
<div v-else-if="!cards.length" class="mp-wv-empty">暂无计划</div>
|
||||
<article
|
||||
v-for="card in cards"
|
||||
:key="card.id"
|
||||
class="mp-wv-card"
|
||||
@click="goDetail(card.id)"
|
||||
>
|
||||
<div class="mp-wv-card__head">
|
||||
<div>
|
||||
<div class="mp-wv-card__no">{{ card.planNo }}</div>
|
||||
<div class="mp-wv-card__sub">{{ card.typeLabel }} · {{ card.planDate }}</div>
|
||||
</div>
|
||||
<el-tag :type="card.statusTagType" size="small">{{ card.statusLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="mp-wv-card__meta">车牌:{{ card.plate }}</div>
|
||||
<div class="mp-wv-card__meta">人员:{{ card.people }}</div>
|
||||
<div class="mp-wv-card__product">{{ card.product }}</div>
|
||||
</article>
|
||||
|
||||
<div v-if="hasMore" class="mp-wv-toolbar">
|
||||
<button class="mp-wv-btn" type="button" :disabled="loading" @click="loadMore">
|
||||
{{ loading ? '加载中…' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MpWebviewShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
||||
</script>
|
||||
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'
|
||||
|
||||
<style scoped>
|
||||
.mp-wv-placeholder {
|
||||
margin: 24px 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7b74;
|
||||
text-align: center;
|
||||
type DateChip = 'today' | 'tomorrow' | 'week' | 'month'
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = ref('')
|
||||
const activeTab = ref<PlPlanListTab>('pending_arrange')
|
||||
const dateShortcut = ref<DateChip>('today')
|
||||
const dateRange = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const total = ref(0)
|
||||
const plans = ref<Plan[]>([])
|
||||
|
||||
const dateChips: { label: string; value: DateChip }[] = [
|
||||
{ label: '今天', value: 'today' },
|
||||
{ label: '明天', value: 'tomorrow' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
]
|
||||
|
||||
const cards = computed(() => plans.value.map((row) => buildPlanCard(row)))
|
||||
const hasMore = computed(() => plans.value.length < total.value)
|
||||
|
||||
function formatYmd(date: Date) {
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const d = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${m}-${d}`
|
||||
}
|
||||
</style>
|
||||
|
||||
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<string, unknown> = {
|
||||
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()
|
||||
})
|
||||
</script>
|
||||
Reference in new issue
Block a user