统一前端共享工具入口

This commit is contained in:
yml
2026-06-09 20:50:57 +08:00
parent 684e30862d
commit 60324765c5
69 changed files with 106 additions and 1142 deletions
@@ -3,7 +3,10 @@ import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { getSystemConfigSelectOptions, type SystemConfigOption } from '@/utils/systemConfigOptions'
import {
getSystemConfigSelectOptions,
type SystemConfigOption,
} from '@/shared/utils/systemConfigOptions'
const props = defineProps<{
modelValue: boolean
@@ -4,7 +4,7 @@ import { ref, watch } from 'vue'
import { defaultHomeAnnouncements, mergeHomeConfig } from '@/features/listings/api/homeConfig'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const props = defineProps<{
modelValue: boolean
@@ -9,7 +9,7 @@ import {
type HomeBannerSlide,
} from '@/features/listings/api/homeConfig'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const props = defineProps<{
modelValue: boolean
@@ -4,7 +4,7 @@ import { ref, watch } from 'vue'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import type { ListingPublishAgreements } from '@/features/listings/api/listingOptions'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const defaultListingPublishAgreements: ListingPublishAgreements = {
virtual_asset_sale: {
@@ -4,7 +4,7 @@ import { ref, watch } from 'vue'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import type { OrderAgreements } from '@/features/orders/api/orders'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const defaultOrderAgreements: OrderAgreements = {
virtual_asset_purchase: {
@@ -10,7 +10,7 @@ import {
type CreatePaymentConfigRequest,
type UpdatePaymentConfigRequest,
} from '@/features/admin/api/paymentConfig'
import { readError } from '@/utils/error'
import { readError } from '@/shared/utils/error'
const props = defineProps<{
modelValue: boolean
@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
import { ref, watch } from 'vue'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
interface PostRentalNotice {
title: string
@@ -8,7 +8,7 @@ import {
type ListingPublishOptions,
} from '@/features/listings/api/listingOptions'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const props = defineProps<{
modelValue: boolean
@@ -8,7 +8,7 @@ import {
type PublishSalePriceConfig,
} from '@/features/listings/api/listingOptions'
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { safeParseJSON } from '@/utils/json'
import { safeParseJSON } from '@/shared/utils/json'
const props = defineProps<{
modelValue: boolean
@@ -23,7 +23,7 @@ import {
type UpdateAnnouncementRequest,
} from '@/features/admin/api/adminAnnouncements'
import type { Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import AnnouncementDialog from '../components/AnnouncementDialog.vue'
import AdminTablePagination from '../components/AdminTablePagination.vue'
@@ -3,7 +3,7 @@ import { Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminAuditLogs, type AdminAuditLog } from '@/features/admin/api/adminAudit'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
@@ -19,7 +19,7 @@ import ChatAttachmentImage from '@/components/ChatAttachmentImage.vue'
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
import NotificationSettings from '@/features/chats/components/NotificationSettings.vue'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
import TransferDialog from '../components/TransferDialog.vue'
import QuickReplyDialog from '../components/QuickReplyDialog.vue'
@@ -18,8 +18,8 @@ import {
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { disputeStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { disputeStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
const {
loading,
@@ -4,9 +4,9 @@ import { computed, onMounted, ref } from 'vue'
import { arbitrateDispute, fetchAdminDisputes, type Dispute } from '@/features/disputes'
import { fetchAdminFileBlob } from '@/shared/api/files'
import { disputeStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { disputeStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
import { yuanToCent } from '@/shared/utils/money'
import AdminTablePagination from '../components/AdminTablePagination.vue'
@@ -8,7 +8,7 @@ import {
type FinanceDailyItem,
} from '@/features/admin/api/adminFinance'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const loading = ref(false)
const dashboard = ref<FinanceDashboard | null>(null)
@@ -4,8 +4,8 @@ import { onMounted, reactive, ref } from 'vue'
import { fetchFinanceDetails, type FinanceDetail } from '@/features/admin/api/adminFinance'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
@@ -12,9 +12,9 @@ import {
fetchAdminListing,
type Listing,
} from '@/features/listings'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingCode } from '@/utils/listingDisplay'
import { listingReviewStatusLabel, listingStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingCode } from '@/shared/utils/listingDisplay'
const route = useRoute()
const loading = ref(false)
@@ -23,8 +23,8 @@ import {
getSkinNames,
readAssetNumber,
readAssetString,
} from '@/utils/listingDisplay'
import { formatDateTime } from '@/utils/time'
} from '@/shared/utils/listingDisplay'
import { formatDateTime } from '@/shared/utils/time'
type RiskLevel = 'danger' | 'warning' | 'info'
@@ -21,7 +21,7 @@ import {
getDailyLoss,
getResourceQuantity,
getSkinGroup,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
const filters = reactive<AdminListingQuery>({
owner_id: '',
@@ -9,7 +9,7 @@ import {
type AdminMgrUser,
} from '@/features/admin/api/adminMgr'
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import AdminUserDialog from '../components/AdminUserDialog.vue'
import AssignRolesDialog from '../components/AssignRolesDialog.vue'
@@ -16,9 +16,9 @@ import {
} from '@/features/orders'
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
import { centToYuan, formatCentWithSymbol, formatMoney } from '@/shared/utils/money'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const route = useRoute()
const loading = ref(false)
@@ -2,10 +2,14 @@
import { computed, onMounted, ref } from 'vue'
import { fetchAdminOrders, type Order } from '@/features/orders'
import { handoffStatusLabel, orderStatusLabel, settlementStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import {
handoffStatusLabel,
orderStatusLabel,
settlementStatusLabel,
} from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import { formatListingNo } from '@/utils/listingDisplay'
import { formatListingNo } from '@/shared/utils/listingDisplay'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const status = ref('')
@@ -10,9 +10,9 @@ import {
importPaymentConfigBackup,
type PaymentConfig,
} from '@/features/admin/api/paymentConfig'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import { formatCent } from '@/shared/utils/money'
import { readError } from '@/utils/error'
import { readError } from '@/shared/utils/error'
import PaymentConfigDialog from '../components/PaymentConfigDialog.vue'
const loading = ref(false)
@@ -4,7 +4,7 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
@@ -18,9 +18,9 @@ import {
} from '@/features/listings/api/homeConfig'
import type { OrderAgreements } from '@/features/orders/api/orders'
import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { formatDateTime } from '@/utils/time'
import { safeParseJSON } from '@/utils/json'
import { formatSystemConfigSelectValue } from '@/utils/systemConfigOptions'
import { formatDateTime } from '@/shared/utils/time'
import { safeParseJSON } from '@/shared/utils/json'
import { formatSystemConfigSelectValue } from '@/shared/utils/systemConfigOptions'
// 导入重构后的 Dialog 子组件
import PublishOptionsDialog from '../components/PublishOptionsDialog.vue'
@@ -11,8 +11,8 @@ import {
} from '@/features/admin/api/adminUsers'
import { centToYuan, formatCent, yuanToCent } from '@/shared/utils/money'
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
import { userStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { userStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const submitting = ref(false)
@@ -4,8 +4,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { balanceTypeLabel, ledgerDirectionLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { balanceTypeLabel, ledgerDirectionLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
@@ -11,7 +11,7 @@ import {
Warning,
} from '@element-plus/icons-vue'
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
import { marked } from 'marked'
const route = useRoute()
@@ -10,7 +10,7 @@ import {
Warning,
} from '@element-plus/icons-vue'
import { fetchAnnouncements, type Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const router = useRouter()
const loading = ref(false)
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
import { marked } from 'marked'
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const route = useRoute()
const router = useRouter()
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { fetchAnnouncements, type Announcement } from '@/features/announcement'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const router = useRouter()
const loading = ref(false)
@@ -11,7 +11,7 @@ import {
type WalletLedger,
} from '@/features/wallet/api/wallet'
import { fetchPostRentalNotice, type PostRentalNotice } from '@/features/orders/api/orders'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
import { uploadFile } from '@/shared/api/files'
import { formatCent } from '@/shared/utils/money'
@@ -3,9 +3,9 @@ import { onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant'
import { getRealnameStatus, startRealname, type RealnameStatus } from '@/features/auth/api/realname'
import { realnameStatusLabel } from '@/utils/statusLabels'
import { realnameStatusLabel } from '@/shared/utils/statusLabels'
import { useSessionStore } from '@/stores/session'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const session = useSessionStore()
const router = useRouter()
@@ -7,7 +7,7 @@ import {
markNotificationRead,
type NotificationItem,
} from '@/features/auth/api/notifications'
import { formatDateTime } from '@/utils/time'
import { formatDateTime } from '@/shared/utils/time'
const loading = ref(false)
const notifications = ref<NotificationItem[]>([])
@@ -23,7 +23,7 @@ import { useRouter } from 'vue-router'
import { uploadFile } from '@/shared/api/files'
import { useSessionStore } from '@/stores/session'
import { realnameStatusLabel } from '@/utils/statusLabels'
import { realnameStatusLabel } from '@/shared/utils/statusLabels'
const session = useSessionStore()
const router = useRouter()
@@ -15,8 +15,8 @@ import { useRoute, useRouter } from 'vue-router'
import { getRealnameStatus, startRealname, type RealnameStatus } from '@/features/auth/api/realname'
import { useSessionStore } from '@/stores/session'
import { realnameStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { realnameStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
const session = useSessionStore()
const route = useRoute()
@@ -1,6 +1,6 @@
import { onBeforeUnmount, ref, type Ref } from 'vue'
import { refreshAccessToken } from '@/shared/api/client'
import { getAccessToken, type AuthScope } from '@/utils/authStorage'
import { getAccessToken, type AuthScope } from '@/shared/utils/authStorage'
export interface SSEMessage {
id: number
@@ -15,7 +15,7 @@ import { uploadFile } from '@/shared/api/files'
import ChatAttachmentImage from '@/components/ChatAttachmentImage.vue'
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
const currentUserId = Number(localStorage.getItem('user_id') || 0)
@@ -7,7 +7,7 @@ import { fetchChats, type ChatConversation } from '@/features/chats/api/chats'
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
import NotificationSettings from '@/features/chats/components/NotificationSettings.vue'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
const router = useRouter()
const currentUserId = Number(localStorage.getItem('user_id') || 0)
@@ -14,7 +14,7 @@ import { uploadFile } from '@/shared/api/files'
import ChatAttachmentImage from '@/components/ChatAttachmentImage.vue'
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
const currentUserId = Number(localStorage.getItem('user_id') || 0)
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { fetchChats, type ChatConversation } from '@/features/chats/api/chats'
import { formatDateMinute } from '@/utils/time'
import { formatDateMinute } from '@/shared/utils/time'
const router = useRouter()
const loading = ref(false)
@@ -21,7 +21,7 @@ import {
readAssetString,
getRatioValue,
type ListingDisplayResource,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
interface Props {
listing: Listing
@@ -31,7 +31,7 @@ import {
getLoginMethod,
getServerRegion,
readAssetString,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
const route = useRoute()
const router = useRouter()
@@ -28,7 +28,7 @@ import {
hasGiftResources,
readAssetNumber,
readAssetString,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
/* ------------------------------------------------------------------ */
/* 数据加载 */
@@ -34,7 +34,7 @@ import {
assetRegions,
hasAcceleratedSaleRatio,
hasGiftResources,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
import { useSessionStore } from '@/stores/session'
const router = useRouter()
@@ -29,7 +29,7 @@ import {
getLoginMethod,
getServerRegion,
readAssetString,
} from '@/utils/listingDisplay'
} from '@/shared/utils/listingDisplay'
const route = useRoute()
const router = useRouter()
@@ -23,9 +23,9 @@ import {
type PaymentOrder,
} from '@/features/orders/api/orders'
import { useSessionStore } from '@/stores/session'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const route = useRoute()
const router = useRouter()
@@ -12,8 +12,8 @@ import {
} from '@/features/orders/api/orders'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import { useSessionStore } from '@/stores/session'
import { formatDateMinute } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { formatDateMinute } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const router = useRouter()
const route = useRoute()
@@ -26,9 +26,9 @@ import {
type PaymentOrder,
} from '@/features/orders'
import { useSessionStore } from '@/stores/session'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const route = useRoute()
const router = useRouter()
@@ -7,9 +7,9 @@ import { CopyDocument, Search } from '@element-plus/icons-vue'
import { fetchOrders, type Order } from '@/features/orders'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import { useSessionStore } from '@/stores/session'
import { orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import { orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const loading = ref(false)
const orders = ref<Order[]>([])
@@ -27,7 +27,12 @@ import {
} from '@/features/seller/composables/usePublishDraft'
import type { PublishForm } from '@/types/publish'
import { yuanToCent } from '@/shared/utils/money'
import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing'
import {
commonOnlineTimes,
dailyLossOptions,
formatNumber,
roundRatio,
} from '@/shared/utils/pricing'
const draftSaveDelay = 400
const multiScreenshotLimit = 3
@@ -3,10 +3,10 @@ import { computed, onMounted, ref } from 'vue'
import { fetchOrders, type Order } from '@/features/orders'
import { useSessionStore } from '@/stores/session'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import { formatListingNo } from '@/utils/listingDisplay'
import { formatListingNo } from '@/shared/utils/listingDisplay'
const session = useSessionStore()
const loading = ref(false)
@@ -10,8 +10,8 @@ import {
type Listing,
} from '@/features/listings'
import { formatCent, formatMoneyWithSymbol } from '@/shared/utils/money'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay'
import { listingReviewStatusLabel, listingStatusLabel } from '@/shared/utils/statusLabels'
import { formatListingCode, getListingSellerPrice } from '@/shared/utils/listingDisplay'
const loading = ref(false)
const submittingID = ref<number | null>(null)
@@ -23,8 +23,12 @@ import {
type WalletLedger,
} from '@/features/wallet'
import type { PaymentOrder } from '@/features/orders'
import { balanceTypeLabel, ledgerDirectionLabel, walletStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import {
balanceTypeLabel,
ledgerDirectionLabel,
walletStatusLabel,
} from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { formatCentWithSymbol, formatMoney } from '@/shared/utils/money'
const router = useRouter()
+1 -1
View File
@@ -93,7 +93,7 @@ import {
getRefreshToken,
setAuthTokens,
type AuthScope,
} from '@/utils/authStorage'
} from '@/shared/utils/authStorage'
import type { ApiResponse } from '@/shared/types/types'
export const apiClient = axios.create({
@@ -21,7 +21,7 @@ import {
readFinalSaleRatio,
roundMoney,
roundRatio,
} from '@/utils/pricing'
} from '@/shared/utils/pricing'
export function usePricingCalculator(options: {
publishOptions: Ref<ListingPublishOptions>
@@ -1,8 +1,5 @@
/**
*
* @param error
* @param fallback
* @returns
*
*/
export function readError(error: unknown, fallback: string): string {
if (typeof error === 'object' && error && 'response' in error) {
+1
View File
@@ -1,5 +1,6 @@
// 通用工具函数
export * from './authStorage'
export * from './error'
export * from './imageUpload'
export * from './json'
export * from './listingDisplay'
+1 -1
View File
@@ -306,7 +306,7 @@ function roundMoney(value: number) {
function formatRatioNumber(value: number) {
const rounded = roundMoney(value)
return rounded.toFixed(1)
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
}
function formatCompactNumber(value: number) {
+1 -1
View File
@@ -11,7 +11,7 @@ import {
getAccessToken,
getRefreshToken,
setAuthTokens,
} from '@/utils/authStorage'
} from '@/shared/utils/authStorage'
export const useAdminSessionStore = defineStore('adminSession', {
state: () => ({
+1 -1
View File
@@ -6,7 +6,7 @@ import {
getAccessToken,
getRefreshToken,
setAuthTokens,
} from '@/utils/authStorage'
} from '@/shared/utils/authStorage'
export const useSessionStore = defineStore('session', {
state: () => ({
-54
View File
@@ -1,54 +0,0 @@
export type AuthScope = 'user' | 'admin'
export interface AuthTokenPair {
access_token: string
refresh_token: string
}
const userKeys = {
accessToken: 'access_token',
refreshToken: 'refresh_token',
profile: ['user_id', 'phone', 'nickname', 'avatar_url', 'realname_status'],
}
const adminKeys = {
accessToken: 'admin_access_token',
refreshToken: 'admin_refresh_token',
profile: ['admin_id', 'admin_username'],
}
function keysFor(scope: AuthScope) {
return scope === 'admin' ? adminKeys : userKeys
}
export function getAccessToken(scope: AuthScope) {
return localStorage.getItem(keysFor(scope).accessToken) || ''
}
export function getRefreshToken(scope: AuthScope) {
return localStorage.getItem(keysFor(scope).refreshToken) || ''
}
export function setAuthTokens(scope: AuthScope, tokens: AuthTokenPair) {
const keys = keysFor(scope)
localStorage.setItem(keys.accessToken, tokens.access_token)
localStorage.setItem(keys.refreshToken, tokens.refresh_token)
notifyAuthStorageChanged(scope)
}
export function clearAuthStorage(scope: AuthScope) {
const keys = keysFor(scope)
localStorage.removeItem(keys.accessToken)
localStorage.removeItem(keys.refreshToken)
keys.profile.forEach(key => localStorage.removeItem(key))
notifyAuthStorageChanged(scope)
}
export function getLoginPath(scope: AuthScope, currentPath: string) {
if (scope === 'admin') return '/admin/login'
return currentPath.startsWith('/m') ? '/m/login' : '/login'
}
export function notifyAuthStorageChanged(scope: AuthScope) {
window.dispatchEvent(new CustomEvent('auth-storage-changed', { detail: { scope } }))
}
-84
View File
@@ -1,84 +0,0 @@
const IMAGE_UPLOAD_MAX_SIDE: Record<string, number> = {
avatar: 512,
chat: 1280,
'home-banner': 1920,
listing: 1920,
dispute: 1920,
handoff: 1920,
realname: 1920,
}
const IMAGE_UPLOAD_QUALITY: Record<string, number> = {
avatar: 0.82,
chat: 0.8,
'home-banner': 0.84,
listing: 0.84,
dispute: 0.86,
handoff: 0.86,
realname: 0.86,
}
export async function optimizeImageForUpload(file: File, scene: string) {
if (!file.type.startsWith('image/')) return file
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) return file
if (typeof document === 'undefined') return file
try {
const image = await loadImage(file)
const maxSide = IMAGE_UPLOAD_MAX_SIDE[scene] || 1600
const quality = IMAGE_UPLOAD_QUALITY[scene] || 0.82
const { width, height } = fitSize(image.naturalWidth, image.naturalHeight, maxSide)
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
if (!context) return file
context.fillStyle = '#ffffff'
context.fillRect(0, 0, width, height)
context.drawImage(image, 0, 0, width, height)
const blob = await canvasToBlob(canvas, 'image/webp', quality)
if (!blob || blob.size >= file.size) return file
return new File([blob], replaceFileExt(file.name, 'webp'), {
type: 'image/webp',
lastModified: Date.now(),
})
} catch {
return file
}
}
function loadImage(file: File) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const url = URL.createObjectURL(file)
const image = new Image()
image.onload = () => {
URL.revokeObjectURL(url)
resolve(image)
}
image.onerror = () => {
URL.revokeObjectURL(url)
reject(new Error('图片读取失败'))
}
image.src = url
})
}
function fitSize(width: number, height: number, maxSide: number) {
if (width <= 0 || height <= 0) return { width: 1, height: 1 }
const scale = Math.min(1, maxSide / Math.max(width, height))
return {
width: Math.max(1, Math.round(width * scale)),
height: Math.max(1, Math.round(height * scale)),
}
}
function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number) {
return new Promise<Blob | null>(resolve => {
canvas.toBlob(resolve, type, quality)
})
}
function replaceFileExt(filename: string, ext: string) {
const base = filename.replace(/\.[^.]+$/, '')
return `${base || 'image'}.${ext}`
}
-8
View File
@@ -1,8 +0,0 @@
export function safeParseJSON<T>(raw: string, fallback: T): T {
if (!raw || !raw.trim()) return fallback
try {
return JSON.parse(raw) as T
} catch {
return fallback
}
}
-315
View File
@@ -1,315 +0,0 @@
import type { Listing } from '@/features/listings/api/listings'
import { centToYuan } from '@/shared/utils/money'
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 formatListingNo(item.listing_no, item.id)
}
export function formatListingNo(listingNo?: string, listingID?: number | string) {
if (listingNo) return listingNo
if (listingID !== undefined && listingID !== null && listingID !== '') {
return `SP${String(listingID).padStart(6, '0')}`
}
return '-'
}
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 centToYuan(item.price_cent)
}
export function getListingRentPrice(item: Listing) {
const buyerCoinBasePrice = readPriceBreakdownNumber(item, 'buyer_coin_base_price')
if (buyerCoinBasePrice > 0) return roundMoney(buyerCoinBasePrice)
return Math.max(0, roundMoney(getListingDisplayPrice(item) - getListingConsumablePrice(item)))
}
export function getListingConsumablePrice(item: Listing) {
const consumablePrice = readPriceBreakdownNumber(item, 'consumable_price')
if (consumablePrice > 0) return roundMoney(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 === '收费'
? roundMoney(
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 ''
if (start === '全天' || end === '全天' || (start === '00:00' && end === '23:59')) 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
return 0
}
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)
}
-327
View File
@@ -1,327 +0,0 @@
import type {
ChargeMode,
ListingPublishOptions,
PublishDepositRecommendConfig,
PublishOptionGroup,
PublishQuantityItem,
PublishRatioConfig,
PublishSalePriceConfig,
} from '@/features/listings/api/listingOptions'
import type { DepositBreakdownItem, PublishForm, PublishPlatformPricing } from '@/types/publish'
export const dailyLossOptions = [10, 20, 30, 40, 50]
export const commonOnlineTimes = [
'00:00',
'08:00',
'10:00',
'12:00',
'14:00',
'18:00',
'20:00',
'22:00',
'23:59',
]
export function roundMoney(value: number) {
return Math.round(value * 10) / 10
}
export function roundRatio(value: number) {
return Math.round(value * 10) / 10
}
export function formatNumber(value: number) {
return (Math.round(value * 10) / 10).toFixed(1)
}
export 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
}
export function calculateDailyLossRatioAdjustment(dailyLossMAmount: number) {
return Math.min(Math.max(Math.floor((dailyLossMAmount - 10) / 10), 0), 4)
}
export function isGridCardQuantityItem(item: { key: string; label: string }) {
return item.key === 'gridCard9' || item.label.includes('9格体验卡')
}
export function isQuantityItemDisabledForInsurance(
item: { key: string; label: string },
seasonInsurance: string
) {
return seasonInsurance === '3*3' && isGridCardQuantityItem(item)
}
export function calculateConsumablePrice(options: {
quantityItems: PublishQuantityItem[]
quantityValues: Record<string, number>
quantityModes: Record<string, ChargeMode>
seasonInsurance: string
}) {
const total = options.quantityItems.reduce((sum, item) => {
const quantity = Number(options.quantityValues[item.key] || 0)
const mode = options.quantityModes[item.key] || '收费'
if (isQuantityItemDisabledForInsurance(item, options.seasonInsurance)) return sum
if (quantity <= 0 || mode !== '收费') return sum
return sum + quantity * readUnitPrice(item.price)
}, 0)
return roundMoney(total)
}
export function calculateRecommendedDeposit(options: {
depositRecommendConfig: PublishDepositRecommendConfig
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
}) {
const baseAmount = Number(options.depositRecommendConfig.base_amount || 0)
const skinAmount = options.depositRecommendConfig.skin_group_rules.reduce((sum, rule) => {
const group = options.skinGroups.find(item => item.key === rule.group_key)
if (!group) return sum
const selectedCount = group.options.filter(skin => options.selectedSkins.includes(skin)).length
return sum + selectedCount * Number(rule.amount_per_item || 0)
}, 0)
return roundMoney(baseAmount + skinAmount)
}
export function buildDepositBreakdownItems(options: {
depositRecommendConfig: PublishDepositRecommendConfig
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
}): DepositBreakdownItem[] {
const items: DepositBreakdownItem[] = [
{
label: '基础押金',
amount: Number(options.depositRecommendConfig.base_amount || 0),
count: 1,
},
]
for (const rule of options.depositRecommendConfig.skin_group_rules) {
const group = options.skinGroups.find(item => item.key === rule.group_key)
if (!group) continue
const count = group.options.filter(skin => options.selectedSkins.includes(skin)).length
if (count <= 0) continue
items.push({
label: rule.label,
amount: Number(rule.amount_per_item || 0) * count,
count,
})
}
return items
}
export function calculateSellerReferenceRatio(options: {
coinMAmount: number
form: PublishForm
ratioConfig: PublishRatioConfig
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
levelOptions: string[]
dailyLossRatioAdjustment: number
}) {
const { coinMAmount, form, ratioConfig } = options
if (coinMAmount <= 0 || !form.season_insurance || !form.stamina_level || !form.load_level)
return 0
const baseRatio = getInsuranceBaseRatio(ratioConfig, form.season_insurance)
if (baseRatio <= 0) return 0
return (
baseRatio +
calculateConfigPenalty(ratioConfig, options) +
getCoinCorrection(ratioConfig, coinMAmount) +
options.dailyLossRatioAdjustment
)
}
export function readFinalSaleRatio(
defaultRatio: number,
acceleratedSaleRatio: number | '',
maxAcceleratedSaleRatio: number
) {
if (defaultRatio <= 0) return 0
if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio
const ratio = Number(acceleratedSaleRatio)
if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio
return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio))
}
export function hasAcceleratedSaleRatioInput(value: number | '') {
return value !== '' && value !== null
}
export function calculatePlatformPricing(options: {
coinMAmount: number
coinWanAmount: number
sellerRatio: number
sellerCoinBasePrice: number
sellerTotalPrice: number
consumablePrice: number
salePriceConfig: PublishSalePriceConfig
}): PublishPlatformPricing {
if (options.sellerRatio <= 0 || options.sellerCoinBasePrice <= 0) return emptyPlatformPricing()
const fixedRule = findSaleFixedMarkupRule(options.salePriceConfig, options.coinMAmount)
if (fixedRule) {
return buildPlatformPricing(
roundMoney(options.sellerCoinBasePrice + Number(fixedRule.markup_amount || 0)),
'fixed_markup',
options
)
}
const ratioRule = findSaleRatioAdjustmentRule(options.salePriceConfig, options.coinMAmount)
const ratioSubtract = ratioRule ? Number(ratioRule.ratio_subtract || 0) : 0
const buyerRatio = options.sellerRatio - ratioSubtract
if (buyerRatio > 0 && ratioRule) {
return buildPlatformPricing(
roundMoney(options.coinWanAmount / buyerRatio),
'ratio_subtract',
options
)
}
return buildPlatformPricing(options.sellerCoinBasePrice, 'none', options)
}
export function emptyPlatformPricing(): PublishPlatformPricing {
return {
buyerCoinBasePrice: 0,
buyerTotalPrice: 0,
buyerRatio: 0,
platformMarkupAmount: 0,
ruleType: 'none',
}
}
function buildPlatformPricing(
buyerCoinBasePrice: number,
ruleType: string,
options: {
coinWanAmount: number
sellerTotalPrice: number
consumablePrice: number
}
): PublishPlatformPricing {
const buyerTotalPrice = roundMoney(buyerCoinBasePrice + options.consumablePrice)
return {
buyerCoinBasePrice,
buyerTotalPrice,
buyerRatio: calculateEffectiveRatio(options.coinWanAmount, buyerCoinBasePrice),
platformMarkupAmount: roundMoney(buyerTotalPrice - options.sellerTotalPrice),
ruleType,
}
}
function findSaleFixedMarkupRule(config: PublishSalePriceConfig, coinMAmount: number) {
return [...config.fixed_markup_rules]
.sort((a, b) => a.min_m - b.min_m)
.find((item, index, rules) =>
isCoinInSaleRange(item, index, rules, coinMAmount, { includeLastMax: true })
)
}
function findSaleRatioAdjustmentRule(config: PublishSalePriceConfig, coinMAmount: number) {
return [...config.ratio_adjustment_rules]
.sort((a, b) => a.min_m - b.min_m)
.find((item, index, rules) =>
isCoinInSaleRange(item, index, rules, coinMAmount, { excludeFirstMin: true })
)
}
function isCoinInSaleRange(
item: { min_m: number; max_m: number },
index: number,
rules: Array<{ min_m: number; max_m: number }>,
coinMAmount: number,
options: { includeLastMax?: boolean; excludeFirstMin?: boolean } = {}
) {
const maxM = Number(item.max_m || 0)
const minM = Number(item.min_m || 0)
const minMatched =
options.excludeFirstMin && index === 0 ? coinMAmount > minM : coinMAmount >= minM
const isLastRule = index === rules.length - 1
const maxMatched =
maxM <= 0 || coinMAmount < maxM || (options.includeLastMax && isLastRule && coinMAmount <= maxM)
return minMatched && maxMatched
}
function calculateEffectiveRatio(coinWanAmount: number, price: number) {
if (price <= 0) return 0
return roundRatio(coinWanAmount / price)
}
function getInsuranceBaseRatio(
config: Pick<ListingPublishOptions['ratio_config'], 'insurance_base_ratios'>,
insurance: string
) {
return config.insurance_base_ratios.find(item => item.insurance === insurance)?.ratio || 0
}
function calculateConfigPenalty(
config: Pick<ListingPublishOptions['ratio_config'], 'config_items'>,
options: {
form: PublishForm
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
levelOptions: string[]
}
) {
return config.config_items.reduce((sum, item) => {
return isRatioConfigItemMatched(item, options) ? sum : sum + Number(item.missing_penalty || 0)
}, 0)
}
function isRatioConfigItemMatched(
item: { kind: string; group_key?: string },
options: {
form: PublishForm
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
levelOptions: string[]
}
) {
if (item.kind === 'skin_group') return hasSelectedSkinGroup(item.group_key || '', options)
if (item.kind === 'max_stamina')
return isMaxLevel(options.form.stamina_level, options.levelOptions)
if (item.kind === 'max_load') return isMaxLevel(options.form.load_level, options.levelOptions)
return false
}
function hasSelectedSkinGroup(
groupKey: string,
options: {
skinGroups: PublishOptionGroup[]
selectedSkins: string[]
}
) {
const group = options.skinGroups.find(item => item.key === groupKey)
if (!group) return false
return group.options.some(skin => options.selectedSkins.includes(skin))
}
function isMaxLevel(value: string, levelOptions: string[]) {
const currentLevel = readLevelNumber(value)
const maxLevel = Math.max(...levelOptions.map(readLevelNumber).filter(Boolean))
if (currentLevel > 0 && maxLevel > 0) return currentLevel >= maxLevel
return value === levelOptions[levelOptions.length - 1]
}
function readLevelNumber(value: string) {
const match = value.match(/\d+/)
return match ? Number(match[0]) : 0
}
function getCoinCorrection(
config: Pick<ListingPublishOptions['ratio_config'], 'coin_corrections'>,
coinM: number
) {
return (
[...config.coin_corrections]
.sort((a, b) => b.threshold_m - a.threshold_m)
.find(item => coinM > item.threshold_m)?.correction || 0
)
}
-175
View File
@@ -1,175 +0,0 @@
import type {
BalanceType,
DisputeStatus,
HandoffStatus,
LedgerDirection,
ListingReviewStatus,
ListingStatus,
OrderStatus,
RealnameStatusValue,
RiskStatus,
SettlementStatus,
UserStatus,
WalletStatus,
} from '@/types/status'
const listingStatusMap: Record<ListingStatus, string> = {
draft: '草稿',
published: '已上架',
rented: '租用中',
offline: '已下架',
abnormal: '异常',
}
const listingReviewStatusMap: Record<ListingReviewStatus, string> = {
none: '未提交',
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
}
const orderStatusMap: Record<OrderStatus, string> = {
pending_confirm: '待确认',
pending_payment: '待支付',
pending_handoff: '待交接',
renting: '使用中',
overdue: '已逾期',
pending_return_confirm: '待结账确认',
pending_checkout_confirm: '待号主确认结账',
pending_checkout_accept: '待租客确认修正',
checkout_disputing: '结账争议中',
completed: '已完成',
cancelled: '已取消',
closed: '已关闭',
disputing: '申诉中',
abnormal: '异常',
}
const handoffStatusMap: Record<HandoffStatus, string> = {
pending_owner: '待号主交接',
pending_renter_confirm: '待租客确认',
received: '已确认收号',
pending_owner_return_confirm: '待号主确认结账',
pending_owner_checkout: '待号主确认结账',
pending_renter_checkout: '待租客确认修正',
checkout_disputed: '结账争议中',
returned: '已归还',
cancelled: '已取消',
owner_timeout: '号主交接超时',
renter_confirm_timeout: '租客确认超时',
return_overdue: '归还逾期',
owner_return_confirm_timeout: '号主确认结账超时',
owner_checkout_confirm_timeout: '号主确认结账超时',
admin_closed: '客服关闭',
admin_abnormal: '客服标记异常',
arbitrated: '已仲裁',
}
const settlementStatusMap: Record<SettlementStatus, string> = {
unsettled: '未结算',
pending: '待结算',
frozen: '冻结中',
settled: '已结算',
refunded: '已退款',
cancelled: '已取消',
closed: '已关闭',
disputed: '争议中',
arbitrated: '已仲裁',
}
const realnameStatusMap: Partial<Record<RealnameStatusValue, string>> = {
unverified: '未认证',
pending: '认证中',
verified: '已认证',
rejected: '认证失败',
}
const userStatusMap: Record<UserStatus, string> = {
active: '正常',
frozen: '已冻结',
disabled: '已禁用',
}
const riskStatusMap: Record<RiskStatus, string> = {
normal: '正常',
watch: '观察',
restricted: '受限',
blocked: '已拦截',
}
const disputeStatusMap: Record<DisputeStatus, string> = {
open: '待处理',
processing: '处理中',
resolved: '已处理',
closed: '已关闭',
}
const walletStatusMap: Record<WalletStatus, string> = {
active: '正常',
frozen: '已冻结',
disabled: '已禁用',
}
const ledgerDirectionMap: Record<LedgerDirection, string> = {
in: '收入',
out: '支出',
freeze: '冻结',
unfreeze: '解冻',
}
const balanceTypeMap: Record<BalanceType, string> = {
available: '可用余额',
frozen: '冻结余额',
}
function readLabel(map: Record<string, string>, value: string) {
return map[value] || value || '-'
}
export function listingStatusLabel(status: string) {
return readLabel(listingStatusMap, status)
}
export function listingReviewStatusLabel(status: string) {
return readLabel(listingReviewStatusMap, status)
}
export function orderStatusLabel(status: string) {
return readLabel(orderStatusMap, status)
}
export function handoffStatusLabel(status: string) {
return readLabel(handoffStatusMap, status)
}
export function settlementStatusLabel(status: string) {
return readLabel(settlementStatusMap, status)
}
export function realnameStatusLabel(status: string) {
return readLabel(realnameStatusMap, status)
}
export function userStatusLabel(status: string) {
return readLabel(userStatusMap, status)
}
export function riskStatusLabel(status: string) {
return readLabel(riskStatusMap, status)
}
export function disputeStatusLabel(status: string) {
return readLabel(disputeStatusMap, status)
}
export function walletStatusLabel(status: string) {
return readLabel(walletStatusMap, status)
}
export function ledgerDirectionLabel(direction: string) {
return readLabel(ledgerDirectionMap, direction)
}
export function balanceTypeLabel(type: string) {
return readLabel(balanceTypeMap, type)
}
-65
View File
@@ -1,65 +0,0 @@
export interface SystemConfigOption {
label: string
value: string
}
const shortTimeoutOptions: SystemConfigOption[] = [
{ label: '不限时', value: '0' },
{ label: '5 分钟', value: '5' },
{ label: '10 分钟', value: '10' },
{ label: '15 分钟', value: '15' },
{ label: '30 分钟', value: '30' },
{ label: '45 分钟', value: '45' },
{ label: '60 分钟', value: '60' },
{ label: '90 分钟', value: '90' },
{ label: '120 分钟', value: '120' },
]
const longTimeoutOptions: SystemConfigOption[] = [
{ label: '不限时', value: '0' },
{ label: '30 分钟', value: '30' },
{ label: '60 分钟', value: '60' },
{ label: '120 分钟', value: '120' },
{ label: '180 分钟', value: '180' },
{ label: '240 分钟', value: '240' },
{ label: '360 分钟', value: '360' },
{ label: '12 小时', value: '720' },
{ label: '24 小时', value: '1440' },
{ label: '48 小时', value: '2880' },
]
const booleanOptions: SystemConfigOption[] = [
{ label: '开启', value: 'true' },
{ label: '关闭', value: 'false' },
]
export const systemConfigSelectOptions: Record<string, SystemConfigOption[]> = {
'handoff.owner_submit_timeout_minutes': shortTimeoutOptions,
'handoff.renter_confirm_timeout_minutes': shortTimeoutOptions,
'handoff.owner_return_confirm_timeout_minutes': longTimeoutOptions,
'order.pending_payment_timeout_minutes': shortTimeoutOptions,
'order.return_overdue_grace_minutes': [
{ label: '无宽限', value: '0' },
{ label: '5 分钟', value: '5' },
{ label: '10 分钟', value: '10' },
{ label: '15 分钟', value: '15' },
{ label: '30 分钟', value: '30' },
{ label: '60 分钟', value: '60' },
{ label: '120 分钟', value: '120' },
],
'listing.review_required': booleanOptions,
'chat.default_support_admin_id': [
{ label: '管理员 ID 1', value: '1' },
{ label: '管理员 ID 2', value: '2' },
{ label: '管理员 ID 3', value: '3' },
],
}
export function getSystemConfigSelectOptions(key: string) {
return systemConfigSelectOptions[key] || null
}
export function formatSystemConfigSelectValue(key: string, value: string) {
const option = getSystemConfigSelectOptions(key)?.find(item => item.value === value)
return option?.label || null
}
-22
View File
@@ -1,22 +0,0 @@
type DateInput = string | number | Date | null | undefined
export function formatDateTime(value: DateInput, fallback = '-') {
if (!value) return fallback
const date = value instanceof Date ? value : new Date(value)
if (Number.isNaN(date.getTime())) {
return typeof value === 'string' && value.trim() ? value.replace('T', ' ') : fallback
}
return (
[date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate())].join('-') +
` ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
)
}
export function formatDateMinute(value: DateInput, fallback = '-') {
const formatted = formatDateTime(value, fallback)
return formatted === fallback ? fallback : formatted.slice(0, 16)
}
function pad(value: number) {
return String(value).padStart(2, '0')
}