增加前端格式检查配置

This commit is contained in:
yml2213
2026-06-08 06:40:33 +08:00
parent 79ea3a476c
commit 500f511185
150 changed files with 7254 additions and 4013 deletions
@@ -73,11 +73,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
selectedSkins,
})
const uploadedScreenshotCount = computed(
() => pricing.screenshotSlots.value.filter((item) => getScreenshotCount(item.key) > 0).length,
() => pricing.screenshotSlots.value.filter(item => getScreenshotCount(item.key) > 0).length
)
const requiredScreenshotCount = ref(0)
const canPublishAfterAgreements = computed(
() => virtualAssetSaleAgreementChecked.value && sellerAgreementChecked.value,
() => virtualAssetSaleAgreementChecked.value && sellerAgreementChecked.value
)
onMounted(() => {
@@ -98,7 +98,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
() => {
saveDraft()
},
{ deep: true },
{ deep: true }
)
watch(
@@ -106,7 +106,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
() => {
syncRecommendedDeposit()
},
{ immediate: true },
{ immediate: true }
)
watch(
@@ -114,15 +114,17 @@ export function usePublishForm(options: UsePublishFormOptions) {
() => {
clearForbiddenQuantityItems()
},
{ deep: true },
{ deep: true }
)
watch(
[pricing.screenshotSlots, () => form.ban_record],
() => {
requiredScreenshotCount.value = pricing.screenshotSlots.value.filter((item) => isScreenshotRequired(item)).length
requiredScreenshotCount.value = pricing.screenshotSlots.value.filter(item =>
isScreenshotRequired(item)
).length
},
{ immediate: true, deep: true },
{ immediate: true, deep: true }
)
async function loadPublishOptions() {
@@ -186,7 +188,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
quantityModes,
screenshotFiles,
selectedSkins: selectedSkins.value,
}),
})
)
}
@@ -256,13 +258,13 @@ export function usePublishForm(options: UsePublishFormOptions) {
function toggleSkin(skin: string) {
selectedSkins.value = selectedSkins.value.includes(skin)
? selectedSkins.value.filter((item) => item !== skin)
? selectedSkins.value.filter(item => item !== skin)
: [...selectedSkins.value, skin]
}
function toggleRegion(region: string) {
form.common_regions = form.common_regions.includes(region)
? form.common_regions.filter((item) => item !== region)
? form.common_regions.filter(item => item !== region)
: [...form.common_regions, region]
}
@@ -288,7 +290,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
const uploadFiles = files.slice(0, availableCount)
if (files.length > availableCount) {
options.notifyWarning(`${getScreenshotLabel(key)}最多上传${getScreenshotLimit(key)}张,本次仅上传${availableCount}`)
options.notifyWarning(
`${getScreenshotLabel(key)}最多上传${getScreenshotLimit(key)}张,本次仅上传${availableCount}`
)
}
uploading.value = true
try {
@@ -302,7 +306,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
throw error
}
}
options.notifySuccess(uploadFiles.length > 1 ? `已上传${uploadFiles.length}张截图` : '截图已上传')
options.notifySuccess(
uploadFiles.length > 1 ? `已上传${uploadFiles.length}张截图` : '截图已上传'
)
} catch (error) {
options.notifyError(readError(error, '截图上传失败'))
} finally {
@@ -353,7 +359,12 @@ export function usePublishForm(options: UsePublishFormOptions) {
async function hydrateScreenshotPreviews() {
for (const [key, fileURLs] of Object.entries(screenshotFiles)) {
for (const [index, fileURL] of fileURLs.entries()) {
if (!fileURL || screenshotPreviews[key]?.[index] || !fileURL.startsWith('/api/files/object')) continue
if (
!fileURL ||
screenshotPreviews[key]?.[index] ||
!fileURL.startsWith('/api/files/object')
)
continue
try {
const blob = await fetchFileBlobByURL(fileURL)
setScreenshotPreview(key, index, URL.createObjectURL(blob))
@@ -411,14 +422,18 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
function clampAcceleratedSaleRatioInput() {
if (!pricing.hasAcceleratedSaleRatioInput() || pricing.calculatedDefaultSaleRatio.value <= 0) return
if (!pricing.hasAcceleratedSaleRatioInput() || pricing.calculatedDefaultSaleRatio.value <= 0)
return
const ratio = Number(form.accelerated_sale_ratio)
if (!Number.isFinite(ratio)) {
form.accelerated_sale_ratio = ''
return
}
form.accelerated_sale_ratio = roundRatio(
Math.min(Math.max(ratio, pricing.calculatedDefaultSaleRatio.value), pricing.maxAcceleratedSaleRatio.value),
Math.min(
Math.max(ratio, pricing.calculatedDefaultSaleRatio.value),
pricing.maxAcceleratedSaleRatio.value
)
)
}
@@ -460,7 +475,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
options.notifySuccess(
listing.status === 'published' && listing.review_status === 'approved'
? '发布成功,已上架'
: '发布成功,等待后台审核',
: '发布成功,等待后台审核'
)
await router.push(options.submitSuccessPath)
} catch (error) {
@@ -475,7 +490,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
if (pricing.coinMAmount.value <= 0) return '请填写哈夫币/M'
if (!form.rank_level) return '请选择段位'
if (!form.fire_level) return '请填写烽火等级'
if (Number(form.fire_level) < pricing.fireLevelMin.value) return `烽火等级低于${pricing.fireLevelMin.value}级的号无法发布`
if (Number(form.fire_level) < pricing.fireLevelMin.value)
return `烽火等级低于${pricing.fireLevelMin.value}级的号无法发布`
if (!form.season_insurance) return '请选择赛季保险'
if (!form.stamina_level) return '请选择体力等级'
if (!form.load_level) return '请选择负重等级'
@@ -483,27 +499,41 @@ export function usePublishForm(options: UsePublishFormOptions) {
if (pricing.hasAcceleratedSaleRatioInput()) {
const ratio = Number(form.accelerated_sale_ratio)
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
if (pricing.calculatedDefaultSaleRatio.value > 0 && ratio < pricing.calculatedDefaultSaleRatio.value) {
if (
pricing.calculatedDefaultSaleRatio.value > 0 &&
ratio < pricing.calculatedDefaultSaleRatio.value
) {
return `加速出售比例不能低于默认比例 1:${formatNumber(pricing.calculatedDefaultSaleRatio.value)}`
}
if (pricing.maxAcceleratedSaleRatio.value > 0 && ratio > pricing.maxAcceleratedSaleRatio.value) {
if (
pricing.maxAcceleratedSaleRatio.value > 0 &&
ratio > pricing.maxAcceleratedSaleRatio.value
) {
return `加速出售比例不能超过 1:${formatNumber(pricing.maxAcceleratedSaleRatio.value)}`
}
}
if (pricing.banRecordOptions.value.length && !form.ban_record) return '请选择封禁记录'
if (form.deposit_amount === '' || Number(form.deposit_amount) < 0) return '请填写押金'
if (!Number.isFinite(Number(form.deposit_amount))) return '押金格式不正确'
if (pricing.recommendedDepositAmount.value > 0 && Number(form.deposit_amount) < pricing.recommendedDepositAmount.value) {
if (
pricing.recommendedDepositAmount.value > 0 &&
Number(form.deposit_amount) < pricing.recommendedDepositAmount.value
) {
return `押金不能低于智能推荐 ¥${pricing.recommendedDepositAmount.value}`
}
if (pricing.calculatedConsumablePrice.value > 0 && Number(form.deposit_amount) <= pricing.calculatedConsumablePrice.value) {
if (
pricing.calculatedConsumablePrice.value > 0 &&
Number(form.deposit_amount) <= pricing.calculatedConsumablePrice.value
) {
return `押金必须大于额外消耗品总价值 ¥${pricing.calculatedConsumablePrice.value}`
}
if (!pricing.calculatedFinalPrice.value) return '请完善币数、保险、体力和负重后再发布'
if (!Number.isFinite(pricing.calculatedFinalPrice.value)) return '发布价格计算异常,请检查填写内容'
if (!Number.isFinite(pricing.calculatedFinalPrice.value))
return '发布价格计算异常,请检查填写内容'
if (!canPublishAfterAgreements.value) return '请先阅读并勾选两份发布协议'
for (const item of pricing.screenshotSlots.value) {
if (isScreenshotRequired(item) && getScreenshotCount(item.key) === 0) return `请上传${item.label}`
if (isScreenshotRequired(item) && getScreenshotCount(item.key) === 0)
return `请上传${item.label}`
}
for (const item of pricing.quantityItems.value) {
if (!pricing.isQuantityItemDisabled(item)) {
@@ -558,7 +588,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
season_insurance: form.season_insurance,
stamina_level: form.stamina_level,
load_level: form.load_level,
resources: pricing.quantityItems.value.map((item) => ({
resources: pricing.quantityItems.value.map(item => ({
key: item.key,
label: item.label,
price: item.price,
@@ -566,7 +596,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
mode: pricing.isQuantityItemDisabled(item) ? '赠送' : quantityModes[item.key] || '收费',
})),
skin_groups: pricing.skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
groups[group.key] = group.options.filter((skin) => selectedSkins.value.includes(skin))
groups[group.key] = group.options.filter(skin => selectedSkins.value.includes(skin))
return groups
}, {}),
screenshot_groups: buildScreenshotGroups(),
@@ -614,7 +644,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
function getScreenshotLabel(key: ScreenshotKey) {
return pricing.screenshotSlots.value.find((item) => item.key === key)?.label || '该截图'
return pricing.screenshotSlots.value.find(item => item.key === key)?.label || '该截图'
}
return {
@@ -128,13 +128,15 @@
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.16s ease, visibility 0.16s ease;
transition:
opacity 0.16s ease,
visibility 0.16s ease;
}
.hint-label::before,
.backend-hint-target.has-hint::before,
.upload-title.has-hint::before {
content: "";
content: '';
top: 50%;
left: calc(100% + 2px);
transform: translateY(-50%);
@@ -753,8 +755,6 @@
background: #ff6a00 !important;
}
@media (min-width: 430px) {
.time-row {
grid-template-columns: 1fr 1fr;
@@ -83,9 +83,9 @@ const {
showCancelButton: true,
})
},
notifySuccess: (message) => showToast({ message, icon: 'passed' }),
notifyWarning: (message) => showToast({ message, icon: 'warning-o' }),
notifyError: (message) => showToast({ message, icon: 'cross' }),
notifySuccess: message => showToast({ message, icon: 'passed' }),
notifyWarning: message => showToast({ message, icon: 'warning-o' }),
notifyError: message => showToast({ message, icon: 'cross' }),
})
const agreementPopupVisible = ref(false)
@@ -151,7 +151,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.server_region === opt }"
@click="selectRadio(opt, (value) => (form.server_region = value))"
@click="selectRadio(opt, value => (form.server_region = value))"
>
{{ opt }}
</button>
@@ -168,7 +168,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.face_owner === opt }"
@click="selectRadio(opt, (value) => (form.face_owner = value))"
@click="selectRadio(opt, value => (form.face_owner = value))"
>
{{ opt }}
</button>
@@ -194,7 +194,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.rank_level === opt }"
@click="selectRadio(opt, (value) => (form.rank_level = value))"
@click="selectRadio(opt, value => (form.rank_level = value))"
>
{{ opt }}
</button>
@@ -227,7 +227,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.season_insurance === opt }"
@click="selectRadio(opt, (value) => (form.season_insurance = value))"
@click="selectRadio(opt, value => (form.season_insurance = value))"
>
{{ opt }}
</button>
@@ -245,7 +245,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="level-btn"
:class="{ active: form.stamina_level === opt }"
@click="selectRadio(opt, (value) => (form.stamina_level = value))"
@click="selectRadio(opt, value => (form.stamina_level = value))"
>
{{ opt }}
</button>
@@ -260,7 +260,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="level-btn"
:class="{ active: form.load_level === opt }"
@click="selectRadio(opt, (value) => (form.load_level = value))"
@click="selectRadio(opt, value => (form.load_level = value))"
>
{{ opt }}
</button>
@@ -287,7 +287,9 @@ function selectOnlineEnd(value: string) {
>
<span>*</span>{{ item.label }}
</strong>
<small>{{ isQuantityItemDisabled(item) ? "3*3 已包含,不能填写" : item.price }}</small>
<small>{{
isQuantityItemDisabled(item) ? '3*3 已包含,不能填写' : item.price
}}</small>
</div>
<input
v-model="quantityValues[item.key]"
@@ -327,10 +329,7 @@ function selectOnlineEnd(value: string) {
<div class="skin-group-title">
<span>{{ group.title }}</span>
<small>
{{
group.options.filter((skin) => selectedSkins.includes(skin))
.length
}}
{{ group.options.filter(skin => selectedSkins.includes(skin)).length }}
</small>
</div>
<div class="multi-chip-group">
@@ -357,7 +356,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.login_method === opt }"
@click="selectRadio(opt, (value) => (form.login_method = value))"
@click="selectRadio(opt, value => (form.login_method = value))"
>
{{ opt }}
</button>
@@ -439,7 +438,7 @@ function selectOnlineEnd(value: string) {
type="button"
class="radio-btn"
:class="{ active: form.ban_record === opt }"
@click="selectRadio(opt, (value) => (form.ban_record = value))"
@click="selectRadio(opt, value => (form.ban_record = value))"
>
{{ opt }}
</button>
@@ -484,11 +483,20 @@ function selectOnlineEnd(value: string) {
{{ slot.label }}<span v-if="isScreenshotRequired(slot)">*</span>
</strong>
<small>{{ slot.hint }}</small>
<small v-if="getScreenshotLimitHint(slot.key)">{{ getScreenshotLimitHint(slot.key) }}</small>
<small v-if="getScreenshotLimitHint(slot.key)">{{
getScreenshotLimitHint(slot.key)
}}</small>
</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}`" />
<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
@@ -499,7 +507,7 @@ function selectOnlineEnd(value: string) {
@click="triggerUpload(slot.key)"
>
<van-icon name="photograph" :size="22" color="#999" />
<span>{{ uploading && activeUploadKey === slot.key ? "上传中" : "继续上传" }}</span>
<span>{{ uploading && activeUploadKey === slot.key ? '上传中' : '继续上传' }}</span>
</button>
</div>
<button
@@ -510,7 +518,7 @@ function selectOnlineEnd(value: string) {
@click="triggerUpload(slot.key)"
>
<van-icon name="photograph" :size="22" color="#999" />
<span>{{ uploading && activeUploadKey === slot.key ? "上传中" : "上传" }}</span>
<span>{{ uploading && activeUploadKey === slot.key ? '上传中' : '上传' }}</span>
</button>
</div>
</div>
@@ -537,7 +545,11 @@ function selectOnlineEnd(value: string) {
class="publish-field"
>
<template #label>
<span class="hint-label deposit-label" :data-hint="priceConfig.deposit_placeholder" tabindex="0">
<span
class="hint-label deposit-label"
:data-hint="priceConfig.deposit_placeholder"
tabindex="0"
>
押金<em v-if="priceConfig.deposit_hint">{{ priceConfig.deposit_hint }}</em>
</span>
</template>
@@ -583,7 +595,7 @@ function selectOnlineEnd(value: string) {
{{
calculatedDefaultSaleRatio > 0
? `1元=${formatNumber(calculatedDefaultSaleRatio)}`
: "自动计算"
: '自动计算'
}}
</span>
</button>
@@ -599,7 +611,7 @@ function selectOnlineEnd(value: string) {
{{
maxAcceleratedSaleRatio > 0
? `1元=${formatNumber(calculatedDefaultSaleRatio)}~${formatNumber(maxAcceleratedSaleRatio)}`
: "自动计算"
: '自动计算'
}}
</span>
</button>
@@ -626,7 +638,9 @@ function selectOnlineEnd(value: string) {
class="publish-field result-field"
>
<template #label>
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">卖家比例</span>
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0"
>卖家比例</span
>
</template>
</van-field>
<van-field
@@ -638,7 +652,9 @@ function selectOnlineEnd(value: string) {
class="publish-field result-field"
>
<template #label>
<span class="hint-label" :data-hint="priceConfig.price_placeholder" tabindex="0">纯币基础价</span>
<span class="hint-label" :data-hint="priceConfig.price_placeholder" tabindex="0"
>纯币基础价</span
>
</template>
</van-field>
<van-field
@@ -656,9 +672,7 @@ function selectOnlineEnd(value: string) {
class="publish-field result-field total-price-field"
/>
</div>
<p class="price-breakdown-hint">
卖家价格 = 纯币基础价 + 额外消耗品
</p>
<p class="price-breakdown-hint">卖家价格 = 纯币基础价 + 额外消耗品</p>
<van-field
v-model="form.remark"
@@ -699,12 +713,7 @@ function selectOnlineEnd(value: string) {
</div>
<div class="publish-actions">
<van-button
round
class="reset-btn"
:disabled="loading"
@click="handleResetDraft"
>
<van-button round class="reset-btn" :disabled="loading" @click="handleResetDraft">
重置
</van-button>
<van-button
@@ -721,7 +730,14 @@ function selectOnlineEnd(value: string) {
</div>
</section>
<van-popup v-model:show="agreementPopupVisible" round closeable position="bottom" lock-scroll class="publish-agreement-popup">
<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>
@@ -12,9 +12,9 @@ const loading = ref(false)
const orders = ref<Order[]>([])
const status = ref('')
const sellerOrders = computed(() => orders.value.filter((order) => order.owner_id === session.userId))
const sellerOrders = computed(() => orders.value.filter(order => order.owner_id === session.userId))
const todoOrders = computed(() =>
sellerOrders.value.filter((order) =>
sellerOrders.value.filter(order =>
[
'pending_handoff',
'renting',
@@ -24,22 +24,28 @@ const todoOrders = computed(() =>
'checkout_disputing',
'disputing',
'abnormal',
].includes(order.status),
),
].includes(order.status)
)
)
const displayOrders = computed(() => {
const source = todoOrders.value
if (!status.value) return source
return source.filter((order) => order.status === status.value)
return source.filter(order => order.status === status.value)
})
const pendingHandoffCount = computed(
() => sellerOrders.value.filter((order) => order.status === 'pending_handoff' && order.handoff_status === 'pending_owner').length,
() =>
sellerOrders.value.filter(
order => order.status === 'pending_handoff' && order.handoff_status === 'pending_owner'
).length
)
const pendingCheckoutCount = computed(
() => sellerOrders.value.filter((order) => order.status === 'pending_checkout_confirm').length,
() => sellerOrders.value.filter(order => order.status === 'pending_checkout_confirm').length
)
const abnormalCount = computed(
() => sellerOrders.value.filter((order) => ['overdue', 'checkout_disputing', 'disputing', 'abnormal'].includes(order.status)).length,
() =>
sellerOrders.value.filter(order =>
['overdue', 'checkout_disputing', 'disputing', 'abnormal'].includes(order.status)
).length
)
onMounted(loadOrders)
@@ -54,10 +60,12 @@ async function loadOrders() {
}
function actionText(order: Order) {
if (order.status === 'pending_handoff' && order.handoff_status === 'pending_owner') return '提交交接'
if (order.status === 'pending_handoff' && order.handoff_status === 'pending_owner')
return '提交交接'
if (order.status === 'pending_checkout_confirm') return '处理结账'
if (order.status === 'pending_checkout_accept') return '等待租客'
if (['overdue', 'checkout_disputing', 'disputing', 'abnormal'].includes(order.status)) return '查看处理'
if (['overdue', 'checkout_disputing', 'disputing', 'abnormal'].includes(order.status))
return '查看处理'
return '详情'
}
@@ -27,7 +27,9 @@
.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;
transition:
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.publish-page :deep(.el-input__wrapper:hover),
@@ -37,7 +39,9 @@
.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;
box-shadow:
0 0 0 1px var(--color-orange-primary) inset,
0 0 8px rgba(255, 106, 0, 0.15) !important;
}
.publish-layout {
@@ -211,7 +215,11 @@
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;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease,
transform 0.16s ease;
}
.level-btn,
@@ -246,7 +254,9 @@
.quantity-header,
.quantity-item {
display: grid;
grid-template-columns: minmax(180px, 3fr) minmax(80px, 1fr) minmax(100px, 1.2fr) minmax(140px, 1.5fr) minmax(80px, 1fr);
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;
}
@@ -397,7 +407,10 @@
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease;
}
.time-chip.active {
@@ -548,7 +561,9 @@
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
transition:
border-color 0.2s ease,
background 0.2s ease;
}
.recommend-button:hover {
@@ -636,7 +651,10 @@
gap: 6px;
border: 1px solid #d6dce5;
background: #fff;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease;
}
.ratio-custom-card strong {
@@ -676,7 +694,9 @@
min-height: 82px;
padding: 14px;
border-left: 4px solid #ccd5e0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.price-cell:hover {
@@ -883,7 +903,9 @@
background-color: var(--color-orange-primary);
color: #fff;
font-weight: 800;
transition: border-color 0.2s ease, background-color 0.2s ease;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
}
.summary-actions :deep(.el-button.btn-publish:hover) {
@@ -1,5 +1,11 @@
<script setup lang="ts">
import { Delete, DocumentChecked, Picture, RefreshRight, UploadFilled } from '@element-plus/icons-vue'
import {
Delete,
DocumentChecked,
Picture,
RefreshRight,
UploadFilled,
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, ref } from 'vue'
@@ -88,9 +94,9 @@ const {
type: 'warning',
})
},
notifySuccess: (message) => ElMessage.success(message),
notifyWarning: (message) => ElMessage.warning(message),
notifyError: (message) => ElMessage.error(message),
notifySuccess: message => ElMessage.success(message),
notifyWarning: message => ElMessage.warning(message),
notifyError: message => ElMessage.error(message),
})
const agreementDialogVisible = ref(false)
@@ -136,22 +142,41 @@ function selectOnlineEnd(value: string | number) {
<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)" />
<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)" />
<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" />
<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" />
<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>
@@ -167,7 +192,11 @@ function selectOnlineEnd(value: string | number) {
<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)" />
<OptionChips
:options="rankOptions"
:model-value="form.rank_level"
@select="form.rank_level = String($event)"
/>
</div>
<div class="field-row panel-field-row">
@@ -213,7 +242,11 @@ function selectOnlineEnd(value: string | number) {
</div>
</PublishSection>
<PublishSection v-if="quantityItems.length" title="额外消耗品" description="收费项会计入发布价格">
<PublishSection
v-if="quantityItems.length"
title="额外消耗品"
description="收费项会计入发布价格"
>
<div class="quantity-table">
<div class="quantity-header">
<span>物资</span>
@@ -229,7 +262,9 @@ function selectOnlineEnd(value: string | number) {
:class="{ disabled: isQuantityItemDisabled(item) }"
>
<div class="quantity-meta">
<strong :title="item.placeholder || item.label"><span>*</span>{{ item.label }}</strong>
<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>
@@ -266,15 +301,15 @@ function selectOnlineEnd(value: string | number) {
<PublishSection title="皮肤与交接">
<div v-if="skinGroups.length" class="skin-groups">
<div
v-for="group in skinGroups"
:key="group.key"
class="skin-group"
>
<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" />
<OptionChips
:options="group.options"
:active-values="selectedSkins"
@select="toggleSkinOption"
/>
</div>
</div>
@@ -285,7 +320,9 @@ function selectOnlineEnd(value: string | number) {
:model-value="form.login_method"
@select="form.login_method = String($event)"
/>
<p class="field-hint">优先推荐账密登录出售速度通常更快请结合账号安全情况自行选择</p>
<p class="field-hint">
优先推荐账密登录出售速度通常更快请结合账号安全情况自行选择
</p>
</div>
<div class="time-preset-panel">
@@ -300,7 +337,12 @@ function selectOnlineEnd(value: string | number) {
class="time-input"
/>
<div class="time-preset-actions">
<button type="button" class="time-chip" :class="{ active: isAllDayOnline() }" @click="selectAllDayOnline">
<button
type="button"
class="time-chip"
:class="{ active: isAllDayOnline() }"
@click="selectAllDayOnline"
>
全天
</button>
<OptionChips
@@ -323,7 +365,12 @@ function selectOnlineEnd(value: string | number) {
class="time-input"
/>
<div class="time-preset-actions">
<button type="button" class="time-chip" :class="{ active: isAllDayOnline() }" @click="selectAllDayOnline">
<button
type="button"
class="time-chip"
:class="{ active: isAllDayOnline() }"
@click="selectAllDayOnline"
>
全天
</button>
<OptionChips
@@ -340,7 +387,11 @@ function selectOnlineEnd(value: string | number) {
<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)" />
<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">
@@ -363,27 +414,53 @@ function selectOnlineEnd(value: string | number) {
</div>
</PublishSection>
<PublishSection title="截图材料" :description="`${uploadedScreenshotCount}/${screenshotSlots.length} 项已上传`">
<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>
<em v-if="getScreenshotLimitHint(slot.key)">{{ getScreenshotLimitHint(slot.key) }}</em>
<em v-if="getScreenshotLimitHint(slot.key)">{{
getScreenshotLimitHint(slot.key)
}}</em>
</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}`" />
<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)">
<el-icon><Delete /></el-icon>
</button>
</div>
<button v-if="canAddScreenshot(slot.key)" type="button" class="upload-add" :disabled="uploading" @click="triggerUpload(slot.key)">
<button
v-if="canAddScreenshot(slot.key)"
type="button"
class="upload-add"
:disabled="uploading"
@click="triggerUpload(slot.key)"
>
<el-icon><Picture /></el-icon>
<span>{{ uploading && activeUploadKey === slot.key ? '上传中' : '继续上传' }}</span>
<span>{{
uploading && activeUploadKey === slot.key ? '上传中' : '继续上传'
}}</span>
</button>
</div>
<button v-else type="button" class="upload-add" :disabled="uploading" @click="triggerUpload(slot.key)">
<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>
@@ -402,7 +479,10 @@ function selectOnlineEnd(value: string | number) {
<PublishSection title="押金与价格" description="系统按资料自动计算售价">
<div class="compact-grid two">
<label class="input-block">
<span class="deposit-label">押金<b>*</b><em v-if="priceConfig.deposit_hint">{{ priceConfig.deposit_hint }}</em></span>
<span class="deposit-label"
>押金<b>*</b
><em v-if="priceConfig.deposit_hint">{{ priceConfig.deposit_hint }}</em></span
>
<div class="deposit-control">
<el-input-number
v-model="form.deposit_amount"
@@ -416,7 +496,9 @@ function selectOnlineEnd(value: string | number) {
</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 }}
{{ item.label }}<template v-if="item.count > 1"> x{{ item.count }}</template> ¥{{
item.amount
}}
</span>
</div>
</label>
@@ -447,21 +529,37 @@ function selectOnlineEnd(value: string | number) {
@click="useReferenceSaleRatio"
>
<strong>参考比例</strong>
<span>{{ calculatedDefaultSaleRatio > 0 ? `1元=${formatNumber(calculatedDefaultSaleRatio)}` : '自动计算' }}</span>
<span>{{
calculatedDefaultSaleRatio > 0
? `1元=${formatNumber(calculatedDefaultSaleRatio)}`
: '自动计算'
}}</span>
</button>
<button
type="button"
class="ratio-mode-btn"
:class="{ active: form.accelerated_sale_ratio === maxAcceleratedSaleRatio && maxAcceleratedSaleRatio > 0 }"
:class="{
active:
form.accelerated_sale_ratio === maxAcceleratedSaleRatio &&
maxAcceleratedSaleRatio > 0,
}"
:disabled="maxAcceleratedSaleRatio <= 0"
@click="useMaxAcceleratedSaleRatio"
>
<strong>最高比例 (加速)</strong>
<span>{{ maxAcceleratedSaleRatio > 0 ? `最高 1元=${formatNumber(maxAcceleratedSaleRatio)}` : '自动计算' }}</span>
<span>{{
maxAcceleratedSaleRatio > 0
? `最高 1元=${formatNumber(maxAcceleratedSaleRatio)}`
: '自动计算'
}}</span>
</button>
<div
class="ratio-custom-card"
:class="{ active: hasAcceleratedSaleRatioInput() && form.accelerated_sale_ratio !== maxAcceleratedSaleRatio }"
:class="{
active:
hasAcceleratedSaleRatioInput() &&
form.accelerated_sale_ratio !== maxAcceleratedSaleRatio,
}"
>
<strong>自定比例</strong>
<el-input-number
@@ -577,14 +675,23 @@ function selectOnlineEnd(value: string | number) {
>
立即发布
</el-button>
<el-button :icon="DocumentChecked" :disabled="loading" @click="handleSaveDraft">保存草稿</el-button>
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft">重置草稿</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>
<el-dialog v-model="agreementDialogVisible" :title="activeAgreementTitle" width="720px" class="publish-agreement-dialog">
<el-dialog
v-model="agreementDialogVisible"
:title="activeAgreementTitle"
width="720px"
class="publish-agreement-dialog"
>
<div class="agreement-dialog-content">{{ activeAgreementContent }}</div>
</el-dialog>
</main>
@@ -3,7 +3,12 @@ import { Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/features/listings'
import {
fetchSellerListings,
offlineListing,
submitListingReview,
type Listing,
} from '@/features/listings'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { getListingSellerPrice } from '@/utils/listingDisplay'
@@ -15,15 +20,27 @@ const statusFilter = ref('all')
const displayListings = computed(() => {
if (statusFilter.value === 'all') return listings.value
if (statusFilter.value === 'pending') return listings.value.filter((item) => isPendingReview(item))
return listings.value.filter((item) => item.status === statusFilter.value)
if (statusFilter.value === 'pending') return listings.value.filter(item => isPendingReview(item))
return listings.value.filter(item => item.status === statusFilter.value)
})
const stats = computed(() => [
{ key: 'all', label: '全部发布', value: listings.value.length },
{ key: 'published', label: '已上架', value: listings.value.filter((item) => item.status === 'published').length },
{ key: 'pending', label: '待审核', value: listings.value.filter((item) => isPendingReview(item)).length },
{ key: 'offline', label: '已架', value: listings.value.filter((item) => item.status === 'offline').length },
{
key: 'published',
label: '已架',
value: listings.value.filter(item => item.status === 'published').length,
},
{
key: 'pending',
label: '待审核',
value: listings.value.filter(item => isPendingReview(item)).length,
},
{
key: 'offline',
label: '已下架',
value: listings.value.filter(item => item.status === 'offline').length,
},
])
onMounted(loadListings)
@@ -45,7 +62,7 @@ async function submitReview(row: Listing) {
ElMessage.success(
listing.status === 'published' && listing.review_status === 'approved'
? '已上架'
: '已提交审核,等待后台处理',
: '已提交审核,等待后台处理'
)
} finally {
submittingID.value = null
@@ -54,11 +71,15 @@ async function submitReview(row: Listing) {
async function offline(row: Listing) {
try {
await ElMessageBox.confirm(`确认下架「${row.title}」吗?下架后后台待审列表会同步移除。`, '下架确认', {
confirmButtonText: '确认下架',
cancelButtonText: '取消',
type: 'warning',
})
await ElMessageBox.confirm(
`确认下架「${row.title}」吗?下架后后台待审列表会同步移除。`,
'下架确认',
{
confirmButtonText: '确认下架',
cancelButtonText: '取消',
type: 'warning',
}
)
} catch {
return
}
@@ -73,7 +94,7 @@ async function offline(row: Listing) {
}
function replaceListing(next: Listing) {
const index = listings.value.findIndex((item) => item.id === next.id)
const index = listings.value.findIndex(item => item.id === next.id)
if (index >= 0) {
listings.value.splice(index, 1, next)
} else {
@@ -166,7 +187,9 @@ function isPendingReview(row: Listing) {
<div class="listing-title-row">
<h3>{{ item.title }}</h3>
<div class="listing-tags">
<span class="status-pill" :class="statusTone(item.status)">{{ listingStatusLabel(item.status) }}</span>
<span class="status-pill" :class="statusTone(item.status)">{{
listingStatusLabel(item.status)
}}</span>
<span class="status-pill" :class="reviewTone(effectiveReviewStatus(item))">
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
</span>
@@ -273,7 +296,10 @@ function isPendingReview(row: Listing) {
color: #64748b;
cursor: pointer;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
transition:
transform 0.18s ease,
border-color 0.18s ease,
box-shadow 0.18s ease;
}
.stat-card span {
@@ -14,7 +14,7 @@ const props = withDefaults(
keyPrefix: '',
compact: false,
suffix: '',
},
}
)
const emit = defineEmits<{
@@ -70,7 +70,10 @@ function selectValue(value: string | number) {
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
transition:
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease;
}
.chip.active {