Files
mom-web/src/views/erp/purchase/evaluation/index.vue
2026-03-05 16:52:12 +08:00

343 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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