179 lines
6.3 KiB
Vue
179 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
|
||
import { fetchAdminFileBlob } from '@/api/files'
|
||
import { adminMarkListingAbnormal, adminOfflineListing, fetchAdminListing, type Listing } from '@/api/listings'
|
||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||
import { formatDateTime } from '@/utils/time'
|
||
|
||
const route = useRoute()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const listing = ref<Listing | null>(null)
|
||
const actionType = ref<'offline' | 'abnormal' | ''>('')
|
||
const reason = ref('')
|
||
|
||
const actionTitle = computed(() => (actionType.value === 'offline' ? '强制下架商品' : '标记商品异常'))
|
||
const canOperate = computed(() => !!listing.value && listing.value.status !== 'rented' && !['offline', 'abnormal'].includes(listing.value.status))
|
||
|
||
onMounted(loadListing)
|
||
|
||
async function loadListing() {
|
||
loading.value = true
|
||
try {
|
||
listing.value = await fetchAdminListing(String(route.params.id))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openAction(type: 'offline' | 'abnormal') {
|
||
actionType.value = type
|
||
reason.value = ''
|
||
}
|
||
|
||
async function submitAction() {
|
||
if (!listing.value || !actionType.value) return
|
||
submitting.value = true
|
||
try {
|
||
if (actionType.value === 'offline') {
|
||
listing.value = await adminOfflineListing(listing.value.id, reason.value)
|
||
ElMessage.success('商品已强制下架')
|
||
} else {
|
||
listing.value = await adminMarkListingAbnormal(listing.value.id, reason.value)
|
||
ElMessage.success('商品已标记异常')
|
||
}
|
||
actionType.value = ''
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '操作失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function money(value: number) {
|
||
return `¥${Number(value || 0).toFixed(2)}`
|
||
}
|
||
|
||
function listingPrice(row: Listing) {
|
||
return money(row.price || row.price_daily || row.price_hourly)
|
||
}
|
||
|
||
function extractObjectKey(url: string) {
|
||
try {
|
||
const parsed = new URL(url, window.location.origin)
|
||
return parsed.searchParams.get('key') || url
|
||
} catch {
|
||
return url
|
||
}
|
||
}
|
||
|
||
async function openScreenshot(url: string) {
|
||
try {
|
||
const blob = await fetchAdminFileBlob(extractObjectKey(url))
|
||
window.open(URL.createObjectURL(blob), '_blank')
|
||
} catch {
|
||
window.open(url, '_blank')
|
||
}
|
||
}
|
||
|
||
function readError(error: unknown, fallback: string) {
|
||
if (typeof error === 'object' && error && 'response' in error) {
|
||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||
return response?.data?.message || fallback
|
||
}
|
||
return fallback
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page" v-loading="loading">
|
||
<div v-if="listing" class="page-header-row">
|
||
<div class="page-header">
|
||
<p class="eyebrow">Listing #{{ listing.id }}</p>
|
||
<h1>商品详情</h1>
|
||
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
||
</div>
|
||
<div class="toolbar-actions">
|
||
<RouterLink to="/admin/listings">
|
||
<el-button>返回列表</el-button>
|
||
</RouterLink>
|
||
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')">强制下架</el-button>
|
||
<el-button type="danger" :disabled="!canOperate" @click="openAction('abnormal')">标记异常</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="listing" class="metric-grid dashboard-metrics">
|
||
<div class="metric-card">
|
||
<span>商品状态</span>
|
||
<strong>{{ listingStatusLabel(listing.status) }}</strong>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>审核状态</span>
|
||
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>价格</span>
|
||
<strong>{{ listingPrice(listing) }}</strong>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>押金</span>
|
||
<strong>{{ money(listing.deposit_amount) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="listing" class="dashboard-panels">
|
||
<div class="order-panel dashboard-panel">
|
||
<h2>账号信息</h2>
|
||
<p>账号 ID:{{ listing.account_id }}</p>
|
||
<p>游戏:{{ listing.game_name }}</p>
|
||
<p>区服:{{ listing.server_region }}</p>
|
||
<p>平台:{{ listing.login_platform }}</p>
|
||
<p>段位:{{ listing.rank_level || '-' }}</p>
|
||
<p>哈夫币:{{ listing.haf_coin_amount }}</p>
|
||
<p>资产截图:{{ listing.screenshot_urls?.length || 0 }} 个</p>
|
||
</div>
|
||
|
||
<div class="order-panel dashboard-panel">
|
||
<h2>号主与价格</h2>
|
||
<p>号主:{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
|
||
<p>号主 ID:{{ listing.owner_id }}</p>
|
||
<p>价格:{{ listingPrice(listing) }}</p>
|
||
<p>押金:{{ money(listing.deposit_amount) }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="listing" class="order-panel dashboard-panel">
|
||
<h2>说明与审核原因</h2>
|
||
<p>{{ listing.description || '暂无商品说明' }}</p>
|
||
<p>审核/后台原因:{{ listing.review_reason || '-' }}</p>
|
||
<p>上架时间:{{ formatDateTime(listing.published_at) }}</p>
|
||
<p>更新时间:{{ formatDateTime(listing.updated_at) }}</p>
|
||
</div>
|
||
|
||
<div v-if="listing" class="order-panel dashboard-panel">
|
||
<h2>资产截图</h2>
|
||
<div v-if="listing.screenshot_urls?.length" class="evidence-list">
|
||
<div v-for="url in listing.screenshot_urls" :key="url" class="evidence-row">
|
||
<span>{{ url }}</span>
|
||
<el-button size="small" @click="openScreenshot(url)">打开</el-button>
|
||
</div>
|
||
</div>
|
||
<p v-else>暂无截图</p>
|
||
</div>
|
||
|
||
<el-dialog :model-value="!!actionType" :title="actionTitle" width="560px" @update:model-value="actionType = ''">
|
||
<div v-if="listing" class="dialog-body">
|
||
<p><strong>{{ listing.title }}</strong></p>
|
||
<el-input v-model="reason" type="textarea" :rows="4" placeholder="填写后台操作原因,会写入审计日志并通知号主" />
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="actionType = ''">取消</el-button>
|
||
<el-button type="danger" :loading="submitting" @click="submitAction">确认操作</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</section>
|
||
</template>
|