deotalandAi/apps/FrontendDesigner/src/views/admin/AdminContentReview.vue

935 lines
21 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="admin-content-review">
<!-- 统计卡片 -->
<div class="review-stats">
<!-- <div class="stat-card">
<div class="stat-icon total">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.total }}</div>
<div class="stat-label">{{ t('admin.review.stats.total') }}</div>
</div>
</div> -->
<div class="stat-card">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.pending }}</div>
<div class="stat-label">{{ t('admin.review.stats.pending') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon approved">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.approved }}</div>
<div class="stat-label">{{ t('admin.review.stats.approved') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon rejected">
<el-icon><Close /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.rejected }}</div>
<div class="stat-label">{{ t('admin.review.stats.rejected') }}</div>
</div>
</div>
</div>
<!-- 筛选和搜索 -->
<div class="review-filters">
<div class="search-group">
<el-input
v-model="searchQuery"
:placeholder="t('admin.review.search')"
:prefix-icon="Search"
clearable
/>
</div>
<div class="filter-actions">
<el-button :icon="Refresh" @click="refresh">
{{ t('admin.common.refresh') }}
</el-button>
</div>
</div>
<!-- 审核列表 -->
<div class="review-table" :style="{ height: tableHeight + 'px' }">
<!-- stripe -->
<el-table
:data="filteredReviewList"
style="width: 100%"
v-loading="loading"
:max-height="tableHeight"
:height="tableHeight"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="creatorName" :label="t('admin.review.creator')" min-width="140" />
<el-table-column :label="t('admin.review.thumbnail')" min-width="80">
<template #default="{ row }">
<div
class="thumbnail-container"
@click="previewThumbnail(row.thumbnail)"
>
<el-image
:src="row.thumbnail"
:lazy="true"
style="width: 50px; height: 50px; cursor: pointer;"
fit="cover"
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
</template>
</el-table-column>
<el-table-column prop="orderPrice" :label="t('admin.review.orderPrice')" min-width="120">
<template #default="{ row }">
¥{{ row.orderPrice.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="status" :label="t('admin.review.status')" min-width="120">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row).status">
{{ t(`${getStatusTagType(row).label}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="t('admin.review.createTime')" min-width="160">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column :label="$t('admin.review.actions')" min-width="300" align="center" fixed="right">
<template #default="{ row }">
<div class="actions-container">
<el-button size="small" @click="previewModel(row)">
{{ t('admin.review.preview3D') }}
</el-button>
<el-button
size="small"
type="primary"
@click="approveReview(row)"
>
{{ t('admin.review.approve') }}
</el-button>
<el-button
size="small"
type="danger"
@click="rejectReview(row)"
>
{{ t('admin.review.reject') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalReviews"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 预览图片对话框 -->
<el-dialog
:title="t('admin.review.previewImage')"
v-model="previewImageVisible"
width="80%"
top="8vh"
:z-index="1000"
:append-to-body="true"
class="image-preview-dialog"
>
<div class="image-preview-container">
<div class="image-preview-content">
<img
:src="currentImage"
class="preview-image"
:style="{ transform: `scale(${imageScale})` }"
/>
</div>
<div class="image-actions">
<el-button
size="small"
@click="zoomIn"
>
<el-icon><ZoomIn /></el-icon>
{{ t('admin.review.zoomIn') }}
</el-button>
<el-button
size="small"
@click="zoomOut"
>
<el-icon><ZoomOut /></el-icon>
{{ t('admin.review.zoomOut') }}
</el-button>
<el-button
size="small"
@click="resetZoom"
>
<el-icon><Refresh /></el-icon>
{{ t('admin.review.resetZoom') }}
</el-button>
<el-button
size="small"
type="primary"
@click="openInNewTab"
>
<el-icon><Link /></el-icon>
{{ t('admin.review.openInNewTab') }}
</el-button>
</div>
</div>
</el-dialog>
<!-- 3D模型预览对话框 -->
<el-dialog
:title="t('admin.review.preview3DModel')"
v-model="preview3DVisible"
width="85%"
top="5vh"
:z-index="2000"
:append-to-body="true"
class="model-preview-dialog"
>
<div class="model-preview-content">
<ModelViewer
ref="modelViewerRef"
:model-url="currentModelUrl"
:show-controls="true"
:loading-text="t('modelViewer.loadingModel')"
style="height: 70vh; width: 100%;"
/>
</div>
</el-dialog>
<!-- 拒绝审核对话框 -->
<el-dialog
:title="t('admin.review.rejectReview')"
v-model="rejectDialogVisible"
width="500px"
>
<el-form :model="rejectForm" :rules="rejectRules" ref="rejectFormRef" label-width="100px">
<el-form-item :label="t('admin.review.rejectionReason')" prop="reason">
<el-input
v-model="rejectForm.reason"
type="textarea"
:rows="4"
:placeholder="t('admin.review.pleaseInputReason')"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="rejectDialogVisible = false">
{{ t('common.cancel') }}
</el-button>
<el-button type="primary" @click="confirmRejectReview">
{{ t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { orderStatus } from '@deotaland/utils'
import {AdminOrders} from './AdminOrders/AdminOrders'
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Refresh,
Search,
Document,
Clock,
Check,
Close,
Picture,
ZoomIn,
ZoomOut,
Link
} from '@element-plus/icons-vue'
const adminOrders = new AdminOrders();
// 引入 useRouter
import { useRouter } from 'vue-router'
import ModelViewer from '@/components/common/ModelViewer.vue'
const { t } = useI18n()
const router = useRouter()
// 响应式数据
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const totalReviews = ref(0)
const searchQuery = ref('')
const tableHeight = ref(600)
// 对话框状态
const previewImageVisible = ref(false)
const preview3DVisible = ref(false)
const rejectDialogVisible = ref(false)
const currentImage = ref('')
const currentModelUrl = ref('') // 当前显示的模型URL
// 图片预览相关
const imageScale = ref(1)
// 拒绝审核表单
const rejectFormRef = ref(null)
const rejectForm = ref({
reason: ''
})
const rejectRules = {
reason: [
{ required: true, message: t('admin.review.pleaseInputReason'), trigger: 'blur' }
]
}
// 选中行数据
const selectedReview = ref(null)
// 计算属性
const filteredReviewList = computed(() => {
let list = ordersList.value.map((item) =>{
return {
...item,
creatorName: (item.order_info?.shipping?.firstName || '')+(item.order_info?.shipping?.lastName || '') ,
thumbnail: item.order_info?.modelData?.imageUrl || '',
orderPrice: item.actual_amount,
createTime: item.created_at,
modelUrl: item.order_info?.modelData?.modelUrl || '',
}
});
return list
})
// 统计数据
const reviewStats = ref({
total: 0,
pending: 0,
approved: 0,
rejected: 0
})
const getOrderStatistics = async () => {
adminOrders.getOrderStatistics().then(res=>{
if(res.code==0){
let data = res.data.approve_maps
reviewStats.value = {
total: data.total_approve_count,
pending: data.pending_count,
approved: data.pass_count,
rejected: data.failure_count,
}
}
})
}
// 方法
const refresh = () => {
loading.value = true
init();
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
}
const handleCurrentChange = (page) => {
currentPage.value = page
init();
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString(
t('admin.review.dateFormat') === 'en-US' ? 'en-US' : 'zh-CN',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}
)
}
const getStatusTagType = (status) => {
return orderStatus.getOrderStatusOptions(status)
}
const previewThumbnail = (imageUrl) => {
if (!imageUrl) {
ElMessage.warning(t('admin.review.noThumbnail'))
return
}
currentImage.value = imageUrl
imageScale.value = 1
previewImageVisible.value = true
}
// 图片预览相关方法
const zoomIn = () => {
if (imageScale.value < 3) {
imageScale.value = Math.round((imageScale.value + 0.2) * 10) / 10
}
}
const zoomOut = () => {
if (imageScale.value > 0.3) {
imageScale.value = Math.round((imageScale.value - 0.2) * 10) / 10
}
}
const resetZoom = () => {
imageScale.value = 1
}
const openInNewTab = () => {
window.open(currentImage.value, '_blank')
}
const previewModel = (review) => {
// 设置3D模型URL如果review中有modelUrl则使用否则使用默认模型
if (review.modelUrl) {
currentModelUrl.value = review.modelUrl
preview3DVisible.value = true
}
}
// 审批同意审核
const approveReview = async (review) => {
try {
await ElMessageBox.confirm(
t('admin.review.confirmApprove'),
t('admin.review.approve'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'success'
}
)
adminOrders.updateOrderStatus(1,'',{id:review.id}).then(res=>{
if(res.code==0){
ElMessage.success(t('admin.review.approveSuccess'))
// 跳转到拆件订单页面
router.push(`/admin/disassembly-orders?order_no=${review.order_no}`)
}
})
} catch {
// 用户取消
}
}
const rejectReview = (review) => {
selectedReview.value = review
rejectForm.value.reason = ''
rejectDialogVisible.value = true
}
const confirmRejectReview = async () => {
if (!rejectFormRef.value) return
try {
await rejectFormRef.value.validate()
adminOrders.updateOrderStatus(0,rejectForm.value.reason,{id:selectedReview.value.id}).then(res=>{
if(res.code==0){
ElMessage.success(t('admin.review.rejectSuccess'))
// 跳转到拆件订单页面
init();
rejectDialogVisible.value = false;
}
})
} catch {
// 验证失败
}
}
const ortherjson = ref({
order_status:[0],
refund_status:[0],
payment_status:[1]
});
const ordersList = ref([]);
const getList = ()=>{
loading.value = true
let params = {
...ortherjson.value,
page_size: pageSize.value,
page: currentPage.value,
order_no:searchQuery.value
}
adminOrders.getOrderList(params).then(res=>{
let data = res.data || [];
ordersList.value = data.items;
loading.value = false
totalReviews.value = data.total;
// ordersList.value = [...data.items,...data.items];
// orderStats.value.pending = data.total || 0;
})
}
const init = ()=>{
getOrderStatistics();
getList()
}
// 根据窗口大小调整表格高度
const updateTableHeight = () => {
const windowHeight = window.innerHeight
const headerHeight = 200 // 估算头部区域高度
const paginationHeight = 80 // 估算分页区域高度
const padding = 40 // 页面padding
if (window.innerWidth <= 768) {
// 移动端使用较小的高度
tableHeight.value = Math.max(400, windowHeight - headerHeight - paginationHeight - padding)
} else {
// 桌面端使用标准高度
tableHeight.value = Math.min(600, windowHeight - headerHeight - paginationHeight - padding-160)
}
}
onMounted(() => {
// 检查路由参数中的order_no
const routeQuery = router.currentRoute.value.query
if (routeQuery.order_no) {
searchQuery.value = routeQuery.order_no
}
// 页面加载时的初始化操作
init();
// 设置初始表格高度
updateTableHeight()
// 监听窗口大小变化
window.addEventListener('resize', updateTableHeight)
})
// 组件卸载时移除监听器
import { onUnmounted } from 'vue'
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.admin-content-review {
padding: 20px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
}
.header-left .title {
font-size: 28px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
}
.header-left .subtitle {
color: #6b7280;
margin: 0;
font-size: 14px;
}
.header-actions {
display: flex;
gap: 12px;
}
.review-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.stat-icon.total { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.stat-icon.pending { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.stat-icon.approved { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.stat-icon.rejected { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
.stat-number {
font-size: 32px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.stat-label {
color: #6b7280;
font-size: 14px;
margin-top: 4px;
}
.review-filters {
padding: 20px;
border-radius: 12px;
margin-bottom: 24px;
display: flex;
gap: 12px;
align-items: flex-end;
justify-content: flex-end;
overflow-x: auto;
white-space: nowrap;
}
.filter-group {
display: flex;
gap: 8px;
align-items: flex-end;
flex-shrink: 0;
}
.search-group {
flex: 1;
min-width: 200px;
max-width: 300px;
flex-shrink: 0;
}
.filter-actions {
display: flex;
align-items: flex-end;
flex-shrink: 0;
}
.review-table {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
margin-bottom: 24px;
}
/* 表格样式优化 */
.review-table :deep(.el-table) {
width: 100% !important;
table-layout: auto;
height: 100% !important;
}
.review-table :deep(.el-table__body-wrapper) {
overflow-y: auto;
height: calc(100% - 60px);
}
/* 优化滚动条样式 */
.review-table :deep(.el-table__body-wrapper::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
.review-table :deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
.review-table :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb) {
background: #c1c1c1;
border-radius: 3px;
}
.review-table :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
background: #a8a8a8;
}
.review-table :deep(.el-table__header) {
width: 100% !important;
}
.review-table :deep(.el-table__body) {
width: 100% !important;
}
.review-table :deep(.el-table__cell) {
padding: 8px 12px;
white-space: nowrap;
vertical-align: middle;
text-align: center;
}
.review-table :deep(.el-table__row) {
height: 66px;
}
.review-table :deep(.el-table__header-wrapper) {
background-color: #f8fafc;
}
.review-table :deep(.el-table__header th) {
background-color: #f8fafc;
color: #374151;
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap !important;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
}
.review-table :deep(.el-table__header th .cell) {
text-align: center;
white-space: nowrap !important;
word-break: keep-all;
overflow: hidden;
text-overflow: ellipsis;
}
/* 操作按钮容器 */
.actions-container {
display: flex;
gap: 6px;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
width: 100%;
overflow-x: auto;
padding-bottom: 4px;
}
.actions-container .el-button {
flex-shrink: 0;
white-space: nowrap;
}
/* 缩略图容器样式 */
.thumbnail-container {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 0;
}
.thumbnail-container .el-image {
border-radius: 6px;
width: 50px !important;
height: 50px !important;
min-width: 50px !important;
max-width: 50px !important;
min-height: 50px !important;
max-height: 50px !important;
overflow: hidden !important;
object-fit: cover;
}
.thumbnail-container .el-image img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
border-radius: 6px !important;
}
/* 图片错误样式优化 */
.image-error {
width: 50px;
height: 50px;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
border-radius: 6px;
}
.rejection-text {
color: #ef4444;
font-size: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
/* 图片预览对话框样式 */
.image-preview-dialog :deep(.el-dialog__body) {
padding: 0;
}
.image-preview-container {
display: flex;
flex-direction: column;
height: 70vh;
}
.image-preview-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 16px;
min-height: 400px;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
cursor: grab;
}
.preview-image:active {
cursor: grabbing;
}
.image-actions {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 0;
border-top: 1px solid #e5e7eb;
}
.image-actions .el-button {
min-width: 80px;
}
/* 原有样式保留 */
.image-preview {
text-align: center;
}
.model-preview {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
border-radius: 8px;
}
.model-placeholder {
text-align: center;
color: #6b7280;
}
.model-placeholder p {
margin: 16px 0;
}
.model-info {
color: #374151;
font-size: 14px;
}
@media (max-width: 768px) {
.admin-content-review {
padding: 12px;
}
.dashboard-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.review-stats {
grid-template-columns: 1fr;
}
.review-filters {
flex-direction: column;
align-items: stretch;
white-space: normal;
overflow-x: visible;
}
.filter-group {
flex-direction: column;
gap: 12px;
}
.search-group {
max-width: none;
}
/* 移动端表格优化 */
.review-table {
overflow-x: auto;
}
.review-table :deep(.el-table) {
width: auto !important;
min-width: 800px;
height: 100% !important;
}
.review-table :deep(.el-table__body-wrapper) {
height: calc(100% - 60px);
}
.review-table :deep(.el-table__cell) {
padding: 6px 8px;
font-size: 12px;
text-align: center;
}
.actions-container {
gap: 4px;
}
.actions-container .el-button {
padding: 4px 8px;
font-size: 12px;
}
}
</style>