Files
smt-used-car-pc/src/views/admin/publish/publish_my_listpro.vue
T

1474 lines
46 KiB
Vue
Raw Normal View History

2026-03-11 11:16:35 +08:00
<template>
<div class="page-content">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
<el-form-item label="审核状态:" label-width="80px">
<el-select v-model="auditStatus" placeholder="请选择状态" style="min-width: 120px">
<el-option label="全部" value="" />
<el-option label="待审核" :value="0" />
<el-option label="审核通过" :value="1" />
<el-option label="审核拒绝" :value="2" />
</el-select>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<!-- <div style="text-align: right; padding: 12px 0">
<el-button type="primary" @click="goToAdd"> 新增发布 </el-button>
</div> -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="编号" prop="code" width="120px" />
<ElTableColumn label="产品名称" prop="product_name" width="150px" />
<ElTableColumn label="产品图" width="80px">
<template #default="scope">
<div class="thumb-list" v-if="parsePreviewList(scope.row.images_pull).length">
<img
:src="parsePreviewList(scope.row.images_pull)[0]"
alt="产品图"
class="avatarimg"
@click="handleAvatarClick1(parsePreviewList(scope.row.images_pull)[0])"
/>
</div>
<span v-else>--</span>
</template>
</ElTableColumn>
<ElTableColumn label="车价(¥)" prop="price" width="100px" />
<ElTableColumn label="车源地" prop="source_place" width="120px" />
<ElTableColumn label="上牌时间" prop="plate_time" width="120px" />
<ElTableColumn label="排量" prop="displacement" width="100px" />
<ElTableColumn label="里程(Km)" prop="mileage" width="100px" />
<ElTableColumn label="马力" prop="power" width="80px" />
<ElTableColumn label="车身颜色" prop="body_color" width="100px" />
<ElTableColumn label="审核状态" prop="status" width="100px">
<template #default="scope">
<el-tag :type="getAuditStatusColor(scope.row.status)">
{{ getAuditStatusText(scope.row.status) }}
</el-tag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" width="160px" />
<ElTableColumn fixed="right" label="操作" width="240px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('detail', scope.row)"> 详情 </el-button>
<el-button link type="primary" @click="editFn(scope.row)"> 编辑 </el-button>
<el-button link type="success" @click="showAuditRecords(scope.row)"> 审核记录 </el-button>
<el-button link type="danger" @click="deleteFn(scope.row)"> 删除 </el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="车辆详情" width="800px">
<el-scrollbar max-height="60vh">
<div>
<el-descriptions v-if="detailData" :column="2" border label-width="100px">
<el-descriptions-item label="编号">{{ detailData.code || '--' }}</el-descriptions-item>
<el-descriptions-item label="产品_name">{{ detailData.product_name || '--' }}</el-descriptions-item>
<el-descriptions-item label="车价(¥)">{{ detailData.price || '--' }}</el-descriptions-item>
<el-descriptions-item label="车源地">{{ detailData.source_place || '--' }}</el-descriptions-item>
<el-descriptions-item label="上牌时间">{{ detailData.plate_time || '--' }}</el-descriptions-item>
<el-descriptions-item label="排量">{{ detailData.displacement || '--' }}</el-descriptions-item>
<el-descriptions-item label="里程(Km)">{{ detailData.mileage || '--' }}</el-descriptions-item>
<el-descriptions-item label="马力">{{ detailData.power || '--' }}</el-descriptions-item>
<el-descriptions-item label="车身颜色">{{ detailData.body_color || '--' }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
<el-tag :type="getAuditStatusColor(detailData.status)">
{{ getAuditStatusText(detailData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.created_at || '--' }}</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2" v-if="detailData.audit_opinion">
{{ detailData.audit_opinion }}
</el-descriptions-item>
<el-descriptions-item label="车况介绍" :span="2">
{{ detailData.condition_desc || '--' }}
</el-descriptions-item>
<!-- 视频展示 -->
<el-descriptions-item label="介绍视频" :span="2" v-if="detailVideoList.length">
<div class="video-preview-list">
<div v-for="(video, index) in detailVideoList" :key="index" class="video-item">
<video :src="video.url" controls class="video-preview"></video>
</div>
</div>
</el-descriptions-item>
<!-- FAQ 展示 -->
<el-descriptions-item label="FAQ 问答" :span="2" v-if="detailData.faqs && detailData.faqs.length">
<div class="faq-display">
<div v-for="(faq, index) in detailData.faqs" :key="index" class="faq-item">
<div class="faq-question">
<span class="faq-label">Q{{ index + 1 }}:</span>
{{ faq.question || '--' }}
</div>
<div class="faq-answer">
<span class="faq-label">A{{ index + 1 }}:</span>
{{ faq.answer || '--' }}
</div>
</div>
</div>
</el-descriptions-item>
<!-- 产品图 -->
<el-descriptions-item label="产品图" :span="2">
<div
class="thumb-list"
v-if="parsePreviewList(detailData.images_pull || detailData.images).length"
>
<img
v-for="(img, index) in parsePreviewList(
detailData.images_pull || detailData.images
)"
:key="index"
:src="img"
alt="产品图"
class="avatarimg"
@click="handleAvatarClick(index)"
/>
</div>
<span v-else>--</span>
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 20px">
<FilePreview title="附件" :files="contractFiles" empty-text="暂无附件" />
</div>
</div>
</el-scrollbar>
</el-dialog>
<!-- 编辑弹窗 -->
<el-dialog v-model="editDialogVisible" title="编辑发布" width="900px" :close-on-click-modal="false">
<el-scrollbar max-height="60vh">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="110px"
label-position="right"
>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="产品名称" prop="product_name">
<div class="bind-name-wrapper">
<el-input
v-model="editForm.product_name"
placeholder="请输入车辆的品牌 系列查找车型"
clearable
/>
<el-button type="primary" @click="onEditBindByName" :loading="editBindState.loading">
绑定
</el-button>
</div>
</el-form-item>
<!-- 绑定结果提示 -->
<el-form-item label=" ">
<div class="bind-tip" v-if="editBindState.boundProduct">
<span class="bind-tip-text">
已绑定:{{
editBindState.boundProduct.name ||
editBindState.boundProduct.product_name ||
editBindState.boundProduct.title ||
'-'
}}
</span>
<el-button link type="danger" @click="clearEditBind">清除</el-button>
</div>
<div class="bind-tip" v-else>
<span>例:比亚迪海狮07DM</span>
</div>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车价(¥)" prop="price">
<el-input v-model="editForm.price" placeholder="请输入车价" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车源地" prop="source_place">
<el-input v-model="editForm.source_place" placeholder="请输入车源地" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="上牌时间" prop="plate_time">
<el-date-picker
v-model="editForm.plate_time"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择时间"
style="width: 100%"
/>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="排量" prop="displacement">
<el-input v-model="editForm.displacement" placeholder="请输入排量" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="马力" prop="power">
<el-input v-model="editForm.power" placeholder="请输入马力" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="里程(Km)" prop="mileage">
<el-input v-model="editForm.mileage" placeholder="请输入里程" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车身颜色" prop="body_color">
<el-input v-model="editForm.body_color" placeholder="请输入车身颜色" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="车况介绍">
<el-input
v-model="editForm.condition_desc"
type="textarea"
:rows="4"
placeholder="请输入车况介绍"
/>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="产品图">
<el-upload
v-model:file-list="editImageFileList"
class="upload-list"
list-type="picture-card"
:limit="maxMedia"
:action="uploadAction"
:headers="uploadHeaders"
name="file"
multiple
:before-upload="handleImagePreview"
:on-success="handleEditImageSuccess"
:on-remove="handleEditImageRemove"
:on-exceed="handleEditImageExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
</ElCol>
<!-- 视频编辑 -->
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="介绍视频">
<el-upload
v-model:file-list="editVideoFileList"
class="upload-list"
:limit="maxVideos"
:action="uploadActionVideo"
:headers="uploadHeaders"
name="file"
accept="video/*"
multiple
:before-upload="handleVideoPreview"
:on-success="handleEditVideoSuccess"
:on-remove="handleEditVideoRemove"
:on-exceed="handleVideoExceed"
>
<el-button type="primary">
<el-icon><Plus /></el-icon>
上传视频
</el-button>
<template #tip>
<div class="el-upload__tip">支持 mp4/avi/mov 等视频格式</div>
</template>
</el-upload>
<!-- 视频预览列表 -->
<div v-if="editVideoPreviewList.length" class="video-preview-list">
<div v-for="(video, index) in editVideoPreviewList" :key="index" class="video-item">
<video :src="video.url" controls class="video-preview"></video>
<el-button link type="danger" @click="removeEditVideoByIndex(index)" size="small">
删除
</el-button>
</div>
</div>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="附件">
<MultiFileUpload
v-model="editAttachmentFileList"
upload-text="点击上传附件"
hint-text="支持多个文件上传"
:action-image-uplode="uploadActionFile"
@change="handleEditAttachmentChange"
/>
</el-form-item>
</ElCol>
<!-- FAQ 编辑 -->
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="FAQ 问答">
<div class="faq-container">
<div v-for="(faq, index) in editForm.faqs" :key="index" class="faq-item">
<div class="faq-header">
<span class="faq-index">Q{{ index + 1 }}</span>
<el-button
link
type="danger"
@click="removeEditFaq(index)"
v-if="editForm.faqs.length > 1"
>
删除
</el-button>
</div>
<el-form-item :label="`问题 ${index + 1}`">
<el-input v-model="faq.question" placeholder="请输入问题" clearable />
</el-form-item>
<el-form-item :label="`答案 ${index + 1}`">
<el-input
v-model="faq.answer"
type="textarea"
:rows="3"
placeholder="请输入答案"
/>
</el-form-item>
</div>
<el-button type="primary" @click="addEditFaq" :icon="Plus">
添加问答
</el-button>
</div>
</el-form-item>
</ElCol>
</ElRow>
</el-form>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" :loading="editSubmitLoading" @click="handleEditSubmit">
确 认
</el-button>
</span>
</template>
</el-dialog>
<!-- 编辑绑定产品弹窗 -->
<el-dialog
v-model="editBindState.showDialog"
title="选择要绑定的产品"
width="700px"
@close="closeEditBindDialog"
>
<div class="bind-search" v-if="editBindState.list && editBindState.list.length">
<el-input
v-model="editBindState.keyword"
placeholder="搜索产品名称"
clearable
style="max-width: 320px"
/>
</div>
<el-scrollbar max-height="360px">
<div v-if="editBindState.loading" class="bind-loading">加载中…</div>
<div v-else-if="!editBindState.list.length" class="bind-empty">未找到匹配的产品</div>
<div v-else-if="!filteredEditBindList.length" class="bind-empty">未找到匹配结果</div>
<div v-else>
<div
class="bind-item"
v-for="(it, idx) in filteredEditBindList"
:key="getEditBindItemId(it, idx)"
:class="{ active: editBindState.selectedId === getEditBindItemId(it, idx) }"
@click="selectEditBind(it, idx)"
>
<div class="bind-item-main">
<div class="bind-item-title">
{{ it.name || it.product_name || it.title || `产品 ${idx + 1}` }}
</div>
<div class="bind-item-sub">
{{ formatEditBindSub(it) }}
</div>
</div>
<div class="bind-item-check">
<span class="check-dot" v-if="editBindState.selectedId === getEditBindItemId(it, idx)" />
</div>
</div>
</div>
</el-scrollbar>
<template #footer>
<el-button @click="closeEditBindDialog">取 消</el-button>
<el-button type="primary" :disabled="!editBindState.selectedId" @click="confirmEditBind">
确认绑定
</el-button>
</template>
</el-dialog>
<!-- 审核记录弹窗 -->
<el-dialog v-model="auditRecordsDialogVisible" title="审核记录" width="700px">
<div v-loading="auditRecordsLoading" class="audit-records-container">
<el-empty v-if="!auditRecordsList.length" description="暂无审核记录" />
<el-timeline v-else>
<el-timeline-item
v-for="(record, index) in auditRecordsList"
:key="index"
:timestamp="record.created_at"
placement="top"
:type="getAuditRecordStatusColor(record.audit_status) === 'success' ? 'success' :
getAuditRecordStatusColor(record.audit_status) === 'danger' ? 'danger' : 'warning'"
>
<el-card class="audit-record-card">
<div class="audit-record-header">
<el-tag :type="getAuditRecordStatusColor(record.audit_status)">
{{ record.audit_status_text || '--' }}
</el-tag>
<span class="auditor-info">
<el-icon><User /></el-icon>
{{ record.auditor_name || '--' }}
</span>
</div>
<div class="audit-record-content" v-if="record.audit_reason">
<div class="content-label">审核理由:</div>
<div class="content-value">{{ record.audit_reason }}</div>
</div>
<div class="audit-record-status-change">
<span class="status-before">{{ getStatusText(record.before_status) }}</span>
<el-icon class="status-arrow"><ArrowRight /></el-icon>
<span class="status-after">{{ getStatusText(record.after_status) }}</span>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="auditRecordsDialogVisible = false">关 闭</el-button>
</span>
</template>
</el-dialog>
<!-- 图片预览组件 -->
<ElImageViewer
v-if="showImageViewer1"
:url-list="[currentPdfFiles]"
:hide-on-click-modal="true"
@close="showImageViewer1 = false"
/>
<ElImageViewer
v-if="showImageViewer"
:url-list="imgList"
:initial-index="viewerIndex"
:hide-on-click-modal="true"
@close="showImageViewer = false"
/>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
import {
ElMessage,
ElMessageBox,
ElLoading,
type FormInstance,
type FormRules,
type UploadFile
} from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { manageService } from '@/api/adminApi'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import FilePreview from '@/components/custom/FilePreview.vue'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import MultiFileUpload from '@/components/core/forms/MultiFileUpload.vue'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import request from '@/utils/http'
defineOptions({ name: 'PublishMyListPro' })
const { uploadHeaders } = useUploadHeaders()
const maxMedia = 200
const maxVideos = 5
interface FileItem {
name: string
url: string
file_name: string
}
interface FAQItem {
question: string
answer: string
}
const keyword = ref<string>('')
const auditStatus = ref<string | number>('')
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableData = ref<any[]>([])
// 审核记录相关
const auditRecordsDialogVisible = ref(false)
const auditRecordsList = ref<any[]>([])
const auditRecordsLoading = ref(false)
// 详情相关
const detailDialogVisible = ref(false)
const detailData = ref<any>(null)
const contractFiles = ref<FileItem[]>([])
const detailVideoList = ref<{ url: string; name: string }[]>([])
// 编辑相关
const editDialogVisible = ref(false)
const editSubmitLoading = ref(false)
const editFormRef = ref<FormInstance>()
interface EditForm {
id: number | string
product_name: string
price: string
source_place: string
plate_time: string
displacement: string
power: string
body_color: string
condition_desc: string
mileage: string
images: string[]
intro_video: string[]
attachment: { name: string; src: string }[]
faqs: FAQItem[]
crawl_title: string
crawl_brand: string
crawl_series: string
crawl_year: string
crawl_msrp: string
crawl_car_id: string
crawl_brand_id: string
crawl_series_id: string
}
const editForm = reactive<EditForm>({
id: '',
product_name: '',
price: '',
source_place: '',
plate_time: '',
displacement: '',
power: '',
body_color: '',
condition_desc: '',
mileage: '',
images: [],
intro_video: [],
attachment: [],
faqs: [{ question: '', answer: '' }],
crawl_title: '',
crawl_brand: '',
crawl_series: '',
crawl_year: '',
crawl_msrp: '',
crawl_car_id: '',
crawl_brand_id: '',
crawl_series_id: ''
})
const editRules = reactive<FormRules>({
product_name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入车价', trigger: 'blur' }]
})
// 编辑绑定相关状态
const editBindState = reactive<{
showDialog: boolean
loading: boolean
list: any[]
selectedId: string
boundProduct: any | null
keyword: string
_selected?: any
}>({
showDialog: false,
loading: false,
list: [],
selectedId: '',
boundProduct: null,
keyword: ''
})
const formatEditBindSub = (it: any) => {
const parts: string[] = []
if (it.sku || it.code) parts.push(`编码:${it.sku || it.code}`)
if (it.category_name) parts.push(`分类:${it.category_name}`)
if (it.price) parts.push(`价格:${it.price}`)
return parts.join(' · ')
}
const filteredEditBindList = computed(() => {
const list = Array.isArray(editBindState.list) ? editBindState.list : []
const kw = String(editBindState.keyword || '')
.trim()
.toLowerCase()
if (!kw) return list
return list.filter((it, idx) => {
const id = String(it.id || it.product_id || it.code || idx).toLowerCase()
const name = String(it.name || it.product_name || it.title || '').toLowerCase()
const sub = String(formatEditBindSub(it) || '').toLowerCase()
return id.includes(kw) || name.includes(kw) || sub.includes(kw)
})
})
const uploadAction = `${import.meta.env.VITE_API_URL}/upload?scene=usedcarimg`
const uploadActionFile = `${import.meta.env.VITE_API_URL}/upload?scene=usedcarfile`
const uploadActionVideo = `${import.meta.env.VITE_API_URL}/upload?scene=usedcarvideo`
type UploadFileWithResponse = UploadFile & {
response?: any
}
const editImageFileList = ref<UploadFileWithResponse[]>([])
const editVideoFileList = ref<UploadFileWithResponse[]>([])
const editAttachmentFileList = ref<FileItem[]>([])
const editVideoPreviewList = ref<{ url: string; name: string }[]>([])
// 图片预览相关
const showImageViewer = ref(false)
const showImageViewer1 = ref(false)
const viewerIndex = ref(0)
const currentPdfFiles = ref('')
const imgList = computed(() =>
parsePreviewList(detailData.value?.images_pull || detailData.value?.images || [])
)
const handleAvatarClick1 = (index: string) => {
if (index !== null && index !== undefined) {
currentPdfFiles.value = index
showImageViewer1.value = true
}
}
const handleAvatarClick = (index: number) => {
if (index !== null && index !== undefined) {
viewerIndex.value = index
showImageViewer.value = true
}
}
const parsePaths = (value: any): string[] => {
if (!value) return []
if (Array.isArray(value)) return value.filter(Boolean)
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) return parsed.filter(Boolean)
} catch {
// ignore
}
return value
.split(',')
.map((p: string) => p.trim())
.filter(Boolean)
}
return []
}
// 审核状态相关方法
const getAuditStatusText = (status: number) => {
const statusMap: Record<number, string> = {
0: '待审核',
1: '审核通过',
2: '审核拒绝'
}
return statusMap[status] || '未知'
}
const getAuditStatusColor = (status: number): 'success' | 'warning' | 'danger' | 'info' => {
const colorMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
0: 'warning',
1: 'success',
2: 'danger'
}
return colorMap[status] || 'info'
}
// 审核记录相关函数
const getStatusText = (status: number) => getAuditStatusText(status)
const getAuditRecordStatusColor = (status: number): 'success' | 'warning' | 'danger' | 'info' => {
const colorMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
0: 'warning',
1: 'success',
2: 'danger'
}
return colorMap[status] || 'info'
}
const showAuditRecords = async (row: any) => {
auditRecordsLoading.value = true
auditRecordsDialogVisible.value = true
try {
const records = await manageService.getAuditRecords({ id: row.id })
auditRecordsList.value = records || []
} catch (error) {
console.error('获取审核记录失败:', error)
ElMessage.error('获取审核记录失败')
} finally {
auditRecordsLoading.value = false
}
}
const parsePreviewList = (value: any): string[] => parsePaths(value)
const processFiles = (files: any[]) => {
if (!files || !Array.isArray(files)) return []
return files.map((file: any) => ({
name: file.name,
url: file.src_pull || file.file_url,
file_name: file.src
}))
}
const showDialog = async (_type: string, row: any) => {
try {
// 根据新的 API 获取详情
const res: any = await request.get({
url: '/common/usedcardetail/show',
params: { id: row.id }
})
detailData.value = res
// 处理合同文件
const file_list = res?.attachment_pull
if (file_list) {
contractFiles.value = processFiles(file_list)
} else {
contractFiles.value = []
}
// 处理视频列表
const videoList = res?.video_pull || []
detailVideoList.value = Array.isArray(videoList)
? videoList.map((v: any) => ({
url: v.file_url || v.src_pull || v,
name: v.name || '视频'
}))
: []
detailDialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
}
}
// 编辑功能
const editFn = async (row: any) => {
try {
const res: any = await request.get({
url: '/common/usedcardetail/show',
params: { id: row.id }
})
// 填充表单
editForm.id = res.id
editForm.product_name = res.product_name || ''
editForm.price = res.price || ''
editForm.source_place = res.source_place || ''
editForm.plate_time = res.plate_time || ''
editForm.displacement = res.displacement || ''
editForm.power = res.power || ''
editForm.body_color = res.body_color || ''
editForm.condition_desc = res.condition_desc || ''
editForm.mileage = res.mileage || ''
// 处理图片
const images = parsePreviewList(res.images_pull || res.images || [])
editForm.images = images
editImageFileList.value = images.map((url, idx) => ({
name: `image_${idx}`,
url,
status: 'success' as const,
uid: idx,
response: { file_url: url, file_name: url }
}))
// 处理视频
const videoList = res.video_pull || []
const videos = Array.isArray(videoList) ? videoList : []
editForm.intro_video = videos.map((v: any) => v.file_name || v.src || v)
editVideoPreviewList.value = videos.map((v: any) => ({
url: v.file_url || v.src_pull || v,
name: v.name || '视频'
}))
editVideoFileList.value = videos.map((v: any, idx: number) => ({
name: v.name || 'video',
url: v.file_url || v.src_pull || v,
status: 'success',
uid: idx,
response: { file_url: v.file_url || v.src_pull || v, file_name: v.file_name || v.src || v }
}))
// 处理附件
const attachmentList = res.attachment_pull || []
editForm.attachment = attachmentList.map((a: any) => ({
name: a.name,
src: a.src
}))
editAttachmentFileList.value = attachmentList.map((a: any, idx: number) => ({
name: a.name,
url: a.src_pull || a.file_url,
file_name: a.src,
status: 'success',
uid: idx,
response: { file_url: a.src_pull || a.file_url, file_name: a.src }
}))
// 处理 FAQ
editForm.faqs =
res.faqs && Array.isArray(res.faqs) && res.faqs.length > 0
? res.faqs
: [{ question: '', answer: '' }]
// 处理绑定信息
if (res.crawl_title || res.crawl_brand) {
editBindState.boundProduct = {
title: res.crawl_title,
brand_name: res.crawl_brand,
series_name: res.crawl_series,
year: res.crawl_year,
msrp: res.crawl_msrp,
car_id: res.crawl_car_id,
brand_id: res.crawl_brand_id,
series_id: res.crawl_series_id
}
editForm.crawl_title = res.crawl_title || ''
editForm.crawl_brand = res.crawl_brand || ''
editForm.crawl_series = res.crawl_series || ''
editForm.crawl_year = res.crawl_year || ''
editForm.crawl_msrp = res.crawl_msrp || ''
editForm.crawl_car_id = res.crawl_car_id || ''
editForm.crawl_brand_id = res.crawl_brand_id || ''
editForm.crawl_series_id = res.crawl_series_id || ''
}
editDialogVisible.value = true
} catch (error) {
console.error('获取编辑数据失败:', error)
ElMessage.error('获取编辑数据失败')
}
}
// 编辑绑定功能
const onEditBindByName = () => {
const name = String(editForm.product_name || '').trim()
if (!name) {
ElMessage.warning('请先输入产品名称')
return
}
editBindState.showDialog = true
editBindState.loading = true
editBindState.list = []
editBindState.selectedId = ''
editBindState.keyword = ''
request
.get({
url: '/common/usedcar/crawl',
params: { keyword: name }
})
.then((res: any) => {
const data = res.model || []
editBindState.list = data
})
.catch(() => {
ElMessage.error('拉取产品失败,请稍后重试')
})
.finally(() => {
editBindState.loading = false
})
}
const closeEditBindDialog = () => {
editBindState.showDialog = false
editBindState.keyword = ''
}
const getEditBindItemId = (it: any, idx: number) =>
String(it.id || it.product_id || it.requisition_item_id || it.code || idx)
const selectEditBind = (it: any, idx: number) => {
editBindState.selectedId = getEditBindItemId(it, idx)
editBindState._selected = it
}
const confirmEditBind = () => {
if (!editBindState.selectedId || !editBindState._selected) return
const it = editBindState._selected
editBindState.boundProduct = it
editForm.crawl_title = it.title || ''
editForm.crawl_brand = it.brand_name || ''
editForm.crawl_series = it.series_name || ''
editForm.crawl_year = it.year || ''
editForm.crawl_msrp = it.msrp || ''
editForm.crawl_car_id = it.car_id || ''
editForm.crawl_brand_id = it.brand_id || ''
editForm.crawl_series_id = it.series_id || ''
closeEditBindDialog()
ElMessage.success('绑定成功')
}
const clearEditBind = () => {
editForm.crawl_title = ''
editForm.crawl_brand = ''
editForm.crawl_series = ''
editForm.crawl_year = ''
editForm.crawl_msrp = ''
editForm.crawl_car_id = ''
editForm.crawl_brand_id = ''
editForm.crawl_series_id = ''
editBindState.boundProduct = null
ElMessage.success('已清除绑定')
}
// 图片上传处理
const loadingInstance = ref<ReturnType<typeof ElLoading.service> | null>(null)
const handleImagePreview = () => {
loadingInstance.value = ElLoading.service({
lock: true,
text: '正在上传中…',
background: 'rgba(0, 0, 0, 0.45)'
})
return true
}
const handleEditImageSuccess = (response: any, file: UploadFile, fileList: UploadFile[]) => {
const url = response?.file_url || response?.data?.file_url || ''
const fileName = response?.file_name || response?.data?.file_name || ''
;(file as UploadFileWithResponse).response = response
if (url && !file.url) file.url = url
if (fileName && !editForm.images.includes(fileName)) {
editForm.images.push(fileName)
}
editImageFileList.value = fileList as UploadFileWithResponse[]
loadingInstance.value?.close()
}
const handleEditImageRemove = (file: UploadFile, fileList: UploadFile[]) => {
const resp = (file as UploadFileWithResponse).response
const fileName = resp?.file_name || resp?.data?.file_name || ''
if (fileName) {
const idx = editForm.images.indexOf(fileName)
if (idx > -1) editForm.images.splice(idx, 1)
}
editImageFileList.value = fileList as UploadFileWithResponse[]
}
const handleEditImageExceed = () => {
ElMessage.warning(`最多只能上传 ${maxMedia} 张图片`)
}
// 视频上传处理
const handleVideoPreview = () => {
loadingInstance.value = ElLoading.service({
lock: true,
text: '正在上传视频…',
background: 'rgba(0, 0, 0, 0.45)'
})
return true
}
const handleEditVideoSuccess = (response: any, file: UploadFile, fileList: UploadFile[]) => {
const url = response?.file_url || response?.data?.file_url || ''
const fileName = response?.file_name || response?.data?.file_name || ''
;(file as UploadFileWithResponse).response = response
if (url && !file.url) file.url = url
if (fileName && !editForm.intro_video.includes(fileName)) {
editForm.intro_video.push(fileName)
}
if (!editVideoPreviewList.value.find((v) => v.url === url)) {
editVideoPreviewList.value.push({ url, name: file.name })
}
editVideoFileList.value = fileList as UploadFileWithResponse[]
loadingInstance.value?.close()
}
const handleEditVideoRemove = (file: UploadFile, fileList: UploadFile[]) => {
const resp = (file as UploadFileWithResponse).response
const fileName = resp?.file_name || resp?.data?.file_name || ''
if (fileName) {
const idx = editForm.intro_video.indexOf(fileName)
if (idx > -1) editForm.intro_video.splice(idx, 1)
}
const url = resp?.file_url || file.url || ''
const previewIdx = editVideoPreviewList.value.findIndex((v) => v.url === url)
if (previewIdx > -1) {
editVideoPreviewList.value.splice(previewIdx, 1)
}
editVideoFileList.value = fileList as UploadFileWithResponse[]
}
const handleVideoExceed = () => {
ElMessage.warning(`最多只能上传 ${maxVideos} 个视频`)
}
const removeEditVideoByIndex = (index: number) => {
editForm.intro_video.splice(index, 1)
editVideoPreviewList.value.splice(index, 1)
editVideoFileList.value.splice(index, 1)
}
// 附件变化处理
const handleEditAttachmentChange = (files: FileItem[]) => {
editForm.attachment = files.map((file) => ({
name: file.name,
src: file.file_name
}))
}
// FAQ 操作
const addEditFaq = () => {
editForm.faqs.push({ question: '', answer: '' })
}
const removeEditFaq = (index: number) => {
if (editForm.faqs.length > 1) {
editForm.faqs.splice(index, 1)
}
}
// 编辑提交
const handleEditSubmit = () => {
if (!editFormRef.value) return
editFormRef.value.validate(async (valid) => {
if (!valid) return
editSubmitLoading.value = true
try {
const validFaqs = editForm.faqs.filter((faq) => faq.question.trim() || faq.answer.trim())
const payload: any = {
id: editForm.id,
product_name: editForm.product_name,
price: editForm.price,
source_place: editForm.source_place,
plate_time: editForm.plate_time,
displacement: editForm.displacement,
power: editForm.power,
body_color: editForm.body_color,
condition_desc: editForm.condition_desc,
mileage: editForm.mileage,
images: editForm.images.map((s) => String(s || '').trim()).filter(Boolean).join(','),
intro_video: editForm.intro_video.map((s) => String(s || '').trim()).filter(Boolean).join(','),
attachment: editForm.attachment,
faqs: validFaqs.length > 0 ? validFaqs : undefined,
crawl_title: editForm.crawl_title,
crawl_brand: editForm.crawl_brand,
crawl_series: editForm.crawl_series,
crawl_year: editForm.crawl_year,
crawl_msrp: editForm.crawl_msrp,
crawl_car_id: editForm.crawl_car_id,
crawl_brand_id: editForm.crawl_brand_id,
crawl_series_id: editForm.crawl_series_id
}
await manageService.publisheditpro(payload)
ElMessage.success('编辑成功')
editDialogVisible.value = false
getWorkList()
} catch (error: any) {
ElMessage.error(error?.message || '编辑失败,请稍后重试')
} finally {
editSubmitLoading.value = false
}
})
}
// 标记售出
const statusfn = (row: any) => {
ElMessageBox.confirm('确定要标记售出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await manageService.updatestatus({ id: row.id, status: 2 })
ElMessage.success('标记售出成功')
getWorkList()
})
.catch(() => {
// 用户取消
})
}
// 删除
const deleteFn = (row: any) => {
ElMessageBox.confirm('确定要删除此发布吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await manageService.publishdeletepro({ id: row.id })
ElMessage.success('删除成功')
getWorkList()
})
.catch(() => {
// 用户取消
})
}
// 跳转新增
const goToAdd = () => {
window.location.href = '/admin/publish/publish_addpro'
}
// 获取列表
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (keyword.value) params.keyword = keyword.value
if (auditStatus.value !== '') params.audit_status = auditStatus.value
// 使用新的 API
const { records, current, size, total } = await manageService.getpublishListpro(params)
tableData.value = records
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
watch([keyword, auditStatus], () => {
pagination.currentPage = 1
getWorkList()
})
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
onMounted(() => {
getWorkList()
})
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.thumb-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.avatarimg {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
.customer-search-group {
display: flex;
gap: 12px;
}
.bind-name-wrapper {
display: flex;
gap: 12px;
width: 100%;
:deep(.el-input) {
flex: 1;
}
}
.bind-tip {
display: flex;
align-items: center;
gap: 8px;
color: #909399;
.bind-tip-text {
color: #606266;
}
}
.video-preview-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
.video-item {
position: relative;
width: 200px;
.video-preview {
width: 100%;
height: auto;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.el-button {
margin-top: 4px;
}
}
}
.faq-display {
.faq-item {
padding: 12px;
margin-bottom: 12px;
background-color: #f5f7fa;
border-radius: 6px;
&:last-child {
margin-bottom: 0;
}
.faq-question,
.faq-answer {
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
.faq-label {
font-weight: 500;
color: #409eff;
}
}
}
.faq-container {
width: 100%;
.faq-item {
padding: 16px;
margin-bottom: 12px;
background-color: #f5f7fa;
border-radius: 6px;
.faq-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.faq-index {
font-weight: 500;
color: #409eff;
}
}
:deep(.el-form-item) {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
}
.upload-list {
:deep(.el-upload-list__item-thumbnail) {
object-fit: cover;
}
}
.bind-search {
margin-bottom: 12px;
}
.bind-loading,
.bind-empty {
padding: 16px;
text-align: center;
color: #909399;
}
.bind-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
& + .bind-item {
margin-top: 6px;
}
&:hover {
background-color: #f5f7fa;
}
&.active {
background-color: #ecf5ff;
border: 1px solid #409eff;
}
}
.bind-item-main {
flex: 1;
min-width: 0;
}
.bind-item-title {
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.bind-item-sub {
font-size: 12px;
color: #909399;
}
.bind-item-check {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
.check-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #409eff;
}
}
// 审核记录时间轴样式
.audit-records-container {
max-height: 500px;
overflow-y: auto;
padding: 10px 0;
}
.audit-record-card {
border-radius: 8px;
.audit-record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.auditor-info {
display: flex;
align-items: center;
gap: 4px;
color: #606266;
font-size: 14px;
}
}
.audit-record-content {
margin-bottom: 12px;
.content-label {
font-weight: 500;
color: #606266;
margin-bottom: 4px;
font-size: 14px;
}
.content-value {
color: #303133;
font-size: 14px;
line-height: 1.5;
}
}
.audit-record-status-change {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
.status-before,
.status-after {
font-size: 14px;
font-weight: 500;
}
.status-before {
color: #909399;
}
.status-after {
color: #67c23a;
}
.status-arrow {
color: #409eff;
}
}
}
</style>