优化订单详情页面 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 { fetchAdminUsers, type AdminUserItem } from '@/features/admin/api/adminUsers'
|
||||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||||
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import { adminPath } from '@/shared/utils/adminPath'
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +24,21 @@ import {
|
|||||||
userStatusLabel,
|
userStatusLabel,
|
||||||
} from '@/shared/utils/statusLabels'
|
} from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
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 route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -51,6 +66,77 @@ const canTransfer = computed(() => !!listing.value && listing.value.status !== '
|
|||||||
const selectedUserAvailable = computed(
|
const selectedUserAvailable = computed(
|
||||||
() => selectedUser.value?.status === 'active' && selectedUser.value.realname_status === 'verified'
|
() => 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)
|
onMounted(loadListing)
|
||||||
|
|
||||||
@@ -158,18 +244,42 @@ function listingPrice(row: Listing) {
|
|||||||
return moneyCent(row.price_cent)
|
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) {
|
function extractObjectKey(url: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url, window.location.origin)
|
const parsed = new URL(url, window.location.origin)
|
||||||
return parsed.searchParams.get('key') || url
|
return parsed.searchParams.get('key') || ''
|
||||||
} catch {
|
} catch {
|
||||||
return url
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openScreenshot(url: string) {
|
async function openScreenshot(url: string) {
|
||||||
try {
|
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')
|
window.open(URL.createObjectURL(blob), '_blank')
|
||||||
} catch {
|
} catch {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
@@ -179,18 +289,30 @@ async function openScreenshot(url: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" v-loading="loading">
|
<section class="page" v-loading="loading">
|
||||||
<div v-if="listing" class="page-header-row">
|
<div v-if="listing" class="listing-detail-header">
|
||||||
<div class="page-header">
|
<div class="listing-title-block">
|
||||||
<p class="eyebrow">商品编号:{{ formatListingCode(listing) }}</p>
|
<div class="listing-code-line">
|
||||||
<h1>商品详情</h1>
|
<span>商品编号</span>
|
||||||
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
<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>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
||||||
<RouterLink :to="adminPath('listings')">
|
<RouterLink :to="adminPath('listings')">
|
||||||
<el-button>返回列表</el-button>
|
<el-button>返回列表</el-button>
|
||||||
</RouterLink>
|
</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
|
||||||
>
|
>
|
||||||
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')"
|
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')"
|
||||||
@@ -202,64 +324,137 @@ async function openScreenshot(url: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="listing" class="metric-grid dashboard-metrics">
|
<div v-if="listing" class="listing-summary-bar">
|
||||||
<div class="metric-card">
|
<div>
|
||||||
<span>商品状态</span>
|
<span>商品状态</span>
|
||||||
<strong>{{ listingStatusLabel(listing.status) }}</strong>
|
<el-tag :type="statusTone" effect="light">{{ listingStatusLabel(listing.status) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div>
|
||||||
<span>审核状态</span>
|
<span>审核状态</span>
|
||||||
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
<el-tag :type="reviewTone" effect="light">{{
|
||||||
|
listingReviewStatusLabel(listing.review_status)
|
||||||
|
}}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div>
|
||||||
<span>价格</span>
|
<span>租金</span>
|
||||||
<strong>{{ listingPrice(listing) }}</strong>
|
<strong>{{ listingPrice(listing) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div>
|
||||||
<span>押金</span>
|
<span>押金</span>
|
||||||
<strong>{{ moneyCent(listing.deposit_amount_cent) }}</strong>
|
<strong>{{ moneyCent(listing.deposit_amount_cent) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<span>哈夫币</span>
|
||||||
<div v-if="listing" class="dashboard-panels">
|
<strong>{{ formatHafCoinM(getCoinWan(listing)) }}</strong>
|
||||||
<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>
|
||||||
|
<div>
|
||||||
<div class="order-panel dashboard-panel">
|
<span>预计可租</span>
|
||||||
<h2>号主与价格</h2>
|
<strong>{{ formatEstimatedRentalDuration(listing) }}</strong>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div v-if="listing" class="order-panel dashboard-panel">
|
<div v-if="listing" class="listing-workspace">
|
||||||
<h2>说明与审核原因</h2>
|
<section class="detail-panel primary-panel">
|
||||||
<p>{{ listing.description || '暂无商品说明' }}</p>
|
<div class="panel-heading">
|
||||||
<p>审核/后台原因:{{ listing.review_reason || '-' }}</p>
|
<h2>账号属性</h2>
|
||||||
<p>上架时间:{{ formatDateTime(listing.published_at) }}</p>
|
<span>{{ listing.game_name || 'delta_force' }}</span>
|
||||||
<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>
|
||||||
</div>
|
<dl class="compact-detail-grid">
|
||||||
<p v-else>暂无截图</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -285,25 +480,47 @@ async function openScreenshot(url: string) {
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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 v-if="listing" class="dialog-body transfer-dialog">
|
||||||
<div class="transfer-current">
|
<div class="transfer-route">
|
||||||
<span>当前号主</span>
|
<div class="transfer-owner-card">
|
||||||
<strong>{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</strong>
|
<span>当前号主</span>
|
||||||
<small>ID:{{ listing.owner_id }}</small>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-input
|
<div class="transfer-section">
|
||||||
v-model="userKeyword"
|
<label>选择目标用户</label>
|
||||||
clearable
|
<el-input
|
||||||
placeholder="搜索手机号 / 昵称 / 用户ID"
|
v-model="userKeyword"
|
||||||
@keyup.enter="searchUsers"
|
clearable
|
||||||
@clear="searchUsers"
|
placeholder="输入手机号 / 昵称 / 用户ID 查询"
|
||||||
>
|
@keyup.enter="searchUsers"
|
||||||
<template #append>
|
@clear="searchUsers"
|
||||||
<el-button :icon="Search" :loading="userSearching" @click="searchUsers">查询</el-button>
|
>
|
||||||
</template>
|
<template #append>
|
||||||
</el-input>
|
<el-button :icon="Search" :loading="userSearching" @click="searchUsers"
|
||||||
|
>查询</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="transfer-user-list">
|
<div class="transfer-user-list">
|
||||||
<button
|
<button
|
||||||
@@ -314,14 +531,13 @@ async function openScreenshot(url: string) {
|
|||||||
type="button"
|
type="button"
|
||||||
@click="selectUser(user)"
|
@click="selectUser(user)"
|
||||||
>
|
>
|
||||||
<span>
|
<span class="transfer-user-main">
|
||||||
<strong>{{ user.nickname || user.phone || `用户${user.id}` }}</strong>
|
<strong>{{ user.nickname || user.phone || `用户${user.id}` }}</strong>
|
||||||
<small>{{ user.phone || '-' }}</small>
|
<small>{{ user.phone || '-' }} · ID {{ user.id }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span class="transfer-user-meta">
|
<span class="transfer-user-meta">
|
||||||
<el-tag size="small" type="success">{{ userStatusLabel(user.status) }}</el-tag>
|
<el-tag size="small" type="success">{{ userStatusLabel(user.status) }}</el-tag>
|
||||||
<el-tag size="small">{{ realnameStatusLabel(user.realname_status) }}</el-tag>
|
<el-tag size="small">{{ realnameStatusLabel(user.realname_status) }}</el-tag>
|
||||||
<small>ID:{{ user.id }}</small>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<el-empty
|
<el-empty
|
||||||
@@ -331,12 +547,15 @@ async function openScreenshot(url: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-input
|
<div class="transfer-section">
|
||||||
v-model="transferReason"
|
<label>转移原因</label>
|
||||||
type="textarea"
|
<el-input
|
||||||
:rows="3"
|
v-model="transferReason"
|
||||||
placeholder="填写转移原因,会写入审计日志"
|
type="textarea"
|
||||||
/>
|
:rows="3"
|
||||||
|
placeholder="填写转移原因,会写入审计日志"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="transferVisible = false">取消</el-button>
|
<el-button @click="transferVisible = false">取消</el-button>
|
||||||
@@ -354,36 +573,345 @@ async function openScreenshot(url: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.transfer-dialog {
|
.listing-detail-header {
|
||||||
display: grid;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
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: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f8fafc;
|
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;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-current strong {
|
.transfer-owner-card.target {
|
||||||
color: #0f172a;
|
border-color: #86efac;
|
||||||
|
background: #f0fdf4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-current small {
|
.transfer-owner-card.target.empty {
|
||||||
margin-left: auto;
|
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 {
|
.transfer-user-list {
|
||||||
min-height: 120px;
|
min-height: 138px;
|
||||||
max-height: 260px;
|
max-height: 280px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-user {
|
.transfer-user {
|
||||||
@@ -392,7 +920,7 @@ async function openScreenshot(url: string) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 12px 14px;
|
padding: 13px 14px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid #eef2f7;
|
border-bottom: 1px solid #eef2f7;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -410,15 +938,29 @@ async function openScreenshot(url: string) {
|
|||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transfer-user.is-selected {
|
||||||
|
box-shadow: inset 3px 0 0 #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-user-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.transfer-user strong {
|
.transfer-user strong {
|
||||||
display: block;
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-user small {
|
.transfer-user small {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-user-meta {
|
.transfer-user-meta {
|
||||||
@@ -427,4 +969,32 @@ async function openScreenshot(url: string) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user