李红攀:V2.6.976,添加采购询比、采购统计
This commit is contained in:
171
src/api/erp/purchase-inquiry/index.ts
Normal file
171
src/api/erp/purchase-inquiry/index.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
export interface PurchaseInquiryQuote {
|
||||
id?: number
|
||||
inquiryId?: number
|
||||
invitationId?: number
|
||||
supplierId?: number
|
||||
supplierName?: string
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
quoteDate?: string | number | Date
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
paymentTerms?: string
|
||||
deliveryCycle?: number
|
||||
expectedArrivalDate?: string | number | Date
|
||||
qualificationFile?: string
|
||||
remark?: string
|
||||
isSelected?: boolean
|
||||
quoteStatus?: number
|
||||
submitSource?: string
|
||||
priceScore?: number
|
||||
qualityScore?: number
|
||||
deliveryScore?: number
|
||||
serviceScore?: number
|
||||
totalScore?: number
|
||||
}
|
||||
|
||||
export interface PurchaseInquiry {
|
||||
id?: number
|
||||
no?: string
|
||||
productId?: number
|
||||
productName?: string
|
||||
requireCount?: number
|
||||
budgetPrice?: number
|
||||
inquiryTime?: string | number | Date
|
||||
deadline?: string | number | Date
|
||||
status?: number
|
||||
remark?: string
|
||||
selectedQuoteId?: number
|
||||
creatorName?: string
|
||||
creator?: number
|
||||
quoteCount?: number
|
||||
minQuotePrice?: number
|
||||
quotes?: PurchaseInquiryQuote[]
|
||||
}
|
||||
|
||||
export interface PurchaseInquiryPublishReq {
|
||||
inquiryId: number
|
||||
supplierIds: number[]
|
||||
linkExpireTime?: string | number | Date
|
||||
}
|
||||
|
||||
export interface PurchaseInquiryInvitation {
|
||||
id?: number
|
||||
inquiryId?: number
|
||||
supplierId?: number
|
||||
supplierName?: string
|
||||
status?: number
|
||||
accessToken?: string
|
||||
portalUrl?: string
|
||||
linkExpireTime?: string | number | Date
|
||||
firstViewTime?: string | number | Date
|
||||
lastViewTime?: string | number | Date
|
||||
submitTime?: string | number | Date
|
||||
sendTime?: string | number | Date
|
||||
quoteId?: number
|
||||
quoteDate?: string | number | Date
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
paymentTerms?: string
|
||||
deliveryCycle?: number
|
||||
expectedArrivalDate?: string | number | Date
|
||||
qualificationFile?: string
|
||||
remark?: string
|
||||
submitSource?: string
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
}
|
||||
|
||||
export interface PurchaseInquiryCompareReq {
|
||||
inquiryId: number
|
||||
priceWeight: number
|
||||
qualityWeight: number
|
||||
deliveryWeight: number
|
||||
serviceWeight: number
|
||||
quotes?: {
|
||||
quoteId: number
|
||||
qualityScore?: number
|
||||
serviceScore?: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface PurchaseInquiryCompareQuote {
|
||||
quoteId: number
|
||||
supplierId: number
|
||||
supplierName?: string
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
deliveryCycle?: number
|
||||
expectedArrivalDate?: string | number | Date
|
||||
qualificationFile?: string
|
||||
priceScore?: number
|
||||
qualityScore?: number
|
||||
deliveryScore?: number
|
||||
serviceScore?: number
|
||||
totalScore?: number
|
||||
historicalAvgTotalScore?: number
|
||||
historicalEvaluationCount?: number
|
||||
attachmentProvided?: boolean
|
||||
recommended?: boolean
|
||||
recommendationReasons?: string[]
|
||||
riskTips?: string[]
|
||||
}
|
||||
|
||||
export interface PurchaseInquiryCompareResp {
|
||||
inquiryId: number
|
||||
inquiryNo?: string
|
||||
productName?: string
|
||||
requireCount?: number
|
||||
budgetPrice?: number
|
||||
recommendedQuoteId?: number
|
||||
recommendedSupplierId?: number
|
||||
recommendedSupplierName?: string
|
||||
recommendedTotalScore?: number
|
||||
analysisSummary?: string
|
||||
analysisTime?: string | number | Date
|
||||
riskTips?: string[]
|
||||
quotes: PurchaseInquiryCompareQuote[]
|
||||
}
|
||||
|
||||
export function getPurchaseInquiryPage(params: PageParam) {
|
||||
return http.get<PageResult<PurchaseInquiry>>('/erp/purchase-inquiry/page', params)
|
||||
}
|
||||
|
||||
export function getPurchaseInquiry(id: number) {
|
||||
return http.get<PurchaseInquiry>(`/erp/purchase-inquiry/get?id=${id}`)
|
||||
}
|
||||
|
||||
export function createPurchaseInquiry(data: PurchaseInquiry) {
|
||||
return http.post<number>('/erp/purchase-inquiry/create', data)
|
||||
}
|
||||
|
||||
export function updatePurchaseInquiry(data: PurchaseInquiry) {
|
||||
return http.put<boolean>('/erp/purchase-inquiry/update', data)
|
||||
}
|
||||
|
||||
export function updatePurchaseInquiryStatus(id: number, status: number) {
|
||||
return http.put<boolean>(`/erp/purchase-inquiry/update-status?id=${id}&status=${status}`)
|
||||
}
|
||||
|
||||
export function publishPurchaseInquiry(data: PurchaseInquiryPublishReq) {
|
||||
return http.post<boolean>('/erp/purchase-inquiry/publish', data)
|
||||
}
|
||||
|
||||
export function comparePurchaseInquiry(data: PurchaseInquiryCompareReq) {
|
||||
return http.post<PurchaseInquiryCompareResp>('/erp/purchase-inquiry/compare-recommend', data)
|
||||
}
|
||||
|
||||
export function getPurchaseInquiryInvitationList(inquiryId: number) {
|
||||
return http.get<PurchaseInquiryInvitation[]>(`/erp/purchase-inquiry/invitation-list?inquiryId=${inquiryId}`)
|
||||
}
|
||||
|
||||
export function selectPurchaseInquiryQuote(inquiryId: number, quoteId: number) {
|
||||
return http.put<boolean>(`/erp/purchase-inquiry/select-quote?inquiryId=${inquiryId}"eId=${quoteId}`)
|
||||
}
|
||||
|
||||
export function deletePurchaseInquiry(ids: number[]) {
|
||||
return http.delete<boolean>(`/erp/purchase-inquiry/delete?ids=${ids.join(',')}`)
|
||||
}
|
||||
75
src/api/erp/purchase-statistics/index.ts
Normal file
75
src/api/erp/purchase-statistics/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
export interface PurchaseSummary {
|
||||
todayPrice: number
|
||||
yesterdayPrice: number
|
||||
monthPrice: number
|
||||
yearPrice: number
|
||||
}
|
||||
|
||||
export interface PurchaseTimeSummary {
|
||||
time: string
|
||||
price: number
|
||||
}
|
||||
|
||||
export interface PurchaseTrend {
|
||||
time: string
|
||||
price: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface PurchaseProductRank {
|
||||
productId: number
|
||||
productName: string
|
||||
count: number
|
||||
price: number
|
||||
inCount: number
|
||||
returnCount: number
|
||||
inPrice: number
|
||||
returnPrice: number
|
||||
orderCount: number
|
||||
avgPrice: number
|
||||
shareRate: number
|
||||
}
|
||||
|
||||
export interface PurchaseSupplierRank {
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
count: number
|
||||
price: number
|
||||
inCount: number
|
||||
returnCount: number
|
||||
inPrice: number
|
||||
returnPrice: number
|
||||
orderCount: number
|
||||
avgPrice: number
|
||||
shareRate: number
|
||||
}
|
||||
|
||||
export function getPurchaseSummary() {
|
||||
return http.get<PurchaseSummary>('/erp/purchase-statistics/summary')
|
||||
}
|
||||
|
||||
export function getPurchaseTimeSummary() {
|
||||
return http.get<PurchaseTimeSummary[]>('/erp/purchase-statistics/time-summary')
|
||||
}
|
||||
|
||||
export function getPurchaseTrendByWeek(count: number = 12) {
|
||||
return http.get<PurchaseTrend[]>(`/erp/purchase-statistics/trend-by-week?count=${count}`)
|
||||
}
|
||||
|
||||
export function getPurchaseTrendByMonth(count: number = 12) {
|
||||
return http.get<PurchaseTrend[]>(`/erp/purchase-statistics/trend-by-month?count=${count}`)
|
||||
}
|
||||
|
||||
export function getPurchaseProductRank(beginTime: string, endTime: string, limit: number = 10, sortBy: 'price' | 'count' = 'price') {
|
||||
return http.get<PurchaseProductRank[]>('/erp/purchase-statistics/product-rank', { beginTime, endTime, limit, sortBy })
|
||||
}
|
||||
|
||||
export function getPurchaseSupplierRank(beginTime: string, endTime: string, limit: number = 10, sortBy: 'price' | 'count' = 'price') {
|
||||
return http.get<PurchaseSupplierRank[]>('/erp/purchase-statistics/supplier-rank', { beginTime, endTime, limit, sortBy })
|
||||
}
|
||||
|
||||
export function analyzePurchaseStatistics(data: Record<string, any>) {
|
||||
return http.post<any>('/erp/purchase-statistics/ai-analysis', data)
|
||||
}
|
||||
239
src/pages-erp/purchase-inquiry/compare/index.vue
Normal file
239
src/pages-erp/purchase-inquiry/compare/index.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar title="自动比价" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
|
||||
|
||||
<view class="erp-page__body">
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">比价单信息</view>
|
||||
<view class="erp-field-row"><text>询比单号</text><text>{{ inquiry?.no || '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>产品名称</text><text>{{ inquiry?.productName || '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>需求数量</text><text>{{ formatCount(inquiry?.requireCount) }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">评分权重(合计 {{ totalWeight }}%)</view>
|
||||
<view class="erp-form-item-list">
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">价格权重</text>
|
||||
<wd-input v-model="weightConfig.priceWeight" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">质量权重</text>
|
||||
<wd-input v-model="weightConfig.qualityWeight" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">交付权重</text>
|
||||
<wd-input v-model="weightConfig.deliveryWeight" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">服务权重</text>
|
||||
<wd-input v-model="weightConfig.serviceWeight" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">供应商报价评分</view>
|
||||
<view v-if="quoteList.length">
|
||||
<view
|
||||
v-for="(item, index) in quoteList"
|
||||
:key="item.id || index"
|
||||
class="erp-form-item-card erp-form-item-card--padded"
|
||||
:class="{ 'border-[2rpx] border-[#52c41a]': bestQuote?.id === item.id }"
|
||||
>
|
||||
<view class="mb-12rpx flex items-center justify-between">
|
||||
<text class="erp-card-title">{{ item.supplierName || '未命名供应商' }}</text>
|
||||
<text class="text-24rpx text-[#1890ff]">{{ item.totalScore != null ? `${item.totalScore.toFixed(2)}分` : '未计算' }}</text>
|
||||
</view>
|
||||
<view class="erp-field-row"><text>单价</text><text>¥{{ formatPrice(item.unitPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>总价</text><text>¥{{ formatPrice(item.totalPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>交货周期</text><text>{{ item.deliveryCycle ? `${item.deliveryCycle}天` : '-' }}</text></view>
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">质量评分</text>
|
||||
<wd-input v-model="item.qualityScore" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
<view class="erp-form-input-row">
|
||||
<text class="erp-form-input-label">服务评分</text>
|
||||
<wd-input v-model="item.serviceScore" type="number" placeholder="0-100" />
|
||||
</view>
|
||||
<view class="mt-12rpx text-24rpx text-[#999]">
|
||||
价格得分:{{ item.priceScore != null ? item.priceScore.toFixed(1) : '-' }},交付得分:{{ item.deliveryScore != null ? item.deliveryScore.toFixed(1) : '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="erp-empty">暂无可比价报价</view>
|
||||
</view>
|
||||
|
||||
<view v-if="bestQuote" class="erp-section">
|
||||
<view class="erp-section__title">推荐结果</view>
|
||||
<view class="text-28rpx text-[#52c41a] font-semibold">
|
||||
推荐供应商:{{ bestQuote.supplierName }},综合得分 {{ bestQuote.totalScore?.toFixed(2) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="analysisSummary" class="erp-section">
|
||||
<view class="erp-section__title">智能分析</view>
|
||||
<view class="text-26rpx leading-[1.7] text-[#666] whitespace-pre-wrap">{{ analysisSummary }}</view>
|
||||
<view v-if="compareRiskTips.length" class="mt-16rpx text-24rpx text-[#f5222d]">
|
||||
<view v-for="item in compareRiskTips" :key="item">{{ item }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button class="flex-1" type="primary" :loading="loading" @click="handleCompare">开始比价</wd-button>
|
||||
<wd-button class="flex-1" type="success" :disabled="!bestQuote" :loading="saving" @click="handleSave">保存结果</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PurchaseInquiry, PurchaseInquiryCompareResp, PurchaseInquiryQuote } from '@/api/erp/purchase-inquiry'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { comparePurchaseInquiry, getPurchaseInquiry, selectPurchaseInquiryQuote, updatePurchaseInquiry } from '@/api/erp/purchase-inquiry'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{ id?: number | any }>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const inquiry = ref<PurchaseInquiry>()
|
||||
const quoteList = ref<PurchaseInquiryQuote[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const analysisSummary = ref('')
|
||||
const compareRiskTips = ref<string[]>([])
|
||||
const bestQuote = ref<PurchaseInquiryQuote | null>(null)
|
||||
const weightConfig = reactive({
|
||||
priceWeight: 40,
|
||||
qualityWeight: 25,
|
||||
deliveryWeight: 20,
|
||||
serviceWeight: 15,
|
||||
})
|
||||
|
||||
const totalWeight = computed(() =>
|
||||
Number(weightConfig.priceWeight || 0)
|
||||
+ Number(weightConfig.qualityWeight || 0)
|
||||
+ Number(weightConfig.deliveryWeight || 0)
|
||||
+ Number(weightConfig.serviceWeight || 0),
|
||||
)
|
||||
|
||||
function formatPrice(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatCount(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-erp/purchase-inquiry/detail/index?id=${props.id}`)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!props.id) return
|
||||
inquiry.value = await getPurchaseInquiry(Number(props.id))
|
||||
quoteList.value = inquiry.value.quotes || []
|
||||
bestQuote.value = quoteList.value.find(v => v.isSelected) || null
|
||||
}
|
||||
|
||||
function applyCompareResult(result: PurchaseInquiryCompareResp) {
|
||||
const currentMap = new Map((quoteList.value || []).map(quote => [quote.id, quote]))
|
||||
quoteList.value = (result.quotes || []).map(quote => {
|
||||
const current = currentMap.get(quote.quoteId)
|
||||
return {
|
||||
...current,
|
||||
id: quote.quoteId,
|
||||
supplierId: quote.supplierId,
|
||||
supplierName: quote.supplierName,
|
||||
unitPrice: quote.unitPrice,
|
||||
totalPrice: quote.totalPrice,
|
||||
deliveryCycle: quote.deliveryCycle,
|
||||
expectedArrivalDate: quote.expectedArrivalDate,
|
||||
qualificationFile: quote.qualificationFile,
|
||||
priceScore: quote.priceScore,
|
||||
qualityScore: quote.qualityScore,
|
||||
deliveryScore: quote.deliveryScore,
|
||||
serviceScore: quote.serviceScore,
|
||||
totalScore: quote.totalScore,
|
||||
isSelected: quote.recommended,
|
||||
}
|
||||
})
|
||||
analysisSummary.value = result.analysisSummary || ''
|
||||
compareRiskTips.value = result.riskTips || []
|
||||
bestQuote.value = quoteList.value.find(v => v.id === result.recommendedQuoteId) || null
|
||||
}
|
||||
|
||||
async function handleCompare() {
|
||||
if (!props.id) return
|
||||
if (totalWeight.value !== 100) {
|
||||
toast.warning('权重合计必须等于 100%')
|
||||
return
|
||||
}
|
||||
if (quoteList.value.length < 2) {
|
||||
toast.warning('至少需要 2 条报价才能比价')
|
||||
return
|
||||
}
|
||||
if (quoteList.value.some(v => !v.unitPrice || Number(v.unitPrice) <= 0)) {
|
||||
toast.warning('请先补全所有报价单价')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await comparePurchaseInquiry({
|
||||
inquiryId: Number(props.id),
|
||||
priceWeight: Number(weightConfig.priceWeight),
|
||||
qualityWeight: Number(weightConfig.qualityWeight),
|
||||
deliveryWeight: Number(weightConfig.deliveryWeight),
|
||||
serviceWeight: Number(weightConfig.serviceWeight),
|
||||
quotes: quoteList.value
|
||||
.filter(v => !!v.id)
|
||||
.map(v => ({
|
||||
quoteId: Number(v.id),
|
||||
qualityScore: v.qualityScore != null ? Number(v.qualityScore) : undefined,
|
||||
serviceScore: v.serviceScore != null ? Number(v.serviceScore) : undefined,
|
||||
})),
|
||||
})
|
||||
applyCompareResult(result)
|
||||
if (bestQuote.value) {
|
||||
toast.success(`比价完成,推荐供应商:${bestQuote.value.supplierName}`)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!inquiry.value?.id || !bestQuote.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await updatePurchaseInquiry({
|
||||
...inquiry.value,
|
||||
quotes: quoteList.value,
|
||||
})
|
||||
if (bestQuote.value.id) {
|
||||
await selectPurchaseInquiryQuote(inquiry.value.id, Number(bestQuote.value.id))
|
||||
}
|
||||
toast.success('保存成功')
|
||||
setTimeout(() => handleBack(), 500)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
241
src/pages-erp/purchase-inquiry/detail/index.vue
Normal file
241
src/pages-erp/purchase-inquiry/detail/index.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar title="采购询比详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
|
||||
|
||||
<view v-if="detail">
|
||||
<wd-cell-group title="基本信息" border>
|
||||
<wd-cell title="询比单号" :value="detail.no || '-'" />
|
||||
<wd-cell title="状态">
|
||||
<view class="rounded-8rpx px-16rpx py-4rpx text-24rpx" :class="getStatusClass(detail.status)">
|
||||
{{ getStatusText(detail.status) }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="产品" :value="detail.productName || '-'" />
|
||||
<wd-cell title="需求数量" :value="formatCount(detail.requireCount)" />
|
||||
<wd-cell title="预算金额" :value="`¥${formatPrice(detail.budgetPrice)}`" />
|
||||
<wd-cell title="询价时间" :value="formatDate(detail.inquiryTime)" />
|
||||
<wd-cell title="截止时间" :value="formatDate(detail.deadline)" />
|
||||
<wd-cell title="创建人" :value="detail.creatorName || '-'" />
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group v-if="detail.remark" title="备注" border>
|
||||
<wd-cell :value="detail.remark" />
|
||||
</wd-cell-group>
|
||||
|
||||
<view class="mx-24rpx mt-24rpx">
|
||||
<view class="mb-16rpx text-32rpx text-[#333] font-semibold">供应商报价({{ detail.quotes?.length || 0 }}项)</view>
|
||||
<view v-if="detail.quotes?.length">
|
||||
<view v-for="(item, index) in detail.quotes" :key="item.id || index" class="mb-16rpx overflow-hidden rounded-12rpx bg-white shadow-sm">
|
||||
<view class="flex items-center justify-between bg-[#f8f8f8] px-24rpx py-16rpx">
|
||||
<text class="text-28rpx text-[#333] font-semibold">{{ item.supplierName || '未命名供应商' }}</text>
|
||||
<text v-if="item.isSelected" class="text-24rpx text-[#52c41a]">已中选</text>
|
||||
</view>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">联系人</text>
|
||||
<text>{{ item.contactName || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">联系电话</text>
|
||||
<text>{{ item.contactPhone || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">单价</text>
|
||||
<text>¥{{ formatPrice(item.unitPrice) }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">总价</text>
|
||||
<text>¥{{ formatPrice(item.totalPrice) }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">交货周期</text>
|
||||
<text>{{ item.deliveryCycle ? `${item.deliveryCycle}天` : '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">预计到货</text>
|
||||
<text>{{ formatDate(item.expectedArrivalDate) }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">付款条件</text>
|
||||
<text class="ml-16rpx line-clamp-1 text-right" style="max-width: 420rpx;">{{ item.paymentTerms || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">评分</text>
|
||||
<text>{{ item.totalScore != null ? item.totalScore.toFixed(2) : '-' }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="text-24rpx text-[#999]">备注:{{ item.remark }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="py-60rpx text-center text-28rpx text-[#999]">暂无报价信息</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button v-if="hasAccessByCodes(['erp:purchase-inquiry:update'])" class="flex-1" type="primary" @click="handlePublish">
|
||||
邀请
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update']) && detail?.status !== 30"
|
||||
class="flex-1"
|
||||
type="warning"
|
||||
@click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update']) && detail?.status === 20 && (detail?.quoteCount || 0) >= 2"
|
||||
class="flex-1"
|
||||
type="success"
|
||||
@click="handleCompare"
|
||||
>
|
||||
比价
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update-status']) && detail?.status === 10"
|
||||
class="flex-1"
|
||||
type="success"
|
||||
@click="handleUpdateStatus(20)"
|
||||
>
|
||||
询价
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update-status']) && detail?.status === 20"
|
||||
class="flex-1"
|
||||
type="success"
|
||||
@click="handleUpdateStatus(30)"
|
||||
>
|
||||
完成
|
||||
</wd-button>
|
||||
<wd-button v-if="hasAccessByCodes(['erp:purchase-inquiry:delete'])" class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PurchaseInquiry } from '@/api/erp/purchase-inquiry'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deletePurchaseInquiry, getPurchaseInquiry, updatePurchaseInquiryStatus } from '@/api/erp/purchase-inquiry'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{ id?: number | any }>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const detail = ref<PurchaseInquiry>()
|
||||
const deleting = ref(false)
|
||||
|
||||
function formatPrice(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatCount(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatDate(value?: string | number | Date) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value).slice(0, 10)
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${month}-${day}`
|
||||
}
|
||||
|
||||
function getStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return '待询价'
|
||||
case 20: return '询价中'
|
||||
case 30: return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return 'bg-[#eef3ff] text-[#3f7cff]'
|
||||
case 20: return 'bg-[#fff7e8] text-[#d48806]'
|
||||
case 30: return 'bg-[#f0f9eb] text-[#52c41a]'
|
||||
default: return 'bg-[#f5f5f5] text-[#999]'
|
||||
}
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-erp/purchase-inquiry/index')
|
||||
}
|
||||
|
||||
async function getDetail() {
|
||||
if (!props.id) return
|
||||
detail.value = await getPurchaseInquiry(props.id)
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/form/index?id=${props.id}` })
|
||||
}
|
||||
|
||||
function handlePublish() {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/publish/index?id=${props.id}` })
|
||||
}
|
||||
|
||||
function handleCompare() {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/compare/index?id=${props.id}` })
|
||||
}
|
||||
|
||||
function handleUpdateStatus(status: number) {
|
||||
if (!props.id) return
|
||||
const actionText = status === 20 ? '开始询价' : '完成'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定${actionText}该询比单吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await updatePurchaseInquiryStatus(props.id, status)
|
||||
toast.success(`${actionText}成功`)
|
||||
getDetail()
|
||||
} catch {
|
||||
// http handled
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!props.id) return
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该采购询比吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await deletePurchaseInquiry([props.id])
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => handleBack(), 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
266
src/pages-erp/purchase-inquiry/form/index.vue
Normal file
266
src/pages-erp/purchase-inquiry/form/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar :title="pageTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
|
||||
|
||||
<view class="erp-form-body">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group title="基本信息" border>
|
||||
<wd-cell title="询比单号" :value="formData.no || '保存后自动生成'" />
|
||||
<wd-cell title="询价时间" title-width="180rpx" prop="inquiryTime" center>
|
||||
<wd-datetime-picker v-model="formData.inquiryTime" type="date" label="" placeholder="请选择询价时间" />
|
||||
</wd-cell>
|
||||
<wd-cell title="截止时间" title-width="180rpx" center>
|
||||
<wd-datetime-picker v-model="formData.deadline" type="date" label="" placeholder="请选择截止时间" />
|
||||
</wd-cell>
|
||||
<wd-cell title="产品" title-width="180rpx" prop="productId" center>
|
||||
<wd-picker v-model="formData.productId" :columns="productColumns" placeholder="请选择产品" @confirm="onProductConfirm" />
|
||||
</wd-cell>
|
||||
<wd-input v-model="formData.requireCount" label="需求数量" label-width="180rpx" type="number" placeholder="请输入需求数量" clearable />
|
||||
<wd-input v-model="formData.budgetPrice" label="预算金额" label-width="180rpx" type="number" placeholder="请输入预算金额" clearable />
|
||||
<wd-textarea v-model="formData.remark" label="备注" label-width="180rpx" placeholder="请输入备注" :maxlength="200" show-word-limit clearable />
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
|
||||
<view class="erp-form-section-heading">
|
||||
<view class="text-32rpx text-[#333] font-semibold">供应商报价({{ formData.quotes.length }}项)</view>
|
||||
<wd-button size="small" type="primary" @click="handleAddQuote">+ 添加报价</wd-button>
|
||||
</view>
|
||||
|
||||
<view v-for="(item, index) in formData.quotes" :key="item.id || index" class="erp-form-item-card">
|
||||
<view class="erp-form-item-card__header">
|
||||
<text class="erp-card-title">报价 #{{ index + 1 }}</text>
|
||||
<wd-button size="small" type="error" plain @click="handleRemoveQuote(index)">删除</wd-button>
|
||||
</view>
|
||||
<view class="erp-form-item-list">
|
||||
<view class="mb-16rpx">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">供应商</text>
|
||||
<wd-picker v-model="item.supplierId" :columns="supplierColumns" placeholder="请选择供应商" @confirm="(e: any) => onSupplierConfirm(e, index)" />
|
||||
</view>
|
||||
<view class="mb-16rpx flex gap-16rpx">
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">联系人</text>
|
||||
<wd-input v-model="item.contactName" placeholder="请输入联系人" clearable />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">联系电话</text>
|
||||
<wd-input v-model="item.contactPhone" placeholder="请输入联系电话" clearable />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-16rpx flex gap-16rpx">
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">单价</text>
|
||||
<wd-input v-model="item.unitPrice" type="number" placeholder="请输入单价" clearable />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">总价</text>
|
||||
<text class="block pt-16rpx text-28rpx text-[#333]">¥{{ calcTotalPrice(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-16rpx flex gap-16rpx">
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">交货周期(天)</text>
|
||||
<wd-input v-model="item.deliveryCycle" type="number" placeholder="请输入交货周期" clearable />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">预计到货</text>
|
||||
<wd-datetime-picker v-model="item.expectedArrivalDate" type="date" label="" placeholder="请选择日期" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-16rpx">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">付款条件</text>
|
||||
<wd-input v-model="item.paymentTerms" placeholder="请输入付款条件" clearable />
|
||||
</view>
|
||||
<view class="mb-16rpx">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">资质附件 URL</text>
|
||||
<wd-input v-model="item.qualificationFile" placeholder="请输入附件地址" clearable />
|
||||
</view>
|
||||
<view>
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">备注</text>
|
||||
<wd-input v-model="item.remark" placeholder="请输入备注" clearable />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="formData.quotes.length === 0" class="py-60rpx text-center text-28rpx text-[#999]">
|
||||
暂无报价,点击“添加报价”开始录入
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">保存</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ProductSimple } from '@/api/erp/product'
|
||||
import type { PurchaseInquiry, PurchaseInquiryQuote } from '@/api/erp/purchase-inquiry'
|
||||
import type { SupplierSimple } from '@/api/erp/supplier'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createPurchaseInquiry, getPurchaseInquiry, updatePurchaseInquiry } from '@/api/erp/purchase-inquiry'
|
||||
import { getProductSimpleList } from '@/api/erp/product'
|
||||
import { getSupplierSimpleList } from '@/api/erp/supplier'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{ id?: number | any }>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const productList = ref<ProductSimple[]>([])
|
||||
const supplierList = ref<SupplierSimple[]>([])
|
||||
|
||||
interface InquiryFormModel extends Omit<PurchaseInquiry, 'inquiryTime' | 'deadline' | 'quotes'> {
|
||||
inquiryTime?: string | number
|
||||
deadline?: string | number
|
||||
quotes: (Omit<PurchaseInquiryQuote, 'quoteDate' | 'expectedArrivalDate'> & {
|
||||
quoteDate?: string | number
|
||||
expectedArrivalDate?: string | number
|
||||
})[]
|
||||
}
|
||||
|
||||
const formData = ref<InquiryFormModel>({
|
||||
id: undefined,
|
||||
no: undefined,
|
||||
productId: undefined,
|
||||
productName: undefined,
|
||||
requireCount: 1,
|
||||
budgetPrice: 0,
|
||||
inquiryTime: Date.now(),
|
||||
deadline: undefined,
|
||||
remark: '',
|
||||
quotes: [],
|
||||
})
|
||||
const formRules = {
|
||||
productId: [{ required: true, message: '产品不能为空' }],
|
||||
inquiryTime: [{ required: true, message: '询价时间不能为空' }],
|
||||
requireCount: [{ required: true, message: '需求数量不能为空' }],
|
||||
}
|
||||
|
||||
const pageTitle = computed(() => props.id ? '编辑采购询比' : '新增采购询比')
|
||||
const productColumns = computed(() => [productList.value.map(v => ({ label: v.name, value: v.id }))])
|
||||
const supplierColumns = computed(() => [supplierList.value.map(v => ({ label: v.name, value: v.id }))])
|
||||
|
||||
function onProductConfirm({ value }: any) {
|
||||
const productId = value?.[0] ? Number(value[0]) : undefined
|
||||
formData.value.productId = productId
|
||||
const product = productList.value.find(v => v.id === productId)
|
||||
formData.value.productName = product?.name
|
||||
}
|
||||
|
||||
function onSupplierConfirm({ value }: any, index: number) {
|
||||
const supplierId = value?.[0] ? Number(value[0]) : undefined
|
||||
const current = formData.value.quotes?.[index]
|
||||
if (!current) return
|
||||
current.supplierId = supplierId
|
||||
const supplier = supplierList.value.find(v => v.id === supplierId)
|
||||
current.supplierName = supplier?.name
|
||||
}
|
||||
|
||||
function calcTotalPrice(item: PurchaseInquiryQuote) {
|
||||
const unitPrice = Number(item.unitPrice || 0)
|
||||
const requireCount = Number(formData.value.requireCount || 0)
|
||||
return (unitPrice * requireCount).toFixed(2)
|
||||
}
|
||||
|
||||
function handleAddQuote() {
|
||||
formData.value.quotes = formData.value.quotes || []
|
||||
formData.value.quotes.push({
|
||||
supplierId: undefined,
|
||||
supplierName: undefined,
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
unitPrice: undefined,
|
||||
totalPrice: undefined,
|
||||
paymentTerms: '',
|
||||
deliveryCycle: undefined,
|
||||
expectedArrivalDate: undefined,
|
||||
qualificationFile: '',
|
||||
remark: '',
|
||||
})
|
||||
}
|
||||
|
||||
function handleRemoveQuote(index: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定删除第 ${index + 1} 条报价吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
formData.value.quotes?.splice(index, 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-erp/purchase-inquiry/index')
|
||||
}
|
||||
|
||||
async function getDetail() {
|
||||
if (!props.id) return
|
||||
const detail = await getPurchaseInquiry(props.id)
|
||||
formData.value = {
|
||||
...detail,
|
||||
inquiryTime: detail.inquiryTime as string | number | undefined,
|
||||
deadline: detail.deadline as string | number | undefined,
|
||||
quotes: (detail.quotes || []).map(item => ({
|
||||
...item,
|
||||
quoteDate: item.quoteDate as string | number | undefined,
|
||||
expectedArrivalDate: item.expectedArrivalDate as string | number | undefined,
|
||||
})),
|
||||
}
|
||||
formData.value.quotes = formData.value.quotes || []
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) return
|
||||
const quotes = formData.value.quotes || []
|
||||
for (let i = 0; i < quotes.length; i++) {
|
||||
const item = quotes[i]
|
||||
if (!item.supplierId) {
|
||||
toast.warning(`第 ${i + 1} 条报价未选择供应商`)
|
||||
return
|
||||
}
|
||||
if (!item.unitPrice || Number(item.unitPrice) <= 0) {
|
||||
toast.warning(`第 ${i + 1} 条报价单价不能为空`)
|
||||
return
|
||||
}
|
||||
item.totalPrice = Number(calcTotalPrice(item))
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updatePurchaseInquiry(formData.value as PurchaseInquiry)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createPurchaseInquiry(formData.value as PurchaseInquiry)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [products, suppliers] = await Promise.all([getProductSimpleList(), getSupplierSimpleList()])
|
||||
productList.value = products || []
|
||||
supplierList.value = suppliers || []
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
392
src/pages-erp/purchase-inquiry/index.vue
Normal file
392
src/pages-erp/purchase-inquiry/index.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar
|
||||
title="采购询比"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<view @click="searchVisible = true">
|
||||
<wd-search :placeholder="searchPlaceholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<view class="px-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-20rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-30rpx text-[#333] font-semibold">
|
||||
{{ item.no || '-' }}
|
||||
</view>
|
||||
<view class="rounded-8rpx px-16rpx py-4rpx text-24rpx" :class="getStatusClass(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">产品</text>
|
||||
<text class="ml-16rpx line-clamp-1 text-right" style="max-width: 420rpx;">{{ item.productName || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">询价时间</text>
|
||||
<text>{{ formatDate(item.inquiryTime) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">截止时间</text>
|
||||
<text>{{ formatDate(item.deadline) }}</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-around mt-12rpx pt-16rpx border-t border-[#f0f0f0]">
|
||||
<view class="text-center">
|
||||
<view class="text-30rpx text-[#333] font-semibold">{{ formatCount(item.requireCount) }}</view>
|
||||
<view class="mt-4rpx text-22rpx text-[#999]">需求数量</view>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<view class="text-30rpx text-[#e6a23c] font-semibold">¥{{ formatPrice(item.budgetPrice) }}</view>
|
||||
<view class="mt-4rpx text-22rpx text-[#999]">预算金额</view>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<view class="text-30rpx text-[#409eff] font-semibold">{{ item.quoteCount || 0 }}</view>
|
||||
<view class="mt-4rpx text-22rpx text-[#999]">报价数</view>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<view class="text-30rpx text-[#52c41a] font-semibold">¥{{ formatPrice(item.minQuotePrice) }}</view>
|
||||
<view class="mt-4rpx text-22rpx text-[#999]">最低报价</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex flex-wrap gap-12rpx px-24rpx pb-20rpx" @click.stop>
|
||||
<wd-button v-if="hasAccessByCodes(['erp:purchase-inquiry:query'])" size="small" plain @click="handleDetail(item)">
|
||||
详情
|
||||
</wd-button>
|
||||
<wd-button v-if="hasAccessByCodes(['erp:purchase-inquiry:update'])" size="small" type="primary" plain @click="handlePublish(item)">
|
||||
邀请
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update']) && item.status !== 30"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleEdit(item)"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update-status']) && item.status === 10"
|
||||
size="small"
|
||||
type="success"
|
||||
plain
|
||||
@click="handleUpdateStatus(item.id!, 20)"
|
||||
>
|
||||
询价
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update']) && item.status === 20 && (item.quoteCount || 0) >= 2"
|
||||
size="small"
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleCompare(item)"
|
||||
>
|
||||
比价
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:update-status']) && item.status === 20"
|
||||
size="small"
|
||||
type="success"
|
||||
plain
|
||||
@click="handleUpdateStatus(item.id!, 30)"
|
||||
>
|
||||
完成
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:delete'])"
|
||||
size="small"
|
||||
type="error"
|
||||
plain
|
||||
@click="handleDelete(item.id!)"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无采购询比数据" />
|
||||
</view>
|
||||
<wd-loadmore v-if="list.length > 0" :state="loadMoreState" @reload="loadMore" />
|
||||
</view>
|
||||
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['erp:purchase-inquiry:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
|
||||
<wd-popup v-model="searchVisible" position="top" @close="searchVisible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">询比单号</view>
|
||||
<wd-input v-model="searchForm.no" placeholder="请输入询比单号" clearable />
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">产品</view>
|
||||
<wd-picker v-model="searchForm.productId" :columns="productColumns" placeholder="请选择产品" @confirm="onProductConfirm" />
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">状态</view>
|
||||
<wd-radio-group v-model="searchForm.status" shape="button">
|
||||
<wd-radio :value="-1">全部</wd-radio>
|
||||
<wd-radio :value="10">待询价</wd-radio>
|
||||
<wd-radio :value="20">询价中</wd-radio>
|
||||
<wd-radio :value="30">已完成</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">创建人</view>
|
||||
<wd-picker v-model="searchForm.creator" :columns="userColumns" placeholder="请选择创建人" @confirm="onUserConfirm" />
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">重置</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">搜索</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PurchaseInquiry } from '@/api/erp/purchase-inquiry'
|
||||
import type { ProductSimple } from '@/api/erp/product'
|
||||
import type { User } from '@/api/system/user'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deletePurchaseInquiry, getPurchaseInquiryPage, updatePurchaseInquiryStatus } from '@/api/erp/purchase-inquiry'
|
||||
import { getProductSimpleList } from '@/api/erp/product'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { getNavbarHeight, navigateBackPlus } from '@/utils'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<PurchaseInquiry[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref<{ pageNo: number; pageSize: number } & Record<string, any>>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const searchVisible = ref(false)
|
||||
const productList = ref<ProductSimple[]>([])
|
||||
const userList = ref<User[]>([])
|
||||
const searchForm = reactive({
|
||||
no: undefined as string | undefined,
|
||||
productId: undefined as number | undefined,
|
||||
status: -1,
|
||||
creator: undefined as number | undefined,
|
||||
})
|
||||
|
||||
const productColumns = computed(() => [[{ label: '全部', value: '' }, ...productList.value.map(v => ({ label: v.name, value: v.id }))]])
|
||||
const userColumns = computed(() => [[{ label: '全部', value: '' }, ...userList.value.map(v => ({ label: v.nickname, value: v.id }))]])
|
||||
const searchPlaceholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (searchForm.no) conditions.push(`单号:${searchForm.no}`)
|
||||
if (searchForm.productId) {
|
||||
const product = productList.value.find(v => v.id === searchForm.productId)
|
||||
conditions.push(`产品:${product?.name || searchForm.productId}`)
|
||||
}
|
||||
if (searchForm.status !== -1) conditions.push(`状态:${getStatusText(searchForm.status)}`)
|
||||
if (searchForm.creator) {
|
||||
const user = userList.value.find(v => v.id === searchForm.creator)
|
||||
conditions.push(`创建人:${user?.nickname || searchForm.creator}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索采购询比'
|
||||
})
|
||||
|
||||
function formatDate(value?: string | number | Date) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value).slice(0, 10)
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${month}-${day}`
|
||||
}
|
||||
|
||||
function formatPrice(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatCount(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function getStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return '待询价'
|
||||
case 20: return '询价中'
|
||||
case 30: return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return 'bg-[#eef3ff] text-[#3f7cff]'
|
||||
case 20: return 'bg-[#fff7e8] text-[#d48806]'
|
||||
case 30: return 'bg-[#f0f9eb] text-[#52c41a]'
|
||||
default: return 'bg-[#f5f5f5] text-[#999]'
|
||||
}
|
||||
}
|
||||
|
||||
function onProductConfirm({ value }: any) {
|
||||
searchForm.productId = value?.[0] ? Number(value[0]) : undefined
|
||||
}
|
||||
|
||||
function onUserConfirm({ value }: any) {
|
||||
searchForm.creator = value?.[0] ? Number(value[0]) : undefined
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getPurchaseInquiryPage({ ...queryParams.value })
|
||||
list.value = [...list.value, ...(data.list || [])]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchVisible.value = false
|
||||
queryParams.value = {
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
no: searchForm.no,
|
||||
productId: searchForm.productId,
|
||||
status: searchForm.status === -1 ? undefined : searchForm.status,
|
||||
creator: searchForm.creator,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.no = undefined
|
||||
searchForm.productId = undefined
|
||||
searchForm.status = -1
|
||||
searchForm.creator = undefined
|
||||
searchVisible.value = false
|
||||
queryParams.value = { pageNo: 1, pageSize: 10 }
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') return
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleCardClick(item: PurchaseInquiry) {
|
||||
if (item.status === 30) {
|
||||
handleDetail(item)
|
||||
} else {
|
||||
handleEdit(item)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uni.navigateTo({ url: '/pages-erp/purchase-inquiry/form/index' })
|
||||
}
|
||||
|
||||
function handleDetail(item: PurchaseInquiry) {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/detail/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
function handleEdit(item: PurchaseInquiry) {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/form/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
function handlePublish(item: PurchaseInquiry) {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/publish/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
function handleCompare(item: PurchaseInquiry) {
|
||||
uni.navigateTo({ url: `/pages-erp/purchase-inquiry/compare/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
function handleUpdateStatus(id: number, status: number) {
|
||||
const actionText = status === 20 ? '开始询价' : '完成'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定${actionText}该询比单吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await updatePurchaseInquiryStatus(id, status)
|
||||
toast.success(`${actionText}成功`)
|
||||
list.value = []
|
||||
queryParams.value.pageNo = 1
|
||||
getList()
|
||||
} catch {
|
||||
// http handled
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该采购询比吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await deletePurchaseInquiry([id])
|
||||
toast.success('删除成功')
|
||||
list.value = []
|
||||
queryParams.value.pageNo = 1
|
||||
getList()
|
||||
} catch {
|
||||
// http handled
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const [products, users] = await Promise.all([getProductSimpleList(), getSimpleUserList()])
|
||||
productList.value = products || []
|
||||
userList.value = users || []
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
218
src/pages-erp/purchase-inquiry/publish/index.vue
Normal file
218
src/pages-erp/purchase-inquiry/publish/index.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar title="询比邀请" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
|
||||
|
||||
<view class="erp-page__body">
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">询比信息</view>
|
||||
<view class="erp-field-row"><text>询比单号</text><text>{{ inquiry?.no || '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>产品</text><text>{{ inquiry?.productName || '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>状态</text><text>{{ getStatusText(inquiry?.status) }}</text></view>
|
||||
<view class="erp-field-row"><text>截止时间</text><text>{{ formatDateTime(inquiry?.deadline) }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">邀请设置</view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="链接失效时间" title-width="180rpx" center>
|
||||
<wd-datetime-picker v-model="formData.linkExpireTime" type="datetime" label="" placeholder="请选择链接失效时间" />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="mt-24rpx text-26rpx text-[#666]">邀请供应商</view>
|
||||
<view class="mt-16rpx">
|
||||
<wd-checkbox-group v-model="formData.supplierIds" cell shape="button">
|
||||
<wd-checkbox v-for="item in supplierList" :key="item.id" :model-value="item.id">
|
||||
{{ item.name }}
|
||||
</wd-checkbox>
|
||||
</wd-checkbox-group>
|
||||
</view>
|
||||
<view class="mt-24rpx">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">发布 / 重新生成链接</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">邀请概览</view>
|
||||
<view class="grid grid-cols-2 gap-16rpx">
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">已邀请</view>
|
||||
<view class="erp-metric-card__value">{{ invitationList.length }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">已查看</view>
|
||||
<view class="erp-metric-card__value">{{ summary.viewedCount }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">已报价</view>
|
||||
<view class="erp-metric-card__value">{{ summary.quotedCount }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">待报价</view>
|
||||
<view class="erp-metric-card__value">{{ summary.pendingCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="erp-section__title">邀请记录</view>
|
||||
<view v-if="invitationList.length">
|
||||
<view v-for="item in invitationList" :key="item.id" class="erp-form-item-card erp-form-item-card--padded">
|
||||
<view class="mb-12rpx flex items-center justify-between">
|
||||
<text class="erp-card-title">{{ item.supplierName || '-' }}</text>
|
||||
<text class="text-24rpx" :class="getInvitationStatusClass(item.status)">{{ getInvitationStatusText(item.status) }}</text>
|
||||
</view>
|
||||
<view class="erp-field-row"><text>报价单价</text><text>{{ item.unitPrice != null ? `¥${Number(item.unitPrice).toFixed(2)}` : '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>报价总额</text><text>{{ item.totalPrice != null ? `¥${Number(item.totalPrice).toFixed(2)}` : '-' }}</text></view>
|
||||
<view class="erp-field-row"><text>预计到货</text><text>{{ formatDateTime(item.expectedArrivalDate) }}</text></view>
|
||||
<view class="erp-field-row"><text>报价时间</text><text>{{ formatDateTime(item.quoteDate) }}</text></view>
|
||||
<view class="erp-field-row"><text>首次查看</text><text>{{ formatDateTime(item.firstViewTime) }}</text></view>
|
||||
<view class="erp-field-row"><text>提交来源</text><text>{{ item.submitSource || '-' }}</text></view>
|
||||
<view class="mt-16rpx flex gap-12rpx">
|
||||
<wd-button size="small" plain @click="copyLink(item.portalUrl)">复制链接</wd-button>
|
||||
<wd-button v-if="item.qualificationFile" size="small" type="primary" plain @click="copyLink(item.qualificationFile)">复制附件地址</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="erp-empty">暂无邀请记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PurchaseInquiry, PurchaseInquiryInvitation, PurchaseInquiryPublishReq } from '@/api/erp/purchase-inquiry'
|
||||
import type { SupplierSimple } from '@/api/erp/supplier'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getPurchaseInquiry, getPurchaseInquiryInvitationList, publishPurchaseInquiry } from '@/api/erp/purchase-inquiry'
|
||||
import { getSupplierSimpleList } from '@/api/erp/supplier'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{ id?: number | any }>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const inquiry = ref<PurchaseInquiry>()
|
||||
const supplierList = ref<SupplierSimple[]>([])
|
||||
const invitationList = ref<PurchaseInquiryInvitation[]>([])
|
||||
const submitting = ref(false)
|
||||
|
||||
type PublishFormModel = Omit<PurchaseInquiryPublishReq, 'linkExpireTime'> & {
|
||||
linkExpireTime?: string | number
|
||||
}
|
||||
|
||||
const formData = reactive<PublishFormModel>({
|
||||
inquiryId: 0,
|
||||
supplierIds: [],
|
||||
linkExpireTime: undefined,
|
||||
})
|
||||
|
||||
const summary = computed(() => {
|
||||
const viewedCount = invitationList.value.filter(v => [20, 30].includes(Number(v.status))).length
|
||||
const quotedCount = invitationList.value.filter(v => !!v.quoteId || Number(v.status) === 30).length
|
||||
const pendingCount = invitationList.value.filter(v => Number(v.status) === 10).length
|
||||
return { viewedCount, quotedCount, pendingCount }
|
||||
})
|
||||
|
||||
function getStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return '待询价'
|
||||
case 20: return '询价中'
|
||||
case 30: return '已完成'
|
||||
default: return '-'
|
||||
}
|
||||
}
|
||||
|
||||
function getInvitationStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 10: return '待报价'
|
||||
case 20: return '已查看'
|
||||
case 30: return '已报价'
|
||||
case 40: return '已失效'
|
||||
case 50: return '已作废'
|
||||
default: return '-'
|
||||
}
|
||||
}
|
||||
|
||||
function getInvitationStatusClass(status?: number) {
|
||||
switch (status) {
|
||||
case 20: return 'text-[#d48806]'
|
||||
case 30: return 'text-[#52c41a]'
|
||||
case 40: return 'text-[#f5222d]'
|
||||
default: return 'text-[#999]'
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | number | Date) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
const hour = `${date.getHours()}`.padStart(2, '0')
|
||||
const minute = `${date.getMinutes()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-erp/purchase-inquiry/detail/index?id=${props.id}`)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!props.id) return
|
||||
formData.inquiryId = Number(props.id)
|
||||
const [detail, suppliers, invitations] = await Promise.all([
|
||||
getPurchaseInquiry(Number(props.id)),
|
||||
getSupplierSimpleList(),
|
||||
getPurchaseInquiryInvitationList(Number(props.id)),
|
||||
])
|
||||
inquiry.value = detail
|
||||
supplierList.value = suppliers || []
|
||||
invitationList.value = invitations || []
|
||||
formData.supplierIds = invitationList.value.map(v => Number(v.supplierId)).filter(Boolean)
|
||||
formData.linkExpireTime = (invitationList.value[0]?.linkExpireTime || detail?.deadline) as string | number | undefined
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.supplierIds.length) {
|
||||
toast.warning('请至少选择一个供应商')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await publishPurchaseInquiry({
|
||||
inquiryId: formData.inquiryId,
|
||||
supplierIds: formData.supplierIds,
|
||||
linkExpireTime: formData.linkExpireTime,
|
||||
} as PurchaseInquiryPublishReq)
|
||||
toast.success('询比邀请已发布')
|
||||
await loadData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink(url?: string) {
|
||||
if (!url) {
|
||||
toast.warning('暂无可复制链接')
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => toast.success('链接已复制'),
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
271
src/pages-erp/purchase-statistics/index.vue
Normal file
271
src/pages-erp/purchase-statistics/index.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<view class="yd-page-container erp-page">
|
||||
<wd-navbar title="采购统计" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
|
||||
|
||||
<view class="erp-page__body">
|
||||
<view class="grid grid-cols-2 gap-16rpx">
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">今日采购</view>
|
||||
<view class="erp-metric-card__value">¥{{ formatPrice(summary?.todayPrice) }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">昨日采购</view>
|
||||
<view class="erp-metric-card__value">¥{{ formatPrice(summary?.yesterdayPrice) }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">本月采购</view>
|
||||
<view class="erp-metric-card__value">¥{{ formatPrice(summary?.monthPrice) }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">今年采购</view>
|
||||
<view class="erp-metric-card__value">¥{{ formatPrice(summary?.yearPrice) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="erp-section__title !mb-0 !p-0 !border-0">采购趋势</view>
|
||||
<wd-radio-group v-model="trendType" shape="button" @change="handleTrendTypeChange">
|
||||
<wd-radio value="week">按周</wd-radio>
|
||||
<wd-radio value="month">按月</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="grid grid-cols-2 gap-16rpx mb-16rpx">
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">净采购金额</view>
|
||||
<view class="erp-metric-card__value">¥{{ formatPrice(trendAnalysis.totalPrice) }}</view>
|
||||
</view>
|
||||
<view class="erp-metric-card">
|
||||
<view class="erp-metric-card__label">净采购数量</view>
|
||||
<view class="erp-metric-card__value">{{ formatCount(trendAnalysis.totalCount) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="trendList.length">
|
||||
<view v-for="item in trendList" :key="item.time" class="erp-form-item-card erp-form-item-card--padded">
|
||||
<view class="mb-8rpx flex items-center justify-between">
|
||||
<text class="erp-card-title">{{ item.time }}</text>
|
||||
<text class="text-24rpx text-[#999]">数量 {{ formatCount(item.count) }}</text>
|
||||
</view>
|
||||
<view class="text-32rpx font-semibold text-[#1890ff]">¥{{ formatPrice(item.price) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="erp-empty">暂无趋势数据</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="erp-section__title !mb-0 !p-0 !border-0">产品采购统计</view>
|
||||
<wd-radio-group v-model="productSortBy" shape="button" @change="getProductRank">
|
||||
<wd-radio value="price">按金额</wd-radio>
|
||||
<wd-radio value="count">按数量</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="mb-16rpx flex gap-16rpx">
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">开始日期</text>
|
||||
<wd-datetime-picker v-model="productDateRange[0]" type="date" label="" placeholder="开始日期" />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">结束日期</text>
|
||||
<wd-datetime-picker v-model="productDateRange[1]" type="date" label="" placeholder="结束日期" />
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="primary" plain block @click="getProductRank">刷新产品榜单</wd-button>
|
||||
<view v-if="productRankList.length" class="mt-16rpx">
|
||||
<view v-for="(item, index) in productRankList" :key="item.productId || index" class="erp-form-item-card erp-form-item-card--padded">
|
||||
<view class="mb-8rpx flex items-center justify-between">
|
||||
<text class="erp-card-title">#{{ index + 1 }} {{ item.productName }}</text>
|
||||
<text class="text-24rpx text-[#999]">占比 {{ formatPercent(item.shareRate) }}</text>
|
||||
</view>
|
||||
<view class="erp-field-row"><text>净采购金额</text><text>¥{{ formatPrice(item.price) }}</text></view>
|
||||
<view class="erp-field-row"><text>净采购数量</text><text>{{ formatCount(item.count) }}</text></view>
|
||||
<view class="erp-field-row"><text>退货金额</text><text>¥{{ formatPrice(item.returnPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>退货数量</text><text>{{ formatCount(item.returnCount) }}</text></view>
|
||||
<view class="erp-field-row"><text>均价</text><text>¥{{ formatPrice(item.avgPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>单据数</text><text>{{ item.orderCount || 0 }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="erp-empty">暂无产品统计数据</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="erp-section__title !mb-0 !p-0 !border-0">供应商采购统计</view>
|
||||
<wd-radio-group v-model="supplierSortBy" shape="button" @change="getSupplierRank">
|
||||
<wd-radio value="price">按金额</wd-radio>
|
||||
<wd-radio value="count">按数量</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="mb-16rpx flex gap-16rpx">
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">开始日期</text>
|
||||
<wd-datetime-picker v-model="supplierDateRange[0]" type="date" label="" placeholder="开始日期" />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="mb-8rpx block text-24rpx text-[#999]">结束日期</text>
|
||||
<wd-datetime-picker v-model="supplierDateRange[1]" type="date" label="" placeholder="结束日期" />
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="primary" plain block @click="getSupplierRank">刷新供应商榜单</wd-button>
|
||||
<view v-if="supplierRankList.length" class="mt-16rpx">
|
||||
<view v-for="(item, index) in supplierRankList" :key="item.supplierId || index" class="erp-form-item-card erp-form-item-card--padded">
|
||||
<view class="mb-8rpx flex items-center justify-between">
|
||||
<text class="erp-card-title">#{{ index + 1 }} {{ item.supplierName }}</text>
|
||||
<text class="text-24rpx text-[#999]">占比 {{ formatPercent(item.shareRate) }}</text>
|
||||
</view>
|
||||
<view class="erp-field-row"><text>净采购金额</text><text>¥{{ formatPrice(item.price) }}</text></view>
|
||||
<view class="erp-field-row"><text>净采购数量</text><text>{{ formatCount(item.count) }}</text></view>
|
||||
<view class="erp-field-row"><text>退货金额</text><text>¥{{ formatPrice(item.returnPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>退货数量</text><text>{{ formatCount(item.returnCount) }}</text></view>
|
||||
<view class="erp-field-row"><text>均价</text><text>¥{{ formatPrice(item.avgPrice) }}</text></view>
|
||||
<view class="erp-field-row"><text>单据数</text><text>{{ item.orderCount || 0 }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="erp-empty">暂无供应商统计数据</view>
|
||||
</view>
|
||||
|
||||
<view class="erp-section">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="erp-section__title !mb-0 !p-0 !border-0">智能分析</view>
|
||||
<wd-button type="primary" size="small" :loading="aiLoading" @click="analyzeWithAI">生成分析</wd-button>
|
||||
</view>
|
||||
<view v-if="aiResult" class="text-26rpx leading-[1.7] text-[#666] whitespace-pre-wrap">{{ aiResult }}</view>
|
||||
<view v-else class="erp-empty">暂无分析内容</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PurchaseProductRank, PurchaseSummary, PurchaseSupplierRank, PurchaseTrend } from '@/api/erp/purchase-statistics'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import {
|
||||
analyzePurchaseStatistics,
|
||||
getPurchaseProductRank,
|
||||
getPurchaseSummary,
|
||||
getPurchaseSupplierRank,
|
||||
getPurchaseTrendByMonth,
|
||||
getPurchaseTrendByWeek,
|
||||
} from '@/api/erp/purchase-statistics'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const summary = ref<PurchaseSummary>()
|
||||
const trendList = ref<PurchaseTrend[]>([])
|
||||
const productRankList = ref<PurchaseProductRank[]>([])
|
||||
const supplierRankList = ref<PurchaseSupplierRank[]>([])
|
||||
const trendType = ref<'week' | 'month'>('month')
|
||||
const productSortBy = ref<'price' | 'count'>('price')
|
||||
const supplierSortBy = ref<'price' | 'count'>('price')
|
||||
const aiLoading = ref(false)
|
||||
const aiResult = ref('')
|
||||
|
||||
const now = new Date()
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1).getTime()
|
||||
const productDateRange = ref<(number | string | undefined)[]>([startOfYear, Date.now()])
|
||||
const supplierDateRange = ref<(number | string | undefined)[]>([startOfYear, Date.now()])
|
||||
|
||||
const trendAnalysis = computed(() => {
|
||||
const prices = trendList.value.map(v => Number(v.price || 0))
|
||||
const counts = trendList.value.map(v => Number(v.count || 0))
|
||||
return {
|
||||
totalPrice: prices.reduce((sum, item) => sum + item, 0),
|
||||
totalCount: counts.reduce((sum, item) => sum + item, 0),
|
||||
}
|
||||
})
|
||||
|
||||
function formatPrice(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatCount(value?: number) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function formatPercent(value?: number) {
|
||||
return `${Number(value || 0).toFixed(2)}%`
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | number | Date) {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${month}-${day} 00:00:00`
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
async function getTrendData() {
|
||||
trendList.value = trendType.value === 'week'
|
||||
? await getPurchaseTrendByWeek(12)
|
||||
: await getPurchaseTrendByMonth(12)
|
||||
}
|
||||
|
||||
async function getProductRank() {
|
||||
const [beginTime, endTime] = productDateRange.value
|
||||
if (!beginTime || !endTime) {
|
||||
toast.warning('请先选择完整的产品统计时间')
|
||||
return
|
||||
}
|
||||
productRankList.value = await getPurchaseProductRank(formatDateTime(beginTime), formatDateTime(endTime).replace('00:00:00', '23:59:59'), 10, productSortBy.value)
|
||||
}
|
||||
|
||||
async function getSupplierRank() {
|
||||
const [beginTime, endTime] = supplierDateRange.value
|
||||
if (!beginTime || !endTime) {
|
||||
toast.warning('请先选择完整的供应商统计时间')
|
||||
return
|
||||
}
|
||||
supplierRankList.value = await getPurchaseSupplierRank(formatDateTime(beginTime), formatDateTime(endTime).replace('00:00:00', '23:59:59'), 10, supplierSortBy.value)
|
||||
}
|
||||
|
||||
async function handleTrendTypeChange() {
|
||||
await getTrendData()
|
||||
}
|
||||
|
||||
async function analyzeWithAI() {
|
||||
aiLoading.value = true
|
||||
try {
|
||||
const result = await analyzePurchaseStatistics({
|
||||
summary: summary.value,
|
||||
trendData: trendList.value,
|
||||
productRankList: productRankList.value,
|
||||
supplierRankList: supplierRankList.value,
|
||||
trendType: trendType.value,
|
||||
productRankSortBy: productSortBy.value,
|
||||
supplierRankSortBy: supplierSortBy.value,
|
||||
})
|
||||
aiResult.value = result?.data || result || ''
|
||||
} catch {
|
||||
aiResult.value = [
|
||||
'采购数据分析',
|
||||
`1. 当前累计净采购金额为 ¥${formatPrice(trendAnalysis.value.totalPrice)}。`,
|
||||
`2. 当前累计净采购数量为 ${formatCount(trendAnalysis.value.totalCount)}。`,
|
||||
`3. 产品榜首为 ${productRankList.value[0]?.productName || '暂无'}。`,
|
||||
`4. 供应商榜首为 ${supplierRankList.value[0]?.supplierName || '暂无'}。`,
|
||||
].join('\n')
|
||||
} finally {
|
||||
aiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
summary.value = await getPurchaseSummary()
|
||||
await Promise.all([getTrendData(), getProductRank(), getSupplierRank()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user