first commit
This commit is contained in:
381
src/views/erp/purchase/evaluation/EvaluationHistoryDialog.vue
Normal file
381
src/views/erp/purchase/evaluation/EvaluationHistoryDialog.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="100%" fullscreen class="mobile-history-dialog">
|
||||
<div v-loading="loading" class="mobile-history-wrapper">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="mobile-history-filter">
|
||||
<el-select v-model="queryParams.scoreRange" placeholder="评分范围" clearable @change="loadHistoryData" size="small" style="flex:1">
|
||||
<el-option label="优秀 (9-10)" value="excellent" />
|
||||
<el-option label="良好 (8-8.9)" value="good" />
|
||||
<el-option label="一般 (7-7.9)" value="average" />
|
||||
<el-option label="及格 (6-6.9)" value="pass" />
|
||||
<el-option label="不及格 (<6)" value="fail" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="loadHistoryData" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="mobile-history-stats">
|
||||
<div class="mobile-history-stats__item">
|
||||
<div class="mobile-history-stats__val">{{ historyStats.totalCount }}</div>
|
||||
<div class="mobile-history-stats__label">总评价次数</div>
|
||||
</div>
|
||||
<div class="mobile-history-stats__item">
|
||||
<div class="mobile-history-stats__val" style="color:#409eff">{{ historyStats.avgScore }}</div>
|
||||
<div class="mobile-history-stats__label">平均评分</div>
|
||||
</div>
|
||||
<div class="mobile-history-stats__item">
|
||||
<div class="mobile-history-stats__val" style="color:#67c23a">{{ historyStats.bestScore }}</div>
|
||||
<div class="mobile-history-stats__label">最高评分</div>
|
||||
</div>
|
||||
<div class="mobile-history-stats__item">
|
||||
<div class="mobile-history-stats__val" style="color:#e6a23c">{{ historyStats.recentTrend }}</div>
|
||||
<div class="mobile-history-stats__label">近期趋势</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评价历史卡片列表 -->
|
||||
<div class="mobile-history-list">
|
||||
<div v-if="historyList.length === 0 && !loading" style="padding:40px 0">
|
||||
<el-empty description="暂无评价历史" />
|
||||
</div>
|
||||
<div v-for="item in historyList" :key="item.id" class="mobile-history-card" @click="viewEvaluationDetail(item)">
|
||||
<div class="mobile-history-card__header">
|
||||
<span class="mobile-history-card__order">{{ item.orderNo }}</span>
|
||||
<el-tag :type="getScoreTagType(item.totalScore)" size="small">
|
||||
{{ item.totalScore }}分 · {{ getScoreLevel(item.totalScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-history-card__scores">
|
||||
<div class="mobile-history-card__score-item">
|
||||
<el-progress :percentage="(item.qualityScore / 10) * 100" :color="getProgressColor(item.qualityScore)" :show-text="false" :stroke-width="6" />
|
||||
<span>质量 {{ item.qualityScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-history-card__score-item">
|
||||
<el-progress :percentage="(item.serviceScore / 10) * 100" :color="getProgressColor(item.serviceScore)" :show-text="false" :stroke-width="6" />
|
||||
<span>服务 {{ item.serviceScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-history-card__score-item">
|
||||
<el-progress :percentage="(item.priceScore / 10) * 100" :color="getProgressColor(item.priceScore)" :show-text="false" :stroke-width="6" />
|
||||
<span>价格 {{ item.priceScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-history-card__score-item">
|
||||
<el-progress :percentage="(item.deliveryScore / 10) * 100" :color="getProgressColor(item.deliveryScore)" :show-text="false" :stroke-width="6" />
|
||||
<span>交付 {{ item.deliveryScore }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-history-card__footer">
|
||||
<span class="mobile-history-card__meta">{{ formatDate(item.createTime) }} · {{ item.creatorName }}</span>
|
||||
<span class="mobile-history-card__remark">{{ item.remark || '无备注' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mobile-pagination" v-if="total > 0">
|
||||
<el-pagination v-model:current-page="queryParams.pageNo" v-model:page-size="queryParams.pageSize" :total="total" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @size-change="loadHistoryData" @current-change="loadHistoryData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="mobile-form-footer">
|
||||
<el-button @click="dialogVisible = false" style="flex:1">关闭</el-button>
|
||||
<el-button type="primary" @click="generateReport" style="flex:1">生成报告</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 评价详情弹窗 -->
|
||||
<SupplierEvaluationDetail ref="evaluationDetailRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation'
|
||||
import SupplierEvaluationDetail from './SupplierEvaluationDetail.vue'
|
||||
|
||||
interface EvaluationHistory {
|
||||
id: number
|
||||
orderId: number
|
||||
orderNo: string
|
||||
totalScore: number
|
||||
qualityScore: number
|
||||
serviceScore: number
|
||||
priceScore: number
|
||||
deliveryScore: number
|
||||
remark: string
|
||||
createTime: string
|
||||
creatorName: string
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const supplierId = ref(0)
|
||||
const supplierName = ref('')
|
||||
const historyList = ref<EvaluationHistory[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
dateRange: [],
|
||||
scoreRange: undefined
|
||||
})
|
||||
|
||||
const historyStats = ref({
|
||||
totalCount: 0,
|
||||
avgScore: '0.0',
|
||||
bestScore: '0.0',
|
||||
recentTrend: '稳定'
|
||||
})
|
||||
|
||||
// 打开历史弹窗
|
||||
const open = async (id: number, name: string) => {
|
||||
dialogVisible.value = true
|
||||
supplierId.value = id
|
||||
supplierName.value = name
|
||||
dialogTitle.value = `${name} - 评价历史`
|
||||
|
||||
// 重置查询参数
|
||||
queryParams.pageNo = 1
|
||||
queryParams.dateRange = []
|
||||
queryParams.scoreRange = undefined
|
||||
|
||||
await loadHistoryData()
|
||||
}
|
||||
|
||||
// 加载历史数据
|
||||
const loadHistoryData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用 - 实际应该调用后端API
|
||||
const mockData = await generateMockHistoryData()
|
||||
historyList.value = mockData.list
|
||||
total.value = mockData.total
|
||||
historyStats.value = mockData.stats
|
||||
} catch (error) {
|
||||
console.error('加载历史数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟历史数据
|
||||
const generateMockHistoryData = async () => {
|
||||
const count = Math.floor(Math.random() * 30) + 10
|
||||
const list: EvaluationHistory[] = []
|
||||
|
||||
for (let i = 0; i < Math.min(count, queryParams.pageSize); i++) {
|
||||
const totalScore = Number((Math.random() * 4 + 6).toFixed(1))
|
||||
list.push({
|
||||
id: i + 1,
|
||||
orderId: 1000 + i,
|
||||
orderNo: `PO${String(1000 + i).padStart(6, '0')}`,
|
||||
totalScore,
|
||||
qualityScore: Number((Math.random() * 3 + 7).toFixed(1)),
|
||||
serviceScore: Number((Math.random() * 3 + 7).toFixed(1)),
|
||||
priceScore: Number((Math.random() * 3 + 7).toFixed(1)),
|
||||
deliveryScore: Number((Math.random() * 3 + 7).toFixed(1)),
|
||||
remark: Math.random() > 0.5 ? '服务态度很好,产品质量符合要求' : '',
|
||||
createTime: new Date(Date.now() - Math.random() * 180 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
creatorName: ['张三', '李四', '王五', '赵六'][Math.floor(Math.random() * 4)]
|
||||
})
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
list.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
|
||||
|
||||
const totalCount = count
|
||||
const avgScore = (list.reduce((sum, item) => sum + item.totalScore, 0) / list.length).toFixed(1)
|
||||
const bestScore = Math.max(...list.map(item => item.totalScore)).toFixed(1)
|
||||
const recentTrend = ['上升', '下降', '稳定'][Math.floor(Math.random() * 3)]
|
||||
|
||||
return {
|
||||
list,
|
||||
total: totalCount,
|
||||
stats: {
|
||||
totalCount,
|
||||
avgScore,
|
||||
bestScore,
|
||||
recentTrend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出历史数据
|
||||
const exportHistory = async () => {
|
||||
exportLoading.value = true
|
||||
try {
|
||||
// 这里应该调用实际的导出API
|
||||
console.log('导出历史数据:', historyList.value)
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const viewOrderDetail = (orderId: number) => {
|
||||
// 这里可以跳转到订单详情页面或打开订单详情弹窗
|
||||
console.log('查看订单详情:', orderId)
|
||||
}
|
||||
|
||||
// 查看评价详情
|
||||
const evaluationDetailRef = ref()
|
||||
const viewEvaluationDetail = (row: EvaluationHistory) => {
|
||||
const evaluationData = {
|
||||
...row,
|
||||
supplierName: supplierName.value
|
||||
}
|
||||
evaluationDetailRef.value.open(evaluationData)
|
||||
}
|
||||
|
||||
// 对比评价
|
||||
const compareEvaluation = (row: EvaluationHistory) => {
|
||||
// 这里可以实现评价对比功能
|
||||
console.log('对比评价:', row)
|
||||
}
|
||||
|
||||
// 生成评价报告
|
||||
const generateReport = () => {
|
||||
// 这里可以生成PDF报告或其他格式的报告
|
||||
console.log('生成评价报告')
|
||||
}
|
||||
|
||||
// 获取评分标签类型
|
||||
const getScoreTagType = (score: number) => {
|
||||
if (score >= 9) return 'success'
|
||||
if (score >= 8) return ''
|
||||
if (score >= 7) return 'warning'
|
||||
if (score >= 6) return 'info'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
// 获取评分样式类
|
||||
const getScoreClass = (score: number) => {
|
||||
if (score >= 9) return 'score-excellent'
|
||||
if (score >= 8) return 'score-good'
|
||||
if (score >= 7) return 'score-average'
|
||||
if (score >= 6) return 'score-pass'
|
||||
return 'score-fail'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (score: number) => {
|
||||
if (score >= 9) return '#67c23a'
|
||||
if (score >= 8) return '#409eff'
|
||||
if (score >= 7) return '#e6a23c'
|
||||
if (score >= 6) return '#909399'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-history-wrapper {
|
||||
padding: 0 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mobile-history-filter {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mobile-history-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 8px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__item { text-align: center; }
|
||||
&__val { font-size: 18px; font-weight: 700; color: #303133; }
|
||||
&__label { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
}
|
||||
.mobile-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.mobile-history-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
&__order {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
&__scores {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
&__score-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&__meta {
|
||||
color: #909399;
|
||||
}
|
||||
&__remark {
|
||||
color: #c0c4cc;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 50%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.mobile-pagination {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
|
||||
}
|
||||
.mobile-form-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
:deep(.mobile-history-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
330
src/views/erp/purchase/evaluation/SupplierDetailDialog.vue
Normal file
330
src/views/erp/purchase/evaluation/SupplierDetailDialog.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="供应商详细信息" width="100%" fullscreen class="mobile-supplier-detail-dialog">
|
||||
<div v-loading="loading" class="mobile-detail-wrapper">
|
||||
<!-- 供应商基本信息 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">供应商基本信息</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">供应商名称</span>
|
||||
<span class="mobile-detail-value">{{ supplierData.supplierName }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">评价次数</span>
|
||||
<span class="mobile-detail-value">{{ supplierData.totalEvaluations }}次</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">综合评分</span>
|
||||
<span class="mobile-detail-value" :class="getScoreClass(supplierData.avgTotalScore)" style="font-weight:700">
|
||||
{{ supplierData.avgTotalScore }}分
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">评价等级</span>
|
||||
<el-tag :type="getScoreTagType(supplierData.avgTotalScore)" size="small">
|
||||
{{ getScoreLevel(supplierData.avgTotalScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">最近评价</span>
|
||||
<span class="mobile-detail-value">{{ formatDate(supplierData.lastEvaluationTime) }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">评价趋势</span>
|
||||
<span :class="getTrendClass(supplierData.trend)">{{ getTrendText(supplierData.trend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 各维度评分详情 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">各维度评分详情</div>
|
||||
<div class="mobile-dimension-item">
|
||||
<div class="mobile-dimension-item__header">
|
||||
<span>质量评分</span>
|
||||
<span class="mobile-dimension-item__score">{{ supplierData.avgQualityScore }}分</span>
|
||||
</div>
|
||||
<el-progress :percentage="(supplierData.avgQualityScore / 10) * 100" :color="getProgressColor(supplierData.avgQualityScore)" :stroke-width="10" />
|
||||
<div class="mobile-dimension-item__desc">产品质量、规格符合度、缺陷率等</div>
|
||||
</div>
|
||||
<div class="mobile-dimension-item">
|
||||
<div class="mobile-dimension-item__header">
|
||||
<span>服务评分</span>
|
||||
<span class="mobile-dimension-item__score">{{ supplierData.avgServiceScore }}分</span>
|
||||
</div>
|
||||
<el-progress :percentage="(supplierData.avgServiceScore / 10) * 100" :color="getProgressColor(supplierData.avgServiceScore)" :stroke-width="10" />
|
||||
<div class="mobile-dimension-item__desc">售前售后服务、响应速度、专业程度等</div>
|
||||
</div>
|
||||
<div class="mobile-dimension-item">
|
||||
<div class="mobile-dimension-item__header">
|
||||
<span>价格评分</span>
|
||||
<span class="mobile-dimension-item__score">{{ supplierData.avgPriceScore }}分</span>
|
||||
</div>
|
||||
<el-progress :percentage="(supplierData.avgPriceScore / 10) * 100" :color="getProgressColor(supplierData.avgPriceScore)" :stroke-width="10" />
|
||||
<div class="mobile-dimension-item__desc">价格合理性、性价比、优惠政策等</div>
|
||||
</div>
|
||||
<div class="mobile-dimension-item">
|
||||
<div class="mobile-dimension-item__header">
|
||||
<span>交付评分</span>
|
||||
<span class="mobile-dimension-item__score">{{ supplierData.avgDeliveryScore }}分</span>
|
||||
</div>
|
||||
<el-progress :percentage="(supplierData.avgDeliveryScore / 10) * 100" :color="getProgressColor(supplierData.avgDeliveryScore)" :stroke-width="10" />
|
||||
<div class="mobile-dimension-item__desc">交付及时性、包装质量、物流配送等</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 近期评价趋势 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">近期评价趋势</div>
|
||||
<div class="mobile-trend-stats">
|
||||
<div class="mobile-trend-stat">
|
||||
<div class="mobile-trend-stat__val">{{ trendData.thisMonth }}次</div>
|
||||
<div class="mobile-trend-stat__label">本月评价</div>
|
||||
</div>
|
||||
<div class="mobile-trend-stat">
|
||||
<div class="mobile-trend-stat__val">{{ trendData.lastMonth }}次</div>
|
||||
<div class="mobile-trend-stat__label">上月评价</div>
|
||||
</div>
|
||||
<div class="mobile-trend-stat">
|
||||
<div class="mobile-trend-stat__val" :class="getTrendClass(supplierData.trend)">{{ trendData.change }}%</div>
|
||||
<div class="mobile-trend-stat__label">评价变化</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="mobile-form-footer">
|
||||
<el-button @click="dialogVisible = false" style="flex:1">关闭</el-button>
|
||||
<el-button type="primary" @click="viewEvaluationHistory" style="flex:1">查看评价历史</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
interface SupplierSummary {
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
totalEvaluations: number
|
||||
avgTotalScore: number
|
||||
avgQualityScore: number
|
||||
avgServiceScore: number
|
||||
avgPriceScore: number
|
||||
avgDeliveryScore: number
|
||||
lastEvaluationTime: string
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const supplierData = ref<SupplierSummary>({} as SupplierSummary)
|
||||
const trendData = ref({
|
||||
thisMonth: 0,
|
||||
lastMonth: 0,
|
||||
change: 0
|
||||
})
|
||||
|
||||
// 打开详情弹窗
|
||||
const open = (data: SupplierSummary) => {
|
||||
dialogVisible.value = true
|
||||
supplierData.value = { ...data }
|
||||
loadTrendData()
|
||||
}
|
||||
|
||||
// 加载趋势数据
|
||||
const loadTrendData = () => {
|
||||
// 模拟趋势数据
|
||||
const thisMonth = Math.floor(Math.random() * 10) + 5
|
||||
const lastMonth = Math.floor(Math.random() * 10) + 5
|
||||
const change = Math.round(((thisMonth - lastMonth) / lastMonth) * 100)
|
||||
|
||||
trendData.value = {
|
||||
thisMonth,
|
||||
lastMonth,
|
||||
change
|
||||
}
|
||||
}
|
||||
|
||||
// 获取评分标签类型
|
||||
const getScoreTagType = (score: number) => {
|
||||
if (score >= 9) return 'success'
|
||||
if (score >= 8) return ''
|
||||
if (score >= 7) return 'warning'
|
||||
if (score >= 6) return 'info'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
// 获取评分样式类
|
||||
const getScoreClass = (score: number) => {
|
||||
if (score >= 9) return 'score-excellent'
|
||||
if (score >= 8) return 'score-good'
|
||||
if (score >= 7) return 'score-average'
|
||||
if (score >= 6) return 'score-pass'
|
||||
return 'score-fail'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (score: number) => {
|
||||
if (score >= 9) return '#67c23a'
|
||||
if (score >= 8) return '#409eff'
|
||||
if (score >= 7) return '#e6a23c'
|
||||
if (score >= 6) return '#909399'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 获取趋势图标
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return 'ep:trend-charts'
|
||||
case 'down': return 'ep:bottom'
|
||||
case 'stable': return 'ep:minus'
|
||||
default: return 'ep:minus'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取趋势样式类
|
||||
const getTrendClass = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return 'trend-up'
|
||||
case 'down': return 'trend-down'
|
||||
case 'stable': return 'trend-stable'
|
||||
default: return 'trend-stable'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取趋势文本
|
||||
const getTrendText = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return '上升'
|
||||
case 'down': return '下降'
|
||||
case 'stable': return '稳定'
|
||||
default: return '稳定'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看评价历史
|
||||
const viewEvaluationHistory = () => {
|
||||
// 触发父组件的评价历史查看
|
||||
emit('viewHistory', supplierData.value.supplierId, supplierData.value.supplierName)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
viewHistory: [supplierId: number, supplierName: string]
|
||||
}>()
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-detail-wrapper {
|
||||
padding: 0 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mobile-detail-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
.mobile-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mobile-detail-label {
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.mobile-detail-value {
|
||||
color: #303133;
|
||||
text-align: right;
|
||||
}
|
||||
.score-excellent { color: #67c23a; }
|
||||
.score-good { color: #409eff; }
|
||||
.score-average { color: #e6a23c; }
|
||||
.score-pass { color: #909399; }
|
||||
.score-fail { color: #f56c6c; }
|
||||
.trend-up { color: #67c23a; }
|
||||
.trend-down { color: #f56c6c; }
|
||||
.trend-stable { color: #909399; }
|
||||
.mobile-dimension-item {
|
||||
margin-bottom: 16px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
&__score {
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
&__desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
.mobile-trend-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 8px;
|
||||
}
|
||||
.mobile-trend-stat {
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
&__val {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
.mobile-form-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
:deep(.mobile-supplier-detail-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
252
src/views/erp/purchase/evaluation/SupplierEvaluationDetail.vue
Normal file
252
src/views/erp/purchase/evaluation/SupplierEvaluationDetail.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="供应商评价详情" width="100%" fullscreen class="mobile-eval-detail-dialog">
|
||||
<div v-loading="loading" class="mobile-detail-wrapper">
|
||||
<!-- 基本信息 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">基本信息</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">供应商</span>
|
||||
<span class="mobile-detail-value">{{ evaluationData.supplierName }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">采购订单号</span>
|
||||
<span class="mobile-detail-value">{{ evaluationData.orderNo }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">评价时间</span>
|
||||
<span class="mobile-detail-value">{{ formatDate(evaluationData.createTime) }}</span>
|
||||
</div>
|
||||
<div class="mobile-detail-row">
|
||||
<span class="mobile-detail-label">评价人</span>
|
||||
<span class="mobile-detail-value">{{ evaluationData.creatorName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评分详情 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">评分详情</div>
|
||||
<div class="mobile-score-item">
|
||||
<div class="mobile-score-item__header">
|
||||
<span class="mobile-score-item__name">质量评分</span>
|
||||
<el-tag :type="getScoreTagType(evaluationData.qualityScore)" size="small">
|
||||
{{ evaluationData.qualityScore }}分 · {{ getScoreLevel(evaluationData.qualityScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-score-item__desc">产品质量、规格符合度、缺陷率等</div>
|
||||
</div>
|
||||
<div class="mobile-score-item">
|
||||
<div class="mobile-score-item__header">
|
||||
<span class="mobile-score-item__name">服务评分</span>
|
||||
<el-tag :type="getScoreTagType(evaluationData.serviceScore)" size="small">
|
||||
{{ evaluationData.serviceScore }}分 · {{ getScoreLevel(evaluationData.serviceScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-score-item__desc">售前售后服务、响应速度、专业程度等</div>
|
||||
</div>
|
||||
<div class="mobile-score-item">
|
||||
<div class="mobile-score-item__header">
|
||||
<span class="mobile-score-item__name">价格评分</span>
|
||||
<el-tag :type="getScoreTagType(evaluationData.priceScore)" size="small">
|
||||
{{ evaluationData.priceScore }}分 · {{ getScoreLevel(evaluationData.priceScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-score-item__desc">价格合理性、性价比、优惠政策等</div>
|
||||
</div>
|
||||
<div class="mobile-score-item">
|
||||
<div class="mobile-score-item__header">
|
||||
<span class="mobile-score-item__name">交付评分</span>
|
||||
<el-tag :type="getScoreTagType(evaluationData.deliveryScore)" size="small">
|
||||
{{ evaluationData.deliveryScore }}分 · {{ getScoreLevel(evaluationData.deliveryScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-score-item__desc">交付及时性、包装质量、物流配送等</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 综合评分 -->
|
||||
<div class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">综合评分</div>
|
||||
<div class="mobile-total-score">
|
||||
<el-progress
|
||||
type="circle"
|
||||
:percentage="(evaluationData.totalScore / 10) * 100"
|
||||
:color="getProgressColor(evaluationData.totalScore)"
|
||||
:width="90"
|
||||
>
|
||||
<template #default>
|
||||
<span class="mobile-total-score__value">{{ evaluationData.totalScore }}</span>
|
||||
<span class="mobile-total-score__label">/ 10分</span>
|
||||
</template>
|
||||
</el-progress>
|
||||
<div class="mobile-total-score__info">
|
||||
<div class="mobile-total-score__big">{{ evaluationData.totalScore }}分</div>
|
||||
<el-tag :type="getScoreTagType(evaluationData.totalScore)" size="large">
|
||||
{{ getScoreLevel(evaluationData.totalScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评价备注 -->
|
||||
<div v-if="evaluationData.remark" class="mobile-detail-section">
|
||||
<div class="mobile-detail-section__title">评价备注</div>
|
||||
<div class="mobile-remark">{{ evaluationData.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false" style="width:100%">关闭</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const evaluationData = ref<SupplierEvaluationVO>({} as SupplierEvaluationVO)
|
||||
|
||||
// 打开详情弹窗
|
||||
const open = (data: SupplierEvaluationVO) => {
|
||||
dialogVisible.value = true
|
||||
evaluationData.value = { ...data }
|
||||
}
|
||||
|
||||
// 获取评分标签类型
|
||||
const getScoreTagType = (score: number) => {
|
||||
if (score >= 9) return 'success'
|
||||
if (score >= 8) return ''
|
||||
if (score >= 7) return 'warning'
|
||||
if (score >= 6) return 'info'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (score: number) => {
|
||||
if (score >= 9) return '#67c23a'
|
||||
if (score >= 8) return '#409eff'
|
||||
if (score >= 7) return '#e6a23c'
|
||||
if (score >= 6) return '#909399'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-detail-wrapper {
|
||||
padding: 0 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mobile-detail-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
.mobile-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mobile-detail-label {
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.mobile-detail-value {
|
||||
color: #303133;
|
||||
text-align: right;
|
||||
}
|
||||
.mobile-score-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
&:last-child { border-bottom: none; }
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&__name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
&__desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
.mobile-total-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 10px 0;
|
||||
justify-content: center;
|
||||
&__value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
display: block;
|
||||
margin-top: -4px;
|
||||
}
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__big {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.mobile-remark {
|
||||
padding: 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #409eff;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.mobile-eval-detail-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.el-progress-circle__text) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
273
src/views/erp/purchase/evaluation/SupplierEvaluationForm.vue
Normal file
273
src/views/erp/purchase/evaluation/SupplierEvaluationForm.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="100%" fullscreen class="mobile-eval-form-dialog">
|
||||
<div class="mobile-form-wrapper" v-loading="formLoading">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
|
||||
<div class="mobile-form-section">
|
||||
<div class="mobile-form-section__title">基本信息</div>
|
||||
<el-form-item label="供应商" prop="supplierName">
|
||||
<el-input v-model="formData.supplierName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单单号" prop="orderNo">
|
||||
<el-input v-model="formData.orderNo" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mobile-form-section">
|
||||
<div class="mobile-form-section__title">评分标准(满分10分)</div>
|
||||
<el-form-item label="质量评分" prop="qualityScore">
|
||||
<div class="rating-container">
|
||||
<el-rate v-model="formData.qualityScore" :max="10" show-score score-template="{value}分" allow-half />
|
||||
<div class="rating-desc">产品质量、规格符合度、缺陷率等</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务评分" prop="serviceScore">
|
||||
<div class="rating-container">
|
||||
<el-rate v-model="formData.serviceScore" :max="10" show-score score-template="{value}分" allow-half />
|
||||
<div class="rating-desc">售前售后服务、响应速度、专业程度等</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格评分" prop="priceScore">
|
||||
<div class="rating-container">
|
||||
<el-rate v-model="formData.priceScore" :max="10" show-score score-template="{value}分" allow-half />
|
||||
<div class="rating-desc">价格合理性、性价比、优惠政策等</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="交付评分" prop="deliveryScore">
|
||||
<div class="rating-container">
|
||||
<el-rate v-model="formData.deliveryScore" :max="10" show-score score-template="{value}分" allow-half />
|
||||
<div class="rating-desc">交付及时性、包装质量、物流配送等</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mobile-form-section">
|
||||
<div class="mobile-form-section__title">综合评分</div>
|
||||
<div class="total-score">
|
||||
<span class="score-value">{{ totalScore.toFixed(1) }}分</span>
|
||||
<span class="score-level">{{ getScoreLevel(totalScore) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-form-section">
|
||||
<div class="mobile-form-section__title">评价备注</div>
|
||||
<el-form-item prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入评价备注(可选)" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="mobile-form-footer">
|
||||
<el-button @click="dialogVisible = false" style="flex:1">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="formLoading" style="flex:1">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation'
|
||||
|
||||
interface SupplierEvaluationForm {
|
||||
id?: number
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
purchaseOrderId: number
|
||||
orderNo: string
|
||||
qualityScore: number
|
||||
serviceScore: number
|
||||
priceScore: number
|
||||
deliveryScore: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
const formRef = ref()
|
||||
const formData = ref<SupplierEvaluationForm>({
|
||||
supplierId: 0,
|
||||
supplierName: '',
|
||||
purchaseOrderId: 0,
|
||||
orderNo: '',
|
||||
qualityScore: 5,
|
||||
serviceScore: 5,
|
||||
priceScore: 5,
|
||||
deliveryScore: 5,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = reactive({
|
||||
qualityScore: [{ required: true, message: '请评价质量分数', trigger: 'change' }],
|
||||
serviceScore: [{ required: true, message: '请评价服务分数', trigger: 'change' }],
|
||||
priceScore: [{ required: true, message: '请评价价格分数', trigger: 'change' }],
|
||||
deliveryScore: [{ required: true, message: '请评价交付分数', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 计算综合评分
|
||||
const totalScore = computed(() => {
|
||||
return (formData.value.qualityScore + formData.value.serviceScore +
|
||||
formData.value.priceScore + formData.value.deliveryScore) / 4
|
||||
})
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = type === 'create' ? '新增供应商评价' : '修改供应商评价'
|
||||
formType.value = type
|
||||
resetForm()
|
||||
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = await SupplierEvaluationApi.getSupplierEvaluation(id)
|
||||
formData.value = {
|
||||
...data,
|
||||
supplierName: data.supplierName || '',
|
||||
orderNo: data.orderNo || ''
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
supplierId: 0,
|
||||
supplierName: '',
|
||||
purchaseOrderId: 0,
|
||||
orderNo: '',
|
||||
qualityScore: 5,
|
||||
serviceScore: 5,
|
||||
priceScore: 5,
|
||||
deliveryScore: 5,
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
formLoading.value = true
|
||||
const data = {
|
||||
...formData.value,
|
||||
totalScore: totalScore.value
|
||||
}
|
||||
|
||||
if (formType.value === 'create') {
|
||||
await SupplierEvaluationApi.createSupplierEvaluation(data)
|
||||
message.success('新增成功')
|
||||
} else {
|
||||
await SupplierEvaluationApi.updateSupplierEvaluation(data)
|
||||
message.success('修改成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('操作失败', error)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-form-wrapper {
|
||||
padding: 0 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mobile-form-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
.mobile-form-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.rating-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.rating-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.total-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.score-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
.score-level {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: #f0f9ff;
|
||||
color: #409eff;
|
||||
border: 1px solid #b3d8ff;
|
||||
}
|
||||
:deep(.el-rate) {
|
||||
height: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
:deep(.el-rate__text) {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
:deep(.mobile-eval-form-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
</style>
|
||||
346
src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue
Normal file
346
src/views/erp/purchase/evaluation/SupplierScoreDashboard.vue
Normal file
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="mobile-dashboard">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="mobile-dashboard-filter">
|
||||
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="选择供应商" @change="getSupplierStats" size="small" style="flex:1">
|
||||
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="getAllSuppliersStats">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div v-if="selectedSupplierStats" class="mobile-dashboard-stats">
|
||||
<div class="mobile-dashboard-stats__item">
|
||||
<div class="mobile-dashboard-stats__val">{{ selectedSupplierStats.totalEvaluations }}</div>
|
||||
<div class="mobile-dashboard-stats__label">总评价次数</div>
|
||||
</div>
|
||||
<div class="mobile-dashboard-stats__item">
|
||||
<div class="mobile-dashboard-stats__val" style="color:#409eff">{{ selectedSupplierStats.avgTotalScore }}</div>
|
||||
<div class="mobile-dashboard-stats__label">平均综合评分</div>
|
||||
</div>
|
||||
<div class="mobile-dashboard-stats__item">
|
||||
<div class="mobile-dashboard-stats__val" style="color:#67c23a">{{ selectedSupplierStats.avgQualityScore }}</div>
|
||||
<div class="mobile-dashboard-stats__label">平均质量评分</div>
|
||||
</div>
|
||||
<div class="mobile-dashboard-stats__item">
|
||||
<div class="mobile-dashboard-stats__val" style="color:#e6a23c">{{ selectedSupplierStats.avgServiceScore }}</div>
|
||||
<div class="mobile-dashboard-stats__label">平均服务评分</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 供应商排行榜 -->
|
||||
<div class="mobile-dashboard-section">
|
||||
<div class="mobile-dashboard-section__title">综合评分排行榜 (TOP 10)</div>
|
||||
<div class="mobile-ranking-list">
|
||||
<div v-for="(item, index) in topSuppliers" :key="item.supplierId" class="mobile-ranking-item">
|
||||
<div class="mobile-ranking-item__rank">
|
||||
<el-tag v-if="index < 3" :type="getRankingTagType(index)" size="small">{{ index + 1 }}</el-tag>
|
||||
<span v-else class="mobile-ranking-item__rank-num">{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="mobile-ranking-item__info">
|
||||
<div class="mobile-ranking-item__name">{{ item.supplierName }}</div>
|
||||
<div class="mobile-ranking-item__meta">{{ item.totalEvaluations }}次评价</div>
|
||||
</div>
|
||||
<div class="mobile-ranking-item__score">
|
||||
<div class="mobile-ranking-item__score-val">{{ item.avgTotalScore }}</div>
|
||||
<el-tag :type="getScoreTagType(item.avgTotalScore)" size="small">
|
||||
{{ getScoreLevel(item.avgTotalScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="topSuppliers.length === 0" style="padding:30px 0; text-align:center">
|
||||
<el-empty description="暂无排行数据" :image-size="60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评分分布统计 -->
|
||||
<div class="mobile-dashboard-section">
|
||||
<div class="mobile-dashboard-section__title">评分分布统计</div>
|
||||
<div class="mobile-distribution">
|
||||
<div class="mobile-distribution__item">
|
||||
<div class="mobile-distribution__header">
|
||||
<span class="mobile-distribution__label">优秀 (9-10)</span>
|
||||
<span class="mobile-distribution__count">{{ excellentCount }} 个</span>
|
||||
</div>
|
||||
<el-progress :percentage="scoreDistribution.excellent" color="#67c23a" :stroke-width="10" />
|
||||
</div>
|
||||
<div class="mobile-distribution__item">
|
||||
<div class="mobile-distribution__header">
|
||||
<span class="mobile-distribution__label">良好 (8-8.9)</span>
|
||||
<span class="mobile-distribution__count">{{ goodCount }} 个</span>
|
||||
</div>
|
||||
<el-progress :percentage="scoreDistribution.good" color="#409eff" :stroke-width="10" />
|
||||
</div>
|
||||
<div class="mobile-distribution__item">
|
||||
<div class="mobile-distribution__header">
|
||||
<span class="mobile-distribution__label">一般 (7-7.9)</span>
|
||||
<span class="mobile-distribution__count">{{ averageCount }} 个</span>
|
||||
</div>
|
||||
<el-progress :percentage="scoreDistribution.average" color="#e6a23c" :stroke-width="10" />
|
||||
</div>
|
||||
<div class="mobile-distribution__item">
|
||||
<div class="mobile-distribution__header">
|
||||
<span class="mobile-distribution__label">及格 (6-6.9)</span>
|
||||
<span class="mobile-distribution__count">{{ passCount }} 个</span>
|
||||
</div>
|
||||
<el-progress :percentage="scoreDistribution.pass" color="#909399" :stroke-width="10" />
|
||||
</div>
|
||||
<div class="mobile-distribution__item">
|
||||
<div class="mobile-distribution__header">
|
||||
<span class="mobile-distribution__label">不及格 (<6)</span>
|
||||
<span class="mobile-distribution__count">{{ failCount }} 个</span>
|
||||
</div>
|
||||
<el-progress :percentage="scoreDistribution.fail" color="#f56c6c" :stroke-width="10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation'
|
||||
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
|
||||
|
||||
/** 供应商评分统计仪表板 */
|
||||
defineOptions({ name: 'SupplierScoreDashboard' })
|
||||
|
||||
const loading = ref(false)
|
||||
const supplierList = ref<SupplierVO[]>([])
|
||||
const allSuppliersStats = ref([])
|
||||
const selectedSupplierStats = ref(null)
|
||||
const topSuppliers = ref([])
|
||||
|
||||
const queryParams = reactive({
|
||||
supplierId: undefined
|
||||
})
|
||||
|
||||
// 评分分布统计
|
||||
const scoreDistribution = ref({
|
||||
excellent: 0,
|
||||
good: 0,
|
||||
average: 0,
|
||||
pass: 0,
|
||||
fail: 0
|
||||
})
|
||||
|
||||
const excellentCount = ref(0)
|
||||
const goodCount = ref(0)
|
||||
const averageCount = ref(0)
|
||||
const passCount = ref(0)
|
||||
const failCount = ref(0)
|
||||
|
||||
// 获取所有供应商统计
|
||||
const getAllSuppliersStats = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const suppliers = await SupplierApi.getSupplierSimpleList()
|
||||
const statsPromises = suppliers.map(async (supplier) => {
|
||||
try {
|
||||
const stats = await SupplierEvaluationApi.getSupplierAverageScore(supplier.id)
|
||||
return stats ? { ...stats, supplierName: supplier.name } : null
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(statsPromises)
|
||||
allSuppliersStats.value = results.filter(item => item && item.totalEvaluations > 0)
|
||||
|
||||
// 计算排行榜
|
||||
topSuppliers.value = allSuppliersStats.value
|
||||
.sort((a, b) => b.avgTotalScore - a.avgTotalScore)
|
||||
.slice(0, 10)
|
||||
|
||||
// 计算评分分布
|
||||
calculateScoreDistribution()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个供应商统计
|
||||
const getSupplierStats = async () => {
|
||||
if (!queryParams.supplierId) {
|
||||
selectedSupplierStats.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await SupplierEvaluationApi.getSupplierAverageScore(queryParams.supplierId)
|
||||
const supplier = supplierList.value.find(s => s.id === queryParams.supplierId)
|
||||
selectedSupplierStats.value = stats ? { ...stats, supplierName: supplier?.name } : null
|
||||
} catch (error) {
|
||||
selectedSupplierStats.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 计算评分分布
|
||||
const calculateScoreDistribution = () => {
|
||||
const total = allSuppliersStats.value.length
|
||||
if (total === 0) return
|
||||
|
||||
excellentCount.value = allSuppliersStats.value.filter(item => item.avgTotalScore >= 9.0).length
|
||||
goodCount.value = allSuppliersStats.value.filter(item => item.avgTotalScore >= 8.0 && item.avgTotalScore < 9.0).length
|
||||
averageCount.value = allSuppliersStats.value.filter(item => item.avgTotalScore >= 7.0 && item.avgTotalScore < 8.0).length
|
||||
passCount.value = allSuppliersStats.value.filter(item => item.avgTotalScore >= 6.0 && item.avgTotalScore < 7.0).length
|
||||
failCount.value = allSuppliersStats.value.filter(item => item.avgTotalScore < 6.0).length
|
||||
|
||||
scoreDistribution.value = {
|
||||
excellent: Math.round((excellentCount.value / total) * 100),
|
||||
good: Math.round((goodCount.value / total) * 100),
|
||||
average: Math.round((averageCount.value / total) * 100),
|
||||
pass: Math.round((passCount.value / total) * 100),
|
||||
fail: Math.round((failCount.value / total) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取排名标签类型
|
||||
const getRankingTagType = (index: number) => {
|
||||
if (index === 0) return 'warning' // 金牌
|
||||
if (index === 1) return 'info' // 银牌
|
||||
if (index === 2) return 'success' // 铜牌
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取评分标签类型
|
||||
const getScoreTagType = (score: number) => {
|
||||
if (score >= 9) return 'success'
|
||||
if (score >= 8) return ''
|
||||
if (score >= 7) return 'warning'
|
||||
if (score >= 6) return 'info'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 加载供应商列表
|
||||
supplierList.value = await SupplierApi.getSupplierSimpleList()
|
||||
// 获取统计数据
|
||||
await getAllSuppliersStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-dashboard {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.mobile-dashboard-filter {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mobile-dashboard-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
&__item {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
&__val { font-size: 20px; font-weight: 700; color: #303133; }
|
||||
&__label { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
}
|
||||
.mobile-dashboard-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
.mobile-ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mobile-ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
&:last-child { border-bottom: none; }
|
||||
&__rank {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
&__rank-num {
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&__meta {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
&__score {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&__score-val {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
.mobile-distribution {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
&__item { }
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
616
src/views/erp/purchase/evaluation/SupplierSummaryTable.vue
Normal file
616
src/views/erp/purchase/evaluation/SupplierSummaryTable.vue
Normal file
@@ -0,0 +1,616 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="供应商评价汇总表" width="100%" fullscreen class="mobile-summary-dialog">
|
||||
<div v-loading="loading" class="mobile-summary-wrapper">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="mobile-summary-section">
|
||||
<div class="mobile-summary-filter">
|
||||
<el-select v-model="queryParams.sortBy" @change="loadSummaryData" placeholder="排序方式" size="small" style="flex:1">
|
||||
<el-option label="综合评分" value="avgTotalScore" />
|
||||
<el-option label="评价次数" value="totalEvaluations" />
|
||||
<el-option label="质量评分" value="avgQualityScore" />
|
||||
<el-option label="服务评分" value="avgServiceScore" />
|
||||
<el-option label="价格评分" value="avgPriceScore" />
|
||||
<el-option label="交付评分" value="avgDeliveryScore" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" :icon="Refresh" circle @click="refreshData" :loading="loading" />
|
||||
<el-button type="warning" size="small" @click="analyzeWithAI" :loading="aiAnalysisLoading">AI分析</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 汇总统计卡片 -->
|
||||
<div class="mobile-summary-stats">
|
||||
<div class="mobile-summary-stats__item">
|
||||
<div class="mobile-summary-stats__val">{{ summaryStats.totalSuppliers }}</div>
|
||||
<div class="mobile-summary-stats__label">供应商总数</div>
|
||||
</div>
|
||||
<div class="mobile-summary-stats__item">
|
||||
<div class="mobile-summary-stats__val" style="color:#409eff">{{ summaryStats.totalEvaluations }}</div>
|
||||
<div class="mobile-summary-stats__label">评价总次数</div>
|
||||
</div>
|
||||
<div class="mobile-summary-stats__item">
|
||||
<div class="mobile-summary-stats__val" style="color:#67c23a">{{ summaryStats.avgScore }}</div>
|
||||
<div class="mobile-summary-stats__label">整体平均分</div>
|
||||
</div>
|
||||
<div class="mobile-summary-stats__item">
|
||||
<div class="mobile-summary-stats__val" style="color:#e6a23c">{{ summaryStats.excellentRate }}%</div>
|
||||
<div class="mobile-summary-stats__label">优秀占比</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 供应商卡片列表 -->
|
||||
<div class="mobile-summary-list">
|
||||
<div v-if="summaryList.length === 0 && !loading" style="padding:40px 0">
|
||||
<el-empty description="暂无汇总数据" />
|
||||
</div>
|
||||
<div v-for="(item, index) in summaryList" :key="item.supplierId" class="mobile-summary-card">
|
||||
<div class="mobile-summary-card__header">
|
||||
<div class="mobile-summary-card__rank">
|
||||
<el-tag v-if="index < 3" :type="getRankingTagType(index)" size="small">{{ index + 1 }}</el-tag>
|
||||
<span v-else class="mobile-summary-card__rank-num">{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="mobile-summary-card__name">
|
||||
{{ item.supplierName }}
|
||||
<el-tag v-if="item.avgTotalScore >= 9" type="success" size="small" style="margin-left:6px">优秀</el-tag>
|
||||
</div>
|
||||
<div class="mobile-summary-card__total" :class="getScoreClass(item.avgTotalScore)">
|
||||
{{ item.avgTotalScore }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-summary-card__body">
|
||||
<div class="mobile-summary-card__row">
|
||||
<span class="mobile-summary-card__label">评价次数</span>
|
||||
<el-tag type="info" size="small">{{ item.totalEvaluations }}次</el-tag>
|
||||
</div>
|
||||
<div class="mobile-summary-card__scores">
|
||||
<div class="mobile-summary-card__score-item">
|
||||
<el-progress :percentage="(item.avgQualityScore / 10) * 100" :color="getProgressColor(item.avgQualityScore)" :show-text="false" :stroke-width="6" />
|
||||
<span class="mobile-summary-card__score-text">质量 {{ item.avgQualityScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-summary-card__score-item">
|
||||
<el-progress :percentage="(item.avgServiceScore / 10) * 100" :color="getProgressColor(item.avgServiceScore)" :show-text="false" :stroke-width="6" />
|
||||
<span class="mobile-summary-card__score-text">服务 {{ item.avgServiceScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-summary-card__score-item">
|
||||
<el-progress :percentage="(item.avgPriceScore / 10) * 100" :color="getProgressColor(item.avgPriceScore)" :show-text="false" :stroke-width="6" />
|
||||
<span class="mobile-summary-card__score-text">价格 {{ item.avgPriceScore }}</span>
|
||||
</div>
|
||||
<div class="mobile-summary-card__score-item">
|
||||
<el-progress :percentage="(item.avgDeliveryScore / 10) * 100" :color="getProgressColor(item.avgDeliveryScore)" :show-text="false" :stroke-width="6" />
|
||||
<span class="mobile-summary-card__score-text">交付 {{ item.avgDeliveryScore }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-summary-card__row">
|
||||
<span class="mobile-summary-card__label">最近评价</span>
|
||||
<span class="mobile-summary-card__value">{{ formatDate(item.lastEvaluationTime) }}</span>
|
||||
</div>
|
||||
<div class="mobile-summary-card__row">
|
||||
<span class="mobile-summary-card__label">趋势</span>
|
||||
<span :class="getTrendClass(item.trend)">{{ getTrendText(item.trend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false" style="width:100%">关闭</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 供应商详情弹窗 -->
|
||||
<SupplierDetailDialog ref="supplierDetailRef" />
|
||||
|
||||
<!-- 评价历史弹窗 -->
|
||||
<EvaluationHistoryDialog ref="evaluationHistoryRef" />
|
||||
|
||||
<!-- AI分析结果弹窗 -->
|
||||
<Dialog v-model="showAnalysisDialog" title="AI分析报告" width="100%" fullscreen class="mobile-analysis-dialog">
|
||||
<div class="mobile-analysis-content">
|
||||
<div v-html="formatAnalysisText(aiAnalysisResult)"></div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="mobile-form-footer">
|
||||
<el-button @click="showAnalysisDialog = false" style="flex:1">关闭</el-button>
|
||||
<el-button type="primary" @click="copyAnalysisResult" style="flex:1">复制结果</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import SupplierDetailDialog from './SupplierDetailDialog.vue'
|
||||
import EvaluationHistoryDialog from './EvaluationHistoryDialog.vue'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
interface SupplierSummary {
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
totalEvaluations: number
|
||||
avgTotalScore: number
|
||||
avgQualityScore: number
|
||||
avgServiceScore: number
|
||||
avgPriceScore: number
|
||||
avgDeliveryScore: number
|
||||
lastEvaluationTime: string
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const fullscreen = ref(false)
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const aiAnalysisLoading = ref(false)
|
||||
const aiAnalysisResult = ref('')
|
||||
const showAnalysisDialog = ref(false)
|
||||
const summaryList = ref<SupplierSummary[]>([])
|
||||
|
||||
const queryParams = reactive({
|
||||
dateRange: [],
|
||||
minEvaluations: 1,
|
||||
sortBy: 'avgTotalScore'
|
||||
})
|
||||
|
||||
const summaryStats = ref({
|
||||
totalSuppliers: 0,
|
||||
totalEvaluations: 0,
|
||||
avgScore: '0.0',
|
||||
excellentRate: 0
|
||||
})
|
||||
|
||||
// 打开弹窗
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
await loadSummaryData()
|
||||
}
|
||||
|
||||
// 加载汇总数据
|
||||
const loadSummaryData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 调用后端API获取供应商汇总数据
|
||||
const data = await SupplierEvaluationApi.getSupplierSummaryList({
|
||||
minEvaluations: queryParams.minEvaluations,
|
||||
sortBy: queryParams.sortBy
|
||||
})
|
||||
|
||||
summaryList.value = data || []
|
||||
|
||||
// 计算统计数据
|
||||
const totalSuppliers = summaryList.value.length
|
||||
const totalEvaluations = summaryList.value.reduce((sum, item) => sum + item.totalEvaluations, 0)
|
||||
const avgScore = totalSuppliers > 0
|
||||
? (summaryList.value.reduce((sum, item) => sum + item.avgTotalScore, 0) / totalSuppliers).toFixed(1)
|
||||
: '0.0'
|
||||
const excellentRate = totalSuppliers > 0
|
||||
? Math.round((summaryList.value.filter(item => item.avgTotalScore >= 9).length / totalSuppliers) * 100)
|
||||
: 0
|
||||
|
||||
summaryStats.value = {
|
||||
totalSuppliers,
|
||||
totalEvaluations,
|
||||
avgScore,
|
||||
excellentRate
|
||||
}
|
||||
|
||||
// 排序数据
|
||||
sortSummaryData()
|
||||
} catch (error) {
|
||||
console.error('加载汇总数据失败:', error)
|
||||
message.error('加载供应商汇总数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 排序数据
|
||||
const sortSummaryData = () => {
|
||||
summaryList.value.sort((a, b) => {
|
||||
const field = queryParams.sortBy as keyof SupplierSummary
|
||||
return (b[field] as number) - (a[field] as number)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格排序
|
||||
const handleSortChange = ({ prop, order }) => {
|
||||
if (order === 'ascending') {
|
||||
summaryList.value.sort((a, b) => (a[prop] as number) - (b[prop] as number))
|
||||
} else {
|
||||
summaryList.value.sort((a, b) => (b[prop] as number) - (a[prop] as number))
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
await loadSummaryData()
|
||||
}
|
||||
|
||||
// 监听查询参数变化,自动刷新数据
|
||||
watch(() => [queryParams.minEvaluations, queryParams.sortBy], () => {
|
||||
if (dialogVisible.value) {
|
||||
loadSummaryData()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 导出汇总表
|
||||
const exportSummary = async () => {
|
||||
exportLoading.value = true
|
||||
try {
|
||||
// 这里应该调用实际的导出API
|
||||
const data = summaryList.value
|
||||
// download.excel(data, '供应商评价汇总表.xlsx')
|
||||
console.log('导出数据:', data)
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取排名标签类型
|
||||
const getRankingTagType = (index: number) => {
|
||||
if (index === 0) return 'warning'
|
||||
if (index === 1) return 'info'
|
||||
if (index === 2) return 'success'
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取评分等级
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
// 获取评分样式类
|
||||
const getScoreClass = (score: number) => {
|
||||
if (score >= 9) return 'score-excellent'
|
||||
if (score >= 8) return 'score-good'
|
||||
if (score >= 7) return 'score-average'
|
||||
if (score >= 6) return 'score-pass'
|
||||
return 'score-fail'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (score: number) => {
|
||||
if (score >= 9) return '#67c23a'
|
||||
if (score >= 8) return '#409eff'
|
||||
if (score >= 7) return '#e6a23c'
|
||||
if (score >= 6) return '#909399'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 获取趋势图标
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return 'ep:trend-charts'
|
||||
case 'down': return 'ep:bottom'
|
||||
case 'stable': return 'ep:minus'
|
||||
default: return 'ep:minus'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取趋势样式类
|
||||
const getTrendClass = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return 'trend-up'
|
||||
case 'down': return 'trend-down'
|
||||
case 'stable': return 'trend-stable'
|
||||
default: return 'trend-stable'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取趋势文本
|
||||
const getTrendText = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return '上升'
|
||||
case 'down': return '下降'
|
||||
case 'stable': return '稳定'
|
||||
default: return '稳定'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看供应商详情
|
||||
const supplierDetailRef = ref()
|
||||
const viewSupplierDetail = (row: SupplierSummary) => {
|
||||
supplierDetailRef.value.open(row)
|
||||
}
|
||||
|
||||
// 查看评价历史
|
||||
const evaluationHistoryRef = ref()
|
||||
const viewEvaluationHistory = (row: SupplierSummary) => {
|
||||
evaluationHistoryRef.value.open(row.supplierId, row.supplierName)
|
||||
}
|
||||
|
||||
// AI分析功能
|
||||
const analyzeWithAI = async () => {
|
||||
aiAnalysisLoading.value = true
|
||||
try {
|
||||
// 准备分析数据
|
||||
const analysisData = {
|
||||
summaryStats: summaryStats.value,
|
||||
supplierData: summaryList.value,
|
||||
queryParams: queryParams
|
||||
}
|
||||
|
||||
// 尝试调用AI分析API
|
||||
try {
|
||||
// 调用实际的AI分析API
|
||||
const aiResult = await SupplierEvaluationApi.analyzeWithAI(analysisData)
|
||||
aiAnalysisResult.value = aiResult.data || aiResult
|
||||
} catch (aiError) {
|
||||
console.warn('AI分析失败,使用文字统计:', aiError)
|
||||
// 生成文字统计分析作为fallback
|
||||
aiAnalysisResult.value = generateTextStatistics(analysisData)
|
||||
}
|
||||
|
||||
showAnalysisDialog.value = true
|
||||
} catch (error) {
|
||||
console.error('分析失败:', error)
|
||||
message.error('分析失败,请稍后重试')
|
||||
} finally {
|
||||
aiAnalysisLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化分析文本为HTML
|
||||
const formatAnalysisText = (text) => {
|
||||
return text
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/^### (.*$)/gm, '<h3 style="color: #409eff; margin: 16px 0 8px 0;">$1</h3>')
|
||||
.replace(/^## (.*$)/gm, '<h2 style="color: #303133; margin: 20px 0 12px 0;">$1</h2>')
|
||||
.replace(/^- (.*$)/gm, '<div style="margin: 4px 0; padding-left: 16px;">• $1</div>')
|
||||
}
|
||||
|
||||
// 复制分析结果
|
||||
const copyAnalysisResult = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(aiAnalysisResult.value)
|
||||
message.success('分析结果已复制到剪贴板')
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文字统计分析
|
||||
const generateTextStatistics = (data) => {
|
||||
const { summaryStats, supplierData } = data
|
||||
|
||||
let analysis = `## 供应商评价汇总分析报告\n\n`
|
||||
|
||||
// 基础统计
|
||||
analysis += `### 📊 基础统计\n`
|
||||
analysis += `- **评价供应商总数**: ${summaryStats.totalSuppliers}家\n`
|
||||
analysis += `- **评价总次数**: ${summaryStats.totalEvaluations}次\n`
|
||||
analysis += `- **整体平均分**: ${summaryStats.avgScore}分\n`
|
||||
analysis += `- **优秀供应商占比**: ${summaryStats.excellentRate}%\n\n`
|
||||
|
||||
// 供应商表现分析
|
||||
if (supplierData.length > 0) {
|
||||
analysis += `### 🏆 供应商表现分析\n`
|
||||
|
||||
// 排名前三的供应商
|
||||
const topThree = supplierData.slice(0, 3)
|
||||
analysis += `**排名前三的供应商**:\n`
|
||||
topThree.forEach((supplier, index) => {
|
||||
const medal = ['🥇', '🥈', '🥉'][index]
|
||||
analysis += `${medal} ${supplier.supplierName} - 综合评分: ${supplier.avgTotalScore}分 (${supplier.totalEvaluations}次评价)\n`
|
||||
})
|
||||
analysis += `\n`
|
||||
|
||||
// 优秀供应商
|
||||
const excellentSuppliers = supplierData.filter(s => s.avgTotalScore >= 9)
|
||||
if (excellentSuppliers.length > 0) {
|
||||
analysis += `**优秀供应商** (评分≥9分): ${excellentSuppliers.length}家\n`
|
||||
excellentSuppliers.forEach(supplier => {
|
||||
analysis += `- ${supplier.supplierName}: ${supplier.avgTotalScore}分\n`
|
||||
})
|
||||
analysis += `\n`
|
||||
}
|
||||
|
||||
// 需要关注的供应商
|
||||
const concernSuppliers = supplierData.filter(s => s.avgTotalScore < 7)
|
||||
if (concernSuppliers.length > 0) {
|
||||
analysis += `**需要关注的供应商** (评分<7分): ${concernSuppliers.length}家\n`
|
||||
concernSuppliers.forEach(supplier => {
|
||||
analysis += `- ${supplier.supplierName}: ${supplier.avgTotalScore}分\n`
|
||||
})
|
||||
analysis += `\n`
|
||||
}
|
||||
}
|
||||
|
||||
// 评分维度分析
|
||||
if (supplierData.length > 0) {
|
||||
analysis += `### 📈 评分维度分析\n`
|
||||
|
||||
const avgQuality = (supplierData.reduce((sum, s) => sum + s.avgQualityScore, 0) / supplierData.length).toFixed(1)
|
||||
const avgService = (supplierData.reduce((sum, s) => sum + s.avgServiceScore, 0) / supplierData.length).toFixed(1)
|
||||
const avgPrice = (supplierData.reduce((sum, s) => sum + s.avgPriceScore, 0) / supplierData.length).toFixed(1)
|
||||
const avgDelivery = (supplierData.reduce((sum, s) => sum + s.avgDeliveryScore, 0) / supplierData.length).toFixed(1)
|
||||
|
||||
analysis += `- **质量评分平均**: ${avgQuality}分\n`
|
||||
analysis += `- **服务评分平均**: ${avgService}分\n`
|
||||
analysis += `- **价格评分平均**: ${avgPrice}分\n`
|
||||
analysis += `- **交付评分平均**: ${avgDelivery}分\n\n`
|
||||
|
||||
// 找出表现最好和最差的维度
|
||||
const dimensions = [
|
||||
{ name: '质量', score: parseFloat(avgQuality) },
|
||||
{ name: '服务', score: parseFloat(avgService) },
|
||||
{ name: '价格', score: parseFloat(avgPrice) },
|
||||
{ name: '交付', score: parseFloat(avgDelivery) }
|
||||
]
|
||||
|
||||
dimensions.sort((a, b) => b.score - a.score)
|
||||
analysis += `**表现最好的维度**: ${dimensions[0].name} (${dimensions[0].score}分)\n`
|
||||
analysis += `**需要改进的维度**: ${dimensions[3].name} (${dimensions[3].score}分)\n\n`
|
||||
}
|
||||
|
||||
// 趋势分析
|
||||
const upTrend = supplierData.filter(s => s.trend === 'up').length
|
||||
const downTrend = supplierData.filter(s => s.trend === 'down').length
|
||||
const stableTrend = supplierData.filter(s => s.trend === 'stable').length
|
||||
|
||||
if (upTrend + downTrend + stableTrend > 0) {
|
||||
analysis += `### 📊 趋势分析\n`
|
||||
analysis += `- **上升趋势**: ${upTrend}家供应商\n`
|
||||
analysis += `- **下降趋势**: ${downTrend}家供应商\n`
|
||||
analysis += `- **稳定趋势**: ${stableTrend}家供应商\n\n`
|
||||
}
|
||||
|
||||
// 建议
|
||||
analysis += `### 💡 建议\n`
|
||||
if (summaryStats.excellentRate < 30) {
|
||||
analysis += `- 优秀供应商占比较低(${summaryStats.excellentRate}%),建议加强供应商培训和管理\n`
|
||||
}
|
||||
if (concernSuppliers && concernSuppliers.length > 0) {
|
||||
analysis += `- 有${concernSuppliers.length}家供应商评分偏低,建议进行重点改进\n`
|
||||
}
|
||||
if (downTrend > upTrend) {
|
||||
analysis += `- 下降趋势供应商较多,建议分析原因并采取改进措施\n`
|
||||
}
|
||||
analysis += `- 建议定期进行供应商评价,持续优化供应商管理体系\n`
|
||||
|
||||
return analysis
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-summary-wrapper {
|
||||
padding: 0 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mobile-summary-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mobile-summary-filter {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.mobile-summary-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 8px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__item { text-align: center; }
|
||||
&__val { font-size: 18px; font-weight: 700; color: #303133; }
|
||||
&__label { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
}
|
||||
.mobile-summary-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.mobile-summary-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
&__rank-num {
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&__total {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&__body {
|
||||
font-size: 13px;
|
||||
}
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
}
|
||||
&__label {
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&__value {
|
||||
color: #606266;
|
||||
}
|
||||
&__scores {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
margin: 4px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
&__score-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
&__score-text {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
.score-excellent { color: #67c23a; }
|
||||
.score-good { color: #409eff; }
|
||||
.score-average { color: #e6a23c; }
|
||||
.score-pass { color: #909399; }
|
||||
.score-fail { color: #f56c6c; }
|
||||
.trend-up { color: #67c23a; }
|
||||
.trend-down { color: #f56c6c; }
|
||||
.trend-stable { color: #909399; }
|
||||
.mobile-form-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.mobile-analysis-content {
|
||||
padding: 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
:deep(.mobile-summary-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.mobile-analysis-dialog .el-dialog__body) {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
342
src/views/erp/purchase/evaluation/index.vue
Normal file
342
src/views/erp/purchase/evaluation/index.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="mobile-evaluation">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="mobile-header">
|
||||
<div class="mobile-header__search">
|
||||
<el-input v-model="queryParams.purchaseOrderNo" placeholder="搜索采购订单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
|
||||
</div>
|
||||
<div class="mobile-header__actions">
|
||||
<el-button :icon="Filter" circle @click="filterVisible = true" />
|
||||
<el-button type="info" circle @click="showStatistics = !showStatistics">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div v-if="showStatistics" class="mobile-stats">
|
||||
<div class="mobile-stats__item">
|
||||
<div class="mobile-stats__val">{{ statistics.totalEvaluations }}</div>
|
||||
<div class="mobile-stats__label">总评价数</div>
|
||||
</div>
|
||||
<div class="mobile-stats__item">
|
||||
<div class="mobile-stats__val" style="color:#409eff">{{ statistics.avgTotalScore }}</div>
|
||||
<div class="mobile-stats__label">平均总分</div>
|
||||
</div>
|
||||
<div class="mobile-stats__item">
|
||||
<div class="mobile-stats__val" style="color:#67c23a">{{ statistics.excellentCount }}</div>
|
||||
<div class="mobile-stats__label">优秀评价</div>
|
||||
</div>
|
||||
<div class="mobile-stats__item">
|
||||
<div class="mobile-stats__val" style="color:#e6a23c">{{ statistics.supplierCount }}</div>
|
||||
<div class="mobile-stats__label">评价供应商</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<div class="mobile-quick-actions">
|
||||
<el-button size="small" type="primary" plain @click="openSupplierSummary" v-hasPermi="['erp:supplier-evaluation:query']">供应商汇总表</el-button>
|
||||
<el-button size="small" type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:supplier-evaluation:export']">导出</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div class="mobile-list" v-loading="loading">
|
||||
<div v-if="list.length === 0 && !loading" class="mobile-empty">
|
||||
<el-empty description="暂无评价记录" />
|
||||
</div>
|
||||
<div v-for="item in list" :key="item.id" class="mobile-card" @click="openDetail(item)">
|
||||
<div class="mobile-card__header">
|
||||
<span class="mobile-card__no">{{ item.supplierName || '-' }}</span>
|
||||
<el-tag :type="getScoreTagType(item.totalScore)" size="small">
|
||||
{{ item.totalScore }}分 · {{ getScoreLevel(item.totalScore) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-card__body">
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">采购订单</span>
|
||||
<span class="mobile-card__value">{{ item.orderNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">评价人</span>
|
||||
<span class="mobile-card__value">{{ item.creatorName || '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__row">
|
||||
<span class="mobile-card__label">备注</span>
|
||||
<span class="mobile-card__value mobile-card__value--ellipsis">{{ item.remark || '无备注' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card__scores">
|
||||
<div class="mobile-card__score-item">
|
||||
<el-tag :type="getScoreTagType(item.qualityScore)" size="small">{{ item.qualityScore }}</el-tag>
|
||||
<div class="mobile-card__score-label">质量</div>
|
||||
</div>
|
||||
<div class="mobile-card__score-item">
|
||||
<el-tag :type="getScoreTagType(item.serviceScore)" size="small">{{ item.serviceScore }}</el-tag>
|
||||
<div class="mobile-card__score-label">服务</div>
|
||||
</div>
|
||||
<div class="mobile-card__score-item">
|
||||
<el-tag :type="getScoreTagType(item.priceScore)" size="small">{{ item.priceScore }}</el-tag>
|
||||
<div class="mobile-card__score-label">价格</div>
|
||||
</div>
|
||||
<div class="mobile-card__score-item">
|
||||
<el-tag :type="getScoreTagType(item.deliveryScore)" size="small">{{ item.deliveryScore }}</el-tag>
|
||||
<div class="mobile-card__score-label">交付</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card__footer">
|
||||
<el-button size="small" @click.stop="openDetail(item)" v-hasPermi="['erp:supplier-evaluation:query']">详情</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:supplier-evaluation:update']">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click.stop="handleDelete([item.id])" v-hasPermi="['erp:supplier-evaluation:delete']">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mobile-pagination" v-if="total > 0">
|
||||
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" />
|
||||
</div>
|
||||
|
||||
<!-- 筛选抽屉 -->
|
||||
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="65%">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
|
||||
<el-form-item label="供应商" prop="supplierId">
|
||||
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="请选择供应商" style="width:100%">
|
||||
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="评价时间" prop="createTime">
|
||||
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评分范围" prop="scoreRange">
|
||||
<el-select v-model="queryParams.scoreRange" placeholder="请选择评分范围" clearable style="width:100%">
|
||||
<el-option label="优秀 (9.0-10.0)" value="excellent" />
|
||||
<el-option label="良好 (8.0-8.9)" value="good" />
|
||||
<el-option label="一般 (7.0-7.9)" value="average" />
|
||||
<el-option label="及格 (6.0-6.9)" value="pass" />
|
||||
<el-option label="不及格 (0-5.9)" value="fail" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建人" prop="creator">
|
||||
<el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" style="width:100%">
|
||||
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 表单弹窗:编辑 -->
|
||||
<SupplierEvaluationForm ref="formRef" @success="getList" />
|
||||
<!-- 详情弹窗 -->
|
||||
<SupplierEvaluationDetail ref="detailRef" />
|
||||
<!-- 供应商汇总表弹窗 -->
|
||||
<SupplierSummaryTable ref="summaryRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search, Filter, DataAnalysis } from '@element-plus/icons-vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation'
|
||||
import SupplierEvaluationForm from './SupplierEvaluationForm.vue'
|
||||
import SupplierEvaluationDetail from './SupplierEvaluationDetail.vue'
|
||||
import SupplierSummaryTable from './SupplierSummaryTable.vue'
|
||||
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
|
||||
import { UserVO } from '@/api/system/user'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
/** ERP 供应商评价记录列表 */
|
||||
defineOptions({ name: 'ErpSupplierEvaluationList' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const list = ref<SupplierEvaluationVO[]>([])
|
||||
const total = ref(0)
|
||||
const filterVisible = ref(false)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
supplierId: undefined,
|
||||
purchaseOrderNo: undefined,
|
||||
createTime: [],
|
||||
scoreRange: undefined,
|
||||
creator: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false)
|
||||
const supplierList = ref<SupplierVO[]>([])
|
||||
const userList = ref<UserVO[]>([])
|
||||
const showStatistics = ref(false)
|
||||
const statistics = ref({
|
||||
totalEvaluations: 0,
|
||||
avgTotalScore: '0.0',
|
||||
excellentCount: 0,
|
||||
supplierCount: 0
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...queryParams }
|
||||
if (params.scoreRange) {
|
||||
switch (params.scoreRange) {
|
||||
case 'excellent': params.minScore = 9.0; params.maxScore = 10.0; break
|
||||
case 'good': params.minScore = 8.0; params.maxScore = 8.9; break
|
||||
case 'average': params.minScore = 7.0; params.maxScore = 7.9; break
|
||||
case 'pass': params.minScore = 6.0; params.maxScore = 6.9; break
|
||||
case 'fail': params.minScore = 0.0; params.maxScore = 5.9; break
|
||||
}
|
||||
delete params.scoreRange
|
||||
}
|
||||
const data = await SupplierEvaluationApi.getSupplierEvaluationPage(params)
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
await calculateStatistics()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const calculateStatistics = async () => {
|
||||
if (list.value.length === 0) {
|
||||
statistics.value = { totalEvaluations: 0, avgTotalScore: '0.0', excellentCount: 0, supplierCount: 0 }
|
||||
return
|
||||
}
|
||||
const totalEvaluations = total.value
|
||||
const avgTotalScore = (list.value.reduce((sum, item) => sum + item.totalScore, 0) / list.value.length).toFixed(1)
|
||||
const excellentCount = list.value.filter(item => item.totalScore >= 9.0).length
|
||||
const supplierIds = new Set(list.value.map(item => item.supplierId))
|
||||
statistics.value = { totalEvaluations, avgTotalScore, excellentCount, supplierCount: supplierIds.size }
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handleFilterConfirm = () => {
|
||||
filterVisible.value = false
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
const detailRef = ref()
|
||||
const openDetail = (row: SupplierEvaluationVO) => {
|
||||
detailRef.value.open(row)
|
||||
}
|
||||
|
||||
const summaryRef = ref()
|
||||
const openSupplierSummary = () => {
|
||||
summaryRef.value.open()
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await SupplierEvaluationApi.deleteSupplierEvaluation(ids[0])
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await SupplierEvaluationApi.exportSupplierEvaluation(queryParams)
|
||||
download.excel(data, '供应商评价记录.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectionList = ref<SupplierEvaluationVO[]>([])
|
||||
|
||||
const getScoreTagType = (score: number) => {
|
||||
if (score >= 9) return 'success'
|
||||
if (score >= 8) return ''
|
||||
if (score >= 7) return 'warning'
|
||||
if (score >= 6) return 'info'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const getScoreLevel = (score: number) => {
|
||||
if (score >= 9) return '优秀'
|
||||
if (score >= 8) return '良好'
|
||||
if (score >= 7) return '一般'
|
||||
if (score >= 6) return '及格'
|
||||
return '不及格'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [suppliers, users] = await Promise.all([
|
||||
SupplierApi.getSupplierSimpleList().catch(() => []),
|
||||
UserApi.getSimpleUserList().catch(() => [])
|
||||
])
|
||||
supplierList.value = suppliers
|
||||
userList.value = users
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
message.error('页面初始化失败,请刷新重试')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-evaluation {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.mobile-header {
|
||||
display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
|
||||
&__search { flex: 1; }
|
||||
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
}
|
||||
.mobile-stats {
|
||||
display: flex; justify-content: space-around; background: #fff; border-radius: 10px; padding: 14px 8px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__item { text-align: center; }
|
||||
&__val { font-size: 20px; font-weight: 700; color: #303133; }
|
||||
&__label { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
}
|
||||
.mobile-quick-actions {
|
||||
display: flex; gap: 8px; margin-bottom: 12px;
|
||||
}
|
||||
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.mobile-empty { padding: 40px 0; }
|
||||
.mobile-card {
|
||||
background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
&__no { font-weight: 600; font-size: 15px; color: #303133; }
|
||||
&__body { font-size: 13px; }
|
||||
&__row { display: flex; justify-content: space-between; padding: 3px 0; }
|
||||
&__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
|
||||
&__value { color: #606266; text-align: right; &--ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; } }
|
||||
&__scores { display: flex; justify-content: space-around; margin-top: 10px; padding: 10px 0; border-top: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; }
|
||||
&__score-item { text-align: center; }
|
||||
&__score-label { font-size: 11px; color: #909399; margin-top: 4px; }
|
||||
&__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
}
|
||||
.mobile-pagination {
|
||||
margin-top: 12px; display: flex; justify-content: center;
|
||||
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user