2026-01-19 11:30:41 +08:00

923 lines
27 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="apaas-custom-page-mydemo custom-page">
<template v-if="!showEngineDetail">
<div class="page-header">
<div class="title">
竞品发动机数据
</div>
<div class="operation">
<el-button v-if="hasEngineDataEditPermission()" type="primary" @click="handleDownloadTemplate">下载模板</el-button>
<el-button v-if="hasEngineDataEditPermission()" class="import-button" @click="handleImport">
导入机型
</el-button>
<el-button v-if="hasEngineDataEditPermission()" type="primary" class="add-button" @click="handleAdd">
添加机型
</el-button>
</div>
</div>
<div class="page-search-panel">
<div class="searched-fields">
<div class="searched-field">
<span class="fielded-label">产品型号</span>
<el-input v-model="searchPanelValue.modelName" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">状态代号</span>
<el-input v-model="searchPanelValue.productNumber" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">板块</span>
<el-input v-model="searchPanelValue.plate" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">平台</span>
<el-input v-model="searchPanelValue.platform" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">系列</span>
<el-input v-model="searchPanelValue.series" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">细分市场</span>
<el-input v-model="searchPanelValue.marketSegment" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">项目名称</span>
<el-input v-model="searchPanelValue.projectName" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">项目编号</span>
<el-input v-model="searchPanelValue.projectNumber" placeholder="请输入" />
</div>
<div class="searched-field">
<span class="fielded-label">任务分发状态</span>
<el-select v-model="searchPanelValue.distributeStatus" placeholder="请选择" clearable>
<el-option label="已分发" value="已分发" />
<el-option label="部分分发" value="部分分发" />
<el-option label="未分发" value="未分发" />
</el-select>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
查询
</el-button>
<el-button @click="resetSearch">
重置
</el-button>
</div>
</div>
</div>
<div class="page-data-list">
<x-vxe-table
ref="vxeTable"
:border="true"
:seqWidth="40"
:colConfigs="tableConfig.colConfigs"
:tableData="tableData"
:rowDraggable="tableConfig.rowDraggable"
:seqType="tableConfig.seqType"
:pagination="pagination"
:layout="tableConfig.layout"
:seqConfig="tableConfig.seqConfig"
:autoSize="tableConfig.autoSize"
:pageConfig="tableConfig.pageConfig"
class="block-table"
@table-row-click="rowClickEvent"
@size-change="pageSizeChange"
@current-page-change="currentChange"
>
<template
v-for="(colConfig, index) in tableConfig.colConfigs"
v-slot:[colConfig.customHeadSlot]="{ column }"
>
<div v-if="colConfig.customHeadSlot" :key="index">
{{ column }} {{ colConfig }}
</div>
</template>
<template
v-for="(colConfig, index) in tableConfig.colConfigs"
v-slot:[colConfig.customSlot]="{ row, rowIndex }"
>
<div v-if="colConfig.customSlot === 'options'" :key="index + '' + rowIndex">
<a
href="javascript:void(0)"
class="el-link el-link--primary"
@click.stop="handleRowDetail(row)"
>
查看
</a>
<span v-if="row.distributeStatus != '已分发' && hasEngineDataEditPermission()" class="link-separator"></span>
<a
v-if="row.distributeStatus != '已分发' && hasEngineDataEditPermission()"
href="javascript:void(0)"
class="el-link el-link--primary"
style="margin-left: 5px;"
@click.stop="handleRowDispatch(row, rowIndex)"
>
分发
</a>
<span v-if="row.distributeStatus === '未分发' && hasEngineDataEditPermission() && row.versionNumber === 1" class="link-separator"></span>
<a
v-if="row.distributeStatus === '未分发' && hasEngineDataEditPermission() && row.versionNumber === 1"
href="javascript:void(0)"
class="el-link el-link--primary"
style="margin-left: 5px;"
@click.stop="handleRowDelete(row, rowIndex)"
>
删除
</a>
</div>
<div v-if="colConfig.customSlot === 'distributeStatus'" :key="index + '' + rowIndex">
<span v-if="row[colConfig.prop] === '已分发'" class="status-tag status-done">
已分发
</span>
<span v-else-if="row[colConfig.prop] === '部分分发'" class="status-tag status-partial">
部分分发
</span>
<span v-else-if="row[colConfig.prop] === '未分发'" class="status-tag status-pending">
未分发
</span>
</div>
<div v-if="colConfig.customSlot === 'versionNumber'" :key="index + '' + rowIndex">
V{{ row[colConfig.prop] || '' }}
</div>
</template>
<template slot="empty">
<div class="table-empty">
<x-empty-page></x-empty-page>
</div>
</template>
</x-vxe-table>
</div>
</template>
<!-- 发动机详情组件 -->
<engine-detail
v-if="showEngineDetail"
:model="currentModel"
:engine-model-i-d="currentEngineModelID"
:is-dispatch-mode="isDispatchMode"
:dispatch-type="dispatchType"
:detail="currentEngineDetail"
class="engine-detail-wrapper"
@back="handleBackFromDetail"
/>
<!-- 添加机型弹窗 -->
<add-engine-model-modal ref="addEngineModelModal" @success="handleAddSuccess" />
<!-- 导入机型弹窗 -->
<el-dialog
title="导入机型"
:visible.sync="importDialogVisible"
width="400px"
append-to-body
@close="handleImportDialogClose"
>
<el-upload
class="import-uploader"
drag
:auto-upload="false"
:limit="1"
accept=".xls,.xlsx"
:file-list="importFileList"
:on-change="handleImportFileChange"
:on-remove="handleImportFileRemove"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<div slot="tip" class="el-upload__tip">
仅支持Excel文件.xlsx .xls
</div>
</el-upload>
<span slot="footer" class="dialog-footer">
<el-button @click="handleImportDialogClose">取消</el-button>
<el-button type="primary" :loading="importing" @click="handleImportConfirm">
开始导入
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import EngineDetail from './engineDetail.vue'
import AddEngineModelModal from './add-engine-model-modal.vue'
import api from '@/api'
import { getUserId } from '@/utils'
const createDefaultSearch = () => ({
loginUserId: getUserId(),
modelName: '',
statusCode: '',
plate: '',
block: '',
platform: '',
series: '',
marketSegment: '',
projectName: '',
projectNumber: '',
distributeStatus: '',
ycOrOth: '竞品',
modelNameSimple: '',
productionDate: ''
})
export default {
name: 'SelfEngineData',
components: {
EngineDetail,
AddEngineModelModal
},
data: function() {
return {
searchPanelValue: createDefaultSearch(),
tableData: [],
pagination: { currentPage: 1, pageSize: 10, total: 0 },
showEngineDetail: false,
currentModel: '',
currentEngineModelID: '',
currentEngineDetail: null, // 当前机型的详情信息
isDispatchMode: false,
dispatchType: 'todo', // 分发类型:'todo' 表示分发责任人,'responsible' 表示分发填写人
importDialogVisible: false,
importing: false,
selectedImportFile: null,
importFileList: [],
downloadingTemplate: false
}
},
computed: {
tableConfig() {
return {
rowDraggable: false,
colConfigs: [
{
prop: 'modelName',
label: '产品型号',
showOverflowTooltip: true,
minWidth: '120'
},
{
prop: 'productNumber',
label: '状态代号',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'plate',
label: '板块',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'platform',
label: '平台',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'series',
label: '系列',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'marketSegment',
label: '细分市场',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'projectName',
label: '项目名称',
showOverflowTooltip: true,
minWidth: '120'
},
{
prop: 'projectNumber',
label: '项目编号',
showOverflowTooltip: true,
minWidth: '100'
},
{
prop: 'parameterNum',
label: '标准参数',
showOverflowTooltip: true,
minWidth: '100',
align: 'center'
},
// {
// prop: 'maintainedParam',
// label: '已维护参数',
// showOverflowTooltip: true,
// minWidth: '110',
// align: 'center'
// },
{
prop: 'distributeStatus',
label: '任务分发状态',
customSlot: 'distributeStatus',
showOverflowTooltip: true,
minWidth: '120',
align: 'center'
},
{
prop: 'versionNumber',
label: '版本',
customSlot: 'versionNumber',
showOverflowTooltip: true,
minWidth: '80',
align: 'center'
},
{
prop: 'options',
label: '操作',
customSlot: 'options',
align: 'center',
width: '120',
fixed: 'right'
}
],
seqType: 'seq',
layout: 'total, prev, pager, next, sizes, jumper'
}
}
},
created() {
this.loadUserSelfRole()
this.loadData()
},
mounted() {
this.loadUserSelfRole()
this.loadData()
},
methods: {
// 检查是否是管理员isAdmin === 1
isAdmin() {
try {
const permissionDataStr = localStorage.getItem('user_self_role')
if (!permissionDataStr) {
return false
}
const permissionData = JSON.parse(permissionDataStr)
if (!Array.isArray(permissionData)) {
return false
}
// 检查是否有 isAdmin === 1 的记录
return permissionData.some((perm) => perm.isAdmin === 1 || perm.isAdmin === '1')
} catch (err) {
console.error('检查管理员权限失败:', err)
return false
}
},
// 检查是否有竞品发动机数据的编辑权限
hasEngineDataEditPermission() {
// 如果是管理员,拥有全部权限
if (this.isAdmin()) {
return true
}
try {
const permissionDataStr = localStorage.getItem('user_self_role')
if (!permissionDataStr) {
return false
}
const permissionData = JSON.parse(permissionDataStr)
if (!Array.isArray(permissionData)) {
return false
}
// 检查是否有 permissionPath 为 "竞品发动机数据" 且 parameterPer 是编辑的权限
// parameterPer 可能是数字 2 或字符串 "编辑" 或 "2"
return permissionData.some((perm) => {
const permissionPath = perm.permissionPath || ''
const parameterPer = perm.parameterPer
return (
permissionPath === '竞品发动机数据' &&
(parameterPer === 2 ||
parameterPer === '2' ||
parameterPer === '编辑')
)
})
} catch (err) {
console.error('检查竞品发动机数据编辑权限失败:', err)
return false
}
},
// 加载用户自身角色权限
loadUserSelfRole() {
const request = {
...api.GET_USER_SELF_ROLE,
params: {
userOrDepID: getUserId()
},
disableSuccessMsg: true
}
this.$request(request)
.asyncThen((resp) => {
if (resp && resp.data) {
// 将权限数据存储到 localStorage
try {
localStorage.setItem('user_self_role', JSON.stringify(resp.data))
} catch (err) {
console.error('存储权限数据到 localStorage 失败:', err)
}
}
})
.asyncErrorCatch((err) => {
console.error('获取用户自身角色失败:', err)
})
},
handleRowDispatch(row, rowIndex) {
// 跳转到详情页面,设置为分发模式(分发责任人)
this.currentModel = row.productFullName || ''
this.currentEngineModelID = row.id || row.rowId || row.engineModelID || ''
this.currentEngineDetail = row // 保存机型详情信息
this.isDispatchMode = true
this.dispatchType = 'todo' // 分发责任人
this.showEngineDetail = true
},
handleRowDelete(row, rowIndex) {
console.log(row, 'row')
const request = {
...api.DEL_ENGINE_MODEL,
params: {
modelId: row.id
},
disableSuccessMsg: true
}
this.$request(request)
.asyncThen((resp) => {
console.log(resp, 'resp')
this.$message({
type: 'success',
message: '删除成功'
})
this.loadData()
})
.asyncErrorCatch((err) => {
console.error('删除机型失败:', err)
this.$message({
type: 'error',
message: '删除失败'
})
})
},
pageSizeChange(size) {
this.pagination.pageSize = size
this.pagination.currentPage = 1
this.loadData()
},
rowClickEvent(row) {
console.warn(row)
},
handleRowDetail(row) {
this.currentModel = row.productFullName || ''
this.currentEngineModelID = row.id
this.currentEngineDetail = row // 保存机型详情信息
this.isDispatchMode = false
this.dispatchType = 'justCheck' // 查看模式,不影响分发类型
this.showEngineDetail = true
},
handleBackFromDetail() {
this.showEngineDetail = false
this.currentModel = ''
this.currentEngineModelID = ''
this.currentEngineDetail = null
this.isDispatchMode = false
this.dispatchType = 'todo'
},
currentChange(currentPage) {
this.pagination.currentPage = currentPage
console.log(this.pagination.currentPage, this.pagination.pageSize, 'currentPage')
this.loadData()
},
selectDataChange(data) {
this.$emit('select-data-change', data)
},
resetSearch() {
Object.assign(this.searchPanelValue, createDefaultSearch())
this.handleSearch()
},
handleSearch() {
this.loadData()
},
handleRefresh() {
this.pagination.currentPage = 1
this.handleSearch()
},
handleImport() {
if (this.importing) {
return
}
this.importDialogVisible = true
},
handleImportFileChange(uploadFile, uploadFiles) {
// 允许的文件类型
const allowedTypes = ['.xls', '.xlsx']
const allowedMimeTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.macroEnabled.12'
]
if (uploadFile && uploadFile.raw) {
const file = uploadFile.raw
const fileName = file.name || ''
const fileExtension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
const fileType = file.type || ''
// 验证文件扩展名
const isValidExtension = allowedTypes.some(type => fileExtension === type)
// 验证 MIME 类型
const isValidMimeType = !fileType || allowedMimeTypes.some(type => fileType.includes(type))
if (!isValidExtension && !isValidMimeType) {
this.$message.error(`文件类型不支持,仅支持 ${allowedTypes.join('、')} 格式的文件`)
// 清空不符合要求的文件
this.selectedImportFile = null
this.importFileList = []
return false
}
this.selectedImportFile = file
this.importFileList = uploadFiles.slice(-1)
} else {
this.selectedImportFile = null
this.importFileList = uploadFiles.slice(-1)
}
},
handleImportFileRemove() {
this.selectedImportFile = null
this.importFileList = []
},
handleImportDialogClose() {
this.importDialogVisible = false
this.selectedImportFile = null
this.importFileList = []
},
handleImportConfirm() {
if (this.importing || !this.selectedImportFile) {
if (!this.selectedImportFile) {
this.$message.warning('请先选择要导入的文件')
}
return
}
const file = this.selectedImportFile
// 再次验证文件类型(双重保险)
const allowedTypes = ['.xls', '.xlsx']
const fileName = file.name || ''
const fileExtension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
if (!allowedTypes.includes(fileExtension)) {
this.$message.error(`文件类型不支持,仅支持 ${allowedTypes.join('、')} 格式的文件`)
this.selectedImportFile = null
this.importFileList = []
return
}
const formData = new FormData()
formData.append('file', file)
formData.append('userId', getUserId())
formData.append('ycOrOt', '竞品')
const request = {
...api.IMPORT_ENGINE_MODEL_PARAM,
params: formData,
disableSuccessMsg: true
}
this.importing = true
// 显示全屏 loading 提示
const loadingInstance = this.$loading({
lock: true,
text: '正在导入机型,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
this.$upload(request, null, false)
.then((resp) => {
console.log('导入成功:', resp)
loadingInstance.close()
// 导入成功后刷新列表
this.handleImportDialogClose()
this.loadData()
})
.catch((err) => {
console.error('导入失败:', err)
loadingInstance.close()
this.$message.error('导入失败,请稍后重试')
})
.finally(() => {
// 确保 loading 在所有情况下都能关闭
if (loadingInstance) {
loadingInstance.close()
}
this.importing = false
})
},
handleDownloadTemplate() {
if (this.downloadingTemplate) {
return
}
const { url, method = 'get', headers = {} } = api.DOWNLOAD_ENGINE_DEMO || {}
const requestConfig = {
url,
method,
params: {},
responseType: 'blob',
headers
}
if (!requestConfig.url) {
console.log('未配置下载地址')
return
}
if ((requestConfig.method || '').toLowerCase() === 'post') {
requestConfig.data = requestConfig.params
delete requestConfig.params
}
const defaultFilename = '机型导入模板.xlsx'
this.downloadingTemplate = true
this.$axios(requestConfig)
.then((response) => {
const blobData = response.data
if (!blobData) {
console.log('下载失败,文件内容为空')
return
}
const contentType =
response.headers &&
(response.headers['content-type'] || response.headers['Content-Type'])
if (contentType && contentType.includes('application/json')) {
try {
const reader = new FileReader()
reader.onload = () => {
console.log(`下载失败: ${reader.result}`)
}
reader.readAsText(blobData)
} catch (err) {
console.error('解析下载失败响应异常:', err)
}
return
}
const disposition =
response.headers &&
(response.headers['content-disposition'] || response.headers['Content-Disposition'])
let filename = defaultFilename
if (disposition) {
const filenameMatch = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(
disposition
)
const matchedName = filenameMatch ? filenameMatch[1] || filenameMatch[2] : ''
if (matchedName) {
try {
filename = decodeURIComponent(matchedName)
} catch (err) {
filename = matchedName
}
}
}
const downloadUrl = window.URL.createObjectURL(blobData)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
this.$message({
type: 'success',
message: '下载成功'
})
console.log('下载开始,请查看浏览器下载列表')
})
.catch((err) => {
console.error('下载失败:', err)
this.$message.error('下载失败,请稍后重试')
})
.finally(() => {
this.downloadingTemplate = false
})
},
handleAdd() {
// 从 selfEnginedata.vue 跳转,设置 ycOrOth 为 "竞品"
this.$refs.addEngineModelModal.showModal('竞品')
},
handleAddSuccess() {
// 子组件已经关闭弹窗,这里只需要刷新列表
this.loadData()
},
loadData() {
const request = {
...api.GET_ENGINE_LIST_SELF,
params: {
page: this.pagination.currentPage,
size: this.pagination.pageSize,
ycOrOth: '竞品',
...this.searchPanelValue
},
disableSuccessMsg: true
}
console.log(this.pagination.currentPage, this.pagination.pageSize, 'request')
this.$request(request)
.asyncThen((resp) => {
this.tableData = resp.data.list
this.pagination.total = resp.data.totalCount
})
.asyncErrorCatch((err) => {
console.error('查询失败:', err)
})
}
}
}
</script>
<style lang="scss">
.custom-page {
width: 100%;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
.page-header {
display: flex;
align-items: center;
height: 56px;
position: relative;
width: 100%;
min-height: 56px;
background: #fff;
border-bottom: 1px solid #ebeef5;
overflow: auto;
.title {
color: #303133;
font-weight: 700;
margin-left: 20px;
flex: 1;
font-size: 18px;
}
.operation {
display: flex;
align-items: center;
gap: 12px;
margin-right: 20px;
.import-button {
background: #fff;
border: 1px solid #409eff;
color: #409eff;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
&:hover {
background: #b3d8ff;
border-color: #66b1ff;
color: #409eff;
}
&:active {
background: #409eff;
color: white;
}
}
.add-button {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
}
}
.page-data-list {
display: flex;
padding: 20px;
.x-empty-page {
top: 0;
}
}
.status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 2px;
font-size: 12px;
line-height: 1.5;
&.status-done {
background: #67c23a;
color: white;
}
&.status-partial {
background: #e6a23c;
color: white;
}
&.status-pending {
background: #f5f7fa;
color: #606266;
}
}
.engine-detail-wrapper {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
.import-uploader {
width: 100%;
.el-upload {
width: 100%;
}
.el-upload-dragger {
width: 100%;
}
}
}
.page-search-panel {
background: white;
padding: 16px 24px !important;
border-bottom: 1px solid #ebeef5 !important;
flex-shrink: 0;
display: flex;
flex-direction: column !important;
gap: 16px;
.search-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
::v-deep .el-button {
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
height: 32px;
line-height: 32px;
}
}
}
.searched-fields {
max-height: 200px !important;
display: flex !important;
flex-wrap: wrap !important;
flex-direction: row !important;
gap: 16px 32px !important;
width: 100% !important;
}
.searched-field {
display: flex !important;
align-items: center !important;
gap: 12px !important;
flex: 1 1 calc((100% - 96px) / 4) !important;
// min-width: 160px !important;
max-width: calc(25% - 32px) !important;
.fielded-label {
font-size: 14px;
font-weight: 600;
color: #303133;
white-space: nowrap;
min-width: 80px !important;
text-align: right;
text-align-last: right;
text-justify: inter-ideograph;
}
.el-input,
.el-select {
width: 170px;
}
::v-deep .el-input__inner,
::v-deep .el-select .el-input__inner {
height: 32px;
line-height: 32px;
}
}
.link-separator {
width: 4px;
height: 14px;
}
::v-deep .el-button {
line-height: 0.85 !important;
}
</style>