feat: Features架构迁移 - P0和P1部分完成
## 完成的工作 ### P0: 基础设施准备 - 创建 features/ 和 shared/ 目录结构 - 迁移共享资源:API基础设施、工具函数、类型定义 - 迁移通用composables:useMoney, useSmsCountdown, usePricingCalculator - 迁移全局样式文件 - 建立模块化导出系统 ### P1.1: 钱包模块 (wallet) - 迁移 API: wallet.ts - 迁移 Views: WalletView.vue - 新增 Composable: useWallet.ts (封装钱包状态管理) - 更新导入路径到 shared/ ### P1.2: 聊天模块 (chats) - 迁移 API: chats.ts - 迁移 Views: ChatView, MessagesView (桌面+移动) - 迁移 Composables: useChatSSE.ts - 迁移 Components: ChatAttachmentImage.vue - 更新导入路径到 shared/ ## 技术改进 - 修复 shared/composables 导出问题 (default → 命名导出) - 修复 shared/api/client.ts 类型导入路径 - 建立清晰的模块边界和导出规范 ## 文档 - 添加完整的迁移计划文档 - 添加进度跟踪文档 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
10acca637e
commit
b5903a169f
@@ -0,0 +1,306 @@
|
||||
import type { Listing } from "@/api/listings";
|
||||
|
||||
export interface ListingDisplayChip {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ListingDisplayResource {
|
||||
key: string;
|
||||
label: string;
|
||||
price: string;
|
||||
quantity: number;
|
||||
mode: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function getCoinWan(item: Listing) {
|
||||
return Math.round(Number(item.haf_coin_amount || 0) / 10000);
|
||||
}
|
||||
|
||||
export function getCoinM(item: Listing) {
|
||||
return getCoinWan(item) / 100;
|
||||
}
|
||||
|
||||
export function formatListingCode(item: Listing) {
|
||||
return `SP${String(item.id).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
export function formatHafCoinM(amountWan: number) {
|
||||
const amountM = amountWan / 100;
|
||||
const rounded = Math.round(amountM * 10) / 10;
|
||||
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`;
|
||||
}
|
||||
|
||||
export function getListingDisplayPrice(item: Listing) {
|
||||
return Number(item.price || 0);
|
||||
}
|
||||
|
||||
export function getListingRentPrice(item: Listing) {
|
||||
const buyerCoinBasePrice = readPriceBreakdownNumber(item, "buyer_coin_base_price");
|
||||
if (buyerCoinBasePrice > 0) return Math.round(buyerCoinBasePrice);
|
||||
return Math.max(0, Math.round(getListingDisplayPrice(item) - getListingConsumablePrice(item)));
|
||||
}
|
||||
|
||||
export function getListingConsumablePrice(item: Listing) {
|
||||
const consumablePrice = readPriceBreakdownNumber(item, "consumable_price");
|
||||
if (consumablePrice > 0) return Math.round(consumablePrice);
|
||||
return getListingResources(item).reduce((sum, resource) => sum + resource.amount, 0);
|
||||
}
|
||||
|
||||
export function getListingSellerPrice(item: Listing) {
|
||||
const priceBreakdown = item.asset_summary?.price_breakdown;
|
||||
if (typeof priceBreakdown === "object" && priceBreakdown !== null) {
|
||||
const price = readUnknownNumber((priceBreakdown as Record<string, unknown>).seller_total_price);
|
||||
if (price > 0) return price;
|
||||
}
|
||||
return getListingDisplayPrice(item);
|
||||
}
|
||||
|
||||
export function getRatioValue(item: Listing) {
|
||||
const ratio = readAssetNumber(item, "publish_ratio");
|
||||
if (ratio > 0) return ratio;
|
||||
const price = getListingDisplayPrice(item);
|
||||
if (price <= 0) return 0;
|
||||
return getCoinWan(item) / price;
|
||||
}
|
||||
|
||||
export function formatRatio(item: Listing) {
|
||||
const ratio = getRatioValue(item);
|
||||
return ratio > 0 ? `1:${formatRatioNumber(ratio)}` : "--";
|
||||
}
|
||||
|
||||
export function getValuePerYuanText(item: Listing) {
|
||||
return formatRatio(item);
|
||||
}
|
||||
|
||||
export function getLoginMethod(item: Listing) {
|
||||
return item.login_platform.trim();
|
||||
}
|
||||
|
||||
export function getServerRegion(item: Listing) {
|
||||
return item.server_region.trim();
|
||||
}
|
||||
|
||||
export function getListingTitle(item: Listing) {
|
||||
const parts = [
|
||||
`纯币${formatHafCoinM(getCoinWan(item))}`,
|
||||
formatInsuranceSlotText(readAssetString(item, "season_insurance")),
|
||||
formatLevelShort(readAssetString(item, "stamina_level"), "体"),
|
||||
formatLevelShort(readAssetString(item, "load_level"), "负"),
|
||||
formatResourceShort(item, "armor6", "六甲"),
|
||||
formatResourceShort(item, "helmet6", "六头"),
|
||||
...getSkinNames(item).slice(0, 4),
|
||||
].filter(Boolean);
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
export function getListingSubtitle(item: Listing) {
|
||||
return getValuePerYuanText(item);
|
||||
}
|
||||
|
||||
export function getListingChips(item: Listing): ListingDisplayChip[] {
|
||||
const totalAsset = readAssetNumber(item, "total_asset_wan");
|
||||
const chips: ListingDisplayChip[] = [
|
||||
{ label: "哈夫币", value: formatHafCoinM(getCoinWan(item)) },
|
||||
{ label: "保险格数", value: readAssetString(item, "season_insurance") },
|
||||
{ label: "体力", value: readAssetString(item, "stamina_level") },
|
||||
{ label: "负重", value: readAssetString(item, "load_level") },
|
||||
{ label: "段位", value: item.rank_level },
|
||||
];
|
||||
const awmAmmo = getResourceQuantity(item, "awmAmmo");
|
||||
if (awmAmmo > 0) {
|
||||
chips.push({ label: "AWM", value: `${awmAmmo}发` });
|
||||
}
|
||||
if (totalAsset > 0) {
|
||||
chips.push({ label: "总资产", value: formatHafCoinM(totalAsset) });
|
||||
}
|
||||
const online = getOnlineTimeText(item);
|
||||
if (online) {
|
||||
chips.push({ label: "方便上号", value: online });
|
||||
}
|
||||
return chips.filter((chip) => chip.value);
|
||||
}
|
||||
|
||||
export function getListingResources(item: Listing): ListingDisplayResource[] {
|
||||
const resources = item.asset_summary?.resources;
|
||||
if (!Array.isArray(resources)) return [];
|
||||
return resources
|
||||
.map((resource) => {
|
||||
if (typeof resource !== "object" || resource === null) return null;
|
||||
const row = resource as Record<string, unknown>;
|
||||
return {
|
||||
key: typeof row.key === "string" ? row.key : "",
|
||||
label: typeof row.label === "string" ? row.label : "",
|
||||
price: typeof row.price === "string" ? row.price : "",
|
||||
quantity: readUnknownNumber(row.quantity),
|
||||
mode: typeof row.mode === "string" ? row.mode : "",
|
||||
amount:
|
||||
row.mode === "收费"
|
||||
? Math.round(readUnknownNumber(row.quantity) * readUnitPrice(typeof row.price === "string" ? row.price : ""))
|
||||
: 0,
|
||||
};
|
||||
})
|
||||
.filter((resource): resource is ListingDisplayResource => {
|
||||
return Boolean(resource?.key && resource.label && resource.quantity > 0);
|
||||
});
|
||||
}
|
||||
|
||||
export function getResourceQuantity(item: Listing, resourceKey: string) {
|
||||
return getListingResources(item).find((resource) => resource.key === resourceKey)?.quantity || 0;
|
||||
}
|
||||
|
||||
export function hasGiftResources(item: Listing) {
|
||||
return getListingResources(item).some((resource) => resource.mode === "赠送");
|
||||
}
|
||||
|
||||
export function hasAcceleratedSaleRatio(item: Listing) {
|
||||
if (item.is_accelerated_sale) return true;
|
||||
|
||||
const priceBreakdown = item.asset_summary?.price_breakdown;
|
||||
if (typeof priceBreakdown !== "object" || priceBreakdown === null) return false;
|
||||
|
||||
const row = priceBreakdown as Record<string, unknown>;
|
||||
const referenceRatio = readUnknownNumber(row.seller_reference_ratio);
|
||||
const sellerRatio = readUnknownNumber(row.seller_ratio);
|
||||
const acceleratedRatio = readUnknownNumber(row.accelerated_sale_ratio);
|
||||
|
||||
if (referenceRatio <= 0) return false;
|
||||
return sellerRatio > referenceRatio || acceleratedRatio > referenceRatio;
|
||||
}
|
||||
|
||||
export function getSkinGroup(item: Listing, groupKey: string) {
|
||||
const skinGroups = item.asset_summary?.skin_groups;
|
||||
if (
|
||||
typeof skinGroups !== "object" ||
|
||||
skinGroups === null ||
|
||||
!Array.isArray((skinGroups as Record<string, unknown>)[groupKey])
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return ((skinGroups as Record<string, unknown>)[groupKey] as unknown[]).filter(
|
||||
(skin): skin is string => typeof skin === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function getSkinNames(item: Listing) {
|
||||
const skinGroups = item.asset_summary?.skin_groups;
|
||||
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) {
|
||||
const regions = item.asset_summary?.common_regions;
|
||||
return Array.isArray(regions)
|
||||
? regions.filter((region): region is string => typeof region === "string")
|
||||
: [];
|
||||
}
|
||||
|
||||
export function getOnlineTimeText(item: Listing) {
|
||||
const onlineTime = item.asset_summary?.online_time;
|
||||
if (typeof onlineTime !== "object" || onlineTime === null) return "";
|
||||
const start = (onlineTime as Record<string, unknown>).start;
|
||||
const end = (onlineTime as Record<string, unknown>).end;
|
||||
if (typeof start !== "string" || typeof end !== "string" || !start || !end) return "";
|
||||
return `${start.replace(":00", "")}-${end.replace(":00", "")}点`;
|
||||
}
|
||||
|
||||
export function getDailyLoss(item: Listing) {
|
||||
const dailyLossM = getDailyLossM(item);
|
||||
return dailyLossM > 0 ? `${formatCompactNumber(dailyLossM)}M` : "";
|
||||
}
|
||||
|
||||
export function getDailyLossM(item: Listing) {
|
||||
const configuredLoss = readAssetNumber(item, "daily_loss_m");
|
||||
if (configuredLoss > 0) return configuredLoss;
|
||||
|
||||
const coinWan = getCoinWan(item);
|
||||
if (coinWan >= 30000) return 30;
|
||||
if (coinWan >= 10000) return 20;
|
||||
return 10;
|
||||
}
|
||||
|
||||
export function getEstimatedRentalDays(item: Listing) {
|
||||
const coinM = getCoinM(item);
|
||||
const dailyLossM = getDailyLossM(item);
|
||||
if (coinM <= 0 || dailyLossM <= 0) return 0;
|
||||
return coinM / dailyLossM;
|
||||
}
|
||||
|
||||
export function formatEstimatedRentalDuration(item: Listing) {
|
||||
const days = getEstimatedRentalDays(item);
|
||||
if (days <= 0) return "--";
|
||||
return `${Math.max(1, Math.round(days))}天`;
|
||||
}
|
||||
|
||||
export function readAssetString(item: Listing, key: string) {
|
||||
const value = item.asset_summary?.[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
export function readAssetNumber(item: Listing, key: string) {
|
||||
return readUnknownNumber(item.asset_summary?.[key]);
|
||||
}
|
||||
|
||||
function readUnknownNumber(value: unknown) {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function readPriceBreakdownNumber(item: Listing, key: string) {
|
||||
const priceBreakdown = item.asset_summary?.price_breakdown;
|
||||
if (typeof priceBreakdown !== "object" || priceBreakdown === null) return 0;
|
||||
return readUnknownNumber((priceBreakdown as Record<string, unknown>)[key]);
|
||||
}
|
||||
|
||||
function readUnitPrice(priceText: string) {
|
||||
const normalized = priceText.replace(/,/g, ",").trim();
|
||||
const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/);
|
||||
if (fractionMatch) {
|
||||
const amount = Number(fractionMatch[1]);
|
||||
const count = Number(fractionMatch[2]);
|
||||
return count > 0 ? amount / count : 0;
|
||||
}
|
||||
const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/);
|
||||
return singleMatch ? Number(singleMatch[1]) : 0;
|
||||
}
|
||||
|
||||
function formatInsuranceSlotText(value: string) {
|
||||
const parts = value.split("*").map((item) => Number(item));
|
||||
const rows = parts[0] || 0;
|
||||
const cols = parts[1] || 0;
|
||||
if (!Number.isFinite(rows) || !Number.isFinite(cols) || rows <= 0 || cols <= 0) {
|
||||
return value;
|
||||
}
|
||||
return `${rows * cols}格`;
|
||||
}
|
||||
|
||||
function formatLevelShort(value: string, suffix: string) {
|
||||
const level = value.match(/\d+/)?.[0];
|
||||
return level ? `${level}${suffix}` : value;
|
||||
}
|
||||
|
||||
function formatResourceShort(item: Listing, key: string, label: string) {
|
||||
const quantity = getResourceQuantity(item, key);
|
||||
return quantity > 0 ? `${quantity}${label}` : "";
|
||||
}
|
||||
|
||||
function roundMoney(value: number) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function formatRatioNumber(value: number) {
|
||||
const rounded = roundMoney(value);
|
||||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2);
|
||||
}
|
||||
|
||||
function formatCompactNumber(value: number) {
|
||||
const rounded = Math.round(value * 10) / 10;
|
||||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
|
||||
}
|
||||
Reference in New Issue
Block a user