修复卖家发布页样式和全天时间选择

This commit is contained in:
yml2213
2026-06-04 21:45:07 +08:00
parent 4ebf7f75fe
commit c1b3dba83d
10 changed files with 2045 additions and 216 deletions
@@ -187,6 +187,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
const draft = readPublishDraft(options.draftKey)
if (!draft) return
Object.assign(form, draft.form)
normalizeOnlineTime()
clearRecord(quantityValues)
clearRecord(quantityModes)
clearRecord(screenshotFiles)
@@ -212,6 +213,13 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
}
function normalizeOnlineTime() {
if (form.online_start === '全天' || form.online_end === '全天') {
form.online_start = '00:00'
form.online_end = '23:59'
}
}
async function handleResetDraft() {
try {
await options.confirmReset?.()
@@ -370,6 +378,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
async function handleSubmit() {
normalizeOnlineTime()
const error = validateForm()
if (error) {
options.notifyWarning(error)
@@ -0,0 +1,686 @@
.mobile-publish {
width: 100%;
min-width: 0;
min-height: 100dvh;
background: #f4f6f8;
padding-bottom: calc(56px + env(safe-area-inset-bottom));
overflow-x: hidden;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
background: #fff;
border-bottom: 1px solid #eceff3;
}
.page-header h1 {
flex: 1;
margin: 0;
font-size: 17px;
font-weight: 700;
text-align: center;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #25282d;
cursor: pointer;
}
.header-spacer {
width: 36px;
}
.form-body {
padding: 12px 14px 24px;
}
.form-section {
padding: 14px;
margin-bottom: 12px;
background: #fff;
border: 1px solid #edf0f4;
border-radius: 8px;
}
.section-title {
margin: 0 0 10px;
font-size: 14px;
font-weight: 700;
color: #171a1f;
}
.field-hint {
margin: -2px 0 8px;
color: #858c96;
font-size: 11px;
line-height: 1.5;
}
.publish-field {
margin-bottom: 8px;
border-radius: 8px;
background: #f8fafc;
overflow: visible;
}
.publish-field :deep(.van-field__label) {
width: 88px;
color: #30343a;
font-size: 13px;
font-weight: 600;
}
.publish-field :deep(.van-field__control) {
color: #20242a;
font-size: 14px;
}
.hint-label,
.backend-hint-target.has-hint,
.upload-title.has-hint {
position: relative;
display: inline-flex;
align-items: center;
max-width: 100%;
line-height: 1.35;
cursor: help;
outline: none;
}
.hint-label::before,
.backend-hint-target.has-hint::before,
.upload-title.has-hint::before,
.hint-label::after,
.backend-hint-target.has-hint::after,
.upload-title.has-hint::after {
position: absolute;
z-index: 20;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.16s ease, visibility 0.16s ease;
}
.hint-label::before,
.backend-hint-target.has-hint::before,
.upload-title.has-hint::before {
content: "";
top: 50%;
left: calc(100% + 2px);
transform: translateY(-50%);
border: 6px solid transparent;
border-right-color: rgba(32, 36, 42, 0.96);
}
.hint-label::after,
.backend-hint-target.has-hint::after,
.upload-title.has-hint::after {
content: attr(data-hint);
top: 50%;
left: calc(100% + 14px);
width: min(242px, calc(100vw - 136px));
max-height: 156px;
overflow: auto;
transform: translateY(-50%);
padding: 8px 10px;
border-radius: 8px;
background: rgba(32, 36, 42, 0.96);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
color: #fff;
font-size: 12px;
font-weight: 500;
line-height: 1.45;
white-space: normal;
}
.hint-label:hover::before,
.hint-label:focus-visible::before,
.backend-hint-target.has-hint:hover::before,
.backend-hint-target.has-hint:focus-visible::before,
.upload-title.has-hint:hover::before,
.upload-title.has-hint:focus-visible::before,
.hint-label:hover::after,
.hint-label:focus-visible::after,
.backend-hint-target.has-hint:hover::after,
.backend-hint-target.has-hint:focus-visible::after,
.upload-title.has-hint:hover::after,
.upload-title.has-hint:focus-visible::after {
opacity: 1;
visibility: visible;
}
.radio-group,
.multi-chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.radio-group.small {
gap: 6px;
}
.radio-btn {
min-height: 28px;
padding: 5px 12px;
border: 1px solid #d7dce3;
border-radius: 14px;
background: #fff;
color: #5c6470;
font-size: 12px;
font-weight: 600;
line-height: 1;
}
.radio-btn.active {
border-color: #ff6a00;
background: #ff6a00;
color: #fff;
}
.time-row,
.result-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
.time-field-group {
display: grid;
gap: 6px;
}
.time-field-group .publish-field {
margin-bottom: 0;
}
.time-preset-chips {
padding: 0 2px 4px;
}
.level-panel {
display: grid;
gap: 8px;
}
.level-row {
display: grid;
grid-template-columns: 58px minmax(0, 1fr);
gap: 8px;
align-items: center;
padding: 10px;
border-radius: 8px;
background: #f8fafc;
}
.level-label {
color: #30343a;
font-size: 13px;
font-weight: 700;
white-space: nowrap;
}
.level-label span {
margin-right: 2px;
color: #ee0a24;
}
.level-options {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 6px;
}
.level-btn {
min-width: 0;
height: 30px;
padding: 0;
border: 1px solid #d7dce3;
border-radius: 15px;
background: #fff;
color: #5c6470;
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.level-btn.active {
border-color: #ff6a00;
background: #ff6a00;
color: #fff;
}
.quantity-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 92px;
gap: 8px;
align-items: stretch;
margin-bottom: 8px;
}
.quantity-row.disabled {
opacity: 0.62;
}
.quantity-card {
display: grid;
grid-template-columns: minmax(0, 1fr) 72px;
gap: 10px;
align-items: center;
min-height: 72px;
padding: 10px 12px;
border-radius: 8px;
background: #f8fafc;
}
.quantity-meta {
display: grid;
min-width: 0;
gap: 5px;
}
.quantity-meta strong {
overflow: hidden;
color: #30343a;
font-size: 13px;
font-weight: 700;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity-meta span {
margin-right: 2px;
color: #ee0a24;
}
.quantity-meta small {
color: #858c96;
font-size: 11px;
font-weight: 600;
}
.quantity-input {
width: 100%;
min-width: 0;
height: 36px;
padding: 0 8px;
border: 1px solid #e0e5ec;
border-radius: 8px;
background: #fff;
color: #20242a;
font-size: 16px;
font-weight: 700;
text-align: center;
outline: none;
}
.quantity-input:focus {
border-color: #ff6a00;
}
.quantity-input:disabled {
cursor: not-allowed;
}
.mode-toggle {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
min-height: 72px;
}
.mode-toggle button {
border: 1px solid #d7dce3;
border-radius: 8px;
background: #fff;
color: #606873;
font-size: 12px;
font-weight: 700;
}
.mode-toggle button.active {
border-color: #ff6a00;
background: #fff3ea;
color: #e65f00;
}
.mode-toggle button:disabled {
cursor: not-allowed;
}
.skin-groups {
display: grid;
gap: 10px;
margin-bottom: 8px;
}
.skin-group {
padding: 10px;
border-radius: 8px;
background: #f8fafc;
}
.skin-group-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
color: #30343a;
font-size: 13px;
font-weight: 700;
}
.skin-group-title small {
color: #858c96;
font-size: 11px;
font-weight: 600;
}
.region-panel {
display: grid;
gap: 8px;
padding: 10px;
border-radius: 8px;
background: #f8fafc;
}
.region-title {
display: flex;
align-items: center;
justify-content: space-between;
color: #30343a;
font-size: 13px;
font-weight: 700;
}
.region-title small {
color: #858c96;
font-size: 11px;
font-weight: 600;
}
.region-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
}
.region-btn {
min-width: 0;
height: 28px;
padding: 0 2px;
border: 1px solid #d7dce3;
border-radius: 14px;
background: #fff;
color: #5c6470;
font-size: 11px;
font-weight: 700;
}
.region-btn.active {
border-color: #ff6a00;
background: #ff6a00;
color: #fff;
}
.upload-list {
display: grid;
gap: 10px;
}
.upload-line {
display: grid;
grid-template-columns: minmax(0, 1fr) 86px;
gap: 10px;
align-items: center;
min-height: 76px;
padding: 10px;
border: 1px solid #edf0f4;
border-radius: 8px;
background: #f8fafc;
}
.upload-meta {
display: grid;
gap: 4px;
}
.upload-meta strong {
color: #242830;
font-size: 13px;
}
.upload-meta span {
color: #f04438;
}
.upload-meta small {
color: #858c96;
font-size: 11px;
line-height: 1.4;
}
.upload-add,
.upload-preview {
width: 76px;
height: 76px;
border-radius: 8px;
}
.upload-add {
display: grid;
place-items: center;
border: 1px dashed #c7cdd6;
background: #fff;
color: #7b8490;
font-size: 11px;
}
.upload-add:disabled {
opacity: 0.5;
}
.upload-preview {
position: relative;
overflow: hidden;
background: #e8ecf1;
}
.upload-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-preview button {
position: absolute;
right: 0;
bottom: 0;
height: 24px;
padding: 0 8px;
border: none;
border-radius: 8px 0 0;
background: rgba(0, 0, 0, 0.62);
color: #fff;
font-size: 11px;
}
.result-field :deep(.van-field__control) {
color: #ff6a00;
font-weight: 800;
}
.ratio-panel {
display: grid;
gap: 10px;
margin-bottom: 8px;
}
.deposit-recommend-btn {
min-height: 32px;
margin: -2px 0 10px 88px;
padding: 0 12px;
border: 1px solid #ffba86;
border-radius: 16px;
background: #fff7f0;
color: #e65f00;
font-size: 12px;
font-weight: 800;
}
.ratio-reference {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 48px;
padding: 10px 12px;
border-radius: 8px;
background: #f8fafc;
}
.ratio-reference span {
color: #30343a;
font-size: 13px;
font-weight: 700;
}
.ratio-reference strong {
color: #ff6a00;
font-size: 18px;
font-weight: 900;
line-height: 1;
}
.ratio-range {
margin: 0;
color: #858c96;
font-size: 11px;
font-weight: 600;
line-height: 1.5;
}
.ratio-mode-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.ratio-mode-btn {
display: grid;
gap: 5px;
min-width: 0;
min-height: 74px;
padding: 12px 8px;
border: 1px solid #d7dce3;
border-radius: 8px;
background: #fff;
color: #30343a;
text-align: center;
}
.ratio-mode-btn strong,
.ratio-mode-btn span {
min-width: 0;
overflow-wrap: anywhere;
}
.ratio-mode-btn strong {
font-size: 14px;
font-weight: 800;
}
.ratio-mode-btn span {
color: #858c96;
font-size: 12px;
font-weight: 700;
line-height: 1.25;
}
.ratio-mode-btn.active {
border-color: #ff6a00;
background: #fff7f0;
color: #ff6a00;
}
.ratio-mode-btn.active span {
color: #ff6a00;
}
.ratio-mode-btn:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.ratio-input-field {
margin-bottom: 0;
}
.ratio-input-hint {
margin: -2px 0 0;
}
.total-price-field :deep(.van-field__control) {
font-size: 16px;
}
.price-breakdown-hint {
margin: -2px 0 10px;
color: #858c96;
font-size: 11px;
line-height: 1.5;
}
.publish-actions {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
gap: 10px;
margin-top: 8px;
}
.reset-btn,
.submit-btn {
height: 44px;
width: 100%;
border: none !important;
font-size: 15px;
font-weight: 700;
}
.reset-btn {
border: 1px solid #d7dce3 !important;
background: #fff !important;
color: #4b5563 !important;
}
.submit-btn {
background: #ff6a00 !important;
}
@media (min-width: 430px) {
.time-row {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 370px) {
.level-options,
.region-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@@ -0,0 +1,666 @@
<script setup lang="ts">
import { showDialog, showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { usePublishForm } from '@/features/seller/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,
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)
}
function isAllDayOnline() {
return form.online_start === '00:00' && form.online_end === '23:59'
}
function isOnlineStartPresetActive(value: string) {
return !isAllDayOnline() && form.online_start === value
}
function isOnlineEndPresetActive(value: string) {
return !isAllDayOnline() && form.online_end === value
}
function selectAllDayOnline() {
form.online_start = '00:00'
form.online_end = '23:59'
}
function selectOnlineStart(value: string) {
form.online_start = value
}
function selectOnlineEnd(value: string) {
form.online_end = 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">
<div class="time-field-group">
<van-field
v-model="form.online_start"
label="在线开始"
type="time"
class="publish-field time-field"
/>
<div class="radio-group small time-preset-chips">
<button
type="button"
class="radio-btn"
:class="{ active: isAllDayOnline() }"
@click="selectAllDayOnline"
>
全天
</button>
<button
v-for="opt in commonOnlineTimes"
:key="`start-${opt}`"
type="button"
class="radio-btn"
:class="{ active: isOnlineStartPresetActive(opt) }"
@click="selectOnlineStart(opt)"
>
{{ opt }}
</button>
</div>
</div>
<div class="time-field-group">
<van-field
v-model="form.online_end"
label="在线结束"
type="time"
class="publish-field time-field"
/>
<div class="radio-group small time-preset-chips">
<button
type="button"
class="radio-btn"
:class="{ active: isAllDayOnline() }"
@click="selectAllDayOnline"
>
全天
</button>
<button
v-for="opt in commonOnlineTimes"
:key="`end-${opt}`"
type="button"
class="radio-btn"
:class="{ active: isOnlineEndPresetActive(opt) }"
@click="selectOnlineEnd(opt)"
>
{{ opt }}
</button>
</div>
</div>
</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>
@@ -1,182 +1,401 @@
/* 发布页局部设计变量 / Publish page local design tokens */
.publish-page {
max-width: 1180px;
margin: 0 auto;
--color-bg-gray: #f7f9fc;
--color-bg-orange: #fff7f0;
--color-orange-primary: #ff6a00;
--color-orange-dark: #e65f00;
--color-text-main: #303743;
--color-text-sub: #7f8895;
--color-text-dark: #20242a;
--color-border: #dfe5ee;
--color-border-light: #edf0f4;
--color-danger: #f04438;
--radius-8: 8px;
min-width: 0;
padding: 24px 20px 40px;
color: var(--color-text-dark);
}
.publish-page :deep(.el-input-number),
.publish-page :deep(.el-input),
.publish-page :deep(.el-date-editor) {
width: 100%;
}
.publish-page :deep(.el-input__wrapper),
.publish-page :deep(.el-textarea__inner) {
border-radius: var(--radius-8);
box-shadow: 0 0 0 1px var(--color-border) inset;
transition: box-shadow 0.2s ease, background-color 0.2s ease;
}
.publish-page :deep(.el-input__wrapper:hover),
.publish-page :deep(.el-textarea__inner:hover) {
box-shadow: 0 0 0 1px var(--color-orange-primary) inset !important;
}
.publish-page :deep(.el-input__wrapper.is-focus),
.publish-page :deep(.el-textarea__inner:focus) {
box-shadow: 0 0 0 1px var(--color-orange-primary) inset, 0 0 8px rgba(255, 106, 0, 0.15) !important;
}
.publish-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 20px;
grid-template-columns: minmax(760px, 1fr) 300px;
align-items: start;
gap: 24px;
max-width: 1580px;
margin: 0 auto;
overflow: visible;
}
.form-column,
.summary-column {
.form-column {
display: grid;
min-width: 0;
gap: 18px;
}
.form-column :deep(.form-section) {
min-width: 0;
}
.summary-column {
align-self: start;
position: sticky;
top: 84px;
}
.field-row,
.input-block {
display: grid;
gap: 8px;
}
.field-row + .field-row,
.compact-grid + .field-row,
.level-panel,
.time-preset-panel,
.panel-field-row,
.panel-input-grid,
.level-row,
.login-method-row,
.quantity-item,
.skin-group,
.region-panel,
.ratio-panel,
.price-grid,
.remark {
margin-top: 16px;
.time-preset-row,
.price-cell {
padding: 12px;
border-radius: var(--radius-8);
background: var(--color-bg-gray);
}
.field-row label,
.field-row {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 14px;
align-items: center;
margin-top: 12px;
}
.field-row > label,
.input-block > span,
.quantity-meta strong {
color: #0f172a;
font-size: 14px;
font-weight: 700;
color: var(--color-text-main);
font-size: 13px;
font-weight: 800;
line-height: 22px;
}
.field-row span,
.field-row label span,
.input-block b,
.quantity-meta span,
.upload-copy span {
color: #ef4444;
color: var(--color-danger);
}
.field-hint {
margin: 6px 0 0;
color: #64748b;
font-size: 13px;
line-height: 1.6;
grid-column: 2;
margin: 0;
color: var(--color-text-sub);
font-size: 12px;
line-height: 1.55;
}
.login-method-row .field-hint {
margin-top: -2px;
}
.compact-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
gap: 16px;
margin-top: 16px;
}
.compact-grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 14px 16px;
border-radius: var(--radius-8);
background: var(--color-bg-gray);
}
.panel-input-grid {
align-items: center;
}
.panel-input-grid .input-block {
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 10px;
}
.panel-input-grid :deep(.el-input-number),
.panel-input-grid :deep(.el-input) {
max-width: 140px;
}
.input-block {
display: grid;
min-width: 0;
gap: 8px;
}
.input-block > span {
white-space: nowrap;
}
.level-panel,
.time-preset-panel,
.region-panel,
.ratio-panel,
.summary-panel {
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
}
.level-row,
.time-preset-row,
.region-title,
.skin-title {
.skin-groups {
display: grid;
gap: 10px;
margin-top: 16px;
}
.level-row + .level-row,
.time-preset-row + .time-preset-row,
.skin-group + .skin-group {
margin-top: 14px;
.level-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 10px 12px;
}
.level-options,
.region-grid,
.mode-toggle,
.ratio-mode-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
.level-row strong {
font-size: 13px;
}
.level-row strong span {
color: var(--color-danger);
}
.level-options {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 9px;
}
.level-btn,
.region-btn,
.mode-toggle button,
.ratio-mode-btn,
.recommend-button,
.upload-add {
border: 1px solid #dbe3ee;
border-radius: 8px;
.ratio-mode-btn {
border: 1px solid #d6dce5;
background: #fff;
color: #334155;
color: #596474;
font-weight: 800;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease;
}
.level-btn,
.region-btn,
.mode-toggle button {
min-height: 32px;
padding: 0 12px;
font-weight: 600;
.region-btn {
min-width: 0;
}
.level-btn {
height: 30px;
padding: 0 8px;
border-radius: 15px;
}
.region-btn {
height: 32px;
padding: 0 10px;
border-radius: 16px;
}
.level-btn.active,
.region-btn.active,
.mode-toggle button.active,
.ratio-mode-btn.active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
.region-btn.active {
border-color: var(--color-orange-primary);
background: var(--color-orange-primary);
color: #fff;
}
.quantity-table {
display: grid;
gap: 10px;
gap: 8px;
}
.quantity-header,
.quantity-item {
display: grid;
grid-template-columns: minmax(160px, 1fr) 100px 120px 116px 80px;
gap: 12px;
grid-template-columns: minmax(180px, 3fr) minmax(80px, 1fr) minmax(100px, 1.2fr) minmax(140px, 1.5fr) minmax(80px, 1fr);
gap: 10px;
align-items: center;
}
.quantity-header {
color: #64748b;
min-height: 30px;
padding: 0 12px;
color: #7b8492;
font-size: 12px;
font-weight: 800;
}
.quantity-header b {
margin-left: 2px;
color: var(--color-danger);
}
.quantity-item {
min-height: 58px;
}
.quantity-item.disabled {
opacity: 0.62;
}
.quantity-item > :deep(.el-input-number) {
width: 100% !important;
min-width: 0 !important;
overflow: hidden;
}
.quantity-meta {
display: grid;
min-width: 0;
gap: 4px;
}
.quantity-meta strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity-meta span {
margin-right: 2px;
}
.quantity-meta small,
.quantity-price,
.quantity-status {
color: var(--color-text-sub);
font-size: 12px;
font-weight: 700;
}
.quantity-item {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
.quantity-price,
.quantity-status {
white-space: nowrap;
}
.quantity-item.disabled {
opacity: 0.65;
}
.quantity-meta small,
.quantity-status,
.quantity-price {
color: #64748b;
font-size: 12px;
}
.time-preset-content,
.deposit-control {
.mode-toggle {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 10px;
grid-template-columns: 1fr 1fr;
gap: 4px;
min-width: 0;
}
.mode-toggle button {
height: 30px;
min-width: 0;
padding: 0 6px;
border-radius: var(--radius-8);
font-size: 12px;
white-space: nowrap;
}
.mode-toggle button.active,
.ratio-mode-btn.active {
border-color: var(--color-orange-primary);
background: var(--color-bg-orange);
color: var(--color-orange-dark);
}
.mode-toggle button:disabled,
.ratio-mode-btn:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.skin-group {
border: none;
}
.skin-title,
.region-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
}
.skin-title {
min-height: 34px;
margin-bottom: 8px;
}
.region-title span {
color: var(--color-text-sub);
font-size: 12px;
font-weight: 700;
}
.time-preset-row {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 12px;
align-items: start;
}
.time-preset-row strong {
color: var(--color-text-main);
font-size: 13px;
font-weight: 800;
line-height: 32px;
}
.time-preset-content {
display: grid;
gap: 8px;
min-width: 0;
}
.time-preset-actions {
display: flex;
flex-wrap: wrap;
gap: 7px;
min-width: 0;
}
.time-chip {
min-height: 30px;
padding: 0 12px;
border: 1px solid #d6dce5;
border-radius: 15px;
background: #fff;
color: #596474;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
}
.time-chip.active {
border-color: var(--color-orange-primary);
background: var(--color-orange-primary);
color: #fff;
}
.time-input {
width: 118px;
}
.login-region-panel {
margin-top: 18px;
}
.region-grid {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 8px;
}
.upload-grid {
@@ -187,34 +406,63 @@
.upload-item {
display: grid;
gap: 10px;
padding: 14px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
grid-template-columns: minmax(0, 1fr) 96px;
gap: 12px;
align-items: center;
min-height: 112px;
padding: 12px;
border: 1px solid var(--color-border-light);
border-radius: var(--radius-8);
background: var(--color-bg-gray);
}
.upload-copy {
display: grid;
gap: 4px;
min-width: 0;
gap: 6px;
}
.upload-copy strong {
color: #0f172a;
color: #232a35;
font-size: 14px;
}
.upload-copy small,
.deposit-breakdown {
color: #64748b;
.upload-copy small {
color: var(--color-text-sub);
font-size: 12px;
font-weight: 700;
line-height: 1.45;
}
.upload-add,
.upload-preview {
width: 96px;
height: 88px;
border-radius: var(--radius-8);
}
.upload-add {
display: grid;
place-items: center;
align-content: center;
gap: 6px;
border: 1px dashed #c7ceda;
background: #fff;
color: #727d8d;
font-size: 12px;
font-weight: 800;
cursor: pointer;
}
.upload-add:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.upload-preview {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: 8px;
background: #e2e8f0;
background: #e8edf4;
}
.upload-preview img {
@@ -225,119 +473,239 @@
.upload-preview button {
position: absolute;
top: 8px;
right: 8px;
}
.upload-add {
right: 6px;
bottom: 6px;
display: grid;
min-height: 112px;
width: 28px;
height: 28px;
place-items: center;
align-content: center;
gap: 6px;
color: #2563eb;
border: none;
border-radius: var(--radius-8);
background: rgba(17, 24, 39, 0.72);
color: #fff;
cursor: pointer;
}
.hidden-file {
display: none;
}
.deposit-control {
display: flex;
gap: 8px;
align-items: center;
}
.deposit-control :deep(.el-input-number) {
width: 140px !important;
flex: 0 0 140px;
}
.recommend-button {
flex: 0 0 auto;
height: 32px;
padding: 0 10px;
color: #2563eb;
font-weight: 700;
padding: 0 12px;
border: 1px solid #ffba86;
border-radius: var(--radius-8);
background: var(--color-bg-orange);
color: var(--color-orange-dark);
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.recommend-button:hover {
border-color: var(--color-orange-primary);
background: #ffe3cd;
}
.deposit-breakdown {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 6px;
}
.deposit-breakdown span {
min-height: 24px;
padding: 4px 8px;
border-radius: 12px;
background: var(--color-bg-orange);
color: var(--color-orange-dark);
font-size: 12px;
font-weight: 800;
}
.ratio-panel {
margin-top: 14px;
}
.ratio-panel p {
margin: 8px 0 14px;
color: var(--color-text-sub);
font-size: 12px;
line-height: 1.5;
}
.ratio-reference {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 52px;
padding: 12px;
border-radius: var(--radius-8);
background: #fff;
}
.ratio-reference span,
.price-cell span,
.summary-main span,
.summary-list span {
color: #64748b;
.ratio-reference span {
color: var(--color-text-main);
font-size: 13px;
font-weight: 800;
}
.ratio-reference strong {
color: #2563eb;
color: var(--color-orange-primary);
font-size: 22px;
}
.ratio-panel p {
margin: 8px 0 14px;
color: #64748b;
font-size: 13px;
font-weight: 900;
}
.ratio-mode-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.ratio-mode-btn,
.ratio-custom-card {
display: grid;
gap: 6px;
min-height: 72px;
padding: 12px;
border: 1px solid #dbe3ee;
border-radius: 8px;
background: #fff;
border-radius: var(--radius-8);
text-align: left;
}
.ratio-mode-btn {
display: grid;
gap: 6px;
}
.ratio-mode-btn span {
color: #64748b;
color: var(--color-text-sub);
font-size: 12px;
}
.ratio-custom-card {
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
border: 1px solid #d6dce5;
background: #fff;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
}
.ratio-custom-card strong {
color: #596474;
font-size: 13px;
font-weight: 800;
}
.ratio-custom-card :deep(.el-input-number) {
width: 100% !important;
}
.ratio-custom-card :deep(.el-input__wrapper) {
background-color: #fff !important;
}
.ratio-custom-card.active {
border-color: var(--color-orange-primary);
background: var(--color-bg-orange);
color: var(--color-orange-primary);
}
.ratio-custom-card.active strong {
color: var(--color-orange-primary);
}
.price-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-top: 14px;
}
.price-cell {
display: grid;
gap: 6px;
padding: 12px;
border-radius: 8px;
background: #f8fafc;
min-height: 82px;
padding: 14px;
border-left: 4px solid #ccd5e0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.price-cell strong,
.summary-list strong {
color: #0f172a;
.price-cell:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
}
.price-cell.accent strong,
.summary-main strong {
color: #2563eb;
.price-cell span,
.summary-main span,
.summary-list span {
color: #6f7a89;
font-size: 12px;
font-weight: 800;
}
.price-cell strong {
overflow-wrap: anywhere;
color: var(--color-orange-primary);
font-size: 21px;
font-weight: 900;
}
.price-cell.accent {
border-left-color: var(--color-orange-primary);
background: var(--color-bg-orange);
}
.remark {
margin-top: 14px;
}
.summary-column {
--summary-sticky-top: clamp(148px, 22vh, 240px);
min-width: 0;
position: sticky;
top: var(--summary-sticky-top);
align-self: start;
max-height: calc(100vh - var(--summary-sticky-top) - 24px);
}
.summary-panel {
padding: 16px;
max-height: inherit;
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid #e6ebf2;
border-radius: var(--radius-8);
background: #fff;
box-shadow: 0 12px 34px rgba(20, 34, 56, 0.05);
}
.summary-main {
display: grid;
gap: 6px;
padding-bottom: 14px;
border-bottom: 1px solid #e2e8f0;
gap: 8px;
padding: 16px;
border-radius: var(--radius-8);
background: var(--color-bg-orange);
}
.summary-main strong {
font-size: 28px;
color: var(--color-orange-primary);
font-size: 34px;
font-weight: 900;
line-height: 1;
}
.summary-list {
@@ -349,7 +717,16 @@
.summary-list div {
display: flex;
justify-content: space-between;
gap: 10px;
gap: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #eef2f6;
}
.summary-list strong {
overflow-wrap: anywhere;
color: var(--color-text-dark);
font-size: 13px;
text-align: right;
}
.summary-actions {
@@ -357,18 +734,46 @@
gap: 10px;
}
.summary-actions .el-button {
.summary-actions :deep(.el-button) {
width: 100%;
margin-left: 0;
border-radius: var(--radius-8);
}
@media (max-width: 960px) {
.summary-actions :deep(.el-button.btn-publish) {
border-color: var(--color-orange-primary);
background-color: var(--color-orange-primary);
color: #fff;
font-weight: 800;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.summary-actions :deep(.el-button.btn-publish:hover) {
border-color: var(--color-orange-dark);
background-color: var(--color-orange-dark);
}
@media (max-width: 1120px) {
.publish-layout {
grid-template-columns: 1fr;
max-width: 920px;
}
.summary-column {
position: static;
display: none;
}
}
@media (max-width: 860px) {
.field-row,
.time-preset-row,
.quantity-header,
.quantity-item {
grid-template-columns: 1fr;
}
.field-hint {
grid-column: auto;
}
.compact-grid,
@@ -376,16 +781,42 @@
.upload-grid,
.price-grid,
.ratio-mode-grid,
.time-preset-content,
.deposit-control {
grid-template-columns: 1fr;
}
.panel-input-grid .input-block {
grid-template-columns: 1fr;
}
.panel-input-grid :deep(.el-input-number),
.panel-input-grid :deep(.el-input) {
max-width: none;
}
.quantity-header {
display: none;
}
.quantity-item {
.upload-item {
grid-template-columns: 1fr;
}
.upload-add,
.upload-preview {
width: 100%;
}
.deposit-control {
display: grid;
}
.deposit-control :deep(.el-input-number) {
width: 100% !important;
flex: 1;
}
.region-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@@ -90,6 +90,23 @@ function toggleSkinOption(value: string | number) {
function selectDailyLoss(value: string | number) {
form.daily_loss_m = Number(value)
}
function isAllDayOnline() {
return form.online_start === '00:00' && form.online_end === '23:59'
}
function selectAllDayOnline() {
form.online_start = '00:00'
form.online_end = '23:59'
}
function selectOnlineStart(value: string | number) {
form.online_start = String(value)
}
function selectOnlineEnd(value: string | number) {
form.online_end = String(value)
}
</script>
<template>
@@ -262,12 +279,17 @@ function selectDailyLoss(value: string | number) {
placeholder="开始时间"
class="time-input"
/>
<OptionChips
:options="commonOnlineTimes"
:model-value="form.online_start"
key-prefix="start-"
@select="form.online_start = String($event)"
/>
<div class="time-preset-actions">
<button type="button" class="time-chip" :class="{ active: isAllDayOnline() }" @click="selectAllDayOnline">
全天
</button>
<OptionChips
:options="commonOnlineTimes"
:model-value="isAllDayOnline() ? '' : form.online_start"
key-prefix="start-"
@select="selectOnlineStart"
/>
</div>
</div>
</div>
<div class="time-preset-row">
@@ -280,12 +302,17 @@ function selectDailyLoss(value: string | number) {
placeholder="结束时间"
class="time-input"
/>
<OptionChips
:options="commonOnlineTimes"
:model-value="form.online_end"
key-prefix="end-"
@select="form.online_end = String($event)"
/>
<div class="time-preset-actions">
<button type="button" class="time-chip" :class="{ active: isAllDayOnline() }" @click="selectAllDayOnline">
全天
</button>
<OptionChips
:options="commonOnlineTimes"
:model-value="isAllDayOnline() ? '' : form.online_end"
key-prefix="end-"
@select="selectOnlineEnd"
/>
</div>
</div>
</div>
</div>
@@ -18,22 +18,29 @@ const props = withDefaults(
)
const emit = defineEmits<{
'update:modelValue': [value: string | number]
select: [value: string | number]
}>()
function isActive(value: string | number) {
return props.activeValues.includes(value) || props.modelValue === value
}
function selectValue(value: string | number) {
emit('update:modelValue', value)
emit('select', value)
}
</script>
<template>
<div class="option-chips" :class="{ compact }">
<div class="chip-group" :class="{ compact }">
<button
v-for="option in options"
:key="`${keyPrefix}${option}`"
type="button"
class="chip"
:class="{ active: isActive(option) }"
@click="emit('select', option)"
@click="selectValue(option)"
>
{{ option }}{{ suffix }}
</button>
@@ -41,33 +48,34 @@ function isActive(value: string | number) {
</template>
<style scoped>
.option-chips {
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 7px;
min-width: 0;
}
.option-chips button {
min-height: 34px;
padding: 0 14px;
border: 1px solid #dbe3ee;
border-radius: 8px;
background: #fff;
color: #334155;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
.chip-group.compact {
align-items: center;
min-height: 32px;
}
.option-chips.compact button {
.chip {
min-height: 30px;
padding: 0 10px;
padding: 0 12px;
border: 1px solid #d6dce5;
border-radius: 15px;
background: #fff;
color: #596474;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
}
.option-chips button.active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
.chip.active {
border-color: #ff6a00;
background: #ff6a00;
color: #fff;
}
</style>
@@ -6,44 +6,44 @@ defineProps<{
</script>
<template>
<section class="publish-section">
<header class="publish-section-header">
<section class="form-section">
<header class="section-heading">
<h2>{{ title }}</h2>
<p v-if="description">{{ description }}</p>
<span v-if="description">{{ description }}</span>
</header>
<slot />
</section>
</template>
<style scoped>
.publish-section {
.form-section {
padding: 20px;
border: 1px solid #e2e8f0;
border: 1px solid #e6ebf2;
border-radius: 8px;
background: #fff;
box-shadow: 0 12px 34px rgba(20, 34, 56, 0.05);
}
.publish-section + .publish-section {
margin-top: 16px;
}
.publish-section-header {
.section-heading {
display: flex;
align-items: baseline;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
min-height: 28px;
margin-bottom: 18px;
}
.publish-section-header h2 {
.section-heading h2 {
margin: 0;
color: #0f172a;
font-size: 18px;
color: #171b24;
font-size: 17px;
}
.publish-section-header p {
.section-heading span {
margin: 0;
color: #64748b;
color: #7b8492;
font-size: 13px;
font-weight: 700;
text-align: right;
}
</style>
+1 -1
View File
@@ -70,7 +70,7 @@ export const mobileRoutes: RouteRecordRaw[] = [
{
path: '/m/seller/listings/create',
name: 'mobile-seller-listing-create',
component: () => import('@/features/seller/views/SellerListingCreateView.vue'),
component: () => import('@/features/seller/views/MobileSellerListingCreateView.vue'),
meta: { layout: 'blank', requiresAuth: true, requiresRealname: true },
},
{
@@ -204,6 +204,7 @@ export function getOnlineTimeText(item: Listing) {
const start = (onlineTime as Record<string, unknown>).start;
const end = (onlineTime as Record<string, unknown>).end;
if (typeof start !== "string" || typeof end !== "string" || !start || !end) return "";
if (start === "全天" || end === "全天" || (start === "00:00" && end === "23:59")) return "全天";
return `${start.replace(":00", "")}-${end.replace(":00", "")}`;
}
+1
View File
@@ -204,6 +204,7 @@ export function getOnlineTimeText(item: Listing) {
const start = (onlineTime as Record<string, unknown>).start;
const end = (onlineTime as Record<string, unknown>).end;
if (typeof start !== "string" || typeof end !== "string" || !start || !end) return "";
if (start === "全天" || end === "全天" || (start === "00:00" && end === "23:59")) return "全天";
return `${start.replace(":00", "")}-${end.replace(":00", "")}`;
}