优化首页分页和图片上传
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { RouterLink, useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import MobileBottomNav from "@/components/MobileBottomNav.vue";
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
emptyListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
} from "@/api/listingOptions";
|
||||
import { fetchListings, type Listing } from "@/api/listings";
|
||||
import { fetchListingsPage, type Listing, type PublicListingQuery } from "@/api/listings";
|
||||
import {
|
||||
defaultHomeAnnouncements,
|
||||
defaultHomeBanners,
|
||||
@@ -20,30 +20,26 @@ import MobileHomeFilterSheet, {
|
||||
type FilterSection,
|
||||
} from "./MobileHomeFilterSheet.vue";
|
||||
import {
|
||||
assetRegions,
|
||||
formatHafCoinM,
|
||||
getCoinM,
|
||||
getCoinWan,
|
||||
getListingChips,
|
||||
getListingDisplayPrice,
|
||||
getListingSubtitle,
|
||||
getListingTitle,
|
||||
getLoginMethod,
|
||||
getResourceQuantity,
|
||||
getServerRegion,
|
||||
getSkinGroup,
|
||||
hasAcceleratedSaleRatio,
|
||||
hasGiftResources,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
} from "@/utils/listingDisplay";
|
||||
import { useSessionStore } from "@/stores/session";
|
||||
|
||||
const router = useRouter();
|
||||
const session = useSessionStore();
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const listings = ref<Listing[]>([]);
|
||||
const totalListings = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const hasMoreListings = ref(true);
|
||||
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||||
const sortOpen = ref(false);
|
||||
const activeSort = ref("comprehensive");
|
||||
@@ -55,6 +51,8 @@ const searchValue = ref("");
|
||||
const announcements = ref<string[]>(defaultHomeAnnouncements);
|
||||
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners);
|
||||
const supportLoading = ref(false);
|
||||
const mobilePageSize = 10;
|
||||
let listingRequestSeq = 0;
|
||||
|
||||
const sortOptions = [
|
||||
{ key: "comprehensive", label: "综合排序" },
|
||||
@@ -159,29 +157,56 @@ const activeFilterCount = computed(() => {
|
||||
});
|
||||
|
||||
const displayListings = computed(() => {
|
||||
const keyword = searchValue.value.trim().toLowerCase();
|
||||
const filtered = listings.value.filter((item) => {
|
||||
if (!matchesFilters(item)) return false;
|
||||
if (!keyword) return true;
|
||||
return searchText(item).includes(keyword);
|
||||
});
|
||||
return sortListings(filtered);
|
||||
return listings.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadListings();
|
||||
loadHomeConfig();
|
||||
window.addEventListener("scroll", handleWindowScroll, { passive: true });
|
||||
});
|
||||
|
||||
async function loadListings() {
|
||||
loading.value = true;
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("scroll", handleWindowScroll);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => listingQuerySignature(),
|
||||
() => {
|
||||
loadListings(true);
|
||||
}
|
||||
);
|
||||
|
||||
async function loadListings(reset = true) {
|
||||
if ((loadingMore.value && !reset) || (!hasMoreListings.value && !reset)) return;
|
||||
const requestSeq = ++listingRequestSeq;
|
||||
if (reset) {
|
||||
loading.value = true;
|
||||
currentPage.value = 1;
|
||||
hasMoreListings.value = true;
|
||||
}
|
||||
loadingMore.value = true;
|
||||
loadFailed.value = false;
|
||||
try {
|
||||
listings.value = await fetchListings();
|
||||
const page = await fetchListingsPage(buildListingQuery(currentPage.value));
|
||||
if (requestSeq !== listingRequestSeq) return;
|
||||
listings.value = reset ? page.items : [...listings.value, ...page.items];
|
||||
totalListings.value = page.total;
|
||||
hasMoreListings.value = listings.value.length < page.total;
|
||||
currentPage.value = page.page + 1;
|
||||
requestAnimationFrame(handleWindowScroll);
|
||||
} catch {
|
||||
loadFailed.value = true;
|
||||
if (reset) {
|
||||
listings.value = [];
|
||||
totalListings.value = 0;
|
||||
hasMoreListings.value = false;
|
||||
loadFailed.value = true;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (requestSeq === listingRequestSeq) {
|
||||
loading.value = false;
|
||||
loadingMore.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,11 +226,10 @@ async function loadHomeConfig() {
|
||||
async function onRefresh() {
|
||||
refreshing.value = true;
|
||||
try {
|
||||
const [nextListings, nextHomeConfig] = await Promise.all([
|
||||
fetchListings(),
|
||||
const [, nextHomeConfig] = await Promise.all([
|
||||
loadListings(true),
|
||||
fetchMobileHomeConfig(),
|
||||
]);
|
||||
listings.value = nextListings;
|
||||
announcements.value = nextHomeConfig.announcements;
|
||||
bannerSlides.value = nextHomeConfig.banners;
|
||||
publishOptions.value = nextHomeConfig.publish_options;
|
||||
@@ -237,91 +261,70 @@ function clearFilters() {
|
||||
searchValue.value = "";
|
||||
}
|
||||
|
||||
function sortListings(items: Listing[]) {
|
||||
const sorted = [...items];
|
||||
if (activeSort.value === "comprehensive") {
|
||||
return sorted;
|
||||
}
|
||||
if (activeSort.value === "priceAsc") {
|
||||
return sorted.sort(
|
||||
(a, b) => getListingDisplayPrice(a) - getListingDisplayPrice(b)
|
||||
);
|
||||
}
|
||||
if (activeSort.value === "priceDesc") {
|
||||
return sorted.sort(
|
||||
(a, b) => getListingDisplayPrice(b) - getListingDisplayPrice(a)
|
||||
);
|
||||
}
|
||||
if (activeSort.value === "awmDesc") {
|
||||
return sorted.sort(
|
||||
(a, b) =>
|
||||
getResourceQuantity(b, "awmAmmo") - getResourceQuantity(a, "awmAmmo") ||
|
||||
getCoinWan(b) - getCoinWan(a)
|
||||
);
|
||||
}
|
||||
return sorted.sort((a, b) => {
|
||||
const bTime = Date.parse(b.published_at || b.created_at || "") || 0;
|
||||
const aTime = Date.parse(a.published_at || a.created_at || "") || 0;
|
||||
return bTime - aTime || b.id - a.id;
|
||||
});
|
||||
}
|
||||
|
||||
function matchesFilters(item: Listing) {
|
||||
const chipOk = Object.entries(selectedFilters.value).every(([key, values]) => {
|
||||
if (!values.length) return true;
|
||||
if (key === "server") return values.includes(getServerRegion(item));
|
||||
if (key === "login") return values.includes(getLoginMethod(item));
|
||||
if (key === "insurance") return values.includes(readAssetString(item, "season_insurance"));
|
||||
if (key === "stamina") return values.includes(readAssetString(item, "stamina_level"));
|
||||
if (key === "load") return values.includes(readAssetString(item, "load_level"));
|
||||
if (key === "rank") return values.includes(item.rank_level);
|
||||
if (isSkinGroupKey(key)) {
|
||||
return getSkinGroup(item, key).some((skin) => values.includes(skin));
|
||||
function buildListingQuery(page: number): PublicListingQuery {
|
||||
const query: PublicListingQuery = {
|
||||
page,
|
||||
page_size: mobilePageSize,
|
||||
keyword: searchValue.value.trim(),
|
||||
sort: activeSort.value,
|
||||
};
|
||||
const skinGroups: string[] = [];
|
||||
const skinNames: string[] = [];
|
||||
for (const [key, values] of Object.entries(selectedFilters.value)) {
|
||||
const value = values.filter(Boolean).join(",");
|
||||
if (!value) continue;
|
||||
if (key === "server") query.server = value;
|
||||
else if (key === "login") query.login_method = value;
|
||||
else if (key === "insurance") query.insurance = value;
|
||||
else if (key === "stamina") query.stamina = value;
|
||||
else if (key === "load") query.load = value;
|
||||
else if (key === "rank") query.rank = value;
|
||||
else if (isSkinGroupKey(key)) {
|
||||
skinGroups.push(key);
|
||||
skinNames.push(...values);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!chipOk) return false;
|
||||
}
|
||||
if (skinGroups.length) query.skin_group = skinGroups.join(",");
|
||||
if (skinNames.length) query.skin_name = skinNames.join(",");
|
||||
|
||||
return Object.entries(rangeFilters.value).every(([key, range]) => {
|
||||
if (!range.min && !range.max) return true;
|
||||
let value = 0;
|
||||
if (key === "price") value = getListingDisplayPrice(item);
|
||||
if (key === "coin") value = getCoinM(item);
|
||||
if (key === "secretKd") value = readAssetNumber(item, "secret_kd");
|
||||
if (key === "deposit") value = Number(item.deposit_amount || 0);
|
||||
if (key.startsWith("resource_")) {
|
||||
value = getResourceQuantity(item, key.replace("resource_", ""));
|
||||
for (const [key, range] of Object.entries(rangeFilters.value)) {
|
||||
if (!range.min && !range.max) continue;
|
||||
const min = parseOptionalNumber(range.min);
|
||||
const max = parseOptionalNumber(range.max);
|
||||
if (key === "price") {
|
||||
query.min_price = min;
|
||||
query.max_price = max;
|
||||
} else if (key === "coin") {
|
||||
query.min_coin = min;
|
||||
query.max_coin = max;
|
||||
} else if (key === "secretKd") {
|
||||
query.min_secret_kd = min;
|
||||
query.max_secret_kd = max;
|
||||
} else if (key === "deposit") {
|
||||
query.min_deposit = min;
|
||||
query.max_deposit = max;
|
||||
} else if (key.startsWith("resource_")) {
|
||||
const resourceKey = key.replace("resource_", "");
|
||||
query[`resource_${resourceKey}_min`] = min;
|
||||
query[`resource_${resourceKey}_max`] = max;
|
||||
}
|
||||
return inRange(value, range);
|
||||
});
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
function inRange(value: number, range: { min: string; max: string }) {
|
||||
const min = range.min === "" ? undefined : Number(range.min);
|
||||
const max = range.max === "" ? undefined : Number(range.max);
|
||||
if (min !== undefined && Number.isFinite(min) && value < min) return false;
|
||||
if (max !== undefined && Number.isFinite(max) && value > max) return false;
|
||||
return true;
|
||||
function listingQuerySignature() {
|
||||
return JSON.stringify(buildListingQuery(1));
|
||||
}
|
||||
|
||||
function searchText(item: Listing) {
|
||||
return [
|
||||
item.title,
|
||||
item.description,
|
||||
getServerRegion(item),
|
||||
getLoginMethod(item),
|
||||
item.rank_level,
|
||||
readAssetString(item, "season_insurance"),
|
||||
readAssetString(item, "stamina_level"),
|
||||
readAssetString(item, "load_level"),
|
||||
formatHafCoinM(getCoinWan(item)),
|
||||
assetRegions(item).join(" "),
|
||||
...publishOptions.value.skin_groups.map((group) =>
|
||||
getSkinGroup(item, group.key).join(" ")
|
||||
),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
function parseOptionalNumber(value: string) {
|
||||
if (value === "") return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : undefined;
|
||||
}
|
||||
|
||||
function handleWindowScroll() {
|
||||
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return;
|
||||
loadListings(false);
|
||||
}
|
||||
|
||||
function isSkinGroupKey(key: string) {
|
||||
@@ -437,7 +440,7 @@ function uniqueOptions(values: string[]) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="result-count">
|
||||
<strong>{{ displayListings.length }}</strong>
|
||||
<strong>{{ totalListings }}</strong>
|
||||
<span>个可租账号</span>
|
||||
</div>
|
||||
|
||||
@@ -475,6 +478,8 @@ function uniqueOptions(values: string[]) {
|
||||
v-if="item.cover_url"
|
||||
:src="item.cover_url"
|
||||
:alt="getListingTitle(item)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<span v-else>图</span>
|
||||
<div
|
||||
@@ -514,6 +519,10 @@ function uniqueOptions(values: string[]) {
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div v-if="!loading && displayListings.length" class="mobile-load-state">
|
||||
<span v-if="loadingMore">正在加载更多账号...</span>
|
||||
<span v-else-if="!hasMoreListings">已经到底了</span>
|
||||
</div>
|
||||
</section>
|
||||
</van-pull-refresh>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user