39 个文件按 .prettierrc.json (printWidth 100 / no semi / singleQuote) 统一格式, 纯空白与换行调整, 无逻辑变更。为接入代码门禁做准备, 让 npm run check (format:check + lint:strict + typecheck) 干净通过。
216 lines
6.8 KiB
Vue
216 lines
6.8 KiB
Vue
<script setup lang="ts">
|
||
import { readError } from '@/shared/utils/error'
|
||
import { CopyDocument } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
|
||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||
import { adminPath } from '@/shared/utils/adminPath'
|
||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||
import {
|
||
adminMarkListingAbnormal,
|
||
adminOfflineListing,
|
||
fetchAdminListing,
|
||
type Listing,
|
||
} from '@/features/listings'
|
||
import { listingReviewStatusLabel, listingStatusLabel } from '@/shared/utils/statusLabels'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
import { formatListingCode } from '@/shared/utils/listingDisplay'
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
async function copyListingCode() {
|
||
if (!listing.value) return
|
||
try {
|
||
await navigator.clipboard.writeText(formatListingCode(listing.value))
|
||
ElMessage.success('商品编号已复制')
|
||
} catch {
|
||
ElMessage.error('复制失败')
|
||
}
|
||
}
|
||
|
||
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 moneyCent(value: number) {
|
||
return formatCentWithSymbol(value)
|
||
}
|
||
|
||
function listingPrice(row: Listing) {
|
||
return moneyCent(row.price_cent)
|
||
}
|
||
|
||
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')
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page" v-loading="loading">
|
||
<div v-if="listing" class="page-header-row">
|
||
<div class="page-header">
|
||
<p class="eyebrow">商品编号:{{ formatListingCode(listing) }}</p>
|
||
<h1>商品详情</h1>
|
||
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
||
</div>
|
||
<div class="toolbar-actions">
|
||
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
||
<RouterLink :to="adminPath('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>{{ moneyCent(listing.deposit_amount_cent) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="listing" class="dashboard-panels">
|
||
<div class="order-panel dashboard-panel">
|
||
<h2>账号信息</h2>
|
||
<p>商品编号:{{ formatListingCode(listing) }}</p>
|
||
<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>押金:{{ moneyCent(listing.deposit_amount_cent) }}</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>
|