优化历史包袱,修复发布页面默认0 恢哈伏笔

This commit is contained in:
yml2213
2026-05-23 09:41:45 +08:00
parent d1f46fd02e
commit 40f5252b2e
12 changed files with 101 additions and 335 deletions
+6 -15
View File
@@ -17,7 +17,6 @@ import {
import MobileHomeFilterSheet, {
type FilterSection,
} from "./MobileHomeFilterSheet.vue";
import { buildFallbackListings } from "./mobileHomeFallback";
import {
assetRegions,
formatHafCoinM,
@@ -32,8 +31,6 @@ import {
getServerRegion,
getSkinGroup,
hasGiftResources,
normalizeLoginMethod,
normalizeServerRegion,
readAssetNumber,
readAssetString,
} from "./listingDisplay";
@@ -77,12 +74,6 @@ const rangePresets: Record<string, Array<{ label: string; min: string; max: stri
],
};
const fallbackListings = computed(() => buildFallbackListings(publishOptions.value));
const sourceListings = computed(() =>
loadFailed.value ? fallbackListings.value : listings.value
);
const activeSortLabel = computed(
() =>
sortOptions.find((option) => option.key === activeSort.value)?.label ||
@@ -92,7 +83,7 @@ const activeSortLabel = computed(
const serverFilterOptions = computed(() =>
uniqueOptions(
publishOptions.value.server_options
.map((item) => normalizeServerRegion(item))
.map((item) => item.trim())
.filter(Boolean)
)
);
@@ -100,7 +91,7 @@ const serverFilterOptions = computed(() =>
const loginMethodFilterOptions = computed(() =>
uniqueOptions(
publishOptions.value.login_method_options
.map((item) => normalizeLoginMethod(item))
.map((item) => item.trim())
.filter(Boolean)
)
);
@@ -145,7 +136,7 @@ const activeFilterCount = computed(() => {
const displayListings = computed(() => {
const keyword = searchValue.value.trim().toLowerCase();
const filtered = sourceListings.value.filter((item) => {
const filtered = listings.value.filter((item) => {
if (!matchesFilters(item)) return false;
if (!keyword) return true;
return searchText(item).includes(keyword);
@@ -432,7 +423,7 @@ function uniqueOptions(values: string[]) {
left-icon="info-o"
color="#6b7a90"
background="transparent"
text="接口暂不可用,当前展示移动端示例数据。"
text="接口暂不可用,请稍后刷新。"
/>
<van-empty
v-else-if="displayListings.length === 0"
@@ -454,8 +445,8 @@ function uniqueOptions(values: string[]) {
>
<div class="card-cover">
<img
v-if="item.screenshot_urls?.[0]"
:src="item.screenshot_urls[0]"
v-if="item.cover_url"
:src="item.cover_url"
:alt="getListingTitle(item)"
/>
<span v-else></span>
@@ -4,15 +4,14 @@ import { useRoute, useRouter } from "vue-router";
import { showToast, showDialog } from "vant";
import { fetchListing, type Listing } from "@/api/listings";
import { defaultListingPublishOptions } from "@/api/listingOptions";
import { createOrder } from "@/api/orders";
import { useSessionStore } from "@/stores/session";
import { buildFallbackListings } from "./mobileHomeFallback";
import {
assetRegions,
formatHafCoinM,
formatRatio,
getCoinWan,
getDailyLoss,
getListingChips,
getListingDisplayPrice,
getListingResources,
@@ -20,7 +19,6 @@ import {
getListingTitle,
getLoginMethod,
getRentDays,
getScreenshotMap,
getServerRegion,
readAssetNumber,
readAssetString,
@@ -40,15 +38,7 @@ onMounted(async () => {
listing.value = await fetchListing(String(route.params.id));
rentHours.value = Math.max(listing.value.min_rent_hours || 1, 1);
} catch {
const fallback = buildFallbackListings(defaultListingPublishOptions).find(
(item) => String(item.id) === String(route.params.id)
);
if (fallback) {
listing.value = fallback;
rentHours.value = Math.max(fallback.min_rent_hours || 1, 1);
} else {
showToast({ message: "加载失败", icon: "warning-o" });
}
showToast({ message: "加载失败", icon: "warning-o" });
} finally {
loading.value = false;
}
@@ -77,20 +67,9 @@ const detailMetrics = computed(() => {
const detailScreenshots = computed(() => {
if (!listing.value) return [];
const screenshotMap = getScreenshotMap(listing.value);
const labels: Record<string, string> = {
coin: "纯币截图",
gameId: "游戏ID",
totalAsset: "总资产",
tencentSecurity: "腾讯安全中心",
skin: "皮肤截图",
};
const mapped = Object.entries(screenshotMap)
.filter(([, url]) => typeof url === "string" && url)
.map(([key, url]) => ({ label: labels[key] || "账号截图", url: String(url) }));
if (mapped.length) return mapped;
const labels = ["纯币截图", "游戏ID截图", "总资产截图", "腾讯安全中心截图", "皮肤截图"];
return (listing.value.screenshot_urls || []).map((url, index) => ({
label: `账号截图${index + 1}`,
label: labels[index] || `账号截图${index + 1}`,
url,
}));
});
@@ -239,12 +218,12 @@ function isNavActive(path: string) {
listing.rank_level
}}</van-tag>
<van-tag
v-if="readAssetString(listing, 'daily_loss')"
v-if="getDailyLoss(listing)"
color="#fff7ed"
text-color="#ea580c"
size="medium"
>
损耗 {{ readAssetString(listing, "daily_loss") }}
损耗 {{ getDailyLoss(listing) }}
</van-tag>
</div>
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
@@ -28,7 +28,7 @@ function isNavActive(path: string) {
const form = reactive({
server_region: "",
face_owner: "",
haf_coin_amount: 0,
haf_coin_amount: "" as number | "",
rank_level: "",
secret_kd: "",
fire_level: "" as number | "",
@@ -43,9 +43,7 @@ const form = reactive({
deposit_amount: "" as number | "",
ratio: "" as number | "",
final_price: 0,
settlement_amount: 0,
rent_days: 0,
daily_loss: "",
remark: "",
});
@@ -94,6 +92,9 @@ const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
const screenshotUrls = computed(() =>
screenshotSlots.value.map((item) => screenshotFiles[item.key]).filter(Boolean)
);
const dailyLossText = computed(() =>
form.rent_days ? calculateDailyLoss(Number(form.haf_coin_amount || 0)) : ""
);
onMounted(loadPublishOptions);
@@ -174,9 +175,7 @@ function calculatePrice() {
const finalPrice = roundMoney(coinWan / ratio);
const rentDays = coinWan >= 30000 ? 7 : coinWan >= 10000 ? 3 : 1;
form.final_price = finalPrice;
form.settlement_amount = finalPrice;
form.rent_days = rentDays;
form.daily_loss = coinWan >= 30000 ? "30M" : coinWan >= 10000 ? "20M" : "10M";
}
async function handleSubmit() {
@@ -232,7 +231,7 @@ function validateForm() {
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
return "请填写押金";
}
if (!form.final_price || !form.settlement_amount || !form.rent_days) {
if (!form.final_price || !form.rent_days) {
return "请先点击计算价格";
}
for (const item of screenshotSlots.value) {
@@ -246,7 +245,6 @@ function validateForm() {
function buildAssetSummary() {
return {
face_owner: form.face_owner,
haf_coin_wan: Number(form.haf_coin_amount),
secret_kd: form.secret_kd,
fire_level: Number(form.fire_level),
season_insurance: form.season_insurance,
@@ -258,7 +256,6 @@ function buildAssetSummary() {
quantity: Number(quantityValues[item.key] || 0),
mode: quantityModes[item.key],
})),
skins: selectedSkins.value,
skin_groups: skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
groups[group.key] = group.options.filter((skin) =>
selectedSkins.value.includes(skin)
@@ -269,16 +266,8 @@ function buildAssetSummary() {
start: form.online_start,
end: form.online_end,
},
login_method: form.login_method,
ban_record: form.ban_record,
common_regions: form.common_regions,
screenshots: { ...screenshotFiles },
deposit_amount: Number(form.deposit_amount),
ratio: Number(form.ratio),
price: Number(form.final_price),
settlement_amount: Number(form.settlement_amount),
rent_days: Number(form.rent_days),
daily_loss: form.daily_loss,
remark: form.remark,
};
}
@@ -287,6 +276,12 @@ 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 readError(error: unknown, fallback: string) {
if (typeof error === "object" && error && "response" in error) {
const response = (error as { response?: { data?: { message?: string } } })
@@ -642,7 +637,7 @@ function readError(error: unknown, fallback: string) {
class="publish-field result-field"
/>
<van-field
:model-value="form.settlement_amount ? `¥${form.settlement_amount}` : ''"
:model-value="form.final_price ? `¥${form.final_price}` : ''"
label="结算金额"
readonly
required
@@ -657,7 +652,7 @@ function readError(error: unknown, fallback: string) {
class="publish-field result-field"
/>
<van-field
v-model="form.daily_loss"
:model-value="dailyLossText"
label="每日损耗"
readonly
placeholder="自动计算 10M/20M/30M"
+14 -49
View File
@@ -13,8 +13,6 @@ export interface ListingDisplayResource {
}
export function getCoinWan(item: Listing) {
const assetCoin = readAssetNumber(item, "haf_coin_wan");
if (assetCoin > 0) return assetCoin;
return Math.round(Number(item.haf_coin_amount || 0) / 10000);
}
@@ -29,8 +27,6 @@ export function formatHafCoinM(amountWan: number) {
}
export function getListingDisplayPrice(item: Listing) {
const assetPrice = readAssetNumber(item, "price");
if (assetPrice > 0) return assetPrice;
const rentDays = getRentDays(item);
if (rentDays > 0 && item.price_daily > 0) {
return roundMoney(item.price_daily * rentDays);
@@ -39,15 +35,11 @@ export function getListingDisplayPrice(item: Listing) {
}
export function getRentDays(item: Listing) {
const assetDays = readAssetNumber(item, "rent_days");
if (assetDays > 0) return assetDays;
if (item.max_rent_hours >= 24) return Math.max(Math.round(item.max_rent_hours / 24), 1);
return 1;
}
export function getRatioValue(item: Listing) {
const ratio = readAssetNumber(item, "ratio");
if (ratio > 0) return ratio;
const price = getListingDisplayPrice(item);
if (price <= 0) return 0;
return getCoinWan(item) / price;
@@ -65,11 +57,11 @@ export function getValuePerYuanText(item: Listing) {
}
export function getLoginMethod(item: Listing) {
return normalizeLoginMethod(readAssetString(item, "login_method") || item.login_platform);
return item.login_platform.trim();
}
export function getServerRegion(item: Listing) {
return normalizeServerRegion(item.server_region);
return item.server_region.trim();
}
export function getListingTitle(item: Listing) {
@@ -134,8 +126,7 @@ export function getListingResources(item: Listing): ListingDisplayResource[] {
}
export function getResourceQuantity(item: Listing, resourceKey: string) {
const keys = getResourceKeyAliases(resourceKey);
return getListingResources(item).find((resource) => keys.includes(resource.key))?.quantity || 0;
return getListingResources(item).find((resource) => resource.key === resourceKey)?.quantity || 0;
}
export function hasGiftResources(item: Listing) {
@@ -158,13 +149,10 @@ export function getSkinGroup(item: Listing, groupKey: string) {
export function getSkinNames(item: Listing) {
const skinGroups = item.asset_summary?.skin_groups;
if (typeof skinGroups === "object" && skinGroups !== null) {
return Object.values(skinGroups as Record<string, unknown>)
.flatMap((group) => (Array.isArray(group) ? group : []))
.filter((skin): skin is string => typeof skin === "string");
}
const skins = item.asset_summary?.skins;
return Array.isArray(skins) ? skins.filter((skin): skin is string => typeof skin === "string") : [];
if (typeof skinGroups !== "object" || skinGroups === null) return [];
return Object.values(skinGroups as Record<string, unknown>)
.flatMap((group) => (Array.isArray(group) ? group : []))
.filter((skin): skin is string => typeof skin === "string");
}
export function assetRegions(item: Listing) {
@@ -174,13 +162,6 @@ export function assetRegions(item: Listing) {
: [];
}
export function getScreenshotMap(item: Listing) {
const screenshots = item.asset_summary?.screenshots;
return typeof screenshots === "object" && screenshots !== null
? (screenshots as Record<string, unknown>)
: {};
}
export function getOnlineTimeText(item: Listing) {
const onlineTime = item.asset_summary?.online_time;
if (typeof onlineTime !== "object" || onlineTime === null) return "";
@@ -190,6 +171,13 @@ export function getOnlineTimeText(item: Listing) {
return `${start.replace(":00", "")}-${end.replace(":00", "")}`;
}
export function getDailyLoss(item: Listing) {
const coinWan = getCoinWan(item);
if (coinWan >= 30000) return "30M";
if (coinWan >= 10000) return "20M";
return "10M";
}
export function readAssetString(item: Listing, key: string) {
const value = item.asset_summary?.[key];
return typeof value === "string" ? value : "";
@@ -208,23 +196,6 @@ function readUnknownNumber(value: unknown) {
return 0;
}
export function normalizeServerRegion(value: string) {
const text = value.trim();
const upper = text.toUpperCase();
if (upper.includes("STEAM")) return "Steam";
if (upper.includes("QQ")) return "QQ";
if (text.includes("微信")) return "微信";
return text;
}
export function normalizeLoginMethod(value: string) {
const text = value.trim();
if (!text) return "";
if (text.includes("扫码")) return "扫码";
if (text.includes("账密") || text.includes("账号密码")) return "账号密码";
return text;
}
function formatInsuranceSlotText(value: string) {
const parts = value.split("*").map((item) => Number(item));
const rows = parts[0] || 0;
@@ -245,12 +216,6 @@ function formatResourceShort(item: Listing, key: string, label: string) {
return quantity > 0 ? `${quantity}${label}` : "";
}
function getResourceKeyAliases(key: string) {
if (key === "level6Ammo") return ["level6Ammo", "armor6Ammo"];
if (key === "armor6Ammo") return ["armor6Ammo", "level6Ammo"];
return [key];
}
function roundMoney(value: number) {
return Math.round(value * 100) / 100;
}
@@ -1,177 +0,0 @@
import {
defaultListingPublishOptions,
type ListingPublishOptions,
} from "@/api/listingOptions";
import type { Listing } from "@/api/listings";
const fallbackRegionOptions = ["广东", "上海", "江苏", "四川", "北京", "浙江"];
const coinWanOptions = [
1800, 3200, 5800, 9600, 12000, 18000, 26000, 36000, 52000, 68000,
];
const depositOptions = [60, 80, 100, 120, 160, 200];
const secretKdOptions = [1.2, 1.6, 2.1, 2.8, 3.4, 4.2];
const onlineWindows = [
["09:00", "23:30"],
["10:30", "01:00"],
["12:00", "24:00"],
["18:00", "02:00"],
] as const;
const resourceQuantityPresets: Record<string, number[]> = {
awmAmmo: [0, 20, 48, 80, 120, 180],
helmet6: [0, 2, 4, 6, 10, 16],
armor6: [0, 1, 3, 5, 8, 12],
kit5: [0, 3, 6, 10, 15, 24],
level6Ammo: [0, 120, 240, 360, 520, 800],
gridCard9: [0, 1, 2, 4, 6, 10],
};
function pick<T>(items: readonly T[], index: number, fallback: T): T {
if (!items.length) return fallback;
return items[index % items.length] ?? fallback;
}
function pickMany(items: string[], start: number, count: number) {
if (!items.length) return [];
const fallback = items[0] || "";
return Array.from({ length: Math.min(count, items.length) }, (_, idx) =>
pick(items, start + idx, fallback)
).filter(Boolean);
}
function buildResources(options: ListingPublishOptions, seed: number) {
return options.quantity_items.map((item, index) => ({
key: item.key,
label: item.label,
quantity: pick(
resourceQuantityPresets[item.key] || [0, 1, 3, 6],
seed + index,
0
),
mode: "收费",
}));
}
function buildSkinGroups(options: ListingPublishOptions, seed: number) {
return options.skin_groups.reduce<Record<string, string[]>>((groups, group, index) => {
const count = group.key === "melee" ? 2 : index % 2 === 0 ? 3 : 2;
groups[group.key] = pickMany(group.options, seed + index * 2, count);
return groups;
}, {});
}
function buildScreenshotMap(options: ListingPublishOptions, seed: number) {
return options.screenshot_slots.reduce<Record<string, string>>((screenshots, slot, index) => {
screenshots[slot.key] = `https://picsum.photos/seed/hfb-${slot.key}-${seed + index}/960/540`;
return screenshots;
}, {});
}
function flattenSkinGroups(groups: Record<string, string[]>) {
return Object.values(groups).flat();
}
function calculateRentDays(coinWan: number) {
return coinWan >= 30000 ? 7 : coinWan >= 10000 ? 3 : 1;
}
function calculateDailyLoss(coinWan: number) {
return coinWan >= 30000 ? "30M" : coinWan >= 10000 ? "20M" : "10M";
}
function roundMoney(value: number) {
return Math.round(value * 100) / 100;
}
function formatHafCoinM(coinWan: number) {
const coinM = Math.round((coinWan / 100) * 10) / 10;
return `${Number.isInteger(coinM) ? coinM : coinM.toFixed(1)}M`;
}
export function buildFallbackListings(
options: ListingPublishOptions = defaultListingPublishOptions
): Listing[] {
return Array.from({ length: 10 }, (_, index) => {
const seed = index + 1;
const server = pick(options.server_options, index, "QQ");
const loginMethod = pick(options.login_method_options, index + 1, "账号密码");
const rank = pick(options.rank_options, index + 3, "钻石");
const insurance = pick(options.insurance_options, index + 1, "2*2");
const stamina = pick(options.level_options, index + 4, "5级");
const load = pick(options.level_options, index + 5, "5级");
const faceOwner = pick(options.face_options, index, "是");
const coinWan = pick(coinWanOptions, index, 12000);
const ratio = 5 + (index % 4);
const rentDays = calculateRentDays(coinWan);
const finalPrice = roundMoney(coinWan / ratio);
const settlementAmount = finalPrice;
const dailyPrice = roundMoney(finalPrice / rentDays);
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
const deposit = pick(depositOptions, index, 100);
const skinGroups = buildSkinGroups(options, seed);
const screenshots = buildScreenshotMap(options, seed);
const commonRegions = pickMany(
options.region_options.length ? options.region_options : fallbackRegionOptions,
index * 2,
2
);
const [onlineStart, onlineEnd] = pick(onlineWindows, index, onlineWindows[0]);
return {
id: 9100 + seed,
account_id: 0,
owner_id: 0,
title: `${server} ${rank} ${formatHafCoinM(coinWan)}哈夫币`,
description: `${loginMethod}交接,${insurance}保险,体力${stamina}、负重${load},示例数据按发布表单随机生成。`,
game_name: "三角洲行动",
server_region: server,
login_platform: loginMethod,
rank_level: rank,
haf_coin_amount: coinWan * 10000,
asset_summary: {
face_owner: faceOwner,
haf_coin_wan: coinWan,
total_asset_wan: Math.round(coinWan * (1.08 + (index % 4) * 0.04)),
secret_kd: pick(secretKdOptions, index, 1.6),
fire_level: 38 + seed * 3,
season_insurance: insurance,
stamina_level: stamina,
load_level: load,
daily_loss: calculateDailyLoss(coinWan),
ratio,
login_method: loginMethod,
common_regions: commonRegions,
resources: buildResources(options, seed),
skins: flattenSkinGroups(skinGroups),
skin_groups: skinGroups,
online_time: {
start: onlineStart,
end: onlineEnd,
},
ban_record: index % 4 === 0 ? "无封禁记录,近期登录稳定" : "",
screenshots,
deposit_amount: deposit,
price: finalPrice,
settlement_amount: settlementAmount,
rent_days: rentDays,
remark:
index % 3 === 0
? "部分仓库材料请勿使用,下单后会在群聊再次提醒。"
: "",
},
screenshot_urls: Object.values(screenshots),
price_hourly: hourlyPrice,
price_daily: dailyPrice,
price_weekly: Math.round(dailyPrice * 7 * 10) / 10,
deposit_amount: deposit,
min_rent_hours: coinWan >= 10000 ? 24 : 8,
max_rent_hours: coinWan >= 30000 ? 168 : 72,
status: "published",
review_status: "approved",
review_reason: "",
published_at: new Date(Date.now() - seed * 18 * 60 * 1000).toISOString(),
created_at: new Date(Date.now() - seed * 20 * 60 * 1000).toISOString(),
updated_at: new Date(Date.now() - seed * 10 * 60 * 1000).toISOString(),
};
});
}
+7 -18
View File
@@ -20,7 +20,6 @@ import {
type ListingPublishOptions,
} from "@/api/listingOptions";
import { fetchListings, type Listing } from "@/api/listings";
import { buildFallbackListings } from "@/views/mobile/mobileHomeFallback";
import {
formatHafCoinM,
getCoinM,
@@ -32,12 +31,9 @@ import {
getLoginMethod,
getServerRegion,
hasGiftResources,
normalizeLoginMethod,
normalizeServerRegion,
} from "@/views/mobile/listingDisplay";
const loading = ref(false);
const loadFailed = ref(false);
const listings = ref<Listing[]>([]);
const announcements = ref<string[]>(defaultHomeAnnouncements);
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners);
@@ -52,29 +48,24 @@ const filters = reactive({
});
const sortBy = ref("recommended");
const fallbackListings = computed(() => buildFallbackListings(publishOptions.value));
const sourceListings = computed(() =>
loadFailed.value ? fallbackListings.value : listings.value
);
const serverOptions = computed(() =>
uniqueOptions(
publishOptions.value.server_options
.map((item) => normalizeServerRegion(item))
.map((item) => item.trim())
.filter(Boolean)
)
);
const loginMethodOptions = computed(() =>
uniqueOptions(
publishOptions.value.login_method_options
.map((item) => normalizeLoginMethod(item))
.map((item) => item.trim())
.filter(Boolean)
)
);
const visibleListings = computed(() => {
const keyword = filters.keyword.trim().toLowerCase();
const filtered = sourceListings.value.filter((item) => {
const filtered = listings.value.filter((item) => {
const text = [
item.title,
item.description,
@@ -114,12 +105,12 @@ const statCards = computed(() => [
{ label: "可租账号", value: `${visibleListings.value.length}`, hint: "当前筛选结果" },
{
label: "高哈夫币",
value: `${sourceListings.value.filter((item) => getCoinM(item) >= 100).length}`,
value: `${listings.value.filter((item) => getCoinM(item) >= 100).length}`,
hint: "100M 以上",
},
{
label: "账密登录",
value: `${sourceListings.value.filter((item) => getLoginMethod(item) === "账号密码").length}`,
value: `${listings.value.filter((item) => getLoginMethod(item) === "账号密码").length}`,
hint: "交接更快",
},
]);
@@ -128,7 +119,6 @@ onMounted(loadHome);
async function loadHome() {
loading.value = true;
loadFailed.value = false;
try {
const [nextListings, config] = await Promise.all([
fetchListings(),
@@ -139,7 +129,6 @@ async function loadHome() {
banners.value = config.banners;
publishOptions.value = config.publish_options;
} catch {
loadFailed.value = true;
announcements.value = defaultHomeAnnouncements;
banners.value = defaultHomeBanners;
publishOptions.value = defaultListingPublishOptions;
@@ -328,8 +317,8 @@ function uniqueOptions(values: string[]) {
>
<div class="desktop-cover">
<img
v-if="item.screenshot_urls?.[0]"
:src="item.screenshot_urls[0]"
v-if="item.cover_url"
:src="item.cover_url"
:alt="getListingTitle(item)"
/>
<span v-else>HFB</span>
+2 -2
View File
@@ -179,8 +179,8 @@ function uniqueOptions(values: string[]) {
>
<div class="resource-cover">
<img
v-if="item.screenshot_urls?.[0]"
:src="item.screenshot_urls[0]"
v-if="item.cover_url"
:src="item.cover_url"
:alt="item.title"
/>
<span v-else>HFB</span>