first commit

This commit is contained in:
2026-03-05 16:52:12 +08:00
commit 8ca2e6d52f
1899 changed files with 321565 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>