Files
smt-used-car-pc/src/views/admin/publish/publish_my_listpro.vue
T
2026-03-16 18:11:41 +08:00

1757 lines
55 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="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-option label="抓取成功" :value="3" />
<el-option label="推送成功" :value="4" />
<el-option label="推送失败" :value="5" />
</el-select>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<div style="text-align: right; padding: 12px 0">
<el-button type="primary" @click="fetchpublishfn"> 抓取 </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)"
v-if="scope.row.status === 0 || scope.row.status === 2"
>
编辑
</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="仅支持单个文件上传"
:max-count="1"
: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>
<!-- 抓取弹窗 -->
<el-dialog
v-model="fetchDialogVisible"
title="抓取数据"
width="800px"
:close-on-click-modal="false"
>
<div class="fetch-dialog-content">
<el-form :model="fetchForm" label-width="70px" style="margin-bottom: 20px; display: flex">
<el-form-item label="抓取链接">
<el-input
v-model="fetchForm.url"
placeholder="请输入要抓取的链接"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFetch" :loading="fetchLoading">抓取</el-button>
</el-form-item>
</el-form>
<!-- 抓取结果显示 -->
<div v-if="fetchLoading" class="fetch-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span style="margin-left: 8px">正在抓取中...</span>
</div>
<el-form v-else-if="fetchData" :model="fetchForm" label-width="100px" class="fetch-result-form">
<ElRow :gutter="12">
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="产品名称">
<el-input v-model="fetchForm.product_name" placeholder="请输入产品名称" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车价(¥)">
<el-input v-model="fetchForm.price" placeholder="请输入车价" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车源地">
<el-input v-model="fetchForm.source_place" placeholder="请输入车源地" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="上牌时间">
<el-input v-model="fetchForm.plate_time" placeholder="请输入上牌时间" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="排量">
<el-input v-model="fetchForm.displacement" placeholder="请输入排量" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="里程(Km)">
<el-input v-model="fetchForm.mileage" placeholder="请输入里程" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="马力">
<el-input v-model="fetchForm.power" placeholder="请输入马力" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="车身颜色">
<el-input v-model="fetchForm.body_color" placeholder="请输入车身颜色" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="车况介绍">
<el-input
v-model="fetchForm.condition_desc"
type="textarea"
:rows="3"
placeholder="请输入车况介绍"
/>
</el-form-item>
</ElCol>
</ElRow>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="fetchDialogVisible = false">关 闭</el-button>
<el-button type="primary" @click="handleFetchConfirm" :disabled="!fetchData">确认添加</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,
ElRow,
ElCol,
type FormInstance,
type FormRules,
type UploadFile
} from 'element-plus'
import { Plus, Loading, User, ArrowRight } 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[] // 用于显示(images_pull)
original_images: string[] // 用于保存提交(原始 images)
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: [],
original_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 fetchImgList = ref<string[]>([])
const imgList = computed(() => {
// 如果抓取弹窗打开且有图片,显示抓取的图片
if (fetchDialogVisible.value && fetchImgList.value.length > 0) {
return fetchImgList.value
}
return 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: '审核拒绝',
3: '推送中',
4: '推送成功',
5: '推送失败'
}
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',
3: 'info',
4: 'success',
5: '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',
3: 'info',
4: 'success',
5: '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 || ''
// 处理图片:images_pull 用于显示,images 用于保存
const displayImages = parsePreviewList(res.images_pull || res.images || [])
const saveImages = parsePreviewList(res.images || [])
editForm.images = displayImages // 用于显示
editForm.original_images = saveImages // 用于保存提交
editImageFileList.value = displayImages.map((url, idx) => ({
name: `image_${idx}`,
url,
status: 'success' as const,
uid: idx,
response: { file_url: url, file_name: saveImages[idx] || 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)
editForm.original_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)
editForm.original_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.original_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,
scene: 'self'
}
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()
}
// 抓取相关
const fetchDialogVisible = ref(false)
const fetchForm = reactive({
url: '',
product_name: '',
price: '',
source_place: '',
plate_time: '',
displacement: '',
power: '',
body_color: '',
condition_desc: '',
mileage: ''
})
const fetchLoading = ref(false)
const fetchData = ref<any>(null)
// 打开抓取弹窗
const fetchpublishfn = () => {
fetchDialogVisible.value = true
fetchForm.url = ''
fetchForm.product_name = ''
fetchForm.price = ''
fetchForm.source_place = ''
fetchForm.plate_time = ''
fetchForm.displacement = ''
fetchForm.power = ''
fetchForm.body_color = ''
fetchForm.condition_desc = ''
fetchForm.mileage = ''
fetchData.value = null
fetchImgList.value = []
}
// 执行抓取
const handleFetch = async () => {
if (!fetchForm.url || !fetchForm.url.trim()) {
ElMessage.warning('请输入抓取链接')
return
}
fetchLoading.value = true
fetchData.value = null
try {
const res: any = await request.post({
url: '/common/usedcardetail/crawl',
params: { url: fetchForm.url.trim() }
})
fetchData.value = res?.data || res || {}
// 填充表单数据
fetchForm.product_name = fetchData.value.product_name || ''
fetchForm.price = fetchData.value.price || ''
fetchForm.source_place = fetchData.value.source_place || ''
fetchForm.plate_time = fetchData.value.plate_time || ''
fetchForm.displacement = fetchData.value.displacement || ''
fetchForm.power = fetchData.value.power || ''
fetchForm.body_color = fetchData.value.body_color || ''
fetchForm.condition_desc = fetchData.value.condition_desc || ''
fetchForm.mileage = fetchData.value.mileage || ''
// 处理图片列表用于预览
fetchImgList.value = parsePreviewList(fetchData.value.image_pull || fetchData.value.image || [])
ElMessage.success(res?.message || '抓取成功')
console.log('抓取接口返回数据:', fetchData.value)
} catch (error: any) {
console.error('抓取失败:', error)
fetchData.value = null
ElMessage.error(error?.message || '抓取失败,请稍后重试')
} finally {
fetchLoading.value = false
}
}
// 确认添加抓取的数据
// 确认添加抓取的数据
const handleFetchConfirm = async () => {
// 合并抓取接口返回的原始数据和用户修改的数据
const formData = {
...fetchData.value, // 保存所有抓取接口返回的原始数据
// 用表单中修改的值替换对应字段
product_name: fetchForm.product_name,
price: fetchForm.price,
source_place: fetchForm.source_place,
plate_time: fetchForm.plate_time,
displacement: fetchForm.displacement,
power: fetchForm.power,
body_color: fetchForm.body_color,
condition_desc: fetchForm.condition_desc,
mileage: fetchForm.mileage
}
console.log('保存抓取数据:', formData)
try {
const res: any = await request.post({
url: '/common/usedcardetail/crawl/store',
params: formData
})
ElMessage.success(res?.message)
fetchDialogVisible.value = false
getWorkList() // 刷新列表
} catch (error: any) {
console.error('保存抓取数据失败:', error)
ElMessage.error(error?.message || '保存失败,请稍后重试')
}
}
// 抓取图片预览点击
const handleFetchAvatarClick = (index: number) => {
viewerIndex.value = index
showImageViewer.value = true
}
// 格式化年月(如:2024年01月 -> 2024-01-01)
function formatCnYearMonth(str: string) {
const match = str.match(/(\d{4})年(\d{1,2})月/)
if (!match) return str
const year = match[1]
const month = match[2].padStart(2, '0')
return `${year}-${month}-01`
}
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;
}
}
}
// 抓取弹窗样式
.fetch-dialog-content {
.fetch-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #909399;
}
.fetch-result {
.thumb-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.avatarimg {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
}
}
</style>