feat(web-admin): add mp webview CRM quote detail actions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
63f64b1375
commit
eff40b2e8f
1 file changed
+725
-5
@@ -1,17 +1,737 @@
|
||||
<template>
|
||||
<MpWebviewShell>
|
||||
<MpWebviewShell @ready="onShellReady">
|
||||
<header class="mp-wv-header">
|
||||
<div class="mp-wv-header__row">
|
||||
<h1 class="mp-wv-header__title">{{ title }}</h1>
|
||||
<button class="mp-wv-header__back" type="button" aria-label="返回列表" @click="goList">
|
||||
←
|
||||
</button>
|
||||
<h1 class="mp-wv-header__title">{{ headerTitle }}</h1>
|
||||
<el-tag v-if="quote && !loading" :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
|
||||
</div>
|
||||
<div v-if="quote && !loading" class="mp-wv-header__sub">
|
||||
{{ quote.quoteNo || '—' }} · {{ quote.customerName || '—' }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="loading"
|
||||
class="mp-plan-form-loading mp-plan-form-loading--page"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="mp-plan-form-loading__orb" aria-hidden="true">
|
||||
<span class="mp-plan-form-loading__dot" />
|
||||
</div>
|
||||
<p class="mp-plan-form-loading__title">正在加载报价单详情</p>
|
||||
<p class="mp-plan-form-loading__desc">请稍候,正在获取报价单信息…</p>
|
||||
<div class="mp-plan-form-loading__skel" aria-hidden="true">
|
||||
<div class="mp-wv-skel mp-wv-skel--block" />
|
||||
<div class="mp-wv-skel mp-wv-skel--mid" />
|
||||
<div class="mp-wv-skel mp-wv-skel--line" />
|
||||
<div class="mp-wv-skel mp-wv-skel--short" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="errorText" class="mp-wv-empty">{{ errorText }}</div>
|
||||
<template v-else-if="quote">
|
||||
<div class="mp-wv-detail-body">
|
||||
<section class="mp-wv-section">
|
||||
<h3>基础信息</h3>
|
||||
<div class="mp-wv-kv"><span>报价单号</span><span>{{ quote.quoteNo || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>报价主题</span><span>{{ quote.quoteTitle || '—' }}</span></div>
|
||||
<div class="mp-wv-kv">
|
||||
<span>报价类型</span>
|
||||
<span>
|
||||
<el-tag :type="quoteTypeTagType(quote.quoteType)" size="small">
|
||||
{{ quoteTypeLabel(quote.quoteType) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mp-wv-kv"><span>客户</span><span>{{ quote.customerName || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>联系人</span><span>{{ quote.contactName || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>联系电话</span><span>{{ quote.contactMobile || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>版本</span><span>{{ quote.version || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>报价日期</span><span>{{ quote.quoteDate || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>有效期至</span><span>{{ quote.expireDate || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>负责人</span><span>{{ quote.ownerUserName || '—' }}</span></div>
|
||||
<div class="mp-wv-kv">
|
||||
<span>状态</span>
|
||||
<span>
|
||||
<el-tag :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="quote.rejectReason" class="mp-wv-kv">
|
||||
<span>拒绝原因</span><span class="mp-wv-pre">{{ quote.rejectReason }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mp-wv-section">
|
||||
<h3>金额与条款</h3>
|
||||
<div class="mp-wv-kv"><span>产品总金额</span><span>{{ formatQuoteMoney(quote.totalAmount) }}</span></div>
|
||||
<div class="mp-wv-kv"><span>折扣率</span><span>{{ discountRateText }}</span></div>
|
||||
<div class="mp-wv-kv"><span>折扣金额</span><span>{{ formatQuoteMoney(quote.discountAmount) }}</span></div>
|
||||
<div class="mp-wv-kv"><span>最终金额</span><span>{{ formatQuoteMoney(quote.finalAmount) }}</span></div>
|
||||
<div class="mp-wv-kv"><span>有效天数</span><span>{{ quote.validityDays ?? '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>付款条件</span><span class="mp-wv-pre">{{ quote.paymentTerms || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>交货条件</span><span class="mp-wv-pre">{{ quote.deliveryTerms || '—' }}</span></div>
|
||||
<div class="mp-wv-kv"><span>运费</span><span>{{ formatQuoteMoney(quote.freight) }}</span></div>
|
||||
<div class="mp-wv-kv"><span>桶押金</span><span>{{ formatQuoteMoney(quote.barrelDeposit) }}</span></div>
|
||||
</section>
|
||||
|
||||
<section class="mp-wv-section">
|
||||
<h3>产品明细</h3>
|
||||
<template v-if="products.length">
|
||||
<article v-for="(p, idx) in products" :key="String(p.id || idx)" class="mp-quote-product-card">
|
||||
<div class="mp-wv-card__title">{{ p.standardProductName || p.productName || '—' }}</div>
|
||||
<div class="mp-wv-card__meta">
|
||||
{{ p.model || '—' }} · {{ p.quantity ?? 0 }} {{ unitLabel(p.unit) }}
|
||||
</div>
|
||||
<div class="mp-wv-card__meta">
|
||||
单价 {{ formatQuoteMoney(p.price) }} · 小计 {{ formatQuoteMoney(p.subtotal) }}
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<p v-else class="mp-wv-card__meta">暂无产品明细</p>
|
||||
</section>
|
||||
|
||||
<section class="mp-wv-section">
|
||||
<h3>审批流</h3>
|
||||
<template v-if="approvalList.length">
|
||||
<div v-for="item in approvalList" :key="String(item.id)" class="mp-quote-approval-step">
|
||||
<div class="mp-wv-kv">
|
||||
<span>第{{ item.step }}步 · {{ item.approverName || '—' }}</span>
|
||||
<span>
|
||||
<el-tag
|
||||
v-if="item.approvalResult === 'approved'"
|
||||
type="success"
|
||||
size="small"
|
||||
>
|
||||
已通过
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="item.approvalResult === 'rejected'"
|
||||
type="danger"
|
||||
size="small"
|
||||
>
|
||||
已拒绝
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">待审批</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="item.approvalOpinion" class="mp-wv-card__meta">
|
||||
意见:{{ item.approvalOpinion }}
|
||||
</p>
|
||||
<p v-if="item.approvalTime" class="mp-wv-card__meta">时间:{{ item.approvalTime }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="mp-wv-card__meta">暂无审批记录</p>
|
||||
</section>
|
||||
|
||||
<section class="mp-wv-section">
|
||||
<h3>备注</h3>
|
||||
<p class="mp-wv-remark">{{ quote.remark?.trim() || '—' }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="fabActions.length" class="mp-wv-fab-bar">
|
||||
<button
|
||||
v-if="moreActions.length"
|
||||
class="mp-wv-btn mp-wv-btn-more"
|
||||
type="button"
|
||||
aria-label="更多"
|
||||
@click="moreOpen = true"
|
||||
>
|
||||
⋯
|
||||
</button>
|
||||
<button
|
||||
v-for="(action, idx) in primaryActions"
|
||||
:key="action.key"
|
||||
class="mp-wv-btn"
|
||||
:class="{
|
||||
'mp-wv-btn--primary': idx === 0,
|
||||
'is-danger': action.danger
|
||||
}"
|
||||
type="button"
|
||||
:disabled="acting || action.disabled"
|
||||
:title="action.title || ''"
|
||||
@click="runAction(action.key)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MpBottomSheet v-model="moreOpen" title="更多操作" half>
|
||||
<button
|
||||
v-for="action in moreActions"
|
||||
:key="action.key"
|
||||
class="mp-wv-filter-opt"
|
||||
:class="{ 'is-danger': action.danger }"
|
||||
type="button"
|
||||
:disabled="acting || action.disabled"
|
||||
@click="runAction(action.key)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</MpBottomSheet>
|
||||
|
||||
<MpBottomSheet v-model="submitOpen" title="提交审批">
|
||||
<div class="mp-quote-sheet-form">
|
||||
<label class="mp-quote-sheet-label">审批人(可多选,按选择顺序)</label>
|
||||
<el-select
|
||||
v-model="submitForm.approverIds"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择审批人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="formatApproverOptionLabel(user)"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
<label class="mp-quote-sheet-label">备注说明</label>
|
||||
<el-input
|
||||
v-model="submitForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="可选"
|
||||
/>
|
||||
<button
|
||||
class="mp-wv-btn mp-wv-btn--primary mp-wv-btn--block"
|
||||
type="button"
|
||||
:disabled="acting"
|
||||
@click="confirmSubmit"
|
||||
>
|
||||
确认提交
|
||||
</button>
|
||||
</div>
|
||||
</MpBottomSheet>
|
||||
|
||||
<MpBottomSheet v-model="approveOpen" :title="approveSheetTitle">
|
||||
<div class="mp-quote-sheet-form">
|
||||
<label class="mp-quote-sheet-label">
|
||||
审批意见{{ approveResult === 'rejected' ? '(必填)' : '(可选)' }}
|
||||
</label>
|
||||
<el-input
|
||||
v-model="approveOpinion"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:placeholder="approveResult === 'rejected' ? '请填写拒绝理由' : '请输入审批意见'"
|
||||
/>
|
||||
<button
|
||||
class="mp-wv-btn mp-wv-btn--primary mp-wv-btn--block"
|
||||
type="button"
|
||||
:disabled="acting"
|
||||
@click="confirmApprove"
|
||||
>
|
||||
确认{{ approveResult === 'rejected' ? '驳回' : '通过' }}
|
||||
</button>
|
||||
</div>
|
||||
</MpBottomSheet>
|
||||
</template>
|
||||
</MpWebviewShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
||||
import MpBottomSheet from '@/components/mp/MpBottomSheet.vue'
|
||||
import {
|
||||
getQuote,
|
||||
listApproval,
|
||||
listQuoteProduct,
|
||||
submitQuote,
|
||||
invalidQuote,
|
||||
delQuote,
|
||||
approveQuote,
|
||||
getQuoteApproverSetting,
|
||||
type QuoteApprovalVO,
|
||||
type QuoteVO
|
||||
} from '@/api/crm/quote'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
formatQuoteMoney,
|
||||
quoteTypeLabel,
|
||||
quoteTypeTagType
|
||||
} from '@/utils/mp-crm-quote-card'
|
||||
import {
|
||||
isQuoteEditableStatus,
|
||||
getQuoteReeditConfirmMessage
|
||||
} from '@/views/crm/quote/quoteStatus'
|
||||
import { MP_CRM_QUOTE_APPROVAL_PATH, MP_CRM_QUOTE_LIST_PATH } from '@/utils/mp-webview'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import '@/styles/mp-webview.scss'
|
||||
|
||||
type FabAction = {
|
||||
key: string
|
||||
label: string
|
||||
danger?: boolean
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
type ApproverOption = {
|
||||
userId: string
|
||||
nickName?: string
|
||||
userName?: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const title = computed(() => String(route.meta.title || ''))
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const acting = ref(false)
|
||||
const errorText = ref('')
|
||||
const quote = ref<QuoteVO | null>(null)
|
||||
const products = ref<any[]>([])
|
||||
const approvalList = ref<QuoteApprovalVO[]>([])
|
||||
|
||||
const moreOpen = ref(false)
|
||||
const submitOpen = ref(false)
|
||||
const approveOpen = ref(false)
|
||||
const approveResult = ref<'approved' | 'rejected'>('approved')
|
||||
const approveOpinion = ref('')
|
||||
const userOptions = ref<ApproverOption[]>([])
|
||||
const submitForm = ref<{ approverIds: string[]; remark: string }>({
|
||||
approverIds: [],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isApproveEntry = computed(() => String(route.query.from || '') === 'approve')
|
||||
|
||||
const resolveCurrentUserId = (): string => {
|
||||
const info = userStore.info || ({} as Api.Auth.UserInfo)
|
||||
const fromInfo = String(info.id || info.userId || '')
|
||||
if (fromInfo) return fromInfo
|
||||
const token = userStore.accessToken
|
||||
if (!token || token.split('.').length < 2) return ''
|
||||
try {
|
||||
const payload = token.split('.')[1]
|
||||
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const pad = '='.repeat((4 - (b64.length % 4)) % 4)
|
||||
const obj = JSON.parse(atob(b64 + pad))
|
||||
return String(obj?.sub || '')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const headerTitle = computed(() => {
|
||||
if (loading.value) return '报价单详情'
|
||||
return quote.value?.quoteTitle || '报价单详情'
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const s = quote.value?.status || ''
|
||||
return dictStore.getLabel('crm_quote_status', s) || s || '—'
|
||||
})
|
||||
|
||||
const statusTagType = computed(() => {
|
||||
return dictStore.getTagType('crm_quote_status', quote.value?.status || '') as
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'info'
|
||||
| 'primary'
|
||||
| 'danger'
|
||||
})
|
||||
|
||||
const discountRateText = computed(() => {
|
||||
const n = quote.value?.discountRate
|
||||
if (n === undefined || n === null || Number.isNaN(Number(n))) return '—'
|
||||
return `${n}%`
|
||||
})
|
||||
|
||||
const pendingApproval = computed(() => {
|
||||
const uid = resolveCurrentUserId()
|
||||
if (!uid) return null
|
||||
return (
|
||||
approvalList.value.find(
|
||||
(item) =>
|
||||
String(item.approverId) === String(uid) && item.approvalResult === 'pending'
|
||||
) || null
|
||||
)
|
||||
})
|
||||
|
||||
const canDelete = computed(() =>
|
||||
canDeleteByCreator((quote.value || {}) as Record<string, unknown>)
|
||||
)
|
||||
|
||||
const fabActions = computed((): FabAction[] => {
|
||||
const q = quote.value
|
||||
if (!q) return []
|
||||
if (isApproveEntry.value) {
|
||||
if (pendingApproval.value) {
|
||||
return [
|
||||
{ key: 'approve', label: '通过' },
|
||||
{ key: 'reject', label: '驳回', danger: true }
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const status = q.status || ''
|
||||
if (status === 'invalid') {
|
||||
return [
|
||||
{ key: 'copy', label: '复制新增' },
|
||||
{ key: 'versions', label: '版本历史' }
|
||||
]
|
||||
}
|
||||
|
||||
const actions: FabAction[] = []
|
||||
if (status === 'draft') {
|
||||
actions.push({ key: 'submit', label: '提交审批' })
|
||||
}
|
||||
if (isQuoteEditableStatus(status)) {
|
||||
actions.push({ key: 'edit', label: '编辑' })
|
||||
}
|
||||
if (status === 'draft') {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
danger: true,
|
||||
disabled: !canDelete.value,
|
||||
title: canDelete.value ? '' : CREATOR_DELETE_TIP
|
||||
})
|
||||
}
|
||||
if (status !== 'invalid') {
|
||||
actions.push({ key: 'void', label: '作废', danger: true })
|
||||
}
|
||||
actions.push({ key: 'copy', label: '复制新增' }, { key: 'versions', label: '版本历史' })
|
||||
return actions
|
||||
})
|
||||
|
||||
const primaryActions = computed(() => fabActions.value.slice(0, 2))
|
||||
const moreActions = computed(() => fabActions.value.slice(2))
|
||||
|
||||
const approveSheetTitle = computed(() =>
|
||||
approveResult.value === 'rejected' ? '驳回报价单' : '通过报价单'
|
||||
)
|
||||
|
||||
function unitLabel(unit?: string) {
|
||||
if (!unit) return ''
|
||||
return dictStore.getLabel('crm_product_unit', unit) || unit
|
||||
}
|
||||
|
||||
function formatApproverOptionLabel(user: ApproverOption) {
|
||||
const nick = (user.nickName || '').trim()
|
||||
const account = (user.userName || '').trim()
|
||||
if (nick && account) return `${nick}(${account})`
|
||||
return nick || account
|
||||
}
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const options = (res as any)?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(options) ? options : [])
|
||||
.map((item: any) => ({
|
||||
userId: String(item.value ?? item.userId ?? item.id ?? ''),
|
||||
nickName: item.nickName ?? item.label ?? item.userName ?? '',
|
||||
userName: item.userNo ?? item.user_no ?? item.userName ?? ''
|
||||
}))
|
||||
.filter((u: ApproverOption) => u.userId)
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePrefillApproverIds() {
|
||||
try {
|
||||
const { data } = await getQuoteApproverSetting()
|
||||
if (!data || String(data.status) !== '0') return [] as string[]
|
||||
const valid = new Set(userOptions.value.map((u) => String(u.userId)))
|
||||
return (data.approverIds || []).map(String).filter((id) => valid.has(id))
|
||||
} catch {
|
||||
return [] as string[]
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
const id = String(route.params.id || '').trim()
|
||||
if (!id) {
|
||||
errorText.value = '缺少报价单 ID'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getQuote(id)
|
||||
const data = ((res as any)?.data ?? res) as QuoteVO
|
||||
quote.value = data
|
||||
errorText.value = ''
|
||||
|
||||
const productList = (data as any)?.productList
|
||||
if (Array.isArray(productList) && productList.length) {
|
||||
products.value = productList
|
||||
} else {
|
||||
try {
|
||||
const prodRes = await listQuoteProduct(id)
|
||||
products.value = prodRes.rows || []
|
||||
} catch {
|
||||
products.value = []
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const appr = await listApproval(id)
|
||||
approvalList.value = (appr.data || []) as QuoteApprovalVO[]
|
||||
} catch {
|
||||
approvalList.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
quote.value = null
|
||||
products.value = []
|
||||
approvalList.value = []
|
||||
errorText.value = error instanceof Error ? error.message : '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goList() {
|
||||
void router.push(
|
||||
isApproveEntry.value ? MP_CRM_QUOTE_APPROVAL_PATH : MP_CRM_QUOTE_LIST_PATH
|
||||
)
|
||||
}
|
||||
|
||||
async function runAction(key: string) {
|
||||
moreOpen.value = false
|
||||
if (key === 'submit') {
|
||||
await openSubmitSheet()
|
||||
return
|
||||
}
|
||||
if (key === 'edit') {
|
||||
await goEdit()
|
||||
return
|
||||
}
|
||||
if (key === 'delete') {
|
||||
await onDelete()
|
||||
return
|
||||
}
|
||||
if (key === 'void') {
|
||||
await onVoid()
|
||||
return
|
||||
}
|
||||
if (key === 'copy') {
|
||||
const id = quote.value?.id
|
||||
if (!id) return
|
||||
void router.push({ path: '/m/crm/quote/new', query: { copyFrom: String(id) } })
|
||||
return
|
||||
}
|
||||
if (key === 'versions') {
|
||||
const id = quote.value?.id
|
||||
if (!id) return
|
||||
void router.push(`/m/crm/quote/${id}/versions`)
|
||||
return
|
||||
}
|
||||
if (key === 'approve') {
|
||||
approveResult.value = 'approved'
|
||||
approveOpinion.value = '审批通过'
|
||||
approveOpen.value = true
|
||||
return
|
||||
}
|
||||
if (key === 'reject') {
|
||||
approveResult.value = 'rejected'
|
||||
approveOpinion.value = ''
|
||||
approveOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function openSubmitSheet() {
|
||||
acting.value = true
|
||||
try {
|
||||
await loadUserOptions()
|
||||
const prefill = await resolvePrefillApproverIds()
|
||||
submitForm.value = { approverIds: prefill, remark: '' }
|
||||
submitOpen.value = true
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSubmit() {
|
||||
const id = quote.value?.id
|
||||
if (!id) return
|
||||
if (!submitForm.value.approverIds.length) {
|
||||
ElMessage.warning('请选择审批人')
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
await submitQuote({
|
||||
id,
|
||||
approverIds: submitForm.value.approverIds,
|
||||
remark: submitForm.value.remark || undefined
|
||||
})
|
||||
ElMessage.success('已提交审批')
|
||||
submitOpen.value = false
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '提交失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function goEdit() {
|
||||
const id = quote.value?.id
|
||||
if (!id) return
|
||||
const msg = getQuoteReeditConfirmMessage(quote.value?.status)
|
||||
if (msg) {
|
||||
try {
|
||||
await ElMessageBox.confirm(msg, '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
void router.push(`/m/crm/quote/${id}/edit`)
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
const id = quote.value?.id
|
||||
if (!id || !canDelete.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该报价单?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
await delQuote([id])
|
||||
ElMessage.success('已删除')
|
||||
goList()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onVoid() {
|
||||
const id = quote.value?.id
|
||||
if (!id) return
|
||||
let reason = ''
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入作废原因', '作废报价单', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '作废原因',
|
||||
inputValidator: (v) => {
|
||||
if (!String(v || '').trim()) return '请填写作废原因'
|
||||
return true
|
||||
}
|
||||
})
|
||||
reason = String(value || '').trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
await invalidQuote(id, reason)
|
||||
ElMessage.success('已作废')
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '作废失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmApprove() {
|
||||
const pending = pendingApproval.value
|
||||
if (!pending) {
|
||||
ElMessage.warning('当前无可审批节点')
|
||||
return
|
||||
}
|
||||
if (approveResult.value === 'rejected' && !approveOpinion.value.trim()) {
|
||||
ElMessage.warning('驳回时请填写拒绝理由')
|
||||
return
|
||||
}
|
||||
const resultText = approveResult.value === 'rejected' ? '驳回' : '通过'
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
approveResult.value === 'rejected'
|
||||
? `确认要拒绝该报价单吗?拒绝后将无法再次提交审批。\n拒绝理由:${approveOpinion.value}`
|
||||
: '确认要通过该报价单吗?',
|
||||
`确认${resultText}`,
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
await approveQuote({
|
||||
id: pending.id,
|
||||
approvalResult: approveResult.value,
|
||||
approvalOpinion: approveOpinion.value.trim() || undefined
|
||||
})
|
||||
ElMessage.success(`已${resultText}`)
|
||||
approveOpen.value = false
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '审批失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onShellReady() {
|
||||
void loadDetail()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-quote-product-card {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e8f2ed;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mp-quote-approval-step {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e8f2ed;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mp-quote-sheet-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
|
||||
.mp-quote-sheet-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #087f57;
|
||||
}
|
||||
|
||||
.mp-wv-btn--block {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mp-wv-pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user