feat(seller): 优化发布页布局并增加草稿自动保存
This commit is contained in:
@@ -408,10 +408,6 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
|
||||
<span>卖家到手价</span>
|
||||
<strong>{formatMoney(sellerPriceCent(listing))}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>买家挂牌价</span>
|
||||
<strong>{formatMoney(listing.price_cent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金</span>
|
||||
<strong>{formatMoney(listing.deposit_amount_cent)}</strong>
|
||||
@@ -457,21 +453,6 @@ export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean }
|
||||
{formatMoney(moneyCent(breakdown.consumable_price_cent, breakdown.consumable_price))}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>买家比例</span>
|
||||
<strong>{ratioText(breakdown.buyer_ratio_bps, breakdown.buyer_ratio)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平台加价</span>
|
||||
<strong>
|
||||
{formatMoney(
|
||||
moneyCent(
|
||||
breakdown.platform_markup_amount_cent,
|
||||
breakdown.platform_markup_amount
|
||||
)
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -8,13 +8,11 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
Divider,
|
||||
Empty,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Segmented,
|
||||
Select,
|
||||
Space,
|
||||
@@ -72,7 +70,7 @@ import {
|
||||
type PublishSalePriceConfig,
|
||||
type PublishScreenshotSlot,
|
||||
} from './options'
|
||||
import { defaultPublishForm, type PublishForm } from './types'
|
||||
import { defaultPublishForm, type PublishDraft, type PublishForm } from './types'
|
||||
|
||||
const MAX_SCREENSHOTS = 9
|
||||
const SCAN_ONLY_MARK = '微信'
|
||||
@@ -164,20 +162,36 @@ function ChoiceField(props: {
|
||||
}) {
|
||||
const { label, required, value, options, placeholder, onChange } = props
|
||||
return (
|
||||
<label className="seller-field">
|
||||
<div className="seller-field">
|
||||
<span className="seller-field__label">
|
||||
{label}
|
||||
{required ? <b>*</b> : null}
|
||||
</span>
|
||||
{options.length > 0 ? (
|
||||
<Select
|
||||
value={value || undefined}
|
||||
placeholder={placeholder || `请选择${label}`}
|
||||
options={options.map(option => ({ value: option, label: option }))}
|
||||
onChange={value => onChange(value ?? '')}
|
||||
allowClear
|
||||
showSearch
|
||||
/>
|
||||
<div className="seller-pill-group" role="radiogroup" aria-label={label}>
|
||||
{options.map(option => {
|
||||
const active = value === option
|
||||
return (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
className={`seller-pill${active ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
if (active && !required) {
|
||||
onChange('')
|
||||
return
|
||||
}
|
||||
onChange(option)
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{!value && placeholder ? <small className="seller-pill-hint">{placeholder}</small> : null}
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={value}
|
||||
@@ -185,10 +199,71 @@ function ChoiceField(props: {
|
||||
onChange={event => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PUBLISH_DRAFT_KEY_PREFIX = 'jieyou:seller:publish-draft:v1'
|
||||
|
||||
interface StoredPublishDraft extends PublishDraft {
|
||||
savedAt: number
|
||||
}
|
||||
|
||||
function draftHasContent(draft: PublishDraft) {
|
||||
const defaults = defaultPublishForm()
|
||||
const formChanged = (Object.keys(defaults) as (keyof PublishForm)[]).some(key => {
|
||||
const current = draft.form[key]
|
||||
const fallback = defaults[key]
|
||||
if (Array.isArray(current) || Array.isArray(fallback)) {
|
||||
return JSON.stringify(current) !== JSON.stringify(fallback)
|
||||
}
|
||||
return current !== fallback
|
||||
})
|
||||
return (
|
||||
formChanged ||
|
||||
draft.selectedSkins.length > 0 ||
|
||||
Object.values(draft.screenshotFiles).some(urls => (urls ?? []).length > 0) ||
|
||||
Object.values(draft.quantityValues).some(value => Number(value) > 0)
|
||||
)
|
||||
}
|
||||
|
||||
function readPublishDraft(key: string): StoredPublishDraft | null {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw) as Partial<StoredPublishDraft>
|
||||
if (!parsed || typeof parsed !== 'object' || !parsed.form) return null
|
||||
return {
|
||||
form: { ...defaultPublishForm(), ...parsed.form },
|
||||
quantityValues: parsed.quantityValues ?? {},
|
||||
quantityModes: parsed.quantityModes ?? {},
|
||||
screenshotFiles: parsed.screenshotFiles ?? {},
|
||||
selectedSkins: parsed.selectedSkins ?? [],
|
||||
savedAt: Number(parsed.savedAt) || 0,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writePublishDraft(key: string, draft: PublishDraft) {
|
||||
try {
|
||||
const savedAt = Date.now()
|
||||
window.localStorage.setItem(key, JSON.stringify({ ...draft, savedAt }))
|
||||
return savedAt
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
function removePublishDraft(key: string) {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
} catch {
|
||||
// 忽略隐私模式等场景下的存储失败
|
||||
}
|
||||
}
|
||||
|
||||
export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } = {}) {
|
||||
const { id } = useParams()
|
||||
const isEdit = Boolean(id)
|
||||
@@ -209,9 +284,21 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
const [passwordConfirmed, setPasswordConfirmed] = useState(false)
|
||||
const [uploadingSlot, setUploadingSlot] = useState('')
|
||||
const [hydratedId, setHydratedId] = useState<number | null>(null)
|
||||
const userId = useSessionStore(state => state.user?.id ?? 0)
|
||||
const draftKey = `${PUBLISH_DRAFT_KEY_PREFIX}:${userId || 'guest'}`
|
||||
const [draftReady, setDraftReady] = useState(false)
|
||||
const [draftRestoredAt, setDraftRestoredAt] = useState(0)
|
||||
const [draftSavedAt, setDraftSavedAt] = useState(0)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const activeSlotRef = useRef('coin')
|
||||
const draftRef = useRef<PublishDraft>({
|
||||
form: defaultPublishForm(),
|
||||
quantityValues: {},
|
||||
quantityModes: {},
|
||||
screenshotFiles: {},
|
||||
selectedSkins: [],
|
||||
})
|
||||
|
||||
const publishDataQuery = useQuery<PublishPageData>({
|
||||
queryKey: ['seller-publish-options'],
|
||||
@@ -238,6 +325,76 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
|
||||
const listing = listingQuery.data
|
||||
|
||||
function clearPublishDraft() {
|
||||
removePublishDraft(draftKey)
|
||||
setDraftSavedAt(0)
|
||||
setDraftRestoredAt(0)
|
||||
}
|
||||
|
||||
// 新建发布页恢复本机草稿;编辑已有商品时不使用草稿。
|
||||
useEffect(() => {
|
||||
if (!hydrated) return
|
||||
if (isEdit) {
|
||||
setDraftReady(true)
|
||||
return
|
||||
}
|
||||
const stored = readPublishDraft(draftKey)
|
||||
if (stored) {
|
||||
setForm(stored.form)
|
||||
setQuantityValues(stored.quantityValues)
|
||||
setQuantityModes(stored.quantityModes)
|
||||
setScreenshotFiles(stored.screenshotFiles)
|
||||
setSelectedSkins(stored.selectedSkins)
|
||||
setDraftRestoredAt(stored.savedAt)
|
||||
setDraftSavedAt(stored.savedAt)
|
||||
}
|
||||
setDraftReady(true)
|
||||
}, [draftKey, hydrated, isEdit])
|
||||
|
||||
// 输入停顿 1 秒后自动写入本地草稿,避免频繁写存储。
|
||||
useEffect(() => {
|
||||
if (isEdit || !hydrated || !draftReady) return
|
||||
draftRef.current = { form, quantityValues, quantityModes, screenshotFiles, selectedSkins }
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!draftHasContent(draftRef.current)) {
|
||||
removePublishDraft(draftKey)
|
||||
setDraftSavedAt(0)
|
||||
return
|
||||
}
|
||||
const savedAt = writePublishDraft(draftKey, draftRef.current)
|
||||
if (savedAt > 0) setDraftSavedAt(savedAt)
|
||||
}, 1000)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [
|
||||
form,
|
||||
quantityValues,
|
||||
quantityModes,
|
||||
screenshotFiles,
|
||||
selectedSkins,
|
||||
draftKey,
|
||||
hydrated,
|
||||
isEdit,
|
||||
draftReady,
|
||||
])
|
||||
|
||||
// 关闭或切走页面时同步落盘,防止最后一次输入丢失。
|
||||
useEffect(() => {
|
||||
if (isEdit) return
|
||||
const flush = () => {
|
||||
if (!hydrated || !draftReady || !draftHasContent(draftRef.current)) return
|
||||
writePublishDraft(draftKey, draftRef.current)
|
||||
}
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'hidden') flush()
|
||||
}
|
||||
window.addEventListener('beforeunload', flush)
|
||||
document.addEventListener('visibilitychange', handleVisibility)
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', flush)
|
||||
document.removeEventListener('visibilitychange', handleVisibility)
|
||||
}
|
||||
}, [draftKey, hydrated, isEdit, draftReady])
|
||||
|
||||
function updateForm<K extends keyof PublishForm>(key: K, value: PublishForm[K]) {
|
||||
setForm(previous => ({ ...previous, [key]: value }))
|
||||
}
|
||||
@@ -463,6 +620,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
onOk: async () => undefined,
|
||||
})
|
||||
if (!confirmed) return
|
||||
clearPublishDraft()
|
||||
setForm(defaultPublishForm())
|
||||
setQuantityValues(Object.fromEntries(options.quantity_items.map(item => [item.key, 0])))
|
||||
setQuantityModes({})
|
||||
@@ -722,6 +880,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
return isEdit ? updateListing(id as string, payload) : createListing(payload)
|
||||
},
|
||||
onSuccess: async (listing, _payload, _context) => {
|
||||
if (!isEdit) clearPublishDraft()
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['seller-listings'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['listings'] }),
|
||||
@@ -848,6 +1007,13 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<Space wrap>
|
||||
{!isEdit && draftReady ? (
|
||||
<Typography.Text className="seller-draft-status">
|
||||
{draftSavedAt
|
||||
? `草稿已自动保存 ${dayjs(draftSavedAt).format('HH:mm:ss')}`
|
||||
: '草稿自动保存已开启'}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void handleScriptReset()}>
|
||||
{isEdit ? '恢复原信息' : '重置'}
|
||||
</Button>
|
||||
@@ -862,6 +1028,21 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
</Space>
|
||||
</header>
|
||||
|
||||
{!isEdit && draftRestoredAt > 0 ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="seller-draft-alert"
|
||||
message={`已恢复 ${dayjs(draftRestoredAt).format('MM-DD HH:mm')} 自动保存的草稿`}
|
||||
description="内容会持续自动保存;发布成功或点击「重置」后草稿会被清除。"
|
||||
action={
|
||||
<Button size="small" onClick={() => void handleScriptReset()}>
|
||||
清空重填
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{publishDataQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
@@ -885,8 +1066,8 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Row gutter={[18, 18]} align="top">
|
||||
<Col xs={24} xl={16}>
|
||||
<div className="seller-form-layout">
|
||||
<div className="seller-form-main">
|
||||
<Card title="基础资料" className="seller-panel">
|
||||
<div className="seller-form-grid">
|
||||
<ChoiceField
|
||||
@@ -896,16 +1077,23 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
options={options.server_options}
|
||||
onChange={value => updateForm('server_region', value)}
|
||||
/>
|
||||
<label className="seller-field">
|
||||
<div className="seller-field">
|
||||
<span className="seller-field__label">上号方式</span>
|
||||
{effectiveLoginMethods.length > 0 ? (
|
||||
<Select
|
||||
value={form.login_method || undefined}
|
||||
placeholder={isScanOnlyServer ? '微信区固定为扫码' : '请选择上号方式'}
|
||||
disabled={isScanOnlyServer}
|
||||
options={effectiveLoginMethods.map(item => ({ value: item, label: item }))}
|
||||
onChange={value => updateForm('login_method', value)}
|
||||
/>
|
||||
<div className="seller-pill-group" role="radiogroup" aria-label="上号方式">
|
||||
{effectiveLoginMethods.map(item => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={form.login_method === item}
|
||||
className={`seller-pill${form.login_method === item ? ' is-active' : ''}`}
|
||||
onClick={() => updateForm('login_method', item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={form.login_method}
|
||||
@@ -919,7 +1107,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
? '微信区账号仅支持扫码登录,上号方式已自动固定为扫码。'
|
||||
: '优先推荐账密登录,出售速度通常更快。'}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<ChoiceField
|
||||
label="段位"
|
||||
required
|
||||
@@ -961,13 +1149,6 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
onChange={event => updateForm('secret_kd', event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<ChoiceField
|
||||
label="赛季保险"
|
||||
required
|
||||
value={form.season_insurance}
|
||||
options={options.insurance_options}
|
||||
onChange={value => updateForm('season_insurance', value)}
|
||||
/>
|
||||
<ChoiceField
|
||||
label="体力等级"
|
||||
required
|
||||
@@ -975,6 +1156,13 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
options={options.level_options}
|
||||
onChange={value => updateForm('stamina_level', value)}
|
||||
/>
|
||||
<ChoiceField
|
||||
label="赛季保险"
|
||||
required
|
||||
value={form.season_insurance}
|
||||
options={options.insurance_options}
|
||||
onChange={value => updateForm('season_insurance', value)}
|
||||
/>
|
||||
<ChoiceField
|
||||
label="负重等级"
|
||||
required
|
||||
@@ -982,19 +1170,25 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
options={options.level_options}
|
||||
onChange={value => updateForm('load_level', value)}
|
||||
/>
|
||||
<label className="seller-field">
|
||||
<div className="seller-field">
|
||||
<span className="seller-field__label">
|
||||
每日损耗<b>*</b>
|
||||
</span>
|
||||
<Select
|
||||
value={form.daily_loss_m === '' ? undefined : Number(form.daily_loss_m)}
|
||||
options={dailyLossOptions.map(item => ({
|
||||
value: item,
|
||||
label: `${item}M/天`,
|
||||
}))}
|
||||
onChange={value => updateForm('daily_loss_m', value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="seller-pill-group" role="radiogroup" aria-label="每日损耗">
|
||||
{dailyLossOptions.map(item => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={Number(form.daily_loss_m) === item}
|
||||
className={`seller-pill${Number(form.daily_loss_m) === item ? ' is-active' : ''}`}
|
||||
onClick={() => updateForm('daily_loss_m', item)}
|
||||
>
|
||||
{item}M/天
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ChoiceField
|
||||
label="人脸情况"
|
||||
value={form.face_owner}
|
||||
@@ -1159,11 +1353,36 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
已选 {group.options.filter(skin => selectedSkins.includes(skin)).length} 项
|
||||
</span>
|
||||
</div>
|
||||
<Checkbox.Group
|
||||
value={group.options.filter(skin => selectedSkins.includes(skin))}
|
||||
options={group.options}
|
||||
onChange={values => toggleGroupSkin(group.key, values as string[])}
|
||||
/>
|
||||
<div
|
||||
className="seller-pill-group seller-skin-pills"
|
||||
role="group"
|
||||
aria-label={group.title}
|
||||
>
|
||||
{group.options.map(skin => {
|
||||
const active = selectedSkins.includes(skin)
|
||||
return (
|
||||
<button
|
||||
key={skin}
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
className={`seller-pill seller-skin-pill${active ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
const current = group.options.filter(item =>
|
||||
selectedSkins.includes(item),
|
||||
)
|
||||
toggleGroupSkin(
|
||||
group.key,
|
||||
active
|
||||
? current.filter(item => item !== skin)
|
||||
: [...current, skin],
|
||||
)
|
||||
}}
|
||||
>
|
||||
{skin}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1369,23 +1588,31 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
我已知晓:完成后须改密并清除登录设备;若遇封号,须解封后查号方可结算,未经查号即结算者,后续账号问题商行概不负责。
|
||||
</Checkbox>
|
||||
</Card>
|
||||
</Col>
|
||||
</div>
|
||||
|
||||
<Col xs={24} xl={8}>
|
||||
<aside className="seller-form-aside">
|
||||
<div className="seller-summary">
|
||||
<Card className="seller-price-panel">
|
||||
<Typography.Text className="seller-eyebrow">BUYER PRICE · 买家挂牌价</Typography.Text>
|
||||
<Typography.Text className="seller-eyebrow">SELLER PRICE · 卖家发布价格</Typography.Text>
|
||||
<div className="seller-price-panel__main">
|
||||
<strong>{formatMoney(pricing.finalPriceCent)}</strong>
|
||||
<span>
|
||||
{pricing.platformPricing.ruleType === 'fixed_markup'
|
||||
? '固定加价'
|
||||
: pricing.platformPricing.ruleType === 'ratio_subtract'
|
||||
? '比例修正'
|
||||
: '无加价'}
|
||||
</span>
|
||||
<strong>{formatMoney(pricing.sellerPriceCent)}</strong>
|
||||
<span>账号价格,押金另计</span>
|
||||
</div>
|
||||
<div className="seller-price-lines">
|
||||
<div>
|
||||
<span>纯币价格</span>
|
||||
<strong>{formatMoney(pricing.coinBasePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>额外商品价格</span>
|
||||
<strong>{formatMoney(pricing.consumablePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金价格</span>
|
||||
<strong>
|
||||
{formatMoney(depositAmountCent || pricing.recommendedDepositCent)}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>参考比例</span>
|
||||
<strong>
|
||||
@@ -1400,30 +1627,6 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
{pricing.finalRatioBPS > 0 ? `1:${formatRatioBPS(pricing.finalRatioBPS)}` : '--'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>买家比例</span>
|
||||
<strong>
|
||||
{pricing.platformPricing.buyerRatioBPS > 0
|
||||
? `1:${formatRatioBPS(pricing.platformPricing.buyerRatioBPS)}`
|
||||
: '--'}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>纯币基础价</span>
|
||||
<strong>{formatMoney(pricing.coinBasePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>消耗品价值</span>
|
||||
<strong>{formatMoney(pricing.consumablePriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>卖家到手价</span>
|
||||
<strong>{formatMoney(pricing.sellerPriceCent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>推荐押金</span>
|
||||
<strong>{formatMoney(pricing.recommendedDepositCent)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="seller-summary-meta">
|
||||
@@ -1457,8 +1660,8 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,6 +365,12 @@ button.seller-code:not(:disabled):hover {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.seller-skin-pills .seller-pill {
|
||||
padding: 3px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.seller-shot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
@@ -508,7 +514,9 @@ button.seller-code:not(:disabled):hover {
|
||||
|
||||
.seller-upload-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.seller-upload-slot {
|
||||
@@ -650,6 +658,117 @@ button.seller-code:not(:disabled):hover {
|
||||
top: 92px;
|
||||
}
|
||||
|
||||
/* 发布页:主表单 + 右侧悬浮价格栏 */
|
||||
.seller-form-page {
|
||||
max-width: 1240px;
|
||||
}
|
||||
|
||||
.seller-draft-status {
|
||||
color: var(--ink-muted) !important;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seller-draft-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 发布页:主体占满内容宽度,结算栏在大屏时独立到右侧留白 */
|
||||
.seller-form-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.seller-form-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.seller-form-aside {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.seller-form-layout {
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 20px;
|
||||
margin-right: -340px;
|
||||
}
|
||||
|
||||
.seller-form-aside {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 点选式选项,替代下拉框 */
|
||||
.seller-pill-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.seller-pill {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.seller-pill:hover {
|
||||
border-color: var(--brand-ink);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
|
||||
.seller-pill.is-active {
|
||||
border-color: var(--brand-ink);
|
||||
background: var(--brand-ink);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.seller-pill:focus-visible {
|
||||
outline: 2px solid var(--brand-brass);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.seller-pill-hint {
|
||||
color: var(--ink-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.seller-form-page .seller-summary {
|
||||
top: 88px;
|
||||
max-height: calc(100vh - 104px);
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.seller-form-page .seller-summary::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.seller-form-page .seller-summary::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: rgba(30, 77, 66, 0.18);
|
||||
}
|
||||
|
||||
.seller-form-page .seller-summary::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.seller-price-panel {
|
||||
border-color: var(--brand-seal-line);
|
||||
background:
|
||||
@@ -724,6 +843,10 @@ button.seller-code:not(:disabled):hover {
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.seller-form-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.seller-stat-strip {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@@ -739,6 +862,8 @@ button.seller-code:not(:disabled):hover {
|
||||
|
||||
.seller-summary {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user