增加前端格式检查配置

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
@@ -1,12 +1,16 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchListing, type Listing } from "@/features/listings/api/listings";
import { createOrder, fetchOrderAgreements, type OrderAgreements } from "@/features/orders/api/orders";
import { useSessionStore } from "@/stores/session";
import { roundMoney, formatMoney } from "@/shared/utils/money";
import { fetchListing, type Listing } from '@/features/listings/api/listings'
import {
createOrder,
fetchOrderAgreements,
type OrderAgreements,
} from '@/features/orders/api/orders'
import { useSessionStore } from '@/stores/session'
import { roundMoney, formatMoney } from '@/shared/utils/money'
import {
assetRegions,
formatEstimatedRentalDuration,
@@ -26,45 +30,45 @@ import {
getServerRegion,
readAssetNumber,
readAssetString,
} from "@/utils/listingDisplay";
} from '@/utils/listingDisplay'
const route = useRoute();
const router = useRouter();
const session = useSessionStore();
const loading = ref(false);
const ordering = ref(false);
const agreementsLoading = ref(false);
const agreementVisible = ref(false);
const agreements = ref<OrderAgreements | null>(null);
const virtualAgreementRead = ref(false);
const renterAgreementRead = ref(false);
const virtualAgreementChecked = ref(false);
const renterAgreementChecked = ref(false);
const virtualAgreementRef = ref<HTMLElement | null>(null);
const renterAgreementRef = ref<HTMLElement | null>(null);
const route = useRoute()
const router = useRouter()
const session = useSessionStore()
const loading = ref(false)
const ordering = ref(false)
const agreementsLoading = ref(false)
const agreementVisible = ref(false)
const agreements = ref<OrderAgreements | null>(null)
const virtualAgreementRead = ref(false)
const renterAgreementRead = ref(false)
const virtualAgreementChecked = ref(false)
const renterAgreementChecked = ref(false)
const virtualAgreementRef = ref<HTMLElement | null>(null)
const renterAgreementRef = ref<HTMLElement | null>(null)
const canCreateOrderAfterAgreement = computed(
() =>
virtualAgreementRead.value &&
renterAgreementRead.value &&
virtualAgreementChecked.value &&
renterAgreementChecked.value,
);
const listing = ref<Listing | null>(null);
renterAgreementChecked.value
)
const listing = ref<Listing | null>(null)
onMounted(async () => {
loading.value = true;
loading.value = true
try {
listing.value = await fetchListing(String(route.params.id));
listing.value = await fetchListing(String(route.params.id))
} finally {
loading.value = false;
loading.value = false
}
});
})
const orderTotal = computed(() => {
if (!listing.value) return "0";
return formatMoney(getListingDisplayPrice(listing.value));
});
if (!listing.value) return '0'
return formatMoney(getListingDisplayPrice(listing.value))
})
const orderPriceBreakdown = computed(() => {
if (!listing.value) {
@@ -72,260 +76,253 @@ const orderPriceBreakdown = computed(() => {
rent: 0,
consumable: 0,
total: 0,
};
}
}
return {
rent: getListingRentPrice(listing.value),
consumable: getListingConsumablePrice(listing.value),
total: roundMoney(getListingDisplayPrice(listing.value)),
};
});
}
})
const coverURL = computed(() => {
if (!listing.value) return "";
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || "";
});
if (!listing.value) return ''
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || ''
})
const detailMetrics = computed(() => {
if (!listing.value) return [];
const dailyLoss = getDailyLoss(listing.value);
if (!listing.value) return []
const dailyLoss = getDailyLoss(listing.value)
return [
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)), tone: 'coin' },
{
label: "日损耗",
value: dailyLoss ? `${dailyLoss}/天` : "--",
tone: "coin",
label: '日损耗',
value: dailyLoss ? `${dailyLoss}/天` : '--',
tone: 'coin',
},
{ label: "价格", value: `¥${orderTotal.value}`, tone: "price" },
{ label: "押金", value: `¥${listing.value.deposit_amount}`, tone: "" },
];
});
{ label: '价格', value: `¥${orderTotal.value}`, tone: 'price' },
{ label: '押金', value: `¥${listing.value.deposit_amount}`, tone: '' },
]
})
const detailScreenshots = computed(() => {
if (!listing.value) return [];
const groupedScreenshots = readGroupedScreenshots(listing.value);
if (groupedScreenshots.length) return groupedScreenshots;
const labels = ["纯币截图", "游戏ID截图", "总资产截图", "腾讯安全中心截图", "皮肤截图"];
if (!listing.value) return []
const groupedScreenshots = readGroupedScreenshots(listing.value)
if (groupedScreenshots.length) return groupedScreenshots
const labels = ['纯币截图', '游戏ID截图', '总资产截图', '腾讯安全中心截图', '皮肤截图']
return (listing.value.screenshot_urls || []).map((url, index) => ({
label: labels[index] || `账号截图${index + 1}`,
url,
}));
});
}))
})
function readGroupedScreenshots(item: Listing) {
const groups = item.asset_summary?.screenshot_groups;
if (typeof groups !== "object" || groups === null) return [];
const groups = item.asset_summary?.screenshot_groups
if (typeof groups !== 'object' || groups === null) return []
const slots = [
{ key: "coin", label: "纯币截图" },
{ key: "gameId", label: "游戏ID截图" },
{ key: "totalAsset", label: "总资产截图" },
{ key: "tencentSecurity", label: "腾讯安全中心截图" },
{ key: "skin", label: "皮肤截图" },
];
return slots.flatMap((slot) => {
const urls = (groups as Record<string, unknown>)[slot.key];
if (!Array.isArray(urls)) return [];
const validUrls = urls.filter((url): url is string => typeof url === "string" && Boolean(url));
{ key: 'coin', label: '纯币截图' },
{ key: 'gameId', label: '游戏ID截图' },
{ key: 'totalAsset', label: '总资产截图' },
{ key: 'tencentSecurity', label: '腾讯安全中心截图' },
{ key: 'skin', label: '皮肤截图' },
]
return slots.flatMap(slot => {
const urls = (groups as Record<string, unknown>)[slot.key]
if (!Array.isArray(urls)) return []
const validUrls = urls.filter((url): url is string => typeof url === 'string' && Boolean(url))
return validUrls.map((url, index) => ({
label: validUrls.length > 1 ? `${slot.label}${index + 1}` : slot.label,
url,
}));
});
}))
})
}
const detailSkinGroups = computed(() => {
if (!listing.value) return [];
const groups = listing.value.asset_summary?.skin_groups;
if (typeof groups !== "object" || groups === null) return [];
if (!listing.value) return []
const groups = listing.value.asset_summary?.skin_groups
if (typeof groups !== 'object' || groups === null) return []
const titles: Record<string, string> = {
melee: "近战皮肤",
operator: "干员皮肤",
operatorGold: "干员金皮",
operatorRed: "干员红皮",
weapon: "武器皮肤",
};
melee: '近战皮肤',
operator: '干员皮肤',
operatorGold: '干员金皮',
operatorRed: '干员红皮',
weapon: '武器皮肤',
}
return Object.entries(groups as Record<string, unknown>)
.map(([key, value]) => ({
key,
title: titles[key] || key,
options: Array.isArray(value)
? value.filter((skin): skin is string => typeof skin === "string")
? value.filter((skin): skin is string => typeof skin === 'string')
: [],
}))
.filter((group) => group.options.length);
});
.filter(group => group.options.length)
})
const accountRows = computed(() => {
if (!listing.value) return [];
const regions = assetRegions(listing.value);
if (!listing.value) return []
const regions = assetRegions(listing.value)
return [
{ label: "所属区服", value: getServerRegion(listing.value) || "--" },
{ label: "上号方式", value: getLoginMethod(listing.value) || "--" },
{ label: "游戏段位", value: listing.value.rank_level || "--" },
{ label: "M单价", value: formatRatio(listing.value) },
{ label: "方便上号", value: getOnlineTimeText(listing.value) || "--" },
{ label: "预计可租", value: formatEstimatedRentalDuration(listing.value) },
{ label: "常用登录地", value: regions.length ? regions.join("、") : "--" },
{ label: "封禁记录", value: readAssetString(listing.value, "ban_record") || "无" },
];
});
{ label: '所属区服', value: getServerRegion(listing.value) || '--' },
{ label: '上号方式', value: getLoginMethod(listing.value) || '--' },
{ label: '游戏段位', value: listing.value.rank_level || '--' },
{ label: 'M单价', value: formatRatio(listing.value) },
{ label: '方便上号', value: getOnlineTimeText(listing.value) || '--' },
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
{ label: '常用登录地', value: regions.length ? regions.join('、') : '--' },
{ label: '封禁记录', value: readAssetString(listing.value, 'ban_record') || '无' },
]
})
async function handleCreateOrder() {
if (!listing.value) return;
if (!listing.value) return
if (!session.token) {
await router.push({ path: "/login", query: { redirect: route.fullPath } });
return;
await router.push({ path: '/login', query: { redirect: route.fullPath } })
return
}
if (session.realnameStatus !== "verified") {
if (session.realnameStatus !== 'verified') {
try {
await session.loadMe();
await session.loadMe()
} catch {
// 登录态失效时由全局请求拦截处理。
}
}
if (session.realnameStatus !== "verified") {
if (session.realnameStatus !== 'verified') {
try {
await ElMessageBox.confirm("租号下单前需要完成实名认证。", "请先实名认证", {
confirmButtonText: "去认证",
cancelButtonText: "取消",
type: "warning",
});
await router.push({ path: "/realname", query: { redirect: route.fullPath } });
await ElMessageBox.confirm('租号下单前需要完成实名认证。', '请先实名认证', {
confirmButtonText: '去认证',
cancelButtonText: '取消',
type: 'warning',
})
await router.push({ path: '/realname', query: { redirect: route.fullPath } })
} catch {
// 用户取消认证时停留在当前页面。
}
return;
return
}
await openAgreementBeforeOrder();
await openAgreementBeforeOrder()
}
async function openAgreementBeforeOrder() {
agreementsLoading.value = true;
agreementsLoading.value = true
try {
agreements.value = await fetchOrderAgreements();
virtualAgreementRead.value = false;
renterAgreementRead.value = false;
virtualAgreementChecked.value = false;
renterAgreementChecked.value = false;
agreementVisible.value = true;
await nextTick();
updateAgreementReadState("virtual");
updateAgreementReadState("renter");
agreements.value = await fetchOrderAgreements()
virtualAgreementRead.value = false
renterAgreementRead.value = false
virtualAgreementChecked.value = false
renterAgreementChecked.value = false
agreementVisible.value = true
await nextTick()
updateAgreementReadState('virtual')
updateAgreementReadState('renter')
} catch (error) {
ElMessage.error(readError(error, "协议加载失败"));
ElMessage.error(readError(error, '协议加载失败'))
} finally {
agreementsLoading.value = false;
agreementsLoading.value = false
}
}
async function handleConfirmAgreementAndCreateOrder() {
if (!canCreateOrderAfterAgreement.value) {
ElMessage.warning("请先阅读并勾选两份协议");
return;
ElMessage.warning('请先阅读并勾选两份协议')
return
}
agreementVisible.value = false;
await submitOrder();
agreementVisible.value = false
await submitOrder()
}
async function submitOrder() {
if (!listing.value) return;
ordering.value = true;
if (!listing.value) return
ordering.value = true
try {
const order = await createOrder(listing.value.id);
ElMessage.success("订单已创建,请完成支付");
await router.push(`/orders/${order.id}`);
const order = await createOrder(listing.value.id)
ElMessage.success('订单已创建,请完成支付')
await router.push(`/orders/${order.id}`)
} catch (error) {
ElMessage.error(readError(error, "下单失败"));
ElMessage.error(readError(error, '下单失败'))
} finally {
ordering.value = false;
ordering.value = false
}
}
function handleAgreementScroll(type: "virtual" | "renter") {
updateAgreementReadState(type);
function handleAgreementScroll(type: 'virtual' | 'renter') {
updateAgreementReadState(type)
}
function updateAgreementReadState(type: "virtual" | "renter") {
const el = type === "virtual" ? virtualAgreementRef.value : renterAgreementRef.value;
if (!el) return;
const read = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
if (type === "virtual") {
virtualAgreementRead.value = read;
function updateAgreementReadState(type: 'virtual' | 'renter') {
const el = type === 'virtual' ? virtualAgreementRef.value : renterAgreementRef.value
if (!el) return
const read = el.scrollTop + el.clientHeight >= el.scrollHeight - 8
if (type === 'virtual') {
virtualAgreementRead.value = read
} else {
renterAgreementRead.value = read;
renterAgreementRead.value = read
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === "object" && error && "response" in error) {
const response = (error as { response?: { data?: { message?: string } } })
.response;
return response?.data?.message || fallback;
if (typeof error === 'object' && error && 'response' in error) {
const response = (error as { response?: { data?: { message?: string } } }).response
return response?.data?.message || fallback
}
return fallback;
return fallback
}
// 手动锁定/解锁背景滚动
watch(agreementVisible, (visible) => {
watch(agreementVisible, visible => {
// 只锁定 .pc-main 容器,不影响 body 和 sticky 导航栏
const pcMain = document.querySelector('.pc-main') as HTMLElement;
if (!pcMain) return;
const pcMain = document.querySelector('.pc-main') as HTMLElement
if (!pcMain) return
if (visible) {
// 保存当前滚动位置
const scrollTop = pcMain.scrollTop || 0;
const scrollTop = pcMain.scrollTop || 0
// 锁定主容器滚动
pcMain.style.overflow = 'hidden';
pcMain.style.position = 'fixed';
pcMain.style.top = `-${scrollTop}px`;
pcMain.style.left = '0';
pcMain.style.right = '0';
pcMain.style.width = '100%';
pcMain.style.overflow = 'hidden'
pcMain.style.position = 'fixed'
pcMain.style.top = `-${scrollTop}px`
pcMain.style.left = '0'
pcMain.style.right = '0'
pcMain.style.width = '100%'
// 保存滚动位置供恢复使用
pcMain.dataset.scrollTop = String(scrollTop);
pcMain.dataset.scrollTop = String(scrollTop)
} else {
// 恢复主容器滚动
const scrollTop = parseInt(pcMain.dataset.scrollTop || '0', 10);
const scrollTop = parseInt(pcMain.dataset.scrollTop || '0', 10)
pcMain.style.overflow = '';
pcMain.style.position = '';
pcMain.style.top = '';
pcMain.style.left = '';
pcMain.style.right = '';
pcMain.style.width = '';
pcMain.style.overflow = ''
pcMain.style.position = ''
pcMain.style.top = ''
pcMain.style.left = ''
pcMain.style.right = ''
pcMain.style.width = ''
// 恢复滚动位置
pcMain.scrollTop = scrollTop;
delete pcMain.dataset.scrollTop;
pcMain.scrollTop = scrollTop
delete pcMain.dataset.scrollTop
}
});
})
function listingPrice(item: Listing) {
return formatMoney(getListingDisplayPrice(item));
return formatMoney(getListingDisplayPrice(item))
}
</script>
<template>
<section class="pc-detail page" v-loading="loading">
<div class="anti-fraud-strip compact">
<span
>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span
>
<span>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span>
</div>
<div v-if="listing" class="pc-detail-layout">
<section class="pc-detail-main">
<div class="detail-hero-card">
<img
v-if="coverURL"
:src="coverURL"
:alt="getListingTitle(listing)"
/>
<img v-if="coverURL" :src="coverURL" :alt="getListingTitle(listing)" />
<span v-else>HFB ACCOUNT</span>
<div class="detail-hero-overlay">
<div class="detail-tags">
@@ -340,7 +337,12 @@ function listingPrice(item: Listing) {
<div class="detail-body">
<div class="detail-summary-row">
<div v-for="metric in detailMetrics" :key="metric.label" class="detail-metric" :class="`is-${metric.tone}`">
<div
v-for="metric in detailMetrics"
:key="metric.label"
class="detail-metric"
:class="`is-${metric.tone}`"
>
<span>{{ metric.label }}</span>
<strong>{{ metric.value }}</strong>
</div>
@@ -349,7 +351,7 @@ function listingPrice(item: Listing) {
<section class="detail-section">
<div class="detail-section-head">
<h2>账号资料</h2>
<span>{{ listing.game_name || "三角洲行动" }}</span>
<span>{{ listing.game_name || '三角洲行动' }}</span>
</div>
<div class="detail-chip-row">
<span v-for="chip in getListingChips(listing)" :key="chip.label">
@@ -370,13 +372,17 @@ function listingPrice(item: Listing) {
<span>{{ getListingResources(listing).length }} </span>
</div>
<div class="detail-resource-grid">
<div v-for="resource in getListingResources(listing)" :key="resource.key" class="detail-resource-card">
<div
v-for="resource in getListingResources(listing)"
:key="resource.key"
class="detail-resource-card"
>
<span>{{ resource.label }}</span>
<strong>{{ resource.quantity }}</strong>
<em>
<b>{{ resource.mode || "--" }}</b>
<b>{{ resource.mode || '--' }}</b>
<small v-if="resource.amount > 0">¥{{ resource.amount }}</small>
<small v-else-if="resource.mode === '收费'">{{ resource.price || "¥0" }}</small>
<small v-else-if="resource.mode === '收费'">{{ resource.price || '¥0' }}</small>
<small v-else>无额外收费</small>
</em>
</div>
@@ -402,7 +408,7 @@ function listingPrice(item: Listing) {
<div class="detail-section-head">
<h2>号主备注</h2>
</div>
<p class="detail-description">{{ listing.description || "号主暂未填写详细说明。" }}</p>
<p class="detail-description">{{ listing.description || '号主暂未填写详细说明。' }}</p>
</section>
<section v-if="detailScreenshots.length" class="detail-section">
@@ -444,11 +450,11 @@ function listingPrice(item: Listing) {
<dl class="order-check-list">
<div>
<dt>账号区服</dt>
<dd>{{ getServerRegion(listing) || "--" }}</dd>
<dd>{{ getServerRegion(listing) || '--' }}</dd>
</div>
<div>
<dt>上号方式</dt>
<dd>{{ getLoginMethod(listing) || "--" }}</dd>
<dd>{{ getLoginMethod(listing) || '--' }}</dd>
</div>
<div>
<dt>预计可租</dt>
@@ -463,7 +469,7 @@ function listingPrice(item: Listing) {
class="full-control"
@click="handleCreateOrder"
>
{{ listing.in_transaction ? "交易中" : "立即下单" }}
{{ listing.in_transaction ? '交易中' : '立即下单' }}
</el-button>
</el-form>
</aside>
@@ -560,7 +566,7 @@ function listingPrice(item: Listing) {
.detail-hero-card::after {
position: absolute;
inset: 0;
content: "";
content: '';
background:
linear-gradient(180deg, rgba(15, 23, 42, 0.06), rgba(15, 23, 42, 0.58)),
linear-gradient(90deg, rgba(15, 23, 42, 0.76), rgba(15, 23, 42, 0.08) 62%);