优化订单详情页面 ui
This commit is contained in:
@@ -7,6 +7,7 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchAdminUsers, type AdminUserItem } from '@/features/admin/api/adminUsers'
|
||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import {
|
||||
@@ -23,7 +24,21 @@ import {
|
||||
userStatusLabel,
|
||||
} from '@/shared/utils/statusLabels'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { formatListingCode } from '@/shared/utils/listingDisplay'
|
||||
import {
|
||||
assetRegions,
|
||||
formatEstimatedRentalDuration,
|
||||
formatHafCoinM,
|
||||
getCoinWan,
|
||||
getDailyLoss,
|
||||
getListingConsumablePrice,
|
||||
getListingResources,
|
||||
getOnlineTimeText,
|
||||
getResourceQuantity,
|
||||
getSkinNames,
|
||||
formatListingCode,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
@@ -51,6 +66,77 @@ const canTransfer = computed(() => !!listing.value && listing.value.status !== '
|
||||
const selectedUserAvailable = computed(
|
||||
() => selectedUser.value?.status === 'active' && selectedUser.value.realname_status === 'verified'
|
||||
)
|
||||
const assetRows = computed(() => {
|
||||
if (!listing.value) return []
|
||||
return [
|
||||
{ label: '商品编号', value: formatListingCode(listing.value) },
|
||||
{ label: '账号 ID', value: listing.value.account_id },
|
||||
{ label: '游戏', value: listing.value.game_name || '-' },
|
||||
{ label: '区服', value: listing.value.server_region || '-' },
|
||||
{ label: '平台', value: listing.value.login_platform || '-' },
|
||||
{ label: '段位', value: listing.value.rank_level || '-' },
|
||||
{ label: '烽火等级', value: assetNumberText(listing.value, 'fire_level') },
|
||||
{ label: '保险格数', value: assetText(listing.value, 'season_insurance') },
|
||||
{
|
||||
label: '体力/负重',
|
||||
value: `${assetText(listing.value, 'stamina_level')} / ${assetText(listing.value, 'load_level')}`,
|
||||
},
|
||||
{ label: '绝密 KD', value: assetNumberText(listing.value, 'secret_kd') },
|
||||
{ label: '常用地区', value: regionText(listing.value) },
|
||||
{ label: '在线时间', value: getOnlineTimeText(listing.value) || '-' },
|
||||
]
|
||||
})
|
||||
const ownerRows = computed(() => {
|
||||
if (!listing.value) return []
|
||||
return [
|
||||
{
|
||||
label: '号主',
|
||||
value: listing.value.owner_phone || listing.value.owner_nickname || listing.value.owner_id,
|
||||
},
|
||||
{ label: '号主 ID', value: listing.value.owner_id },
|
||||
{ label: '价格', value: listingPrice(listing.value) },
|
||||
{ label: '押金', value: moneyCent(listing.value.deposit_amount_cent) },
|
||||
{ label: '哈夫币', value: formatHafCoinM(getCoinWan(listing.value)) },
|
||||
{ label: '每日损耗', value: getDailyLoss(listing.value) || '-' },
|
||||
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
|
||||
{
|
||||
label: '额外消耗',
|
||||
value: moneyCent(Math.round(getListingConsumablePrice(listing.value) * 100)),
|
||||
},
|
||||
]
|
||||
})
|
||||
const resourceCards = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const core = [
|
||||
{ key: 'awmAmmo', label: 'AWM 子弹', quantity: getResourceQuantity(listing.value, 'awmAmmo') },
|
||||
{ key: 'helmet6', label: '六级头', quantity: getResourceQuantity(listing.value, 'helmet6') },
|
||||
{ key: 'armor6', label: '六级甲', quantity: getResourceQuantity(listing.value, 'armor6') },
|
||||
].filter(item => item.quantity > 0)
|
||||
const extra = getListingResources(listing.value)
|
||||
.filter(item => !core.some(coreItem => coreItem.key === item.key))
|
||||
.slice(0, 6)
|
||||
.map(item => ({ key: item.key, label: item.label, quantity: item.quantity }))
|
||||
return [...core, ...extra]
|
||||
})
|
||||
const skinNames = computed(() => (listing.value ? getSkinNames(listing.value).slice(0, 16) : []))
|
||||
const hiddenSkinCount = computed(() => {
|
||||
if (!listing.value) return 0
|
||||
return Math.max(0, getSkinNames(listing.value).length - skinNames.value.length)
|
||||
})
|
||||
const statusTone = computed(() => {
|
||||
if (!listing.value) return 'info'
|
||||
if (listing.value.status === 'published') return 'success'
|
||||
if (['offline', 'abnormal'].includes(listing.value.status)) return 'danger'
|
||||
if (listing.value.status === 'rented') return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
const reviewTone = computed(() => {
|
||||
if (!listing.value) return 'info'
|
||||
if (listing.value.review_status === 'approved') return 'success'
|
||||
if (listing.value.review_status === 'rejected') return 'danger'
|
||||
if (listing.value.review_status === 'pending') return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
onMounted(loadListing)
|
||||
|
||||
@@ -158,18 +244,42 @@ function listingPrice(row: Listing) {
|
||||
return moneyCent(row.price_cent)
|
||||
}
|
||||
|
||||
function assetText(row: Listing, key: string) {
|
||||
const value = readAssetString(row, key)
|
||||
if (value) return value
|
||||
const numberValue = readAssetNumber(row, key)
|
||||
return numberValue > 0 ? String(numberValue) : '-'
|
||||
}
|
||||
|
||||
function assetNumberText(row: Listing, key: string) {
|
||||
const value = readAssetNumber(row, key)
|
||||
if (value <= 0) return '-'
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)
|
||||
}
|
||||
|
||||
function regionText(row: Listing) {
|
||||
const regions = assetRegions(row)
|
||||
return regions.length ? regions.join('、') : '-'
|
||||
}
|
||||
|
||||
function extractObjectKey(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin)
|
||||
return parsed.searchParams.get('key') || url
|
||||
return parsed.searchParams.get('key') || ''
|
||||
} catch {
|
||||
return url
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function openScreenshot(url: string) {
|
||||
try {
|
||||
const blob = await fetchAdminFileBlob(extractObjectKey(url))
|
||||
const key = extractObjectKey(url)
|
||||
if (!key) {
|
||||
window.open(url, '_blank')
|
||||
return
|
||||
}
|
||||
const blob = await fetchAdminFileBlob(key)
|
||||
window.open(URL.createObjectURL(blob), '_blank')
|
||||
} catch {
|
||||
window.open(url, '_blank')
|
||||
@@ -179,18 +289,30 @@ async function openScreenshot(url: string) {
|
||||
|
||||
<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 v-if="listing" class="listing-detail-header">
|
||||
<div class="listing-title-block">
|
||||
<div class="listing-code-line">
|
||||
<span>商品编号</span>
|
||||
<strong>{{ formatListingCode(listing) }}</strong>
|
||||
</div>
|
||||
<h1>{{ listing.title || '商品详情' }}</h1>
|
||||
<div class="listing-subtitle">
|
||||
<span>{{ listing.server_region || '-' }}</span>
|
||||
<span>{{ listing.rank_level || '-' }}</span>
|
||||
<span>{{ listing.login_platform || '-' }}</span>
|
||||
<span>账号 {{ listing.account_id }}</span>
|
||||
</div>
|
||||
</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="primary" plain :disabled="!canTransfer" @click="openTransfer"
|
||||
<el-button
|
||||
class="transfer-owner-trigger"
|
||||
type="success"
|
||||
:disabled="!canTransfer"
|
||||
@click="openTransfer"
|
||||
>转移所有权</el-button
|
||||
>
|
||||
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')"
|
||||
@@ -202,64 +324,137 @@ async function openScreenshot(url: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="listing" class="metric-grid dashboard-metrics">
|
||||
<div class="metric-card">
|
||||
<div v-if="listing" class="listing-summary-bar">
|
||||
<div>
|
||||
<span>商品状态</span>
|
||||
<strong>{{ listingStatusLabel(listing.status) }}</strong>
|
||||
<el-tag :type="statusTone" effect="light">{{ listingStatusLabel(listing.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div>
|
||||
<span>审核状态</span>
|
||||
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
||||
<el-tag :type="reviewTone" effect="light">{{
|
||||
listingReviewStatusLabel(listing.review_status)
|
||||
}}</el-tag>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>价格</span>
|
||||
<div>
|
||||
<span>租金</span>
|
||||
<strong>{{ listingPrice(listing) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div>
|
||||
<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>
|
||||
<span>哈夫币</span>
|
||||
<strong>{{ formatHafCoinM(getCoinWan(listing)) }}</strong>
|
||||
</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>
|
||||
<span>预计可租</span>
|
||||
<strong>{{ formatEstimatedRentalDuration(listing) }}</strong>
|
||||
</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 v-if="listing" class="listing-workspace">
|
||||
<section class="detail-panel primary-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>账号属性</h2>
|
||||
<span>{{ listing.game_name || 'delta_force' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>暂无截图</p>
|
||||
<dl class="compact-detail-grid">
|
||||
<div v-for="row in assetRows" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>{{ row.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel side-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>号主与价格</h2>
|
||||
<span>{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</span>
|
||||
</div>
|
||||
<dl class="compact-detail-grid single">
|
||||
<div v-for="row in ownerRows" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>{{ row.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel primary-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>库存资产</h2>
|
||||
<span
|
||||
>{{ resourceCards.length }} 项资源 ·
|
||||
{{ skinNames.length + hiddenSkinCount }} 个皮肤</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="resourceCards.length" class="resource-grid">
|
||||
<div v-for="item in resourceCards" :key="item.key" class="resource-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.quantity }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="empty-line">暂无库存资源</p>
|
||||
<div class="skin-chip-list">
|
||||
<el-tag v-for="skin in skinNames" :key="skin" size="small" effect="plain">{{
|
||||
skin
|
||||
}}</el-tag>
|
||||
<el-tag v-if="hiddenSkinCount" size="small" effect="plain">+{{ hiddenSkinCount }}</el-tag>
|
||||
<span v-if="!skinNames.length" class="empty-line">暂无皮肤数据</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel side-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>说明与时间</h2>
|
||||
<span>{{ formatDateTime(listing.updated_at) }}</span>
|
||||
</div>
|
||||
<dl class="compact-detail-grid single">
|
||||
<div>
|
||||
<dt>商品说明</dt>
|
||||
<dd>{{ listing.description || '暂无商品说明' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>审核/后台原因</dt>
|
||||
<dd>{{ listing.review_reason || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>上架时间</dt>
|
||||
<dd>{{ formatDateTime(listing.published_at) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>更新时间</dt>
|
||||
<dd>{{ formatDateTime(listing.updated_at) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel screenshots-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>资产截图</h2>
|
||||
<span>{{ listing.screenshot_urls?.length || 0 }} 张</span>
|
||||
</div>
|
||||
<div v-if="listing.screenshot_urls?.length" class="screenshot-grid">
|
||||
<button
|
||||
v-for="(url, index) in listing.screenshot_urls"
|
||||
:key="url"
|
||||
class="screenshot-thumb"
|
||||
type="button"
|
||||
@click="openScreenshot(url)"
|
||||
>
|
||||
<AuthImage
|
||||
:source="url"
|
||||
admin
|
||||
:alt="`资产截图 ${index + 1}`"
|
||||
image-class="screenshot-image"
|
||||
fallback-class="screenshot-image"
|
||||
fit="cover"
|
||||
/>
|
||||
<span>查看 {{ index + 1 }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="empty-line">暂无截图</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -285,25 +480,47 @@ async function openScreenshot(url: string) {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="transferVisible" title="转移发布所有权" width="640px">
|
||||
<el-dialog
|
||||
v-model="transferVisible"
|
||||
title="转移发布所有权"
|
||||
width="680px"
|
||||
class="transfer-owner-dialog"
|
||||
>
|
||||
<div v-if="listing" class="dialog-body transfer-dialog">
|
||||
<div class="transfer-current">
|
||||
<span>当前号主</span>
|
||||
<strong>{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</strong>
|
||||
<small>ID:{{ listing.owner_id }}</small>
|
||||
<div class="transfer-route">
|
||||
<div class="transfer-owner-card">
|
||||
<span>当前号主</span>
|
||||
<strong>{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</strong>
|
||||
<small>ID:{{ listing.owner_id }}</small>
|
||||
</div>
|
||||
<div class="transfer-arrow">→</div>
|
||||
<div class="transfer-owner-card target" :class="{ empty: !selectedUser }">
|
||||
<span>目标号主</span>
|
||||
<strong v-if="selectedUser">{{ selectedUser.nickname || selectedUser.phone }}</strong>
|
||||
<strong v-else>请选择用户</strong>
|
||||
<small v-if="selectedUser"
|
||||
>ID:{{ selectedUser.id }} · {{ selectedUser.phone || '-' }}</small
|
||||
>
|
||||
<small v-else>仅支持正常且已实名用户</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="userKeyword"
|
||||
clearable
|
||||
placeholder="搜索手机号 / 昵称 / 用户ID"
|
||||
@keyup.enter="searchUsers"
|
||||
@clear="searchUsers"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" :loading="userSearching" @click="searchUsers">查询</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="transfer-section">
|
||||
<label>选择目标用户</label>
|
||||
<el-input
|
||||
v-model="userKeyword"
|
||||
clearable
|
||||
placeholder="输入手机号 / 昵称 / 用户ID 查询"
|
||||
@keyup.enter="searchUsers"
|
||||
@clear="searchUsers"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" :loading="userSearching" @click="searchUsers"
|
||||
>查询</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="transfer-user-list">
|
||||
<button
|
||||
@@ -314,14 +531,13 @@ async function openScreenshot(url: string) {
|
||||
type="button"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<span>
|
||||
<span class="transfer-user-main">
|
||||
<strong>{{ user.nickname || user.phone || `用户${user.id}` }}</strong>
|
||||
<small>{{ user.phone || '-' }}</small>
|
||||
<small>{{ user.phone || '-' }} · ID {{ user.id }}</small>
|
||||
</span>
|
||||
<span class="transfer-user-meta">
|
||||
<el-tag size="small" type="success">{{ userStatusLabel(user.status) }}</el-tag>
|
||||
<el-tag size="small">{{ realnameStatusLabel(user.realname_status) }}</el-tag>
|
||||
<small>ID:{{ user.id }}</small>
|
||||
</span>
|
||||
</button>
|
||||
<el-empty
|
||||
@@ -331,12 +547,15 @@ async function openScreenshot(url: string) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="transferReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="填写转移原因,会写入审计日志"
|
||||
/>
|
||||
<div class="transfer-section">
|
||||
<label>转移原因</label>
|
||||
<el-input
|
||||
v-model="transferReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="填写转移原因,会写入审计日志"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="transferVisible = false">取消</el-button>
|
||||
@@ -354,36 +573,345 @@ async function openScreenshot(url: string) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.transfer-dialog {
|
||||
.listing-detail-header {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.transfer-current {
|
||||
.listing-title-block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.listing-code-line {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.listing-code-line strong {
|
||||
color: #2563eb;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.listing-title-block h1 {
|
||||
margin: 8px 0 10px;
|
||||
color: #0f172a;
|
||||
font-size: 30px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.listing-subtitle {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.listing-subtitle span {
|
||||
padding: 4px 9px;
|
||||
border: 1px solid #dbe3ef;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.listing-summary-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.listing-summary-bar > div {
|
||||
display: grid;
|
||||
min-height: 74px;
|
||||
align-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.listing-summary-bar span {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.listing-summary-bar strong {
|
||||
color: #0f172a;
|
||||
font-size: 21px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.listing-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.3fr) minmax(360px, 0.7fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.screenshots-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.panel-heading h2 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.panel-heading span {
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.compact-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 8px;
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.compact-detail-grid.single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.compact-detail-grid div {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
align-items: start;
|
||||
min-height: 38px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.compact-detail-grid dt {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.compact-detail-grid dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.resource-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.resource-item span {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.resource-item strong {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.skin-chip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.screenshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.screenshot-thumb {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.screenshot-thumb:hover {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.screenshot-thumb img {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.screenshot-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.screenshot-thumb span {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.empty-line {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.transfer-owner-trigger {
|
||||
min-width: 120px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.transfer-dialog {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.transfer-route {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 32px minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.transfer-owner-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.transfer-current strong {
|
||||
color: #0f172a;
|
||||
.transfer-owner-card.target {
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.transfer-current small {
|
||||
margin-left: auto;
|
||||
.transfer-owner-card.target.empty {
|
||||
border-color: #bfdbfe;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.transfer-owner-card span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.transfer-owner-card strong {
|
||||
overflow: hidden;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transfer-owner-card small {
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transfer-arrow {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #2563eb;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.transfer-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.transfer-section label {
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.transfer-user-list {
|
||||
min-height: 120px;
|
||||
max-height: 260px;
|
||||
min-height: 138px;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.transfer-user {
|
||||
@@ -392,7 +920,7 @@ async function openScreenshot(url: string) {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
padding: 13px 14px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
background: #fff;
|
||||
@@ -410,15 +938,29 @@ async function openScreenshot(url: string) {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.transfer-user.is-selected {
|
||||
box-shadow: inset 3px 0 0 #2563eb;
|
||||
}
|
||||
|
||||
.transfer-user-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.transfer-user strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #0f172a;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transfer-user small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transfer-user-meta {
|
||||
@@ -427,4 +969,32 @@ async function openScreenshot(url: string) {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.listing-detail-header,
|
||||
.listing-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.listing-summary-bar {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.compact-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.transfer-route {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.transfer-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.transfer-user {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user