Files
hfb_sys/frontend/src/features/seller/views/SellerListingCreateView.vue
T

491 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Delete, DocumentChecked, Picture, RefreshRight, UploadFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
import OptionChips from './components/OptionChips.vue'
import PublishSection from './components/PublishSection.vue'
const {
commonOnlineTimes,
dailyLossOptions,
formatNumber,
loading,
uploading,
fileInput,
activeUploadKey,
form,
quantityValues,
quantityModes,
screenshotFiles,
selectedSkins,
uploadedScreenshotCount,
requiredScreenshotCount,
serverOptions,
faceOptions,
rankOptions,
insuranceOptions,
levelOptions,
loginMethodOptions,
regionOptions,
banRecordOptions,
skinGroups,
quantityItems,
screenshotSlots,
priceConfig,
fireLevelPlaceholder,
coinMAmount,
dailyLossMAmount,
calculatedDefaultSaleRatio,
maxAcceleratedSaleRatio,
calculatedCoinBasePrice,
calculatedConsumablePrice,
calculatedSellerPrice,
calculatedRatioText,
calculatedDefaultSaleRatioText,
saleRatioRangeText,
acceleratedSaleRatioPlaceholder,
recommendedDepositAmount,
depositBreakdownItems,
publishTitle,
hasAcceleratedSaleRatioInput,
isQuantityItemDisabled,
handleSaveDraft,
handleResetDraft,
toggleSkin,
toggleRegion,
triggerUpload,
handleScreenshotUpload,
removeScreenshot,
getScreenshotPreviewURL,
handleFireLevelInput,
handleAcceleratedSaleRatioInput,
useRecommendedDeposit,
setQuantityMode,
clampAcceleratedSaleRatioInput,
useReferenceSaleRatio,
useMaxAcceleratedSaleRatio,
handleSubmit,
isScreenshotRequired,
} = usePublishForm({
draftKey: 'hfb.pc.publish.draft',
submitSuccessPath: '/seller/listings',
persistBeforeUnload: true,
async confirmReset() {
await ElMessageBox.confirm('将清空当前填写内容和本地草稿。', '重置发布内容', {
confirmButtonText: '重置',
cancelButtonText: '取消',
type: 'warning',
})
},
notifySuccess: (message) => ElMessage.success(message),
notifyWarning: (message) => ElMessage.warning(message),
notifyError: (message) => ElMessage.error(message),
})
function toggleSkinOption(value: string | number) {
toggleSkin(String(value))
}
function selectDailyLoss(value: string | number) {
form.daily_loss_m = Number(value)
}
</script>
<template>
<main class="publish-page">
<div class="publish-layout">
<section class="form-column">
<PublishSection title="基础资料" :description="publishTitle">
<div class="field-row panel-field-row">
<label>区服<span>*</span></label>
<OptionChips :options="serverOptions" :model-value="form.server_region" @select="form.server_region = String($event)" />
</div>
<div v-if="faceOptions.length" class="field-row panel-field-row">
<label>是否本人人脸</label>
<OptionChips :options="faceOptions" :model-value="form.face_owner" @select="form.face_owner = String($event)" />
</div>
<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" />
</label>
<label class="input-block">
<span>绝密KD</span>
<el-input-number v-model="form.secret_kd" :min="0" :controls="false" :step="0.1" placeholder="填写绝密KD" />
</label>
<label class="input-block">
<span>烽火等级<b>*</b></span>
<el-input-number
:model-value="form.fire_level"
:min="0"
:controls="false"
:placeholder="fireLevelPlaceholder"
@update:model-value="handleFireLevelInput"
/>
</label>
</div>
<div class="field-row panel-field-row">
<label>段位<span>*</span></label>
<OptionChips :options="rankOptions" :model-value="form.rank_level" @select="form.rank_level = String($event)" />
</div>
<div class="field-row panel-field-row">
<label>赛季保险<span>*</span></label>
<OptionChips
:options="insuranceOptions"
:model-value="form.season_insurance"
@select="form.season_insurance = String($event)"
/>
</div>
<div class="level-panel">
<div class="level-row">
<strong><span>*</span>体力</strong>
<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="form.stamina_level = opt"
>
{{ opt }}
</button>
</div>
</div>
<div class="level-row">
<strong><span>*</span>负重</strong>
<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="form.load_level = opt"
>
{{ opt }}
</button>
</div>
</div>
</div>
</PublishSection>
<PublishSection v-if="quantityItems.length" title="额外消耗品" description="收费项会计入发布价格">
<div class="quantity-table">
<div class="quantity-header">
<span>物资</span>
<span>单价</span>
<span>数量<b>*</b></span>
<span>计费</span>
<span>状态</span>
</div>
<div
v-for="item in quantityItems"
:key="item.key"
class="quantity-item"
:class="{ disabled: isQuantityItemDisabled(item) }"
>
<div class="quantity-meta">
<strong :title="item.placeholder || item.label"><span>*</span>{{ item.label }}</strong>
<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"
:controls="false"
:disabled="isQuantityItemDisabled(item)"
/>
<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>
<span class="quantity-status">
{{ isQuantityItemDisabled(item) ? '3*3 已包含' : '可填写' }}
</span>
</div>
</div>
</PublishSection>
<PublishSection title="皮肤与交接">
<div v-if="skinGroups.length" class="skin-groups">
<div
v-for="group in skinGroups"
:key="group.key"
class="skin-group"
>
<div class="skin-title">
<strong>{{ group.title }}</strong>
</div>
<OptionChips :options="group.options" :active-values="selectedSkins" @select="toggleSkinOption" />
</div>
</div>
<div class="field-row login-method-row">
<label>上号方式</label>
<OptionChips
:options="loginMethodOptions"
:model-value="form.login_method"
@select="form.login_method = String($event)"
/>
<p class="field-hint">优先推荐账密登录出售速度通常更快请结合账号安全情况自行选择</p>
</div>
<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"
/>
<OptionChips
:options="commonOnlineTimes"
:model-value="form.online_start"
key-prefix="start-"
@select="form.online_start = String($event)"
/>
</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"
/>
<OptionChips
:options="commonOnlineTimes"
:model-value="form.online_end"
key-prefix="end-"
@select="form.online_end = String($event)"
/>
</div>
</div>
</div>
<p class="field-hint">请填写能稳定联系上您的时间便于扫码冻结人脸和订单交接</p>
<div v-if="banRecordOptions.length" class="field-row">
<label>封禁记录<span>*</span></label>
<OptionChips :options="banRecordOptions" :model-value="form.ban_record" @select="form.ban_record = String($event)" />
</div>
<div v-if="regionOptions.length" class="region-panel login-region-panel">
<div class="region-title">
<strong>常用登录地区</strong>
<span>已选 {{ form.common_regions.length }}</span>
</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>
</PublishSection>
<PublishSection title="截图材料" :description="`${uploadedScreenshotCount}/${screenshotSlots.length} 已上传`">
<div class="upload-grid">
<div v-for="slot in screenshotSlots" :key="slot.key" class="upload-item">
<div class="upload-copy">
<strong>{{ 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)">
<el-icon><Delete /></el-icon>
</button>
</div>
<button v-else type="button" class="upload-add" :disabled="uploading" @click="triggerUpload(slot.key)">
<el-icon><Picture /></el-icon>
<span>{{ uploading && activeUploadKey === slot.key ? '上传中' : '上传' }}</span>
</button>
</div>
</div>
<input
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
class="hidden-file"
@change="handleScreenshotUpload"
/>
</PublishSection>
<PublishSection title="押金与价格" description="系统按资料自动计算售价">
<div class="compact-grid two">
<label class="input-block">
<span>押金<b>*</b></span>
<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>
<OptionChips
:options="dailyLossOptions"
:model-value="dailyLossMAmount"
compact
suffix="M/天"
@select="selectDailyLoss"
/>
</label>
</div>
<div class="ratio-panel">
<div class="ratio-reference">
<span>参考比例</span>
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
</div>
<p>{{ 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: form.accelerated_sale_ratio === maxAcceleratedSaleRatio && maxAcceleratedSaleRatio > 0 }"
:disabled="maxAcceleratedSaleRatio <= 0"
@click="useMaxAcceleratedSaleRatio"
>
<strong>最高比例 (加速)</strong>
<span>{{ maxAcceleratedSaleRatio > 0 ? `最高 1元=${formatNumber(maxAcceleratedSaleRatio)}万` : '自动计算' }}</span>
</button>
<div
class="ratio-custom-card"
:class="{ active: hasAcceleratedSaleRatioInput() && form.accelerated_sale_ratio !== maxAcceleratedSaleRatio }"
>
<strong>自定比例</strong>
<el-input-number
:model-value="form.accelerated_sale_ratio"
:min="0"
:controls="false"
:placeholder="acceleratedSaleRatioPlaceholder"
@update:model-value="handleAcceleratedSaleRatioInput"
@blur="clampAcceleratedSaleRatioInput"
/>
</div>
</div>
</div>
<div class="price-grid">
<div class="price-cell">
<span>卖家比例</span>
<strong>{{ calculatedRatioText }}</strong>
</div>
<div class="price-cell">
<span>纯币基础价</span>
<strong>{{ calculatedCoinBasePrice ? ${calculatedCoinBasePrice}` : '--' }}</strong>
</div>
<div class="price-cell">
<span>额外消耗品</span>
<strong>¥{{ calculatedConsumablePrice }}</strong>
</div>
<div class="price-cell accent">
<span>发布价格</span>
<strong>{{ calculatedSellerPrice ? ${calculatedSellerPrice}` : '--' }}</strong>
</div>
</div>
<label class="input-block remark">
<span>备注</span>
<el-input
v-model="form.remark"
type="textarea"
:rows="4"
placeholder="如有不可使用的物资,请在此备注,并在买家下单后主动提醒。"
/>
</label>
</PublishSection>
</section>
<aside class="summary-column">
<div class="summary-panel">
<div class="summary-main">
<span>卖家发布价格</span>
<strong>{{ calculatedSellerPrice ? ${calculatedSellerPrice}` : '--' }}</strong>
</div>
<div class="summary-list">
<div>
<span>哈夫币</span>
<strong>{{ coinMAmount || '--' }}M</strong>
</div>
<div>
<span>押金</span>
<strong>{{ form.deposit_amount === '' ? '--' : ${form.deposit_amount}` }}</strong>
</div>
<div>
<span>推荐押金</span>
<strong>¥{{ recommendedDepositAmount }}</strong>
</div>
<div>
<span>截图材料</span>
<strong>{{ uploadedScreenshotCount }}/{{ requiredScreenshotCount }}</strong>
</div>
</div>
<div class="summary-actions">
<el-button :icon="UploadFilled" class="btn-publish" 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>
</aside>
</div>
</main>
</template>
<style scoped src="./SellerListingCreateView.css"></style>