优化pc 出售界面

This commit is contained in:
yml
2026-05-24 23:59:49 +08:00
parent 95ded72b66
commit a8dc96aa3b
2 changed files with 324 additions and 84 deletions
@@ -490,7 +490,7 @@ async function handleSubmit() {
loading.value = true;
try {
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`;
const dailyPrice = calculatedFinalPrice.value;
const listingPrice = calculatedFinalPrice.value;
const listing = await createListing({
title,
@@ -501,7 +501,7 @@ async function handleSubmit() {
haf_coin_amount: coinMAmount.value * 1000000,
asset_summary: buildAssetSummary(),
screenshot_urls: screenshotUrls.value,
price: dailyPrice,
price: listingPrice,
deposit_amount: Number(form.deposit_amount),
});
localStorage.removeItem(draftKey);
@@ -2,7 +2,7 @@
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Picture, RefreshRight, UploadFilled } from '@element-plus/icons-vue'
import { Delete, DocumentChecked, Picture, RefreshRight, UploadFilled } from '@element-plus/icons-vue'
import { fetchFileBlobByURL, uploadFile } from '@/api/files'
import {
@@ -49,10 +49,12 @@ interface PublishDraft {
const draftKey = 'hfb.pc.publish.draft'
const dailyLossOptions = [10, 20, 30, 40, 50]
const commonOnlineTimes = ['00:00', '08:00', '10:00', '12:00', '14:00', '18:00', '20:00', '22:00', '23:59']
const router = useRouter()
const loading = ref(false)
const uploading = ref(false)
const suppressDraftSave = ref(false)
const draftReady = ref(false)
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
const salePriceConfig = ref<PublishSalePriceConfig>(emptyListingSalePriceConfig)
const fileInput = ref<HTMLInputElement | null>(null)
@@ -141,10 +143,18 @@ const acceleratedSaleRatioPlaceholder = computed(() => {
if (calculatedDefaultSaleRatio.value <= 0) return '填写资料后自动生成可设置范围'
return `默认 ${formatNumber(calculatedDefaultSaleRatio.value)},最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`
})
const selectedSkinCount = computed(() => selectedSkins.value.length)
const uploadedScreenshotCount = computed(() => screenshotUrls.value.length)
const requiredScreenshotCount = computed(() => screenshotSlots.value.filter((item) => isScreenshotRequired(item)).length)
const recommendedDepositAmount = computed(() => calculateRecommendedDeposit())
const depositBreakdownItems = computed(() => buildDepositBreakdownItems())
const platformRuleLabel = computed(() => {
const labels: Record<string, string> = {
fixed_markup: '固定加价',
ratio_subtract: '比例修正',
none: '无加价',
}
return labels[calculatedPlatformPricing.value.ruleType] || calculatedPlatformPricing.value.ruleType
})
const publishTitle = computed(() => {
const parts = [form.server_region, form.rank_level, coinMAmount.value ? `${coinMAmount.value}M哈夫币` : ''].filter(Boolean)
return parts.length ? parts.join(' ') : '待完善账号信息'
@@ -152,10 +162,14 @@ const publishTitle = computed(() => {
onMounted(() => {
restoreDraft()
draftReady.value = true
window.addEventListener('beforeunload', handleBeforeUnload)
loadPublishOptions()
})
onBeforeUnmount(() => {
saveDraft({ force: true })
window.removeEventListener('beforeunload', handleBeforeUnload)
revokeAllScreenshotPreviews()
})
@@ -206,11 +220,21 @@ function buildDraft(): PublishDraft {
}
}
function saveDraft() {
function saveDraft(options: { force?: boolean } = {}) {
if (suppressDraftSave.value) return
if (!options.force && !draftReady.value) return
localStorage.setItem(draftKey, JSON.stringify(buildDraft()))
}
function handleSaveDraft() {
saveDraft({ force: true })
ElMessage.success('草稿已保存')
}
function handleBeforeUnload() {
saveDraft({ force: true })
}
function restoreDraft() {
const raw = localStorage.getItem(draftKey)
if (!raw) return
@@ -388,6 +412,28 @@ function calculateRecommendedDeposit() {
return roundMoney(baseAmount + skinAmount)
}
function buildDepositBreakdownItems() {
const items = [
{
label: '基础押金',
amount: Number(depositRecommendConfig.value.base_amount || 0),
count: 1,
},
]
for (const rule of depositRecommendConfig.value.skin_group_rules) {
const group = skinGroups.value.find((item) => item.key === rule.group_key)
if (!group) continue
const count = group.options.filter((skin) => selectedSkins.value.includes(skin)).length
if (count <= 0) continue
items.push({
label: rule.label,
amount: Number(rule.amount_per_item || 0) * count,
count,
})
}
return items
}
function useRecommendedDeposit() {
if (recommendedDepositAmount.value > 0) {
form.deposit_amount = recommendedDepositAmount.value
@@ -457,6 +503,7 @@ async function handleSubmit() {
deposit_amount: Number(form.deposit_amount),
})
localStorage.removeItem(draftKey)
suppressDraftSave.value = true
ElMessage.success(
listing.status === 'published' && listing.review_status === 'approved'
? '发布成功,已上架'
@@ -753,7 +800,7 @@ function readError(error: unknown, fallback: string) {
<span>{{ publishTitle }}</span>
</div>
<div class="field-row">
<div class="field-row panel-field-row">
<label>区服<span>*</span></label>
<div class="chip-group">
<button
@@ -769,7 +816,7 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div v-if="faceOptions.length" class="field-row">
<div v-if="faceOptions.length" class="field-row panel-field-row">
<label>是否本人人脸</label>
<div class="chip-group">
<button
@@ -785,7 +832,7 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div class="compact-grid">
<div class="compact-grid panel-input-grid">
<label class="input-block">
<span>哈夫币/M<b>*</b></span>
<el-input-number v-model="form.haf_coin_amount" :min="0" :controls="false" placeholder="100M 写 100" />
@@ -806,7 +853,7 @@ function readError(error: unknown, fallback: string) {
</label>
</div>
<div class="field-row">
<div class="field-row panel-field-row">
<label>段位<span>*</span></label>
<div class="chip-group">
<button
@@ -822,7 +869,7 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div class="field-row">
<div class="field-row panel-field-row">
<label>赛季保险<span>*</span></label>
<div class="chip-group">
<button
@@ -875,9 +922,16 @@ function readError(error: unknown, fallback: string) {
<section v-if="quantityItems.length" class="form-section">
<div class="section-heading">
<h2>额外消耗品</h2>
<span>收费项会计入每日价格</span>
<span>收费项会计入发布价格</span>
</div>
<div class="quantity-grid">
<div class="quantity-table">
<div class="quantity-header">
<span>物资</span>
<span>单价</span>
<span>数量</span>
<span>计费</span>
<span>状态</span>
</div>
<div
v-for="item in quantityItems"
:key="item.key"
@@ -886,8 +940,9 @@ function readError(error: unknown, fallback: string) {
>
<div class="quantity-meta">
<strong :title="item.placeholder || item.label">{{ item.label }}</strong>
<small>{{ isQuantityItemDisabled(item) ? '3*3 已包含,不能填写' : item.price }}</small>
<small v-if="item.placeholder">{{ item.placeholder }}</small>
</div>
<span class="quantity-price">{{ item.price }}</span>
<el-input-number
v-model="quantityValues[item.key]"
:min="0"
@@ -912,6 +967,9 @@ function readError(error: unknown, fallback: string) {
收费
</button>
</div>
<span class="quantity-status">
{{ isQuantityItemDisabled(item) ? '3*3 已包含' : '可填写' }}
</span>
</div>
</div>
</section>
@@ -919,14 +977,16 @@ function readError(error: unknown, fallback: string) {
<section class="form-section">
<div class="section-heading">
<h2>皮肤与交接</h2>
<span>已选 {{ selectedSkinCount }} 项皮肤</span>
</div>
<div v-if="skinGroups.length" class="skin-groups">
<div v-for="group in skinGroups" :key="group.key" class="skin-group">
<div
v-for="group in skinGroups"
:key="group.key"
class="skin-group"
>
<div class="skin-title">
<strong>{{ group.title }}</strong>
<span>{{ group.options.filter((skin) => selectedSkins.includes(skin)).length }} </span>
</div>
<div class="chip-group">
<button
@@ -943,7 +1003,7 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div class="field-row">
<div class="field-row login-method-row">
<label>上号方式</label>
<div class="chip-group">
<button
@@ -960,15 +1020,55 @@ function readError(error: unknown, fallback: string) {
<p class="field-hint">优先推荐账密登录出售速度通常更快请结合账号安全情况自行选择</p>
</div>
<div class="compact-grid two">
<label class="input-block">
<span>在线开始</span>
<el-time-picker v-model="form.online_start" value-format="HH:mm" format="HH:mm" placeholder="开始时间" />
</label>
<label class="input-block">
<span>在线结束</span>
<el-time-picker v-model="form.online_end" value-format="HH:mm" format="HH:mm" placeholder="结束时间" />
</label>
<div class="time-preset-panel">
<div class="time-preset-row">
<strong>在线开始</strong>
<div class="time-preset-content">
<el-time-picker
v-model="form.online_start"
value-format="HH:mm"
format="HH:mm"
placeholder="开始时间"
class="time-input"
/>
<div class="chip-group">
<button
v-for="time in commonOnlineTimes"
:key="`start-${time}`"
type="button"
class="chip"
:class="{ active: form.online_start === time }"
@click="form.online_start = time"
>
{{ time }}
</button>
</div>
</div>
</div>
<div class="time-preset-row">
<strong>在线结束</strong>
<div class="time-preset-content">
<el-time-picker
v-model="form.online_end"
value-format="HH:mm"
format="HH:mm"
placeholder="结束时间"
class="time-input"
/>
<div class="chip-group">
<button
v-for="time in commonOnlineTimes"
:key="`end-${time}`"
type="button"
class="chip"
:class="{ active: form.online_end === time }"
@click="form.online_end = time"
>
{{ time }}
</button>
</div>
</div>
</div>
</div>
<p class="field-hint">请填写能稳定联系上您的时间便于扫码冻结人脸和订单交接</p>
@@ -988,7 +1088,7 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div v-if="regionOptions.length" class="region-panel">
<div v-if="regionOptions.length" class="region-panel login-region-panel">
<div class="region-title">
<strong>常用登录地区</strong>
<span>已选 {{ form.common_regions.length }}</span>
@@ -1049,15 +1149,22 @@ function readError(error: unknown, fallback: string) {
<div class="compact-grid two">
<label class="input-block">
<span>押金<b>*</b></span>
<el-input-number
v-model="form.deposit_amount"
:min="0"
:controls="false"
:placeholder="priceConfig.deposit_placeholder"
/>
<button class="recommend-button" type="button" @click="useRecommendedDeposit">
推荐押金 ¥{{ recommendedDepositAmount }}
</button>
<div class="deposit-control">
<el-input-number
v-model="form.deposit_amount"
:min="0"
:controls="false"
:placeholder="priceConfig.deposit_placeholder"
/>
<button class="recommend-button" type="button" @click="useRecommendedDeposit">
使用推荐 ¥{{ recommendedDepositAmount }}
</button>
</div>
<div class="deposit-breakdown">
<span v-for="item in depositBreakdownItems" :key="`${item.label}-${item.count}`">
{{ item.label }}<template v-if="item.count > 1"> x{{ item.count }}</template> ¥{{ item.amount }}
</span>
</div>
</label>
<label class="input-block">
<span>每日损耗<b>*</b></span>
@@ -1131,7 +1238,7 @@ function readError(error: unknown, fallback: string) {
<strong>¥{{ calculatedConsumablePrice }}</strong>
</div>
<div class="price-cell accent">
<span>买家每日价</span>
<span>发布价格</span>
<strong>{{ calculatedFinalPrice ? `¥${calculatedFinalPrice}` : '--' }}</strong>
</div>
</div>
@@ -1151,7 +1258,7 @@ function readError(error: unknown, fallback: string) {
<aside class="summary-column">
<div class="summary-panel">
<div class="summary-main">
<span>预计买家每日价</span>
<span>预计发布价格</span>
<strong>{{ calculatedFinalPrice ? `¥${calculatedFinalPrice}` : '--' }}</strong>
</div>
<div class="summary-list">
@@ -1173,11 +1280,12 @@ function readError(error: unknown, fallback: string) {
</div>
<div>
<span>加价规则</span>
<strong>{{ calculatedPlatformPricing.ruleType }}</strong>
<strong>{{ platformRuleLabel }}</strong>
</div>
</div>
<div class="summary-actions">
<el-button :icon="UploadFilled" type="primary" :loading="loading" @click="handleSubmit">保存发布</el-button>
<el-button :icon="DocumentChecked" :disabled="loading" @click="handleSaveDraft">保存草稿</el-button>
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft">重置草稿</el-button>
</div>
</div>
@@ -1194,16 +1302,17 @@ function readError(error: unknown, fallback: string) {
.publish-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 20px;
max-width: 1480px;
grid-template-columns: minmax(760px, 920px) 300px;
align-items: start;
gap: 18px;
max-width: 1238px;
margin: 0 auto;
}
.form-column {
display: grid;
min-width: 0;
gap: 16px;
gap: 18px;
}
.form-section,
@@ -1215,15 +1324,16 @@ function readError(error: unknown, fallback: string) {
}
.form-section {
padding: 18px;
padding: 20px;
}
.section-heading {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
min-height: 28px;
margin-bottom: 18px;
}
.section-heading h2 {
@@ -1241,10 +1351,21 @@ function readError(error: unknown, fallback: string) {
.field-row {
display: grid;
grid-template-columns: 118px minmax(0, 1fr);
gap: 12px;
align-items: start;
margin-top: 14px;
grid-template-columns: 112px minmax(0, 1fr);
gap: 14px;
align-items: center;
margin-top: 12px;
}
.panel-field-row,
.panel-input-grid {
padding: 12px;
border-radius: 8px;
background: #f7f9fc;
}
.panel-input-grid {
align-items: end;
}
.field-row > label,
@@ -1252,7 +1373,7 @@ function readError(error: unknown, fallback: string) {
color: #303743;
font-size: 13px;
font-weight: 800;
line-height: 32px;
line-height: 22px;
}
.field-row label span,
@@ -1265,7 +1386,7 @@ function readError(error: unknown, fallback: string) {
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 7px;
min-width: 0;
}
@@ -1288,10 +1409,10 @@ function readError(error: unknown, fallback: string) {
}
.chip {
min-height: 32px;
padding: 0 13px;
border-radius: 16px;
font-size: 13px;
min-height: 30px;
padding: 0 12px;
border-radius: 15px;
font-size: 12px;
}
.chip.active,
@@ -1305,8 +1426,8 @@ function readError(error: unknown, fallback: string) {
.compact-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
gap: 16px;
margin-top: 16px;
}
.compact-grid.two {
@@ -1316,7 +1437,7 @@ function readError(error: unknown, fallback: string) {
.input-block {
display: grid;
min-width: 0;
gap: 7px;
gap: 8px;
}
.input-block :deep(.el-input-number),
@@ -1339,10 +1460,10 @@ function readError(error: unknown, fallback: string) {
.level-row {
display: grid;
grid-template-columns: 74px minmax(0, 1fr);
gap: 10px;
grid-template-columns: 72px minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 12px;
padding: 10px 12px;
border-radius: 8px;
background: #f7f9fc;
}
@@ -1354,13 +1475,13 @@ function readError(error: unknown, fallback: string) {
.level-options {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 8px;
gap: 9px;
}
.level-btn {
height: 32px;
height: 30px;
min-width: 0;
border-radius: 16px;
border-radius: 15px;
}
.field-hint {
@@ -1371,18 +1492,39 @@ function readError(error: unknown, fallback: string) {
line-height: 1.55;
}
.quantity-grid {
.login-method-row {
padding: 12px;
border-radius: 8px;
background: #f7f9fc;
}
.login-method-row .field-hint {
margin-top: -2px;
}
.quantity-table {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.quantity-header,
.quantity-item {
display: grid;
grid-template-columns: minmax(160px, 1fr) 90px 112px 112px 92px;
gap: 12px;
align-items: center;
}
.quantity-header {
min-height: 30px;
padding: 0 12px;
color: #7b8492;
font-size: 12px;
font-weight: 800;
}
.quantity-item {
display: grid;
grid-template-columns: minmax(0, 1fr) 112px 112px;
gap: 10px;
align-items: center;
min-height: 76px;
min-height: 58px;
padding: 12px;
border-radius: 8px;
background: #f7f9fc;
@@ -1391,7 +1533,7 @@ function readError(error: unknown, fallback: string) {
.quantity-meta {
display: grid;
min-width: 0;
gap: 5px;
gap: 4px;
}
.quantity-meta strong {
@@ -1402,14 +1544,20 @@ function readError(error: unknown, fallback: string) {
}
.quantity-meta small,
.quantity-price,
.quantity-status,
.upload-copy small,
.skin-title span,
.region-title span {
color: #7f8895;
font-size: 12px;
font-weight: 700;
}
.quantity-price,
.quantity-status {
white-space: nowrap;
}
.quantity-item.disabled {
opacity: 0.62;
}
@@ -1427,10 +1575,10 @@ function readError(error: unknown, fallback: string) {
}
.mode-toggle button {
height: 32px;
height: 30px;
min-width: 0;
border-radius: 8px;
font-size: 13px;
font-size: 12px;
white-space: nowrap;
}
@@ -1445,11 +1593,11 @@ function readError(error: unknown, fallback: string) {
}
.recommend-button {
justify-self: start;
height: 28px;
padding: 0 10px;
flex: 0 0 auto;
height: 32px;
padding: 0 12px;
border: 1px solid #ffba86;
border-radius: 14px;
border-radius: 8px;
background: #fff7f0;
color: #e65f00;
font-size: 12px;
@@ -1457,9 +1605,32 @@ function readError(error: unknown, fallback: string) {
cursor: pointer;
}
.deposit-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
}
.deposit-breakdown {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.deposit-breakdown span {
min-height: 24px;
padding: 4px 8px;
border-radius: 12px;
background: #fff7f0;
color: #e65f00;
font-size: 12px;
font-weight: 800;
}
.skin-groups {
display: grid;
gap: 12px;
gap: 10px;
}
.skin-group,
@@ -1475,11 +1646,63 @@ function readError(error: unknown, fallback: string) {
.skin-title,
.region-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
}
.skin-group {
border: none;
}
.skin-title {
min-height: 34px;
margin-bottom: 8px;
}
.time-preset-panel {
display: grid;
gap: 10px;
margin-top: 16px;
}
.time-preset-row {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 12px;
align-items: start;
padding: 12px;
border-radius: 8px;
background: #f7f9fc;
}
.time-preset-row strong {
color: #303743;
font-size: 13px;
font-weight: 800;
line-height: 32px;
}
.time-preset-content {
display: grid;
gap: 8px;
min-width: 0;
}
.time-input {
width: 118px;
}
.time-input :deep(.el-input__wrapper) {
border-radius: 8px;
box-shadow: 0 0 0 1px #dfe5ee inset;
}
.login-region-panel {
margin-top: 18px;
}
.region-grid {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
@@ -1684,11 +1907,13 @@ function readError(error: unknown, fallback: string) {
.summary-column {
min-width: 0;
position: sticky;
top: 76px;
align-self: start;
height: fit-content;
}
.summary-panel {
position: sticky;
top: 152px;
padding: 16px;
}
@@ -1730,6 +1955,7 @@ function readError(error: unknown, fallback: string) {
color: #20242a;
font-size: 13px;
text-align: right;
overflow-wrap: anywhere;
}
.summary-actions {
@@ -1745,6 +1971,7 @@ function readError(error: unknown, fallback: string) {
@media (max-width: 1180px) {
.publish-layout {
grid-template-columns: 1fr;
max-width: 920px;
}
.summary-column {
@@ -1756,12 +1983,15 @@ function readError(error: unknown, fallback: string) {
.field-row,
.compact-grid,
.compact-grid.two,
.quantity-grid,
.upload-grid,
.price-grid {
grid-template-columns: 1fr;
}
.quantity-header {
display: none;
}
.field-hint {
grid-column: auto;
}
@@ -1771,6 +2001,16 @@ function readError(error: unknown, fallback: string) {
grid-template-columns: 1fr;
}
.deposit-control,
.ratio-mode-grid,
.time-preset-row {
grid-template-columns: 1fr;
}
.time-input {
width: 140px;
}
.upload-add,
.upload-preview {
width: 100%;