优化历史包袱,修复发布页面默认0 恢哈伏笔
This commit is contained in:
@@ -17,6 +17,7 @@ type ListingDTO struct {
|
||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||
AssetSummary map[string]any `json:"asset_summary,omitempty"`
|
||||
ScreenshotURLS []string `json:"screenshot_urls"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
PriceHourly float64 `json:"price_hourly"`
|
||||
PriceDaily float64 `json:"price_daily"`
|
||||
PriceWeekly float64 `json:"price_weekly"`
|
||||
|
||||
@@ -448,6 +448,8 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
|
||||
}
|
||||
|
||||
func (row listingRow) toDTO() ListingDTO {
|
||||
assetSummary := decodeAssetSummary(row.AssetSummary)
|
||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
|
||||
return ListingDTO{
|
||||
ID: row.ID,
|
||||
AccountID: row.AccountID,
|
||||
@@ -461,8 +463,9 @@ func (row listingRow) toDTO() ListingDTO {
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
RankLevel: row.RankLevel,
|
||||
HafCoinAmount: row.HafCoinAmount,
|
||||
AssetSummary: decodeAssetSummary(row.AssetSummary),
|
||||
ScreenshotURLS: decodeScreenshots(row.ScreenshotURLS),
|
||||
AssetSummary: assetSummary,
|
||||
ScreenshotURLS: screenshotURLS,
|
||||
CoverURL: firstScreenshotURL(screenshotURLS),
|
||||
PriceHourly: row.PriceHourly,
|
||||
PriceDaily: row.PriceDaily,
|
||||
PriceWeekly: row.PriceWeekly,
|
||||
@@ -479,6 +482,8 @@ func (row listingRow) toDTO() ListingDTO {
|
||||
}
|
||||
|
||||
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||
assetSummary := decodeAssetSummary(account.AssetSummary)
|
||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
|
||||
return &ListingDTO{
|
||||
ID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
@@ -490,8 +495,9 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||
LoginPlatform: account.LoginPlatform,
|
||||
RankLevel: account.RankLevel,
|
||||
HafCoinAmount: account.HafCoinAmount,
|
||||
AssetSummary: decodeAssetSummary(account.AssetSummary),
|
||||
ScreenshotURLS: decodeScreenshots(account.ScreenshotURLS),
|
||||
AssetSummary: assetSummary,
|
||||
ScreenshotURLS: screenshotURLS,
|
||||
CoverURL: firstScreenshotURL(screenshotURLS),
|
||||
PriceHourly: listing.PriceHourly,
|
||||
PriceDaily: listing.PriceDaily,
|
||||
PriceWeekly: listing.PriceWeekly,
|
||||
@@ -508,21 +514,9 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||
}
|
||||
|
||||
func marshalScreenshots(urls []string) (datatypes.JSON, error) {
|
||||
cleaned := make([]string, 0, len(urls))
|
||||
seen := make(map[string]struct{}, len(urls))
|
||||
for _, url := range urls {
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[url]; ok {
|
||||
continue
|
||||
}
|
||||
seen[url] = struct{}{}
|
||||
cleaned = append(cleaned, url)
|
||||
if len(cleaned) >= 12 {
|
||||
break
|
||||
}
|
||||
cleaned := cleanScreenshotURLs(urls)
|
||||
if len(cleaned) > 12 {
|
||||
cleaned = cleaned[:12]
|
||||
}
|
||||
raw, err := json.Marshal(cleaned)
|
||||
if err != nil {
|
||||
@@ -564,6 +558,30 @@ func decodeAssetSummary(raw datatypes.JSON) map[string]any {
|
||||
return summary
|
||||
}
|
||||
|
||||
func cleanScreenshotURLs(urls []string) []string {
|
||||
cleaned := make([]string, 0, len(urls))
|
||||
seen := make(map[string]struct{}, len(urls))
|
||||
for _, url := range urls {
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[url]; ok {
|
||||
continue
|
||||
}
|
||||
seen[url] = struct{}{}
|
||||
cleaned = append(cleaned, url)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func firstScreenshotURL(urls []string) string {
|
||||
if len(urls) == 0 {
|
||||
return ""
|
||||
}
|
||||
return urls[0]
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
raw, err := json.Marshal(detail)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package listing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -151,5 +152,17 @@ func validateRequest(req CreateRequest) error {
|
||||
if req.HafCoinAmount < 0 {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
if !hasScreenshotURL(req.ScreenshotURLS) {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasScreenshotURL(urls []string) bool {
|
||||
for _, url := range urls {
|
||||
if strings.TrimSpace(url) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -127,16 +127,7 @@ export function mergeListingPublishOptions(options?: Partial<ListingPublishOptio
|
||||
login_method_options: options?.login_method_options?.length ? options.login_method_options : defaultListingPublishOptions.login_method_options,
|
||||
region_options: options?.region_options?.length ? options.region_options : defaultListingPublishOptions.region_options,
|
||||
skin_groups: options?.skin_groups?.length ? options.skin_groups : defaultListingPublishOptions.skin_groups,
|
||||
quantity_items: options?.quantity_items?.length
|
||||
? normalizeQuantityItems(options.quantity_items as PublishQuantityItem[])
|
||||
: defaultListingPublishOptions.quantity_items,
|
||||
quantity_items: options?.quantity_items?.length ? options.quantity_items as PublishQuantityItem[] : defaultListingPublishOptions.quantity_items,
|
||||
screenshot_slots: options?.screenshot_slots?.length ? options.screenshot_slots as PublishScreenshotSlot[] : defaultListingPublishOptions.screenshot_slots,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeQuantityItems(items: PublishQuantityItem[]): PublishQuantityItem[] {
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
key: String(item.key) === 'armor6Ammo' ? 'level6Ammo' : item.key,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface Listing {
|
||||
haf_coin_amount: number
|
||||
asset_summary?: Record<string, unknown>
|
||||
screenshot_urls: string[]
|
||||
cover_url: string
|
||||
price_hourly: number
|
||||
price_daily: number
|
||||
price_weekly: number
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user