feat(web-admin): redesign mp transport plan list to match demo.
Use dual dropdown filters, richer cards, and a speed dial for create actions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
92bb4bf0d0
commit
8d1d5c19b5
1 file changed
+327
-61
@@ -5,7 +5,6 @@
|
||||
<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
|
||||
@@ -17,61 +16,135 @@
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div class="mp-wv-chips">
|
||||
<div class="mp-wv-filter-wrap">
|
||||
<div class="mp-wv-filter-bar">
|
||||
<button
|
||||
class="mp-wv-filter-trig"
|
||||
:class="{ open: openFilter === 'date' }"
|
||||
type="button"
|
||||
@click="toggleFilter('date')"
|
||||
>
|
||||
<span>{{ dateLabel }}</span>
|
||||
<span>▼</span>
|
||||
</button>
|
||||
<button
|
||||
class="mp-wv-filter-trig"
|
||||
:class="{ open: openFilter === 'tab' }"
|
||||
type="button"
|
||||
@click="toggleFilter('tab')"
|
||||
>
|
||||
<span>{{ tabLabel }}</span>
|
||||
<span>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mp-wv-filter-panel" :class="{ open: openFilter === 'date' }">
|
||||
<button
|
||||
v-for="item in dateOptions"
|
||||
:key="item.value"
|
||||
class="mp-wv-filter-opt"
|
||||
:class="{ active: dateShortcut === item.value }"
|
||||
type="button"
|
||||
@click="pickDate(item.value)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="dateShortcut === item.value">✓</span>
|
||||
</button>
|
||||
<div v-if="dateShortcut === 'custom'" class="mp-wv-custom-date">
|
||||
<div class="mp-wv-custom-date__row">
|
||||
<input v-model="customFrom" class="mp-wv-custom-date__input" type="date" />
|
||||
<input v-model="customTo" class="mp-wv-custom-date__input" type="date" />
|
||||
</div>
|
||||
<button class="mp-wv-btn mp-wv-btn--primary" type="button" style="width: 100%" @click="applyCustomDate">
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mp-wv-filter-panel" :class="{ open: openFilter === 'tab' }">
|
||||
<button
|
||||
v-for="tab in PL_PLAN_LIST_TABS"
|
||||
:key="tab.name"
|
||||
class="mp-wv-filter-opt"
|
||||
:class="{ active: activeTab === tab.name }"
|
||||
type="button"
|
||||
@click="pickTab(tab.name)"
|
||||
>
|
||||
<span>{{ tab.label }}</span>
|
||||
<span v-if="activeTab === tab.name">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mp-wv-filter-mask"
|
||||
:class="{ open: !!openFilter }"
|
||||
@click="closeFilter"
|
||||
/>
|
||||
|
||||
<div class="mp-wv-pad-speed">
|
||||
<div v-if="loading && !planRows.length" class="mp-wv-empty">加载中…</div>
|
||||
<div v-else-if="!planRows.length" class="mp-wv-empty">暂无计划</div>
|
||||
<article
|
||||
v-for="row in planRows"
|
||||
:key="row.plan.id"
|
||||
class="mp-wv-card"
|
||||
@click="goDetail(row.plan.id)"
|
||||
>
|
||||
<div class="mp-wv-card__head">
|
||||
<div>
|
||||
<div class="mp-wv-card__no">{{ row.card.planNo }}</div>
|
||||
<div class="mp-wv-card__sub">{{ row.card.typeLabel }} · {{ row.card.planDate }}</div>
|
||||
</div>
|
||||
<el-tag :type="row.card.statusTagType" size="small">{{ row.card.statusLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="mp-wv-card__meta">供方 → 需方:{{ row.card.partyLine }}</div>
|
||||
<div class="mp-wv-card__meta">跟进人:{{ row.card.follower }}</div>
|
||||
<div class="mp-wv-card__meta">车牌:{{ row.card.plate }}</div>
|
||||
<div class="mp-wv-card__meta">人员:{{ row.card.people }}</div>
|
||||
<div class="mp-wv-card__product">{{ row.card.product }}</div>
|
||||
<div class="mp-wv-card-actions" @click.stop>
|
||||
<button class="mp-wv-btn" type="button" @click="openCardMore(row.plan)">更多</button>
|
||||
<button class="mp-wv-btn" type="button" @click="goCopy(row.plan.id)">复制</button>
|
||||
<button
|
||||
v-if="row.canArrange"
|
||||
class="mp-wv-btn mp-wv-btn--primary"
|
||||
type="button"
|
||||
@click="goArrange(row.plan.id)"
|
||||
>
|
||||
{{ row.arrangeLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div v-if="hasMore" class="mp-wv-toolbar" style="padding: 0 0 12px">
|
||||
<button class="mp-wv-btn" type="button" style="width: 100%" :disabled="loading" @click="loadMore">
|
||||
{{ loading ? '加载中…' : `加载更多(${plans.length}/${total})` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mp-wv-speed-mask" :class="{ open: speedOpen }" @click="speedOpen = false" />
|
||||
<div class="mp-wv-speed-dial" :class="{ open: speedOpen }">
|
||||
<div class="mp-wv-speed-menu">
|
||||
<button class="mp-wv-speed-item" type="button" @click="goPaste">三方人车快速创建</button>
|
||||
<button class="mp-wv-speed-item primary" type="button" @click="goNew">新增计划</button>
|
||||
</div>
|
||||
<button class="mp-wv-speed-main" type="button" aria-label="快捷操作" @click="toggleSpeed">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MpBottomSheet v-model="moreOpen" title="卡片操作" half>
|
||||
<button
|
||||
v-for="item in dateChips"
|
||||
:key="item.value"
|
||||
v-for="item in moreActions"
|
||||
:key="item.key"
|
||||
class="mp-wv-filter-opt"
|
||||
:class="{ 'is-danger': item.danger }"
|
||||
type="button"
|
||||
class="mp-wv-chip"
|
||||
:class="{ 'is-active': dateShortcut === item.value }"
|
||||
@click="applyDate(item.value)"
|
||||
:disabled="acting"
|
||||
@click="runMoreAction(item.key)"
|
||||
>
|
||||
{{ 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>
|
||||
</MpBottomSheet>
|
||||
</template>
|
||||
</MpWebviewShell>
|
||||
</template>
|
||||
@@ -79,7 +152,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
||||
import MpBottomSheet from '@/components/mp/MpBottomSheet.vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import { buildPlanCard } from '@/utils/mp-pl-plan-card'
|
||||
import { getWxMiniProgram } from '@/utils/mp-webview'
|
||||
@@ -88,17 +163,32 @@
|
||||
plPlanListTabToFilters,
|
||||
type PlPlanListTab
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
arrangeTransportLabel,
|
||||
canArrangeTransport,
|
||||
canCancelWaitNotice,
|
||||
canVoidPlan,
|
||||
canWaitNotice
|
||||
} from '@/utils/pl-plan-action-guards'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import '@/styles/mp-webview.scss'
|
||||
|
||||
type DateChip = 'today' | 'tomorrow' | 'week' | 'month'
|
||||
type DateShortcut = 'today' | 'tomorrow' | 'week' | 'month' | 'custom'
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = ref('')
|
||||
const activeTab = ref<PlPlanListTab>('pending_arrange')
|
||||
const dateShortcut = ref<DateChip>('today')
|
||||
const dateShortcut = ref<DateShortcut>('today')
|
||||
const dateRange = ref<string[]>([])
|
||||
const customFrom = ref('')
|
||||
const customTo = ref('')
|
||||
const customDateLabel = ref('')
|
||||
const openFilter = ref<'' | 'date' | 'tab'>('')
|
||||
const speedOpen = ref(false)
|
||||
const moreOpen = ref(false)
|
||||
const morePlan = ref<Plan | null>(null)
|
||||
const acting = ref(false)
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const page = ref(1)
|
||||
@@ -106,23 +196,52 @@
|
||||
const total = ref(0)
|
||||
const plans = ref<Plan[]>([])
|
||||
|
||||
const dateChips: { label: string; value: DateChip }[] = [
|
||||
const dateOptions: { label: string; value: DateShortcut }[] = [
|
||||
{ label: '今天', value: 'today' },
|
||||
{ label: '明天', value: 'tomorrow' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
{ label: '本月', value: 'month' },
|
||||
{ label: '自定义', value: 'custom' }
|
||||
]
|
||||
|
||||
const cards = computed(() => plans.value.map((row) => buildPlanCard(row)))
|
||||
const tabLabel = computed(
|
||||
() => PL_PLAN_LIST_TABS.find((t) => t.name === activeTab.value)?.label || '待安排'
|
||||
)
|
||||
const dateLabel = computed(() => {
|
||||
if (dateShortcut.value === 'custom') return customDateLabel.value || '自定义'
|
||||
return dateOptions.find((d) => d.value === dateShortcut.value)?.label || '今天'
|
||||
})
|
||||
|
||||
const planRows = computed(() =>
|
||||
plans.value.map((plan) => ({
|
||||
plan,
|
||||
card: buildPlanCard(plan),
|
||||
canArrange: canArrangeTransport(plan),
|
||||
arrangeLabel: arrangeTransportLabel(plan)
|
||||
}))
|
||||
)
|
||||
const hasMore = computed(() => plans.value.length < total.value)
|
||||
|
||||
const moreActions = computed(() => {
|
||||
const p = morePlan.value
|
||||
if (!p) return [] as { key: string; label: string; danger?: boolean }[]
|
||||
const items: { key: string; label: string; danger?: boolean }[] = [
|
||||
{ key: 'detail', label: '查看详情' },
|
||||
{ key: 'copy', label: '复制' }
|
||||
]
|
||||
if (canWaitNotice(p)) items.push({ key: 'wait', label: '等客户通知' })
|
||||
if (canCancelWaitNotice(p)) items.push({ key: 'cancel-wait', label: '取消等通知' })
|
||||
if (canVoidPlan(p)) items.push({ key: 'void', label: '作废', danger: true })
|
||||
return items
|
||||
})
|
||||
|
||||
function formatYmd(date: Date) {
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const d = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${m}-${d}`
|
||||
}
|
||||
|
||||
function rangeFor(type: DateChip): string[] {
|
||||
function rangeFor(type: Exclude<DateShortcut, 'custom'>): string[] {
|
||||
const today = new Date()
|
||||
if (type === 'today') {
|
||||
const d = formatYmd(today)
|
||||
@@ -170,8 +289,9 @@
|
||||
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 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)
|
||||
@@ -192,14 +312,45 @@
|
||||
void fetchPage(true)
|
||||
}
|
||||
|
||||
function applyDate(type: DateChip) {
|
||||
function closeFilter() {
|
||||
openFilter.value = ''
|
||||
}
|
||||
|
||||
function toggleFilter(kind: 'date' | 'tab') {
|
||||
speedOpen.value = false
|
||||
openFilter.value = openFilter.value === kind ? '' : kind
|
||||
}
|
||||
|
||||
function pickDate(type: DateShortcut) {
|
||||
dateShortcut.value = type
|
||||
if (type === 'custom') {
|
||||
if (!customFrom.value || !customTo.value) {
|
||||
const [from, to] = rangeFor('month')
|
||||
customFrom.value = from
|
||||
customTo.value = to
|
||||
}
|
||||
return
|
||||
}
|
||||
dateRange.value = rangeFor(type)
|
||||
closeFilter()
|
||||
reload()
|
||||
}
|
||||
|
||||
function applyTab(tab: PlPlanListTab) {
|
||||
function applyCustomDate() {
|
||||
if (!customFrom.value || !customTo.value) {
|
||||
ElMessage.warning('请选择起止日期')
|
||||
return
|
||||
}
|
||||
dateShortcut.value = 'custom'
|
||||
dateRange.value = [customFrom.value, customTo.value]
|
||||
customDateLabel.value = `${customFrom.value} ~ ${customTo.value}`
|
||||
closeFilter()
|
||||
reload()
|
||||
}
|
||||
|
||||
function pickTab(tab: PlPlanListTab) {
|
||||
activeTab.value = tab
|
||||
closeFilter()
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -208,6 +359,11 @@
|
||||
void fetchPage(false)
|
||||
}
|
||||
|
||||
function toggleSpeed() {
|
||||
closeFilter()
|
||||
speedOpen.value = !speedOpen.value
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const mini = getWxMiniProgram()
|
||||
if (mini?.navigateBack) {
|
||||
@@ -218,10 +374,12 @@
|
||||
}
|
||||
|
||||
function goNew() {
|
||||
speedOpen.value = false
|
||||
void router.push('/m/pl/plan/new')
|
||||
}
|
||||
|
||||
function goPaste() {
|
||||
speedOpen.value = false
|
||||
void router.push('/m/pl/plan/paste')
|
||||
}
|
||||
|
||||
@@ -229,8 +387,116 @@
|
||||
void router.push(`/m/pl/plan/${id}`)
|
||||
}
|
||||
|
||||
function goCopy(id: string) {
|
||||
void router.push({ path: '/m/pl/plan/new', query: { copyFrom: id } })
|
||||
}
|
||||
|
||||
function goArrange(id: string) {
|
||||
void router.push(`/m/pl/plan/${id}/arrange`)
|
||||
}
|
||||
|
||||
function openCardMore(plan: Plan) {
|
||||
morePlan.value = plan
|
||||
moreOpen.value = true
|
||||
}
|
||||
|
||||
async function runMoreAction(key: string) {
|
||||
const p = morePlan.value
|
||||
if (!p) return
|
||||
if (key === 'detail') {
|
||||
moreOpen.value = false
|
||||
goDetail(p.id)
|
||||
return
|
||||
}
|
||||
if (key === 'copy') {
|
||||
moreOpen.value = false
|
||||
goCopy(p.id)
|
||||
return
|
||||
}
|
||||
if (key === 'wait') {
|
||||
await ElMessageBox.confirm('确认将该计划标记为等客户通知?', '等客户通知', {
|
||||
type: 'warning'
|
||||
})
|
||||
acting.value = true
|
||||
try {
|
||||
await planApi.waitNotice(p.id)
|
||||
ElMessage.success('已标记为等客户通知')
|
||||
moreOpen.value = false
|
||||
reload()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key === 'cancel-wait') {
|
||||
await ElMessageBox.confirm('确认取消等通知,恢复为待安排?', '取消等通知', {
|
||||
type: 'warning'
|
||||
})
|
||||
acting.value = true
|
||||
try {
|
||||
await planApi.cancelWaitNotice(p.id)
|
||||
ElMessage.success('已取消等通知')
|
||||
moreOpen.value = false
|
||||
reload()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key === 'void') {
|
||||
await ElMessageBox.confirm('确认作废该运输计划?此操作不可恢复。', '作废运输计划', {
|
||||
type: 'warning'
|
||||
})
|
||||
acting.value = true
|
||||
try {
|
||||
await planApi.cancel(p.id)
|
||||
ElMessage.success('已作废')
|
||||
moreOpen.value = false
|
||||
reload()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
dateRange.value = rangeFor('today')
|
||||
reload()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-wv-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mp-wv-card-actions .mp-wv-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mp-wv-custom-date {
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
|
||||
.mp-wv-custom-date__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mp-wv-custom-date__input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid #dcebe4;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.is-danger {
|
||||
color: #d14343 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user