feat(web-admin): add mobile plan detail and shared action guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-14 16:25:09 +08:00
1 parent 17edd7016f
commit 1a5aa1c934
3 files changed
+193 -40

No files matched your search

@@ -0,0 +1,34 @@
export function canEditPlan(row: { archiveStatus?: string; followStatus?: string }) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
export function canArrangeTransport(row: {
transportStatus?: string
archiveStatus?: string
followStatus?: string
}) {
return (
(row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
row.archiveStatus !== 'archived' &&
row.followStatus !== 'cancelled'
)
}
export function arrangeTransportLabel(row: { transportStatus?: string }) {
return row.transportStatus === 'arranged' ? '调整运输' : '安排运输'
}
export function canVoidPlan(row: { archiveStatus?: string; followStatus?: string }) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
export function canWaitNotice(row: { followStatus?: string; archiveStatus?: string }) {
return (
(row.followStatus === 'pending_arrange' || row.followStatus === 'drafting') &&
row.archiveStatus !== 'archived'
)
}
export function canCancelWaitNotice(row: { followStatus?: string; archiveStatus?: string }) {
return row.followStatus === 'wait_notice' && row.archiveStatus !== 'archived'
}
+151 -9
View File
@@ -1,18 +1,160 @@
<template>
<MpWebviewShell>
<p class="mp-wv-placeholder">计划详情加载中…</p>
<div v-if="loading" class="mp-wv-empty">加载中…</div>
<div v-else-if="errorText" class="mp-wv-empty">{{ errorText }}</div>
<template v-else-if="plan">
<header class="mp-wv-header">
<div class="mp-wv-header__row">
<button class="mp-wv-header__back" type="button" @click="goList">← 列表</button>
<el-tag :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
</div>
<h1>{{ plan.planNo || '计划详情' }}</h1>
<div class="mp-wv-card__sub" style="color: rgba(255, 255, 255, 0.9)">
{{ typeLabel }} · 计划日期 {{ plan.planDate || '—' }}
</div>
</header>
<section class="mp-wv-card">
<div class="mp-wv-card__meta">送货时间:{{ deliveryTimeText }}</div>
<div class="mp-wv-card__meta">车牌:{{ plate }}</div>
<div class="mp-wv-card__meta">人员:{{ people }}</div>
<div class="mp-wv-card__meta">跟进人:{{ plan.followUserName || '—' }}</div>
<div class="mp-wv-card__product">{{ product }}</div>
</section>
<div class="mp-wv-toolbar" style="flex-wrap: wrap">
<button v-if="canArrange" class="mp-wv-btn mp-wv-btn--primary" type="button" @click="goArrange">
{{ arrangeLabel }}
</button>
<button v-if="canEdit" class="mp-wv-btn" type="button" @click="goEdit">编辑计划</button>
<button class="mp-wv-btn" type="button" @click="goCopy">复制计划</button>
<button v-if="canWait" class="mp-wv-btn" type="button" :disabled="acting" @click="doWaitNotice">
等客户通知
</button>
<button class="mp-wv-btn" type="button" @click="goPrint">打印单据</button>
<button v-if="canVoid" class="mp-wv-btn" type="button" :disabled="acting" @click="doVoid">
作废
</button>
</div>
</template>
</MpWebviewShell>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
</script>
import { planApi, type Plan } from '@/api/pl'
import { buildPlanCard } from '@/utils/mp-pl-plan-card'
import {
arrangeTransportLabel,
canArrangeTransport,
canEditPlan,
canVoidPlan,
canWaitNotice
} from '@/utils/pl-plan-action-guards'
import '@/styles/mp-webview.scss'
<style scoped>
.mp-wv-placeholder {
margin: 24px 0 0;
font-size: 14px;
color: #6b7b74;
text-align: center;
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const acting = ref(false)
const errorText = ref('')
const plan = ref<Plan | null>(null)
const card = computed(() => (plan.value ? buildPlanCard(plan.value) : null))
const statusLabel = computed(() => card.value?.statusLabel || '—')
const statusTagType = computed(() => card.value?.statusTagType || 'info')
const typeLabel = computed(() => card.value?.typeLabel || '—')
const plate = computed(() => card.value?.plate || '—')
const people = computed(() => card.value?.people || '—')
const product = computed(() => card.value?.product || '—')
const deliveryTimeText = computed(() => {
const times = plan.value?.deliveryTimes || []
if (times.length) return times.join('、')
return plan.value?.deliveryTime || '—'
})
const canArrange = computed(() => (plan.value ? canArrangeTransport(plan.value) : false))
const canEdit = computed(() => (plan.value ? canEditPlan(plan.value) : false))
const canVoid = computed(() => (plan.value ? canVoidPlan(plan.value) : false))
const canWait = computed(() => (plan.value ? canWaitNotice(plan.value) : false))
const arrangeLabel = computed(() =>
plan.value ? arrangeTransportLabel(plan.value) : '安排运输'
)
async function loadDetail() {
const id = String(route.params.id || '')
if (!id) {
errorText.value = '缺少计划 ID'
loading.value = false
return
}
loading.value = true
try {
const res = await planApi.get(id)
plan.value = ((res as { data?: Plan }).data ?? res) as Plan
errorText.value = ''
} catch (error) {
errorText.value = error instanceof Error ? error.message : '加载失败'
} finally {
loading.value = false
}
}
</style>
function goList() {
void router.push('/m/pl/plan')
}
function goEdit() {
if (!plan.value) return
void router.push(`/m/pl/plan/${plan.value.id}/edit`)
}
function goArrange() {
if (!plan.value) return
void router.push(`/m/pl/plan/${plan.value.id}/arrange`)
}
function goCopy() {
if (!plan.value) return
void router.push({ path: '/m/pl/plan/new', query: { copyFrom: plan.value.id } })
}
function goPrint() {
if (!plan.value) return
void router.push(`/m/pl/plan/${plan.value.id}/print`)
}
async function doWaitNotice() {
if (!plan.value) return
await ElMessageBox.confirm('确认将该计划标记为等客户通知?', '等客户通知', {
type: 'warning'
})
acting.value = true
try {
const res = await planApi.waitNotice(plan.value.id)
plan.value = ((res as { data?: Plan }).data ?? res) as Plan
ElMessage.success('已标记为等客户通知')
} finally {
acting.value = false
}
}
async function doVoid() {
if (!plan.value) return
await ElMessageBox.confirm('确认作废该运输计划?此操作不可恢复。', '作废', { type: 'warning' })
acting.value = true
try {
const res = await planApi.cancel(plan.value.id)
plan.value = ((res as { data?: Plan }).data ?? res) as Plan
ElMessage.success('已作废')
} finally {
acting.value = false
}
}
onMounted(() => {
void loadDetail()
})
</script>
+8 -31
View File
@@ -870,6 +870,14 @@ import { useListTableViewportHeight } from '@/composables/useListTableViewportHe
import { canBatchPrintSlip } from '@/utils/pl-batch-print'
import { listDeliveryPrintCustomers } from '@/utils/pl-delivery-slip-print'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
import {
arrangeTransportLabel,
canArrangeTransport,
canCancelWaitNotice,
canEditPlan,
canVoidPlan,
canWaitNotice
} from '@/utils/pl-plan-action-guards'
import {
buildPlPlanListSpanMethod,
flattenPlanListRows,
@@ -1433,37 +1441,6 @@ tableEl: planTableWrapRef,
handleCopy(selectedPlan.value)
}
function canEditPlan(row: Plan) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
function canArrangeTransport(row: Plan) {
return (
(row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
row.archiveStatus !== 'archived' &&
row.followStatus !== 'cancelled'
)
}
function arrangeTransportLabel(row: Plan) {
return row.transportStatus === 'arranged' ? '调整运输' : '安排运输'
}
function canVoidPlan(row: Plan) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
function canWaitNotice(row: Plan) {
return (
(row.followStatus === 'pending_arrange' || row.followStatus === 'drafting') &&
row.archiveStatus !== 'archived'
)
}
function canCancelWaitNotice(row: Plan) {
return row.followStatus === 'wait_notice' && row.archiveStatus !== 'archived'
}
function syncViewingPlan(updated: Plan) {
if (viewingPlan.value?.id === updated.id) {
viewingPlan.value = { ...viewingPlan.value, ...updated }