feat(web-admin): add mobile plan detail and shared action guards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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'
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
|
||||
Reference in new issue
Block a user