增加前端格式检查配置

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 { computed, nextTick, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { showToast, showDialog } from "vant";
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant'
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 { 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 { formatMoney } from '@/shared/utils/money'
import {
assetRegions,
formatHafCoinM,
@@ -23,241 +27,240 @@ import {
getLoginMethod,
getServerRegion,
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 listing = ref<Listing | null>(null);
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 listing = ref<Listing | null>(null)
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,
);
renterAgreementChecked.value
)
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))
} catch {
showToast({ message: "加载失败", icon: "warning-o" });
showToast({ message: '加载失败', icon: 'warning-o' })
} finally {
loading.value = false;
loading.value = false
}
});
})
const orderTotal = computed(() => {
if (!listing.value) return "0.0";
return formatMoney(getListingDisplayPrice(listing.value));
});
if (!listing.value) return '0.0'
return formatMoney(getListingDisplayPrice(listing.value))
})
const orderPriceBreakdown = computed(() => {
if (!listing.value) {
return {
rent: 0,
consumable: 0,
};
}
}
return {
rent: getListingRentPrice(listing.value),
consumable: getListingConsumablePrice(listing.value),
};
});
}
})
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: `¥${getListingDisplayPrice(listing.value)}`, tone: "price" },
{ label: "押金", value: `¥${listing.value.deposit_amount}`, tone: "" },
];
});
{ label: '价格', value: `¥${getListingDisplayPrice(listing.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)
})
/* 下单 */
async function handleCreateOrder() {
if (!listing.value) return;
if (!listing.value) return
if (!session.token) {
showDialog({
title: "请先登录",
message: "下单需要登录账号,是否前往登录?",
confirmButtonText: "去登录",
cancelButtonText: "取消",
title: '请先登录',
message: '下单需要登录账号,是否前往登录?',
confirmButtonText: '去登录',
cancelButtonText: '取消',
showCancelButton: true,
}).then(() => {
router.push({ path: "/m/login", query: { redirect: route.fullPath } });
});
return;
router.push({ path: '/m/login', query: { redirect: route.fullPath } })
})
return
}
if (session.realnameStatus !== "verified") {
if (session.realnameStatus !== 'verified') {
try {
await session.loadMe();
await session.loadMe()
} catch {
// 401 会由全局拦截器处理。
}
}
if (session.realnameStatus !== "verified") {
if (session.realnameStatus !== 'verified') {
showDialog({
title: "请先实名认证",
message: "租号下单前需要完成实名认证。",
confirmButtonText: "去认证",
cancelButtonText: "取消",
title: '请先实名认证',
message: '租号下单前需要完成实名认证。',
confirmButtonText: '去认证',
cancelButtonText: '取消',
showCancelButton: true,
}).then(() => {
router.push({ path: "/m/realname", query: { redirect: route.fullPath } });
});
return;
router.push({ path: '/m/realname', query: { redirect: route.fullPath } })
})
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) {
showToast({ message: readError(error, "协议加载失败"), icon: "cross" });
showToast({ message: readError(error, '协议加载失败'), icon: 'cross' })
} finally {
agreementsLoading.value = false;
agreementsLoading.value = false
}
}
async function handleConfirmAgreementAndCreateOrder() {
if (!canCreateOrderAfterAgreement.value) {
showToast("请先阅读并勾选两份协议");
return;
showToast('请先阅读并勾选两份协议')
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 {
await createOrder(listing.value.id);
showToast({ message: "订单已创建,请完成支付", icon: "passed" });
await router.push(`/m/orders`);
await createOrder(listing.value.id)
showToast({ message: '订单已创建,请完成支付', icon: 'passed' })
await router.push(`/m/orders`)
} catch (error) {
showToast({ message: readError(error, "下单失败"), icon: "cross" });
showToast({ message: readError(error, '下单失败'), icon: 'cross' })
} 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
}
/** 判断当前底部导航是否激活 */
function isNavActive(path: string) {
if (path === "/m") return route.path === "/m";
return route.path.startsWith(path);
if (path === '/m') return route.path === '/m'
return route.path.startsWith(path)
}
</script>
@@ -280,9 +283,7 @@ function isNavActive(path: string) {
<!-- 防骗提示 -->
<div class="fraud-tip">
<van-icon name="shield-o" :size="14" color="#ff9800" />
<span
>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span
>
<span>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span>
</div>
<!-- 封面图 -->
@@ -299,10 +300,7 @@ function isNavActive(path: string) {
<span>暂无截图</span>
</div>
<!-- 截图指示器 -->
<div
v-if="detailScreenshots.length > 1"
class="cover-count"
>
<div v-if="detailScreenshots.length > 1" class="cover-count">
{{ detailScreenshots.length }}
</div>
</div>
@@ -310,15 +308,11 @@ function isNavActive(path: string) {
<!-- 标题信息 -->
<div class="info-card">
<div class="info-tag-row">
<van-tag plain type="primary" size="medium">{{
getServerRegion(listing)
}}</van-tag>
<van-tag plain type="primary" size="medium">{{ getServerRegion(listing) }}</van-tag>
<van-tag v-if="getLoginMethod(listing)" plain type="primary" size="medium">
{{ getLoginMethod(listing) }}
</van-tag>
<van-tag v-if="listing.rank_level" plain size="medium">{{
listing.rank_level
}}</van-tag>
<van-tag v-if="listing.rank_level" plain size="medium">{{ listing.rank_level }}</van-tag>
</div>
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
<p class="detail-desc">{{ getListingSubtitle(listing) }}</p>
@@ -336,10 +330,7 @@ function isNavActive(path: string) {
<div class="info-card">
<h3 class="card-subtitle">账号资料</h3>
<div class="detail-chip-row">
<span
v-for="chip in getListingChips(listing)"
:key="chip.label"
>
<span v-for="chip in getListingChips(listing)" :key="chip.label">
{{ chip.label }}:{{ chip.value }}
</span>
</div>
@@ -349,11 +340,11 @@ function isNavActive(path: string) {
</div>
<div class="info-line">
<span class="info-label">常用登录地</span>
<span class="info-text">{{ assetRegions(listing).join("、") || "--" }}</span>
<span class="info-text">{{ assetRegions(listing).join('、') || '--' }}</span>
</div>
<div v-if="readAssetString(listing, 'ban_record')" class="info-line">
<span class="info-label">封禁记录</span>
<span class="info-text">{{ readAssetString(listing, "ban_record") }}</span>
<span class="info-text">{{ readAssetString(listing, 'ban_record') }}</span>
</div>
</div>
@@ -368,9 +359,9 @@ function isNavActive(path: string) {
<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>
@@ -427,11 +418,18 @@ function isNavActive(path: string) {
loading-text="下单中..."
@click="handleCreateOrder"
>
{{ listing.in_transaction ? "交易中" : "立即下单" }}
{{ listing.in_transaction ? '交易中' : '立即下单' }}
</van-button>
</div>
<van-popup v-model:show="agreementVisible" round closeable position="bottom" lock-scroll class="agreement-popup">
<van-popup
v-model:show="agreementVisible"
round
closeable
position="bottom"
lock-scroll
class="agreement-popup"
>
<div v-if="agreements" class="agreement-popup-body">
<h3>下单协议确认</h3>
<p>请完整阅读并勾选以下两份协议后继续下单</p>
@@ -444,7 +442,11 @@ function isNavActive(path: string) {
>
{{ agreements.virtual_asset_purchase.content }}
</div>
<van-checkbox v-model="virtualAgreementChecked" :disabled="!virtualAgreementRead" icon-size="18px">
<van-checkbox
v-model="virtualAgreementChecked"
:disabled="!virtualAgreementRead"
icon-size="18px"
>
我已阅读并同意{{ agreements.virtual_asset_purchase.title }}
</van-checkbox>
<span v-if="!virtualAgreementRead" class="agreement-read-hint">请下拉阅读至底部</span>
@@ -458,7 +460,11 @@ function isNavActive(path: string) {
>
{{ agreements.renter_agreement.content }}
</div>
<van-checkbox v-model="renterAgreementChecked" :disabled="!renterAgreementRead" icon-size="18px">
<van-checkbox
v-model="renterAgreementChecked"
:disabled="!renterAgreementRead"
icon-size="18px"
>
我已阅读并同意{{ agreements.renter_agreement.title }}
</van-checkbox>
<span v-if="!renterAgreementRead" class="agreement-read-hint">请下拉阅读至底部</span>