|
|
|
@@ -5,7 +5,7 @@ import { showDialog, showToast } from "vant";
|
|
|
|
|
|
|
|
|
|
import { uploadFile } from "@/api/files";
|
|
|
|
|
import {
|
|
|
|
|
defaultListingPublishOptions,
|
|
|
|
|
emptyListingPublishOptions,
|
|
|
|
|
fetchListingPublishOptions,
|
|
|
|
|
type ChargeMode,
|
|
|
|
|
type ListingPublishOptions,
|
|
|
|
@@ -30,9 +30,6 @@ type PublishForm = {
|
|
|
|
|
ban_record: string;
|
|
|
|
|
common_regions: string[];
|
|
|
|
|
deposit_amount: number | "";
|
|
|
|
|
ratio: number | "";
|
|
|
|
|
final_price: number;
|
|
|
|
|
rent_days: number;
|
|
|
|
|
remark: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -50,7 +47,7 @@ const route = useRoute();
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const uploading = ref(false);
|
|
|
|
|
const suppressDraftSave = ref(false);
|
|
|
|
|
const publishOptions = ref<ListingPublishOptions>(defaultListingPublishOptions);
|
|
|
|
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
|
|
|
|
|
|
|
|
|
function isNavActive(path: string) {
|
|
|
|
|
if (path === "/m") return route.path === "/m";
|
|
|
|
@@ -74,43 +71,20 @@ function defaultForm(): PublishForm {
|
|
|
|
|
ban_record: "",
|
|
|
|
|
common_regions: [],
|
|
|
|
|
deposit_amount: "",
|
|
|
|
|
ratio: "",
|
|
|
|
|
final_price: 0,
|
|
|
|
|
rent_days: 0,
|
|
|
|
|
remark: "",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultQuantityValues(): Record<QuantityKey, number> {
|
|
|
|
|
return {
|
|
|
|
|
awmAmmo: 0,
|
|
|
|
|
helmet6: 0,
|
|
|
|
|
armor6: 0,
|
|
|
|
|
kit5: 0,
|
|
|
|
|
level6Ammo: 0,
|
|
|
|
|
gridCard9: 0,
|
|
|
|
|
};
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultQuantityModes(): Record<QuantityKey, ChargeMode> {
|
|
|
|
|
return {
|
|
|
|
|
awmAmmo: "收费",
|
|
|
|
|
helmet6: "收费",
|
|
|
|
|
armor6: "收费",
|
|
|
|
|
kit5: "收费",
|
|
|
|
|
level6Ammo: "收费",
|
|
|
|
|
gridCard9: "收费",
|
|
|
|
|
};
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultScreenshotFiles(): Record<ScreenshotKey, string> {
|
|
|
|
|
return {
|
|
|
|
|
coin: "",
|
|
|
|
|
gameId: "",
|
|
|
|
|
totalAsset: "",
|
|
|
|
|
tencentSecurity: "",
|
|
|
|
|
skin: "",
|
|
|
|
|
};
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const form = reactive<PublishForm>(defaultForm());
|
|
|
|
@@ -138,10 +112,17 @@ const skinGroups = computed(() => publishOptions.value.skin_groups);
|
|
|
|
|
const quantityItems = computed(() => publishOptions.value.quantity_items);
|
|
|
|
|
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
|
|
|
|
|
const screenshotUrls = computed(() =>
|
|
|
|
|
screenshotSlots.value.map((item) => screenshotFiles[item.key]).filter(Boolean)
|
|
|
|
|
screenshotSlots.value
|
|
|
|
|
.map((item) => screenshotFiles[item.key])
|
|
|
|
|
.filter((url): url is string => Boolean(url))
|
|
|
|
|
);
|
|
|
|
|
const dailyLossText = computed(() =>
|
|
|
|
|
form.rent_days ? calculateDailyLoss(Number(form.haf_coin_amount || 0)) : ""
|
|
|
|
|
const coinWanAmount = computed(() => Number(form.haf_coin_amount || 0));
|
|
|
|
|
const calculatedRatio = computed(() => calculatePublishRatio());
|
|
|
|
|
const calculatedFinalPrice = computed(() =>
|
|
|
|
|
calculatedRatio.value > 0 ? roundMoney(coinWanAmount.value / calculatedRatio.value) : 0
|
|
|
|
|
);
|
|
|
|
|
const calculatedRentDays = computed(() =>
|
|
|
|
|
coinWanAmount.value > 0 ? calculateRentDays(coinWanAmount.value) : 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
@@ -161,7 +142,7 @@ async function loadPublishOptions() {
|
|
|
|
|
try {
|
|
|
|
|
publishOptions.value = await fetchListingPublishOptions();
|
|
|
|
|
} catch {
|
|
|
|
|
publishOptions.value = defaultListingPublishOptions;
|
|
|
|
|
publishOptions.value = emptyListingPublishOptions;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -188,10 +169,7 @@ function restoreDraft() {
|
|
|
|
|
if (!raw) return;
|
|
|
|
|
try {
|
|
|
|
|
const draft = JSON.parse(raw) as Partial<PublishDraft>;
|
|
|
|
|
Object.assign(form, defaultForm(), draft.form || {});
|
|
|
|
|
form.common_regions = Array.isArray(draft.form?.common_regions)
|
|
|
|
|
? [...draft.form.common_regions]
|
|
|
|
|
: [];
|
|
|
|
|
Object.assign(form, normalizeDraftForm(draft.form));
|
|
|
|
|
Object.assign(quantityValues, defaultQuantityValues(), draft.quantityValues || {});
|
|
|
|
|
Object.assign(quantityModes, defaultQuantityModes(), draft.quantityModes || {});
|
|
|
|
|
Object.assign(screenshotFiles, defaultScreenshotFiles(), draft.screenshotFiles || {});
|
|
|
|
@@ -203,6 +181,27 @@ function restoreDraft() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeDraftForm(value: unknown): PublishForm {
|
|
|
|
|
const next = defaultForm();
|
|
|
|
|
if (!isRecord(value)) return next;
|
|
|
|
|
|
|
|
|
|
for (const key of Object.keys(next) as Array<keyof PublishForm>) {
|
|
|
|
|
if (key === "common_regions") continue;
|
|
|
|
|
const draftValue = value[key];
|
|
|
|
|
if (draftValue !== undefined) {
|
|
|
|
|
next[key] = draftValue as never;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
next.common_regions = Array.isArray(value.common_regions)
|
|
|
|
|
? value.common_regions.filter((region): region is string => typeof region === "string")
|
|
|
|
|
: [];
|
|
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
return typeof value === "object" && value !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetDraftState() {
|
|
|
|
|
Object.assign(form, defaultForm());
|
|
|
|
|
Object.assign(quantityValues, defaultQuantityValues());
|
|
|
|
@@ -288,23 +287,6 @@ function handleFireLevelInput(value: string | number) {
|
|
|
|
|
form.fire_level = Math.trunc(level);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculatePrice() {
|
|
|
|
|
const coinWan = Number(form.haf_coin_amount || 0);
|
|
|
|
|
const ratio = Number(form.ratio || 0);
|
|
|
|
|
if (coinWan <= 0) {
|
|
|
|
|
showToast({ message: "请先填写哈夫币/万", icon: "warning-o" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (ratio <= 0) {
|
|
|
|
|
showToast({ message: "比例需大于0", icon: "warning-o" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const finalPrice = roundMoney(coinWan / ratio);
|
|
|
|
|
const rentDays = coinWan >= 30000 ? 7 : coinWan >= 10000 ? 3 : 1;
|
|
|
|
|
form.final_price = finalPrice;
|
|
|
|
|
form.rent_days = rentDays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSubmit() {
|
|
|
|
|
const error = validateForm();
|
|
|
|
|
if (error) {
|
|
|
|
@@ -314,8 +296,8 @@ async function handleSubmit() {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}万哈夫币`;
|
|
|
|
|
const rentDays = Math.max(Number(form.rent_days || 1), 1);
|
|
|
|
|
const dailyPrice = roundMoney(Number(form.final_price) / rentDays);
|
|
|
|
|
const rentDays = Math.max(calculatedRentDays.value, 1);
|
|
|
|
|
const dailyPrice = roundMoney(calculatedFinalPrice.value / rentDays);
|
|
|
|
|
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
|
|
|
|
|
|
|
|
|
|
await createListing({
|
|
|
|
@@ -359,8 +341,8 @@ function validateForm() {
|
|
|
|
|
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
|
|
|
|
return "请填写押金";
|
|
|
|
|
}
|
|
|
|
|
if (!form.final_price || !form.rent_days) {
|
|
|
|
|
return "请先点击计算价格";
|
|
|
|
|
if (!calculatedFinalPrice.value || !calculatedRentDays.value) {
|
|
|
|
|
return "请完善币数、保险、体力和负重后再发布";
|
|
|
|
|
}
|
|
|
|
|
for (const item of screenshotSlots.value) {
|
|
|
|
|
if (item.required && !screenshotFiles[item.key]) {
|
|
|
|
@@ -382,7 +364,7 @@ function buildAssetSummary() {
|
|
|
|
|
key: item.key,
|
|
|
|
|
label: item.label,
|
|
|
|
|
quantity: Number(quantityValues[item.key] || 0),
|
|
|
|
|
mode: quantityModes[item.key],
|
|
|
|
|
mode: quantityModes[item.key] || "收费",
|
|
|
|
|
})),
|
|
|
|
|
skin_groups: skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
|
|
|
|
|
groups[group.key] = group.options.filter((skin) =>
|
|
|
|
@@ -404,10 +386,79 @@ function roundMoney(value: number) {
|
|
|
|
|
return Math.round(value * 100) / 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateDailyLoss(coinWan: number) {
|
|
|
|
|
if (coinWan >= 30000) return "30M";
|
|
|
|
|
if (coinWan >= 10000) return "20M";
|
|
|
|
|
return "10M";
|
|
|
|
|
function calculatePublishRatio() {
|
|
|
|
|
if (
|
|
|
|
|
coinWanAmount.value <= 0 ||
|
|
|
|
|
!form.season_insurance ||
|
|
|
|
|
!form.stamina_level ||
|
|
|
|
|
!form.load_level
|
|
|
|
|
) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const baseRatio = getInsuranceBaseRatio(form.season_insurance);
|
|
|
|
|
if (baseRatio <= 0) return 0;
|
|
|
|
|
|
|
|
|
|
return baseRatio + (5 - getConfigHitCount()) + getCoinCorrection(coinWanAmount.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getInsuranceBaseRatio(insurance: string) {
|
|
|
|
|
const slots = getInsuranceSlots(insurance);
|
|
|
|
|
const ratioBySlots: Record<number, number> = {
|
|
|
|
|
9: 40,
|
|
|
|
|
6: 42,
|
|
|
|
|
4: 47,
|
|
|
|
|
2: 48,
|
|
|
|
|
};
|
|
|
|
|
return ratioBySlots[slots] || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getInsuranceSlots(insurance: string) {
|
|
|
|
|
const match = insurance.match(/^(\d+)\*(\d+)$/);
|
|
|
|
|
if (!match) return 0;
|
|
|
|
|
return Number(match[1]) * Number(match[2]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getConfigHitCount() {
|
|
|
|
|
const checks = [
|
|
|
|
|
hasSelectedSkinGroup("operator"),
|
|
|
|
|
hasSelectedSkinGroup("melee"),
|
|
|
|
|
isMaxLevel(form.stamina_level),
|
|
|
|
|
isMaxLevel(form.load_level),
|
|
|
|
|
hasSelectedSkinGroup("weapon"),
|
|
|
|
|
];
|
|
|
|
|
return checks.filter(Boolean).length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasSelectedSkinGroup(groupKey: string) {
|
|
|
|
|
const group = skinGroups.value.find((item) => item.key === groupKey);
|
|
|
|
|
if (!group) return false;
|
|
|
|
|
return group.options.some((skin) => selectedSkins.value.includes(skin));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isMaxLevel(value: string) {
|
|
|
|
|
const currentLevel = readLevelNumber(value);
|
|
|
|
|
const maxLevel = Math.max(...levelOptions.value.map(readLevelNumber).filter(Boolean));
|
|
|
|
|
if (currentLevel > 0 && maxLevel > 0) return currentLevel >= maxLevel;
|
|
|
|
|
return value === levelOptions.value[levelOptions.value.length - 1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readLevelNumber(value: string) {
|
|
|
|
|
const match = value.match(/\d+/);
|
|
|
|
|
return match ? Number(match[0]) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCoinCorrection(coinWan: number) {
|
|
|
|
|
if (coinWan > 53000) return 5;
|
|
|
|
|
if (coinWan > 38000) return 4;
|
|
|
|
|
if (coinWan > 23000) return 2.5;
|
|
|
|
|
if (coinWan > 13000) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateRentDays(coinWan: number) {
|
|
|
|
|
if (coinWan >= 30000) return 7;
|
|
|
|
|
if (coinWan >= 10000) return 3;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readError(error: unknown, fallback: string) {
|
|
|
|
@@ -562,7 +613,7 @@ function readError(error: unknown, fallback: string) {
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
<h2 class="section-title">资源道具</h2>
|
|
|
|
|
<h2 class="section-title">额外消耗品</h2>
|
|
|
|
|
<div v-for="item in quantityItems" :key="item.key" class="quantity-row">
|
|
|
|
|
<div class="quantity-card">
|
|
|
|
|
<div class="quantity-meta">
|
|
|
|
@@ -588,7 +639,7 @@ function readError(error: unknown, fallback: string) {
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ active: quantityModes[item.key] === '收费' }"
|
|
|
|
|
:class="{ active: (quantityModes[item.key] || '收费') === '收费' }"
|
|
|
|
|
@click="quantityModes[item.key] = '收费'"
|
|
|
|
|
>
|
|
|
|
|
收费
|
|
|
|
@@ -731,7 +782,7 @@ function readError(error: unknown, fallback: string) {
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
<h2 class="section-title">价格结算</h2>
|
|
|
|
|
<h2 class="section-title">押金与价格</h2>
|
|
|
|
|
<van-field
|
|
|
|
|
v-model="form.deposit_amount"
|
|
|
|
|
label="押金"
|
|
|
|
@@ -741,49 +792,13 @@ function readError(error: unknown, fallback: string) {
|
|
|
|
|
suffix="元"
|
|
|
|
|
class="publish-field"
|
|
|
|
|
/>
|
|
|
|
|
<van-field
|
|
|
|
|
v-model="form.ratio"
|
|
|
|
|
label="比例"
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="完成基础信息填写后请点击计算比例价格 1:请点击计算价格获取比例,最低比例0-建议6,比例越高出的速度越快,自行选择"
|
|
|
|
|
class="publish-field"
|
|
|
|
|
>
|
|
|
|
|
<template #left-icon>1:</template>
|
|
|
|
|
</van-field>
|
|
|
|
|
|
|
|
|
|
<van-button block class="calc-btn" @click="calculatePrice">
|
|
|
|
|
计算价格
|
|
|
|
|
</van-button>
|
|
|
|
|
|
|
|
|
|
<div class="result-grid">
|
|
|
|
|
<van-field
|
|
|
|
|
:model-value="form.final_price ? `¥${form.final_price}` : ''"
|
|
|
|
|
:model-value="calculatedFinalPrice ? `¥${calculatedFinalPrice}` : ''"
|
|
|
|
|
label="价格"
|
|
|
|
|
readonly
|
|
|
|
|
required
|
|
|
|
|
placeholder="请点击计算价格"
|
|
|
|
|
class="publish-field result-field"
|
|
|
|
|
/>
|
|
|
|
|
<van-field
|
|
|
|
|
:model-value="form.final_price ? `¥${form.final_price}` : ''"
|
|
|
|
|
label="结算金额"
|
|
|
|
|
readonly
|
|
|
|
|
required
|
|
|
|
|
placeholder="请点击计算价格获取结算金额"
|
|
|
|
|
class="publish-field result-field"
|
|
|
|
|
/>
|
|
|
|
|
<van-field
|
|
|
|
|
:model-value="form.rent_days ? `${form.rent_days}天` : ''"
|
|
|
|
|
label="租期"
|
|
|
|
|
readonly
|
|
|
|
|
placeholder="自动计算后显示"
|
|
|
|
|
class="publish-field result-field"
|
|
|
|
|
/>
|
|
|
|
|
<van-field
|
|
|
|
|
:model-value="dailyLossText"
|
|
|
|
|
label="每日损耗"
|
|
|
|
|
readonly
|
|
|
|
|
placeholder="自动计算 10M/20M/30M"
|
|
|
|
|
placeholder="填写币数、保险、体力和负重后自动计算"
|
|
|
|
|
class="publish-field result-field"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|