599 lines
20 KiB
Vue
599 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { showDialog, showToast } from 'vant'
|
||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||
|
||
import { usePublishForm } from '@/composables/usePublishForm'
|
||
|
||
const {
|
||
commonOnlineTimes,
|
||
dailyLossOptions,
|
||
formatNumber,
|
||
router,
|
||
loading,
|
||
uploading,
|
||
fileInput,
|
||
activeUploadKey,
|
||
form,
|
||
quantityValues,
|
||
quantityModes,
|
||
screenshotFiles,
|
||
selectedSkins,
|
||
serverOptions,
|
||
faceOptions,
|
||
rankOptions,
|
||
insuranceOptions,
|
||
levelOptions,
|
||
loginMethodOptions,
|
||
regionOptions,
|
||
banRecordOptions,
|
||
skinGroups,
|
||
quantityItems,
|
||
screenshotSlots,
|
||
priceConfig,
|
||
fireLevelPlaceholder,
|
||
coinMAmount,
|
||
dailyLossMAmount,
|
||
calculatedDefaultSaleRatio,
|
||
maxAcceleratedSaleRatio,
|
||
calculatedCoinBasePrice,
|
||
calculatedConsumablePrice,
|
||
calculatedSellerPrice,
|
||
calculatedFinalPrice,
|
||
calculatedRatioText,
|
||
calculatedDefaultSaleRatioText,
|
||
saleRatioRangeText,
|
||
acceleratedSaleRatioPlaceholder,
|
||
recommendedDepositAmount,
|
||
hasAcceleratedSaleRatioInput,
|
||
isQuantityItemDisabled,
|
||
handleResetDraft,
|
||
toggleSkin,
|
||
toggleRegion,
|
||
triggerUpload,
|
||
handleScreenshotUpload,
|
||
removeScreenshot,
|
||
getScreenshotPreviewURL,
|
||
handleFireLevelInput,
|
||
handleAcceleratedSaleRatioInput,
|
||
useRecommendedDeposit,
|
||
setQuantityMode,
|
||
clampAcceleratedSaleRatioInput,
|
||
useReferenceSaleRatio,
|
||
useMaxAcceleratedSaleRatio,
|
||
handleSubmit,
|
||
isScreenshotRequired,
|
||
} = usePublishForm({
|
||
draftKey: 'hfb.mobile.publish.draft',
|
||
submitSuccessPath: '/m/profile',
|
||
async confirmReset() {
|
||
await showDialog({
|
||
title: '重置发布内容',
|
||
message: '将清空当前填写内容和本地草稿。',
|
||
confirmButtonText: '重置',
|
||
cancelButtonText: '取消',
|
||
showCancelButton: true,
|
||
})
|
||
},
|
||
notifySuccess: (message) => showToast({ message, icon: 'passed' }),
|
||
notifyWarning: (message) => showToast({ message, icon: 'warning-o' }),
|
||
notifyError: (message) => showToast({ message, icon: 'cross' }),
|
||
})
|
||
|
||
|
||
|
||
function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||
setter(value)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<main class="mobile-publish">
|
||
<header class="page-header">
|
||
<button class="back-btn" type="button" @click="router.back()">
|
||
<van-icon name="arrow-left" :size="20" />
|
||
</button>
|
||
<h1>发布账号</h1>
|
||
<span class="header-spacer"></span>
|
||
</header>
|
||
|
||
<section class="form-body">
|
||
<div class="form-section">
|
||
<h2 class="section-title">基础资料</h2>
|
||
|
||
<van-field label="区服" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in serverOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.server_region === opt }"
|
||
@click="selectRadio(opt, (value) => (form.server_region = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<van-field label="是否本人人脸" class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in faceOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.face_owner === opt }"
|
||
@click="selectRadio(opt, (value) => (form.face_owner = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<van-field
|
||
v-model="form.haf_coin_amount"
|
||
label="哈夫币/M"
|
||
type="digit"
|
||
required
|
||
placeholder="只填写仓库右上角纯币数额,总资产不计算在内;100M 写 100"
|
||
class="publish-field"
|
||
/>
|
||
|
||
<van-field label="段位" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in rankOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.rank_level === opt }"
|
||
@click="selectRadio(opt, (value) => (form.rank_level = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<van-field
|
||
v-model="form.secret_kd"
|
||
label="绝密KD"
|
||
type="number"
|
||
placeholder="填写绝密KD,如果数据对不上买家可以申请退款"
|
||
class="publish-field"
|
||
/>
|
||
<van-field
|
||
:model-value="form.fire_level"
|
||
label="烽火等级"
|
||
type="digit"
|
||
required
|
||
:placeholder="fireLevelPlaceholder"
|
||
class="publish-field"
|
||
@update:model-value="handleFireLevelInput"
|
||
/>
|
||
<van-field label="赛季保险" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in insuranceOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.season_insurance === opt }"
|
||
@click="selectRadio(opt, (value) => (form.season_insurance = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<div class="level-panel">
|
||
<div class="level-row">
|
||
<div class="level-label"><span>*</span>体力</div>
|
||
<div class="level-options">
|
||
<button
|
||
v-for="opt in levelOptions"
|
||
:key="`stamina-${opt}`"
|
||
type="button"
|
||
class="level-btn"
|
||
:class="{ active: form.stamina_level === opt }"
|
||
@click="selectRadio(opt, (value) => (form.stamina_level = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="level-row">
|
||
<div class="level-label"><span>*</span>负重</div>
|
||
<div class="level-options">
|
||
<button
|
||
v-for="opt in levelOptions"
|
||
:key="`load-${opt}`"
|
||
type="button"
|
||
class="level-btn"
|
||
:class="{ active: form.load_level === opt }"
|
||
@click="selectRadio(opt, (value) => (form.load_level = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h2 class="section-title">额外消耗品</h2>
|
||
<div
|
||
v-for="item in quantityItems"
|
||
:key="item.key"
|
||
class="quantity-row"
|
||
:class="{ disabled: isQuantityItemDisabled(item) }"
|
||
>
|
||
<div class="quantity-card">
|
||
<div class="quantity-meta">
|
||
<strong
|
||
class="backend-hint-target"
|
||
:class="{ 'has-hint': Boolean(item.placeholder) }"
|
||
:data-hint="item.placeholder"
|
||
:tabindex="item.placeholder ? 0 : -1"
|
||
>
|
||
<span>*</span>{{ item.label }}
|
||
</strong>
|
||
<small>{{ isQuantityItemDisabled(item) ? "3*3 已包含,不能填写" : item.price }}</small>
|
||
</div>
|
||
<input
|
||
v-model="quantityValues[item.key]"
|
||
type="number"
|
||
inputmode="numeric"
|
||
min="0"
|
||
placeholder="0"
|
||
class="quantity-input"
|
||
:disabled="isQuantityItemDisabled(item)"
|
||
/>
|
||
</div>
|
||
<div class="mode-toggle">
|
||
<button
|
||
type="button"
|
||
:class="{ active: quantityModes[item.key] === '赠送' }"
|
||
:disabled="isQuantityItemDisabled(item)"
|
||
@click="setQuantityMode(item, '赠送')"
|
||
>
|
||
赠送
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: (quantityModes[item.key] || '收费') === '收费' }"
|
||
:disabled="isQuantityItemDisabled(item)"
|
||
@click="setQuantityMode(item, '收费')"
|
||
>
|
||
收费
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h2 class="section-title">皮肤与交接</h2>
|
||
<div class="skin-groups">
|
||
<div v-for="group in skinGroups" :key="group.key" class="skin-group">
|
||
<div class="skin-group-title">
|
||
<span>{{ group.title }}</span>
|
||
<small>
|
||
{{
|
||
group.options.filter((skin) => selectedSkins.includes(skin))
|
||
.length
|
||
}} 项
|
||
</small>
|
||
</div>
|
||
<div class="multi-chip-group">
|
||
<button
|
||
v-for="skin in group.options"
|
||
:key="skin"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: selectedSkins.includes(skin) }"
|
||
@click="toggleSkin(skin)"
|
||
>
|
||
{{ skin }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<van-field label="上号方式" class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in loginMethodOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.login_method === opt }"
|
||
@click="selectRadio(opt, (value) => (form.login_method = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
<p class="field-hint">
|
||
优先推荐【账密登录】,出售速度约为扫码的数倍,两种方式安全性一致,账密登录更便捷,请自行衡量
|
||
</p>
|
||
|
||
<div class="time-row">
|
||
<van-field
|
||
v-model="form.online_start"
|
||
label="在线开始"
|
||
type="time"
|
||
class="publish-field time-field"
|
||
/>
|
||
<van-field
|
||
v-model="form.online_end"
|
||
label="在线结束"
|
||
type="time"
|
||
class="publish-field time-field"
|
||
/>
|
||
</div>
|
||
<p class="field-hint">
|
||
此在线时间指的是百分百能够联系上您的时间,若是在此期间联系不上您导致无法上号会扣除您的部分订单金额或上架押金,在线时长太短可能无法上架,请预留充足时间用于扫码以及冻结人脸,请谨慎填写
|
||
</p>
|
||
|
||
<van-field label="封禁记录" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in banRecordOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.ban_record === opt }"
|
||
@click="selectRadio(opt, (value) => (form.ban_record = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<div class="region-panel">
|
||
<div class="region-title">
|
||
<span>常用登录地区</span>
|
||
<small>已选 {{ form.common_regions.length }}</small>
|
||
</div>
|
||
<div class="region-grid">
|
||
<button
|
||
v-for="region in regionOptions"
|
||
:key="region"
|
||
type="button"
|
||
class="region-btn"
|
||
:class="{ active: form.common_regions.includes(region) }"
|
||
@click="toggleRegion(region)"
|
||
>
|
||
{{ region }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="field-hint">
|
||
推荐勾选账号常用登录地,便于同地区玩家租用,可大幅减少账号触发异地登录保护。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h2 class="section-title">截图材料</h2>
|
||
<div class="upload-list">
|
||
<div v-for="slot in screenshotSlots" :key="slot.key" class="upload-line">
|
||
<div class="upload-meta">
|
||
<strong
|
||
class="upload-title"
|
||
:class="{ 'has-hint': Boolean(slot.hint) }"
|
||
:data-hint="slot.hint"
|
||
:tabindex="slot.hint ? 0 : -1"
|
||
>
|
||
{{ slot.label }}<span v-if="isScreenshotRequired(slot)">*</span>
|
||
</strong>
|
||
<small>{{ slot.hint }}</small>
|
||
</div>
|
||
<div v-if="screenshotFiles[slot.key]" class="upload-preview">
|
||
<img :src="getScreenshotPreviewURL(slot.key)" :alt="slot.label" />
|
||
<button type="button" @click="removeScreenshot(slot.key)">移除</button>
|
||
</div>
|
||
<button
|
||
v-else
|
||
type="button"
|
||
class="upload-add"
|
||
:disabled="uploading"
|
||
@click="triggerUpload(slot.key)"
|
||
>
|
||
<van-icon name="photograph" :size="22" color="#999" />
|
||
<span>{{ uploading && activeUploadKey === slot.key ? "上传中" : "上传" }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
accept="image/jpeg,image/png,image/webp"
|
||
style="display: none"
|
||
@change="handleScreenshotUpload"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h2 class="section-title">押金与价格</h2>
|
||
<van-field
|
||
v-model="form.deposit_amount"
|
||
label="押金"
|
||
type="number"
|
||
required
|
||
:placeholder="priceConfig.deposit_placeholder"
|
||
suffix="元"
|
||
class="publish-field"
|
||
>
|
||
<template #label>
|
||
<span class="hint-label" :data-hint="priceConfig.deposit_placeholder" tabindex="0">押金</span>
|
||
</template>
|
||
</van-field>
|
||
<button class="deposit-recommend-btn" type="button" @click="useRecommendedDeposit">
|
||
推荐押金 ¥{{ recommendedDepositAmount }}
|
||
</button>
|
||
<van-field label="每日损耗" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="loss in dailyLossOptions"
|
||
:key="loss"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: dailyLossMAmount === loss }"
|
||
@click="form.daily_loss_m = loss"
|
||
>
|
||
{{ loss }}M/天
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
<p class="field-hint">
|
||
比例调整:在默认10M/天,每增加10M,出租比例相应+1,请根据实际需求合理设置,感谢您的配合。
|
||
</p>
|
||
<div class="ratio-panel">
|
||
<div class="ratio-reference">
|
||
<span>参考比例</span>
|
||
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
|
||
</div>
|
||
<p class="ratio-range">{{ saleRatioRangeText }},比例越高出租速度越快,自行选择。</p>
|
||
<div class="ratio-mode-grid">
|
||
<button
|
||
type="button"
|
||
class="ratio-mode-btn"
|
||
:class="{ active: !hasAcceleratedSaleRatioInput() }"
|
||
:disabled="calculatedDefaultSaleRatio <= 0"
|
||
@click="useReferenceSaleRatio"
|
||
>
|
||
<strong>参考比例</strong>
|
||
<span>
|
||
{{
|
||
calculatedDefaultSaleRatio > 0
|
||
? `1元=${formatNumber(calculatedDefaultSaleRatio)}万`
|
||
: "自动计算"
|
||
}}
|
||
</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="ratio-mode-btn"
|
||
:class="{ active: hasAcceleratedSaleRatioInput() }"
|
||
:disabled="maxAcceleratedSaleRatio <= 0"
|
||
@click="useMaxAcceleratedSaleRatio"
|
||
>
|
||
<strong>加速比例</strong>
|
||
<span>
|
||
{{
|
||
maxAcceleratedSaleRatio > 0
|
||
? `1元=${formatNumber(calculatedDefaultSaleRatio)}~${formatNumber(maxAcceleratedSaleRatio)}万`
|
||
: "自动计算"
|
||
}}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<van-field
|
||
:model-value="form.accelerated_sale_ratio"
|
||
label="自定比例"
|
||
type="number"
|
||
:placeholder="acceleratedSaleRatioPlaceholder"
|
||
class="publish-field ratio-input-field"
|
||
@update:model-value="handleAcceleratedSaleRatioInput"
|
||
@blur="clampAcceleratedSaleRatioInput"
|
||
/>
|
||
<p class="field-hint ratio-input-hint">
|
||
留空使用参考比例;填写后范围为参考比例到参考比例+10,请根据实际需求合理设置。
|
||
</p>
|
||
</div>
|
||
<div class="result-grid">
|
||
<van-field
|
||
:model-value="calculatedRatioText"
|
||
label="卖家比例"
|
||
readonly
|
||
:placeholder="priceConfig.ratio_description"
|
||
class="publish-field result-field"
|
||
>
|
||
<template #label>
|
||
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">卖家比例</span>
|
||
</template>
|
||
</van-field>
|
||
<van-field
|
||
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
|
||
label="纯币基础价"
|
||
readonly
|
||
required
|
||
:placeholder="priceConfig.price_placeholder"
|
||
class="publish-field result-field"
|
||
>
|
||
<template #label>
|
||
<span class="hint-label" :data-hint="priceConfig.price_placeholder" tabindex="0">纯币基础价</span>
|
||
</template>
|
||
</van-field>
|
||
<van-field
|
||
:model-value="`¥${calculatedConsumablePrice}`"
|
||
label="额外消耗品"
|
||
readonly
|
||
class="publish-field result-field"
|
||
/>
|
||
<van-field
|
||
:model-value="calculatedSellerPrice ? `¥${calculatedSellerPrice}` : ''"
|
||
label="卖家价格"
|
||
readonly
|
||
required
|
||
:placeholder="priceConfig.price_placeholder"
|
||
class="publish-field result-field total-price-field"
|
||
/>
|
||
</div>
|
||
<p class="price-breakdown-hint">
|
||
卖家价格 = 纯币基础价 + 额外消耗品;加速比例仅影响纯币基础价。
|
||
</p>
|
||
|
||
<van-field
|
||
v-model="form.remark"
|
||
label="备注"
|
||
type="textarea"
|
||
rows="3"
|
||
autosize
|
||
placeholder="如有一些不可使用的物资,请在此备注,并且在买家下单后,主动在群聊处,再次提醒买家"
|
||
class="publish-field"
|
||
/>
|
||
</div>
|
||
|
||
<div class="publish-actions">
|
||
<van-button
|
||
round
|
||
class="reset-btn"
|
||
:disabled="loading"
|
||
@click="handleResetDraft"
|
||
>
|
||
重置
|
||
</van-button>
|
||
<van-button
|
||
type="primary"
|
||
round
|
||
class="submit-btn"
|
||
:loading="loading"
|
||
loading-text="发布中..."
|
||
@click="handleSubmit"
|
||
>
|
||
保存发布
|
||
</van-button>
|
||
</div>
|
||
</section>
|
||
|
||
<MobileBottomNav />
|
||
</main>
|
||
</template>
|
||
|
||
<style scoped src="./MobileSellerListingCreateView.css"></style>
|