809 lines
28 KiB
Vue
809 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { showDialog, showToast } from 'vant'
|
||
import { computed, ref } from 'vue'
|
||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||
|
||
type OnlineTimeSide = 'start' | 'end'
|
||
|
||
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
|
||
import { formatMoney } from '@/shared/utils/money'
|
||
import type { AgreementContent } from '@/features/listings/api/listingOptions'
|
||
import GuideImage from '@/components/GuideImage.vue'
|
||
import tencentSecurityGuide from '@/assets/tencent-security-guide.webp'
|
||
|
||
const {
|
||
dailyLossOptions,
|
||
formatNumber,
|
||
router,
|
||
loading,
|
||
uploading,
|
||
isEditMode,
|
||
pageTitle,
|
||
submitButtonText,
|
||
submitLoadingText,
|
||
publishAgreements,
|
||
virtualAssetSaleAgreementChecked,
|
||
sellerAgreementChecked,
|
||
passwordAndDeviceConfirmed,
|
||
canPublishAfterAgreements,
|
||
showBanRecordRiskHint,
|
||
fileInput,
|
||
activeUploadKey,
|
||
form,
|
||
quantityValues,
|
||
quantityModes,
|
||
selectedSkins,
|
||
serverOptions,
|
||
faceOptions,
|
||
rankOptions,
|
||
insuranceOptions,
|
||
levelOptions,
|
||
effectiveLoginMethodOptions,
|
||
isScanOnlyServer,
|
||
regionOptions,
|
||
banRecordOptions,
|
||
skinGroups,
|
||
quantityItems,
|
||
screenshotSlots,
|
||
priceConfig,
|
||
fireLevelPlaceholder,
|
||
dailyLossMAmount,
|
||
calculatedDefaultSaleRatio,
|
||
maxAcceleratedSaleRatio,
|
||
calculatedCoinBasePrice,
|
||
calculatedConsumablePrice,
|
||
calculatedSellerPrice,
|
||
calculatedRatioText,
|
||
calculatedDefaultSaleRatioText,
|
||
saleRatioRangeText,
|
||
acceleratedSaleRatioPlaceholder,
|
||
recommendedDepositAmount,
|
||
hasAcceleratedSaleRatioInput,
|
||
isQuantityItemDisabled,
|
||
handleResetDraft,
|
||
toggleSkin,
|
||
toggleRegion,
|
||
triggerUpload,
|
||
handleScreenshotUpload,
|
||
removeScreenshot,
|
||
getScreenshotURLs,
|
||
getScreenshotCount,
|
||
getScreenshotPreviewURL,
|
||
isMultiScreenshotSlot,
|
||
canAddScreenshot,
|
||
getScreenshotLimitHint,
|
||
isAllDayOnline,
|
||
onlineTimeRangeHint,
|
||
selectAllDayOnline,
|
||
handleOnlineStartChange,
|
||
handleOnlineEndChange,
|
||
handleFireLevelInput,
|
||
handleAcceleratedSaleRatioInput,
|
||
useRecommendedDeposit,
|
||
handleDepositBlur,
|
||
setQuantityMode,
|
||
clampAcceleratedSaleRatioInput,
|
||
useReferenceSaleRatio,
|
||
useMaxAcceleratedSaleRatio,
|
||
handleSubmit,
|
||
isScreenshotRequired,
|
||
} = usePublishForm({
|
||
draftKey: 'hfb.mobile.publish.draft',
|
||
submitSuccessPath: '/m/profile',
|
||
listingGroupChatPathPrefix: '/m/chats/',
|
||
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' }),
|
||
})
|
||
|
||
const agreementPopupVisible = ref(false)
|
||
const activeAgreement = ref<AgreementContent | null>(null)
|
||
const activeAgreementTitle = computed(() => activeAgreement.value?.title || '协议内容')
|
||
const activeAgreementContent = computed(() => activeAgreement.value?.content || '')
|
||
|
||
function openPublishAgreement(agreement: AgreementContent) {
|
||
activeAgreement.value = agreement
|
||
agreementPopupVisible.value = true
|
||
}
|
||
|
||
function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||
setter(value)
|
||
}
|
||
|
||
const unlockSaidOptions = ['是', '否']
|
||
|
||
// 在线时间:通栏自定义块 + 底部时间选择,避免 van-field 窄列截断原生 time
|
||
const onlineTimePickerVisible = ref(false)
|
||
const onlineTimePickerSide = ref<OnlineTimeSide>('start')
|
||
const onlineTimePickerValue = ref<string[]>(['00', '00'])
|
||
const onlineTimePickerTitle = computed(() =>
|
||
onlineTimePickerSide.value === 'start' ? '选择开始时间' : '选择结束时间'
|
||
)
|
||
|
||
function parseTimeToColumns(value: string): string[] {
|
||
const match = /^(\d{1,2}):(\d{2})$/.exec(String(value || '').trim())
|
||
if (!match?.[1] || !match[2]) return ['00', '00']
|
||
return [match[1].padStart(2, '0'), match[2]]
|
||
}
|
||
|
||
function openOnlineTimePicker(side: OnlineTimeSide) {
|
||
onlineTimePickerSide.value = side
|
||
const current = side === 'start' ? form.online_start : form.online_end
|
||
onlineTimePickerValue.value = parseTimeToColumns(current)
|
||
onlineTimePickerVisible.value = true
|
||
}
|
||
|
||
function onOnlineTimePickerConfirm(payload: { selectedValues?: Array<string | number> }) {
|
||
const values = payload.selectedValues || onlineTimePickerValue.value
|
||
const hour = String(values[0] ?? '00').padStart(2, '0')
|
||
const minute = String(values[1] ?? '00').padStart(2, '0')
|
||
const time = `${hour}:${minute}`
|
||
if (onlineTimePickerSide.value === 'start') {
|
||
handleOnlineStartChange(time)
|
||
} else {
|
||
handleOnlineEndChange(time)
|
||
}
|
||
onlineTimePickerVisible.value = false
|
||
}
|
||
|
||
// 截图槽位的指导示例图(仅展示、点击放大,不占用上传数量)
|
||
const screenshotGuides: Record<string, string> = {
|
||
tencentSecurity: tencentSecurityGuide,
|
||
}
|
||
</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>{{ pageTitle }}</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="number"
|
||
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 effectiveLoginMethodOptions"
|
||
: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">
|
||
{{
|
||
isScanOnlyServer
|
||
? '微信账号仅支持扫码登录,上号方式固定为扫码。'
|
||
: '优先推荐【账密登录】,出售速度约为扫码的数倍,两种方式安全性一致,账密登录更便捷,请自行衡量'
|
||
}}
|
||
</p>
|
||
|
||
<van-field label="解锁赛伊德" required class="publish-field">
|
||
<template #input>
|
||
<div class="radio-group">
|
||
<button
|
||
v-for="opt in unlockSaidOptions"
|
||
:key="opt"
|
||
type="button"
|
||
class="radio-btn"
|
||
:class="{ active: form.unlock_said === opt }"
|
||
@click="selectRadio(opt, value => (form.unlock_said = value))"
|
||
>
|
||
{{ opt }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
|
||
<div class="online-time-block">
|
||
<div class="online-time-head">
|
||
<div class="online-time-title">
|
||
<span class="req">*</span>
|
||
<span>在线时间</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="radio-btn online-time-allday"
|
||
:class="{ active: isAllDayOnline() }"
|
||
@click="selectAllDayOnline"
|
||
>
|
||
全天
|
||
</button>
|
||
</div>
|
||
|
||
<div class="online-time-grid">
|
||
<button type="button" class="online-time-pick" @click="openOnlineTimePicker('start')">
|
||
<span class="online-time-label">开始</span>
|
||
<strong :class="{ placeholder: !form.online_start }">
|
||
{{ form.online_start || '请选择' }}
|
||
</strong>
|
||
</button>
|
||
<button type="button" class="online-time-pick" @click="openOnlineTimePicker('end')">
|
||
<span class="online-time-label">
|
||
结束
|
||
<em v-if="onlineTimeRangeHint()">次日</em>
|
||
</span>
|
||
<strong :class="{ placeholder: !form.online_end }">
|
||
{{ form.online_end || '请选择' }}
|
||
</strong>
|
||
</button>
|
||
</div>
|
||
|
||
<p v-if="onlineTimeRangeHint()" class="online-time-summary">
|
||
{{ onlineTimeRangeHint() }}
|
||
</p>
|
||
</div>
|
||
<p class="field-hint">
|
||
请填写能百分百联系上您的时间。该时段联系不上可能影响上架或扣款,请预留扫码与冻结人脸时间。
|
||
</p>
|
||
|
||
<van-popup v-model:show="onlineTimePickerVisible" position="bottom" round teleport="body">
|
||
<van-time-picker
|
||
v-model="onlineTimePickerValue"
|
||
:title="onlineTimePickerTitle"
|
||
:columns-type="['hour', 'minute']"
|
||
@confirm="onOnlineTimePickerConfirm"
|
||
@cancel="onlineTimePickerVisible = false"
|
||
/>
|
||
</van-popup>
|
||
|
||
<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>
|
||
<p v-if="showBanRecordRiskHint && banRecordOptions.length" class="field-hint">
|
||
刚出租结算完的账号需冷号七天才可再次出租,IP频繁变动会导致封号。
|
||
</p>
|
||
|
||
<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>
|
||
<small v-if="getScreenshotLimitHint(slot.key)">{{
|
||
getScreenshotLimitHint(slot.key)
|
||
}}</small>
|
||
<GuideImage
|
||
v-if="screenshotGuides[slot.key]"
|
||
:src="screenshotGuides[slot.key] || ''"
|
||
label="点击查看处罚截图教程"
|
||
/>
|
||
</div>
|
||
<div v-if="getScreenshotCount(slot.key)" class="upload-preview-list">
|
||
<div
|
||
v-for="(url, index) in getScreenshotURLs(slot.key)"
|
||
:key="`${slot.key}-${url}`"
|
||
class="upload-preview"
|
||
>
|
||
<img
|
||
:src="getScreenshotPreviewURL(slot.key, index)"
|
||
:alt="`${slot.label}${index + 1}`"
|
||
/>
|
||
<button type="button" @click="removeScreenshot(slot.key, index)">移除</button>
|
||
</div>
|
||
<button
|
||
v-if="canAddScreenshot(slot.key)"
|
||
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>
|
||
<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"
|
||
:multiple="isMultiScreenshotSlot(activeUploadKey)"
|
||
style="display: none"
|
||
@change="handleScreenshotUpload"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h2 class="section-title">押金与价格</h2>
|
||
<div class="deposit-card">
|
||
<label class="deposit-heading">
|
||
<span>押金<b>*</b></span>
|
||
<em v-if="priceConfig.deposit_hint">{{ priceConfig.deposit_hint }}</em>
|
||
</label>
|
||
<div class="deposit-input-wrap">
|
||
<input
|
||
v-model="form.depositAmountYuan"
|
||
type="number"
|
||
inputmode="decimal"
|
||
:placeholder="priceConfig.deposit_placeholder"
|
||
@blur="handleDepositBlur"
|
||
/>
|
||
<span>元</span>
|
||
</div>
|
||
<p v-if="priceConfig.deposit_placeholder" class="deposit-help">
|
||
{{ priceConfig.deposit_placeholder }}
|
||
</p>
|
||
<button class="deposit-recommend-btn" type="button" @click="useRecommendedDeposit">
|
||
推荐押金 ¥{{ formatMoney(recommendedDepositAmount) }}
|
||
</button>
|
||
</div>
|
||
<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 ? `¥${formatMoney(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="`¥${formatMoney(calculatedConsumablePrice)}`"
|
||
label="额外消耗品"
|
||
readonly
|
||
class="publish-field result-field"
|
||
/>
|
||
<van-field
|
||
:model-value="calculatedSellerPrice ? `¥${formatMoney(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-agreement-card">
|
||
<van-checkbox v-model="virtualAssetSaleAgreementChecked" icon-size="18px">
|
||
<span class="agreement-check-label">
|
||
我已阅读并同意
|
||
<button
|
||
type="button"
|
||
class="agreement-link"
|
||
@click.stop.prevent="openPublishAgreement(publishAgreements.virtual_asset_sale)"
|
||
>
|
||
《{{ publishAgreements.virtual_asset_sale.title }}》
|
||
</button>
|
||
</span>
|
||
</van-checkbox>
|
||
<van-checkbox v-model="sellerAgreementChecked" icon-size="18px">
|
||
<span class="agreement-check-label">
|
||
我已阅读并同意
|
||
<button
|
||
type="button"
|
||
class="agreement-link"
|
||
@click.stop.prevent="openPublishAgreement(publishAgreements.seller_agreement)"
|
||
>
|
||
《{{ publishAgreements.seller_agreement.title }}》
|
||
</button>
|
||
</span>
|
||
</van-checkbox>
|
||
<van-checkbox v-model="passwordAndDeviceConfirmed" icon-size="18px">
|
||
<span class="agreement-check-label"
|
||
>我已知晓:完成后须改密并清除登录设备;若遇封号,须解封后查号方可结算,未经查号即结算者,后续账号问题商行概不负责。</span
|
||
>
|
||
</van-checkbox>
|
||
</div>
|
||
|
||
<div class="publish-actions">
|
||
<van-button round class="reset-btn" :disabled="loading" @click="handleResetDraft">
|
||
{{ isEditMode ? '恢复' : '重置' }}
|
||
</van-button>
|
||
<van-button
|
||
type="primary"
|
||
round
|
||
class="submit-btn"
|
||
:loading="loading"
|
||
:disabled="!canPublishAfterAgreements"
|
||
:loading-text="submitLoadingText"
|
||
@click="handleSubmit"
|
||
>
|
||
{{ submitButtonText }}
|
||
</van-button>
|
||
</div>
|
||
</section>
|
||
|
||
<van-popup
|
||
v-model:show="agreementPopupVisible"
|
||
round
|
||
closeable
|
||
position="bottom"
|
||
lock-scroll
|
||
class="publish-agreement-popup"
|
||
>
|
||
<div class="agreement-popup-body">
|
||
<h3>{{ activeAgreementTitle }}</h3>
|
||
<div class="agreement-popup-content">{{ activeAgreementContent }}</div>
|
||
</div>
|
||
</van-popup>
|
||
|
||
<MobileBottomNav />
|
||
</main>
|
||
</template>
|
||
|
||
<style scoped src="./MobileSellerListingCreateView.css"></style>
|