增加前端格式检查配置
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user