- 深度优化商品审核工作台:指标概览卡片、风险边条队列、标价管道联动、核心资产高密度展示、底部决策浮动 Dock、全套键盘快捷键 - 重构截图大图查看体验:移除旧版底部溢出的全局画廊,封装全屏居中弹窗 AdminListingReviewPreviewDialog,支持「按住速览(松手即关闭)」与「常驻弹窗(缩放/翻页/打码/原图)」双模交互 - 新增在线截图编辑器 ImageEditorDialog:支持平滑马赛克打码、自由裁剪与旋转保存 - 弹窗组件解耦:抽离审核拒绝、调价、全量核验、快捷键速查等子组件,严格控制单文件行数 - 后端支持:新增商品审核调整价格接口与配套单元测试
224 lines
5.3 KiB
Vue
224 lines
5.3 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import { Check, InfoFilled } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { Listing } from '@/features/listings'
|
||
import { formatListingCode, readAssetString } from '@/shared/utils/listingDisplay'
|
||
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
listing: Listing | null
|
||
submitting: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:visible', value: boolean): void
|
||
(e: 'reject', payload: { listingId: number; reason: string }): void
|
||
}>()
|
||
|
||
const rejectReasonOptions = [
|
||
'默认截图,需补充真实截图',
|
||
'账号资产信息不完整',
|
||
'价格或押金异常',
|
||
'封禁记录需补充说明',
|
||
'联系方式异常',
|
||
'截图与所填信息不符',
|
||
'缺少安全中心实机截图',
|
||
]
|
||
|
||
const rejectReason = ref('')
|
||
|
||
watch(
|
||
() => [props.visible, props.listing] as const,
|
||
([visible, listing]) => {
|
||
if (visible && listing) {
|
||
rejectReason.value = listing.review_reason || ''
|
||
const ban = readAssetString(listing, 'ban_record')
|
||
const hasBan = ban && ban !== '-' && !ban.includes('无')
|
||
const hasDefaultShot = listing.screenshot_urls?.some(url =>
|
||
url.includes('/api/listings/default-upload-screenshot')
|
||
)
|
||
|
||
if (hasBan && !rejectReason.value.includes('封禁记录')) {
|
||
toggleRejectReason('封禁记录需补充说明')
|
||
} else if (hasDefaultShot && !rejectReason.value.includes('默认截图')) {
|
||
toggleRejectReason('默认截图,需补充真实截图')
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
function toggleRejectReason(reason: string) {
|
||
const current = rejectReason.value.trim()
|
||
if (!current) {
|
||
rejectReason.value = reason
|
||
return
|
||
}
|
||
const parts = current
|
||
.split(/[;;]/)
|
||
.map(p => p.trim())
|
||
.filter(Boolean)
|
||
const idx = parts.indexOf(reason)
|
||
if (idx >= 0) {
|
||
parts.splice(idx, 1)
|
||
} else {
|
||
parts.push(reason)
|
||
}
|
||
rejectReason.value = parts.join(';')
|
||
}
|
||
|
||
function isReasonSelected(reason: string) {
|
||
const parts = rejectReason.value
|
||
.split(/[;;]/)
|
||
.map(p => p.trim())
|
||
.filter(Boolean)
|
||
return parts.includes(reason)
|
||
}
|
||
|
||
function handleConfirm() {
|
||
if (!props.listing) return
|
||
const reason = rejectReason.value.trim()
|
||
if (!reason) {
|
||
ElMessage.warning('请填写拒绝原因')
|
||
return
|
||
}
|
||
emit('reject', {
|
||
listingId: props.listing.id,
|
||
reason,
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog
|
||
:model-value="visible"
|
||
title="拒绝发布商品"
|
||
width="640px"
|
||
append-to-body
|
||
@update:model-value="emit('update:visible', $event)"
|
||
>
|
||
<div v-if="listing" class="dialog-body reject-dialog">
|
||
<div class="dialog-item-header">
|
||
<span class="dialog-code">#{{ formatListingCode(listing) }}</span>
|
||
<strong>{{ listing.title }}</strong>
|
||
</div>
|
||
|
||
<div class="reject-quick-tags">
|
||
<p class="quick-tags-title">快捷原因选择(点击可追加或取消):</p>
|
||
<div class="quick-tags-list">
|
||
<button
|
||
v-for="reason in rejectReasonOptions"
|
||
:key="reason"
|
||
type="button"
|
||
class="quick-reason-pill"
|
||
:class="{ selected: isReasonSelected(reason) }"
|
||
@click="toggleRejectReason(reason)"
|
||
>
|
||
<el-icon v-if="isReasonSelected(reason)"><Check /></el-icon>
|
||
{{ reason }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reject-input-block">
|
||
<el-input
|
||
v-model="rejectReason"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请详细填写审核拒绝原因,将通过短信与站内信通知号主修改后重新提交..."
|
||
/>
|
||
</div>
|
||
<div class="reject-tip">
|
||
<el-icon><InfoFilled /></el-icon>
|
||
号主修改后可重新提交审核,原商品信息将进入已下架/待重新发布状态。
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="emit('update:visible', false)">取消</el-button>
|
||
<el-button
|
||
type="danger"
|
||
:loading="submitting"
|
||
:disabled="!rejectReason.trim()"
|
||
@click="handleConfirm"
|
||
>
|
||
确认拒绝并通知号主
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dialog-item-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.dialog-code {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.reject-quick-tags {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.quick-tags-title {
|
||
margin: 0 0 8px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.quick-tags-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.quick-reason-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
padding: 5px 10px;
|
||
color: #334155;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.quick-reason-pill:hover {
|
||
border-color: #2563eb;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.quick-reason-pill.selected {
|
||
border-color: #2563eb;
|
||
background: #eff6ff;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.reject-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|