优化了后台管理界面,
This commit is contained in:
@@ -1,14 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DataLine, Document, DocumentChecked, Operation, ScaleToOriginal, Shop, SwitchButton, Tickets, User, Wallet } from '@element-plus/icons-vue'
|
import {
|
||||||
|
DataLine,
|
||||||
|
Document,
|
||||||
|
DocumentChecked,
|
||||||
|
Expand,
|
||||||
|
Fold,
|
||||||
|
Operation,
|
||||||
|
ScaleToOriginal,
|
||||||
|
Shop,
|
||||||
|
SwitchButton,
|
||||||
|
Tickets,
|
||||||
|
User,
|
||||||
|
Wallet,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { logoutAdmin } from '@/api/adminAuth'
|
import { logoutAdmin } from '@/api/adminAuth'
|
||||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const adminSession = useAdminSessionStore()
|
const adminSession = useAdminSessionStore()
|
||||||
|
const collapsed = ref(localStorage.getItem('admin_sidebar_collapsed') === '1')
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: '仪表盘', to: '/admin/dashboard', icon: DataLine },
|
{ label: '仪表盘', to: '/admin/dashboard', icon: DataLine },
|
||||||
@@ -23,6 +38,18 @@ const navItems = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const adminName = computed(() => adminSession.nickname || adminSession.username || '管理员')
|
const adminName = computed(() => adminSession.nickname || adminSession.username || '管理员')
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
const matched = [...navItems]
|
||||||
|
.sort((a, b) => b.to.length - a.to.length)
|
||||||
|
.find((item) => path === item.to || path.startsWith(`${item.to}/`))
|
||||||
|
return matched?.to || '/admin/dashboard'
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
localStorage.setItem('admin_sidebar_collapsed', collapsed.value ? '1' : '0')
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
try {
|
try {
|
||||||
@@ -37,28 +64,51 @@ async function handleLogout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-shell">
|
<div class="admin-shell" :class="{ 'is-sidebar-collapsed': collapsed }">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<RouterLink class="admin-brand" to="/admin/dashboard">
|
<div class="admin-sidebar-head">
|
||||||
<span class="brand-mark">H</span>
|
<RouterLink class="admin-brand" to="/admin/dashboard">
|
||||||
<span>哈夫币后台</span>
|
<span class="brand-mark">H</span>
|
||||||
</RouterLink>
|
<span class="admin-brand-text">哈夫币后台</span>
|
||||||
|
|
||||||
<nav class="admin-nav">
|
|
||||||
<RouterLink v-for="item in navItems" :key="item.to" class="admin-nav-link" :to="item.to">
|
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</div>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
class="admin-menu"
|
||||||
|
:collapse="collapsed"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:default-active="activeMenu"
|
||||||
|
router
|
||||||
|
background-color="#111827"
|
||||||
|
text-color="#b7c3d2"
|
||||||
|
active-text-color="#ffffff"
|
||||||
|
>
|
||||||
|
<el-menu-item v-for="item in navItems" :key="item.to" :index="item.to">
|
||||||
|
<el-icon><component :is="item.icon" /></el-icon>
|
||||||
|
<template #title>{{ item.label }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<div class="admin-sidebar-footer">
|
||||||
|
<el-tooltip :content="collapsed ? '退出后台' : ''" placement="right" :disabled="!collapsed">
|
||||||
|
<el-button class="admin-logout-btn" :icon="SwitchButton" @click="handleLogout">
|
||||||
|
<span v-if="!collapsed">退出后台</span>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="admin-workspace">
|
<section class="admin-workspace">
|
||||||
<header class="admin-topbar">
|
<header class="admin-topbar">
|
||||||
<div>
|
<div class="admin-topbar-left">
|
||||||
<span>管理系统</span>
|
<el-tooltip :content="collapsed ? '展开导航' : '折叠导航'" placement="bottom">
|
||||||
<strong>{{ adminName }}</strong>
|
<el-button class="admin-topbar-collapse" :icon="collapsed ? Expand : Fold" @click="toggleCollapse" />
|
||||||
|
</el-tooltip>
|
||||||
|
<div>
|
||||||
|
<span>管理系统</span>
|
||||||
|
<strong>{{ adminName }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button :icon="SwitchButton" @click="handleLogout">退出</el-button>
|
|
||||||
</header>
|
</header>
|
||||||
<main class="admin-main">
|
<main class="admin-main">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
+252
-37
@@ -82,19 +82,35 @@ a {
|
|||||||
|
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
grid-template-columns: 260px 1fr;
|
grid-template-columns: 220px 1fr;
|
||||||
background: #f5f7fb;
|
background: #f5f7fb;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: grid-template-columns 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed {
|
||||||
|
grid-template-columns: 68px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-right: 1px solid #dfe5ec;
|
border-right: 1px solid #dfe5ec;
|
||||||
background: #111827;
|
background: #111827;
|
||||||
padding: 20px 16px;
|
padding: 20px 12px;
|
||||||
color: #dbe4ee;
|
color: #dbe4ee;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-brand {
|
.admin-brand {
|
||||||
@@ -102,38 +118,164 @@ a {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
|
min-width: 0;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav {
|
.admin-brand-text {
|
||||||
display: grid;
|
overflow: hidden;
|
||||||
gap: 6px;
|
white-space: nowrap;
|
||||||
margin-top: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-link {
|
.admin-menu {
|
||||||
display: flex;
|
flex: 1;
|
||||||
align-items: center;
|
min-width: 0;
|
||||||
gap: 10px;
|
margin-top: 26px;
|
||||||
min-height: 42px;
|
border-right: 0;
|
||||||
|
overflow: hidden auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu:not(.el-menu--collapse) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu.el-menu--collapse {
|
||||||
|
width: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item {
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 0 6px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
color: #b7c3d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav-link.router-link-active {
|
|
||||||
background: #0f766e;
|
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-menu.el-menu--collapse .el-menu-item {
|
||||||
|
width: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu.el-menu--collapse .el-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-menu,
|
||||||
|
.is-sidebar-collapsed .admin-menu.el-menu--collapse {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-menu .el-menu-item,
|
||||||
|
.is-sidebar-collapsed .admin-menu.el-menu--collapse .el-menu-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px !important;
|
||||||
|
min-width: 44px !important;
|
||||||
|
max-width: 44px;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-menu .el-menu-tooltip__trigger,
|
||||||
|
.is-sidebar-collapsed .admin-menu .el-tooltip__trigger {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-menu .el-icon {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item.is-active {
|
||||||
|
background: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item:hover {
|
||||||
|
background: rgba(15, 118, 110, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-color: rgba(183, 195, 210, 0.28);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: #dbe4ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout-btn:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.32);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-sidebar {
|
||||||
|
align-items: center;
|
||||||
|
padding: 22px 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-sidebar-head {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-brand {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .brand-mark {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 13px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-brand-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-collapsed .admin-logout-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-workspace {
|
.admin-workspace {
|
||||||
|
height: 100vh;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-topbar {
|
.admin-topbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 8;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -143,6 +285,19 @@ a {
|
|||||||
padding: 0 28px;
|
padding: 0 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-topbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-topbar-collapse {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-topbar span {
|
.admin-topbar span {
|
||||||
display: block;
|
display: block;
|
||||||
color: #6b7785;
|
color: #6b7785;
|
||||||
@@ -587,6 +742,17 @@ h1 {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-tag-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tag-row.compact {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.code-panel {
|
.code-panel {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
@@ -1274,50 +1440,103 @@ h1 {
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
grid-template-columns: 220px 1fr;
|
grid-template-columns: 208px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed {
|
||||||
|
grid-template-columns: 68px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
padding: 18px 12px;
|
padding: 18px 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav-link {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.admin-shell {
|
.admin-shell,
|
||||||
|
.admin-shell.is-sidebar-collapsed {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
height: auto;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid #1f2937;
|
border-bottom: 1px solid #1f2937;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-workspace {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed .admin-sidebar {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed .admin-sidebar-head {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed .admin-brand {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed .admin-brand-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell.is-sidebar-collapsed .brand-mark {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-brand {
|
.admin-brand {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav {
|
.admin-menu,
|
||||||
grid-auto-flow: column;
|
.admin-menu.el-menu--collapse {
|
||||||
grid-auto-columns: max-content;
|
display: flex;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-link {
|
.admin-menu .el-menu-item,
|
||||||
min-height: 38px;
|
.admin-menu.el-menu--collapse .el-menu-item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 38px;
|
||||||
|
margin: 0 8px 0 0;
|
||||||
|
padding: 0 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-footer {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout-btn,
|
||||||
|
.is-sidebar-collapsed .admin-logout-btn {
|
||||||
|
width: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-topbar {
|
.admin-topbar {
|
||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
@@ -1381,10 +1600,6 @@ h1 {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-topbar .el-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-panels {
|
.dashboard-panels {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ import { useRoute } from 'vue-router'
|
|||||||
|
|
||||||
import { fetchAdminFileBlob } from '@/api/files'
|
import { fetchAdminFileBlob } from '@/api/files'
|
||||||
import { adminMarkListingAbnormal, adminOfflineListing, fetchAdminListing, type Listing } from '@/api/listings'
|
import { adminMarkListingAbnormal, adminOfflineListing, fetchAdminListing, type Listing } from '@/api/listings'
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatRatio,
|
||||||
|
getAdminListingPricing,
|
||||||
|
listingAssetTags,
|
||||||
|
listingBadgeTags,
|
||||||
|
listingBuyerPrice,
|
||||||
|
listingDisplayTitle,
|
||||||
|
listingResourceTags,
|
||||||
|
listingRiskTags,
|
||||||
|
listingSellerPrice,
|
||||||
|
ownerName,
|
||||||
|
} from '@/views/admin/listingAdminDisplay'
|
||||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
@@ -53,14 +66,6 @@ async function submitAction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function money(value: number) {
|
|
||||||
return `¥${Number(value || 0).toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
|
||||||
return money(row.price_daily || row.price_hourly)
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractObjectKey(url: string) {
|
function extractObjectKey(url: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url, window.location.origin)
|
const parsed = new URL(url, window.location.origin)
|
||||||
@@ -94,7 +99,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="eyebrow">Listing #{{ listing.id }}</p>
|
<p class="eyebrow">Listing #{{ listing.id }}</p>
|
||||||
<h1>商品详情</h1>
|
<h1>商品详情</h1>
|
||||||
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
<p>{{ listingDisplayTitle(listing) }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<RouterLink to="/admin/listings">
|
<RouterLink to="/admin/listings">
|
||||||
@@ -115,18 +120,20 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>价格</span>
|
<span>买家日租</span>
|
||||||
<strong>{{ listingPrice(listing) }}</strong>
|
<strong>{{ listingBuyerPrice(listing) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>押金</span>
|
<span>押金</span>
|
||||||
<strong>{{ money(listing.deposit_amount) }}</strong>
|
<strong>{{ formatMoney(listing.deposit_amount) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="listing" class="dashboard-panels">
|
<div v-if="listing" class="dashboard-panels">
|
||||||
<div class="order-panel dashboard-panel">
|
<div class="order-panel dashboard-panel">
|
||||||
<h2>账号信息</h2>
|
<h2>账号信息</h2>
|
||||||
|
<p>展示标题:{{ listingDisplayTitle(listing) }}</p>
|
||||||
|
<p>原始标题:{{ listing.title }}</p>
|
||||||
<p>账号 ID:{{ listing.account_id }}</p>
|
<p>账号 ID:{{ listing.account_id }}</p>
|
||||||
<p>游戏:{{ listing.game_name }}</p>
|
<p>游戏:{{ listing.game_name }}</p>
|
||||||
<p>区服:{{ listing.server_region }}</p>
|
<p>区服:{{ listing.server_region }}</p>
|
||||||
@@ -134,17 +141,58 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<p>段位:{{ listing.rank_level || '-' }}</p>
|
<p>段位:{{ listing.rank_level || '-' }}</p>
|
||||||
<p>哈夫币:{{ listing.haf_coin_amount }}</p>
|
<p>哈夫币:{{ listing.haf_coin_amount }}</p>
|
||||||
<p>资产截图:{{ listing.screenshot_urls?.length || 0 }} 个</p>
|
<p>资产截图:{{ listing.screenshot_urls?.length || 0 }} 个</p>
|
||||||
|
<div v-if="listingBadgeTags(listing).length" class="admin-tag-row">
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in listingBadgeTags(listing)"
|
||||||
|
:key="tag"
|
||||||
|
size="small"
|
||||||
|
:type="tag === '特惠' ? 'danger' : 'warning'"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="order-panel dashboard-panel">
|
<div class="order-panel dashboard-panel">
|
||||||
<h2>号主与价格</h2>
|
<h2>号主与价格</h2>
|
||||||
<p>号主:{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
|
<p>号主:{{ ownerName(listing) }}</p>
|
||||||
<p>号主 ID:{{ listing.owner_id }}</p>
|
<p>号主 ID:{{ listing.owner_id }}</p>
|
||||||
<p>价格:{{ listingPrice(listing) }}</p>
|
<p>买家日租:{{ listingBuyerPrice(listing) }}</p>
|
||||||
<p>押金:{{ money(listing.deposit_amount) }}</p>
|
<p>卖家价格:{{ listingSellerPrice(listing) }}</p>
|
||||||
|
<p>押金:{{ formatMoney(listing.deposit_amount) }}</p>
|
||||||
|
<p>卖家比例:{{ formatRatio(getAdminListingPricing(listing).sellerRatio) }}</p>
|
||||||
|
<p>买家比例:{{ formatRatio(getAdminListingPricing(listing).buyerRatio) }}</p>
|
||||||
|
<p>平台加价:{{ formatMoney(getAdminListingPricing(listing).platformMarkup) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="listing" class="dashboard-panels">
|
||||||
|
<div class="order-panel dashboard-panel">
|
||||||
|
<h2>资产配置</h2>
|
||||||
|
<div class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingAssetTags(listing)" :key="tag" effect="plain">{{ tag }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-panel dashboard-panel">
|
||||||
|
<h2>交接风控</h2>
|
||||||
|
<div class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingRiskTags(listing)" :key="tag" type="info" effect="plain">{{ tag }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="listing" class="order-panel dashboard-panel">
|
||||||
|
<h2>额外资源</h2>
|
||||||
|
<div v-if="listingResourceTags(listing, 99).length" class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingResourceTags(listing, 99)" :key="tag" type="info" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<p v-else>暂无额外资源</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="listing" class="order-panel dashboard-panel">
|
<div v-if="listing" class="order-panel dashboard-panel">
|
||||||
<h2>说明与审核原因</h2>
|
<h2>说明与审核原因</h2>
|
||||||
<p>{{ listing.description || '暂无商品说明' }}</p>
|
<p>{{ listing.description || '暂无商品说明' }}</p>
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ import { onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
import { fetchAdminFileBlob } from '@/api/files'
|
import { fetchAdminFileBlob } from '@/api/files'
|
||||||
import { approveListing, fetchPendingReviewListings, rejectListing, type Listing } from '@/api/listings'
|
import { approveListing, fetchPendingReviewListings, rejectListing, type Listing } from '@/api/listings'
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatRatio,
|
||||||
|
getAdminListingPricing,
|
||||||
|
listingAssetTags,
|
||||||
|
listingBadgeTags,
|
||||||
|
listingBuyerPrice,
|
||||||
|
listingDisplayTitle,
|
||||||
|
listingResourceTags,
|
||||||
|
listingSellerPrice,
|
||||||
|
ownerName,
|
||||||
|
} from '@/views/admin/listingAdminDisplay'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -62,10 +74,6 @@ function openEvidence(row: Listing) {
|
|||||||
evidenceListing.value = row
|
evidenceListing.value = row
|
||||||
}
|
}
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
|
||||||
return `¥${Number(row.price_daily || row.price_hourly || 0).toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractObjectKey(url: string) {
|
function extractObjectKey(url: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url, window.location.origin)
|
const parsed = new URL(url, window.location.origin)
|
||||||
@@ -105,16 +113,60 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="listings">
|
<el-table v-loading="loading" class="table-panel" :data="listings">
|
||||||
<el-table-column prop="title" label="标题" min-width="180" />
|
<el-table-column label="商品" min-width="270" fixed="left">
|
||||||
<el-table-column prop="owner_id" label="号主" width="90" />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="server_region" label="区服" width="120" />
|
<strong>{{ listingDisplayTitle(row) }}</strong>
|
||||||
<el-table-column prop="login_platform" label="平台" width="120" />
|
<span class="table-subtext">{{ row.server_region }} / {{ row.login_platform }}</span>
|
||||||
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
|
<div v-if="listingBadgeTags(row).length" class="admin-tag-row compact">
|
||||||
<el-table-column prop="rank_level" label="段位" width="110" />
|
<el-tag
|
||||||
<el-table-column label="价格" width="90">
|
v-for="tag in listingBadgeTags(row)"
|
||||||
<template #default="{ row }">{{ listingPrice(row) }}</template>
|
:key="`${row.id}-${tag}`"
|
||||||
|
size="small"
|
||||||
|
:type="tag === '特惠' ? 'danger' : 'warning'"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="号主" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<strong>{{ ownerName(row) }}</strong>
|
||||||
|
<span class="table-subtext">ID: {{ row.owner_id }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="资产配置" min-width="260">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingAssetTags(row).slice(0, 8)" :key="`${row.id}-${tag}`" size="small" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="资源" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="listingResourceTags(row).length" class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingResourceTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="table-subtext">无额外资源</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格" min-width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<strong>{{ listingBuyerPrice(row) }}</strong>
|
||||||
|
<span class="table-subtext">卖家 {{ listingSellerPrice(row) }}</span>
|
||||||
|
<span class="table-subtext">
|
||||||
|
卖家 {{ formatRatio(getAdminListingPricing(row).sellerRatio) }} / 买家 {{ formatRatio(getAdminListingPricing(row).buyerRatio) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="押金" width="100">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.deposit_amount) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="deposit_amount" label="押金" width="90" />
|
|
||||||
<el-table-column label="截图" width="90">
|
<el-table-column label="截图" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" :disabled="!row.screenshot_urls?.length" @click="openEvidence(row)">
|
<el-button size="small" :disabled="!row.screenshot_urls?.length" @click="openEvidence(row)">
|
||||||
@@ -135,7 +187,9 @@ function readError(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
<el-dialog :model-value="!!activeListing" title="拒绝发布" width="560px" @update:model-value="activeListing = null">
|
<el-dialog :model-value="!!activeListing" title="拒绝发布" width="560px" @update:model-value="activeListing = null">
|
||||||
<div v-if="activeListing" class="dialog-body">
|
<div v-if="activeListing" class="dialog-body">
|
||||||
<p><strong>{{ activeListing.title }}</strong></p>
|
<p><strong>{{ listingDisplayTitle(activeListing) }}</strong></p>
|
||||||
|
<p>买家日租:{{ listingBuyerPrice(activeListing) }},卖家价格:{{ listingSellerPrice(activeListing) }}</p>
|
||||||
|
<p>押金:{{ formatMoney(activeListing.deposit_amount) }}</p>
|
||||||
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="填写拒绝原因,号主会在通知中看到审核结果" />
|
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="填写拒绝原因,号主会在通知中看到审核结果" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -146,7 +200,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
<el-dialog :model-value="!!evidenceListing" title="账号资产截图" width="680px" @update:model-value="evidenceListing = null">
|
<el-dialog :model-value="!!evidenceListing" title="账号资产截图" width="680px" @update:model-value="evidenceListing = null">
|
||||||
<div v-if="evidenceListing" class="dialog-body">
|
<div v-if="evidenceListing" class="dialog-body">
|
||||||
<p><strong>{{ evidenceListing.title }}</strong></p>
|
<p><strong>{{ listingDisplayTitle(evidenceListing) }}</strong></p>
|
||||||
<div v-if="evidenceListing.screenshot_urls?.length" class="evidence-list">
|
<div v-if="evidenceListing.screenshot_urls?.length" class="evidence-list">
|
||||||
<div v-for="url in evidenceListing.screenshot_urls" :key="url" class="evidence-row">
|
<div v-for="url in evidenceListing.screenshot_urls" :key="url" class="evidence-row">
|
||||||
<span>{{ url }}</span>
|
<span>{{ url }}</span>
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ import { Search } from '@element-plus/icons-vue'
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchAdminListings, type Listing } from '@/api/listings'
|
import { fetchAdminListings, type Listing } from '@/api/listings'
|
||||||
|
import {
|
||||||
|
formatMoney,
|
||||||
|
formatRatio,
|
||||||
|
getAdminListingPricing,
|
||||||
|
listingAssetTags,
|
||||||
|
listingBadgeTags,
|
||||||
|
listingBuyerPrice,
|
||||||
|
listingDisplayTitle,
|
||||||
|
listingResourceTags,
|
||||||
|
listingRiskTags,
|
||||||
|
listingSellerPrice,
|
||||||
|
ownerName,
|
||||||
|
} from '@/views/admin/listingAdminDisplay'
|
||||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
@@ -38,18 +51,6 @@ function resetFilters() {
|
|||||||
void loadListings()
|
void loadListings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function money(value: number) {
|
|
||||||
return `¥${Number(value || 0).toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
|
||||||
return money(row.price_daily || row.price_hourly)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ownerName(row: Listing) {
|
|
||||||
return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusType(status: string) {
|
function statusType(status: string) {
|
||||||
if (status === 'published') return 'success'
|
if (status === 'published') return 'success'
|
||||||
if (status === 'rented') return 'warning'
|
if (status === 'rented') return 'warning'
|
||||||
@@ -126,10 +127,21 @@ function reviewType(status: string) {
|
|||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="listings">
|
<el-table v-loading="loading" class="table-panel" :data="listings">
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column label="商品" min-width="220">
|
<el-table-column label="商品" min-width="270" fixed="left">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<strong>{{ row.title }}</strong>
|
<strong>{{ listingDisplayTitle(row) }}</strong>
|
||||||
<span class="table-subtext">账号 ID: {{ row.account_id }}</span>
|
<span class="table-subtext">账号 ID: {{ row.account_id }}</span>
|
||||||
|
<div v-if="listingBadgeTags(row).length" class="admin-tag-row compact">
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in listingBadgeTags(row)"
|
||||||
|
:key="`${row.id}-${tag}`"
|
||||||
|
size="small"
|
||||||
|
:type="tag === '特惠' ? 'danger' : 'warning'"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="号主" min-width="150">
|
<el-table-column label="号主" min-width="150">
|
||||||
@@ -138,15 +150,49 @@ function reviewType(status: string) {
|
|||||||
<span class="table-subtext">ID: {{ row.owner_id }}</span>
|
<span class="table-subtext">ID: {{ row.owner_id }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="server_region" label="区服" width="120" />
|
<el-table-column label="资产配置" min-width="260">
|
||||||
<el-table-column prop="login_platform" label="平台" width="120" />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="rank_level" label="段位" width="110" />
|
<div class="admin-tag-row">
|
||||||
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
|
<el-tag v-for="tag in listingAssetTags(row).slice(0, 8)" :key="`${row.id}-${tag}`" size="small" effect="plain">
|
||||||
<el-table-column label="价格" width="100">
|
{{ tag }}
|
||||||
<template #default="{ row }">{{ listingPrice(row) }}</template>
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="资源" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="listingResourceTags(row).length" class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingResourceTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="table-subtext">无额外资源</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="交接风控" min-width="230">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="admin-tag-row">
|
||||||
|
<el-tag v-for="tag in listingRiskTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span class="table-subtext">{{ row.server_region }} / {{ row.login_platform }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格" min-width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<strong>{{ listingBuyerPrice(row) }}</strong>
|
||||||
|
<span class="table-subtext">卖家 {{ listingSellerPrice(row) }}</span>
|
||||||
|
<span class="table-subtext">
|
||||||
|
卖家 {{ formatRatio(getAdminListingPricing(row).sellerRatio) }} / 买家 {{ formatRatio(getAdminListingPricing(row).buyerRatio) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="getAdminListingPricing(row).platformMarkup > 0" class="table-subtext">
|
||||||
|
平台加价 {{ formatMoney(getAdminListingPricing(row).platformMarkup) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="押金" width="100">
|
<el-table-column label="押金" width="100">
|
||||||
<template #default="{ row }">{{ money(row.deposit_amount) }}</template>
|
<template #default="{ row }">{{ formatMoney(row.deposit_amount) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品状态" width="110">
|
<el-table-column label="商品状态" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type { Listing } from '@/api/listings'
|
||||||
|
import {
|
||||||
|
assetRegions,
|
||||||
|
formatHafCoinM,
|
||||||
|
getCoinWan,
|
||||||
|
getListingResources,
|
||||||
|
getListingTitle,
|
||||||
|
getOnlineTimeText,
|
||||||
|
hasAcceleratedSaleRatio,
|
||||||
|
hasGiftResources,
|
||||||
|
readAssetNumber,
|
||||||
|
readAssetString,
|
||||||
|
} from '@/views/mobile/listingDisplay'
|
||||||
|
|
||||||
|
export interface AdminListingPricing {
|
||||||
|
buyerDaily: number
|
||||||
|
sellerDaily: number
|
||||||
|
sellerRatio: number
|
||||||
|
buyerRatio: number
|
||||||
|
platformMarkup: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ownerName(row: Listing) {
|
||||||
|
return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingDisplayTitle(row: Listing) {
|
||||||
|
return getListingTitle(row) || row.title
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMoney(value: number) {
|
||||||
|
return `¥${Number(value || 0).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdminListingPricing(row: Listing): AdminListingPricing {
|
||||||
|
const breakdown = readBreakdown(row)
|
||||||
|
const buyerDaily = readBreakdownNumber(breakdown, 'buyer_total_price') || Number(row.price_daily || row.price_hourly || 0)
|
||||||
|
const sellerDaily = readBreakdownNumber(breakdown, 'seller_total_price') || buyerDaily
|
||||||
|
return {
|
||||||
|
buyerDaily,
|
||||||
|
sellerDaily,
|
||||||
|
sellerRatio: readBreakdownNumber(breakdown, 'seller_ratio'),
|
||||||
|
buyerRatio: readBreakdownNumber(breakdown, 'buyer_ratio') || readAssetNumber(row, 'publish_ratio'),
|
||||||
|
platformMarkup: readBreakdownNumber(breakdown, 'platform_markup_amount'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingBuyerPrice(row: Listing) {
|
||||||
|
return formatMoney(getAdminListingPricing(row).buyerDaily)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingSellerPrice(row: Listing) {
|
||||||
|
return formatMoney(getAdminListingPricing(row).sellerDaily)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRatio(value: number) {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return '-'
|
||||||
|
const rounded = Math.round(value * 100) / 100
|
||||||
|
return `1:${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingAssetTags(row: Listing) {
|
||||||
|
return [
|
||||||
|
`哈夫币:${formatHafCoinM(getCoinWan(row))}`,
|
||||||
|
tagValue('保险', readAssetString(row, 'season_insurance')),
|
||||||
|
tagValue('体力', readAssetString(row, 'stamina_level')),
|
||||||
|
tagValue('负重', readAssetString(row, 'load_level')),
|
||||||
|
tagValue('段位', row.rank_level),
|
||||||
|
tagValue('日损', formatDailyLoss(row)),
|
||||||
|
tagValue('烽火', readAssetNumber(row, 'fire_level') || ''),
|
||||||
|
tagValue('KD', readAssetString(row, 'secret_kd')),
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingRiskTags(row: Listing) {
|
||||||
|
return [
|
||||||
|
tagValue('人脸', readAssetString(row, 'face_owner')),
|
||||||
|
tagValue('封禁', readAssetString(row, 'ban_record')),
|
||||||
|
tagValue('地区', assetRegions(row).join('/')),
|
||||||
|
tagValue('在线', getOnlineTimeText(row)),
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingResourceTags(row: Listing, limit = 4) {
|
||||||
|
const resources = getListingResources(row).map((resource) => {
|
||||||
|
const mode = resource.mode === '赠送' ? '赠送' : '收费'
|
||||||
|
return `${resource.label}:${resource.quantity}${mode === '赠送' ? '(赠)' : ''}`
|
||||||
|
})
|
||||||
|
if (resources.length <= limit) return resources
|
||||||
|
return [...resources.slice(0, limit), `+${resources.length - limit}项`]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listingBadgeTags(row: Listing) {
|
||||||
|
return [
|
||||||
|
hasAcceleratedSaleRatio(row) ? '特惠' : '',
|
||||||
|
hasGiftResources(row) ? '有赠送' : '',
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDailyLoss(row: Listing) {
|
||||||
|
const value = readAssetNumber(row, 'daily_loss_m')
|
||||||
|
return value > 0 ? `${value}M/天` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagValue(label: string, value: string | number) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
return text ? `${label}:${text}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBreakdown(row: Listing) {
|
||||||
|
const breakdown = row.asset_summary?.price_breakdown
|
||||||
|
return typeof breakdown === 'object' && breakdown !== null ? (breakdown as Record<string, unknown>) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBreakdownNumber(row: Record<string, unknown> | null, key: string) {
|
||||||
|
if (!row) return 0
|
||||||
|
const value = row[key]
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user