前端死代码清理

This commit is contained in:
yml
2026-06-14 18:42:06 +08:00
parent 965832c3cb
commit 2dc5a8121e
18 changed files with 0 additions and 4864 deletions
@@ -1,89 +0,0 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
const props = defineProps<{
source: string
admin?: boolean
}>()
const objectURL = ref('')
const failed = ref(false)
function extractObjectKey(value: string) {
try {
const parsed = new URL(value, window.location.origin)
return parsed.searchParams.get('key') || ''
} catch {
return ''
}
}
function revokeCurrentURL() {
if (!objectURL.value) return
URL.revokeObjectURL(objectURL.value)
objectURL.value = ''
}
async function loadImage() {
revokeCurrentURL()
failed.value = false
if (!props.source) {
failed.value = true
return
}
try {
const key = extractObjectKey(props.source)
const blob =
props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(props.source)
objectURL.value = URL.createObjectURL(blob)
} catch {
failed.value = true
}
}
function openImage() {
if (!objectURL.value) return
window.open(objectURL.value, '_blank')
}
watch(() => [props.source, props.admin] as const, loadImage, { immediate: true })
onBeforeUnmount(revokeCurrentURL)
</script>
<template>
<button v-if="objectURL" class="chat-image-button" type="button" @click="openImage">
<img :src="objectURL" alt="聊天图片" loading="lazy" decoding="async" />
</button>
<span v-else class="chat-image-fallback">{{ failed ? '图片加载失败' : '图片加载中' }}</span>
</template>
<style scoped>
.chat-image-button {
display: block;
max-width: 220px;
padding: 0;
overflow: hidden;
border: 0;
border-radius: 8px;
background: transparent;
cursor: zoom-in;
}
.chat-image-button img {
display: block;
width: 100%;
max-height: 260px;
object-fit: cover;
}
.chat-image-fallback {
display: inline-block;
padding: 8px 10px;
border-radius: 8px;
background: #eef2f7;
color: #6b7280;
font-size: 12px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,242 +0,0 @@
import { computed, ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import type { Order, HandoffRecord } from '../api/orders'
import {
fetchOrder,
fetchHandoffRecords,
cancelOrder,
startOrderPayment,
submitHandoff,
confirmReceive,
} from '../api/orders'
import { createDispute } from '@/features/disputes/api/disputes'
import { uploadFile } from '@/shared/api/files'
import { fetchOrderChat } from '@/features/chats/api/chats'
import { useSessionStore } from '@/stores/session'
import { usePaymentPolling } from './usePaymentPolling'
import { useSettlement } from './useSettlement'
/**
* 订单详情 Composable
* 负责订单的核心流程:加载、取消、支付、交接、收货
* 结算和支付轮询逻辑已拆分到独立 composables
*/
export function useOrderDetail() {
const route = useRoute()
const session = useSessionStore()
// Loading states
const loading = ref(false)
const cancelling = ref(false)
const startingPayment = ref(false)
const handoffing = ref(false)
const confirming = ref(false)
const disputing = ref(false)
const uploadingEvidence = ref(false)
const openingChat = ref(false)
// Data
const order = ref<Order | null>(null)
const handoffRecords = ref<HandoffRecord[]>([])
const handoffContent = ref('')
// Dispute form
const disputeType = ref('cannot_login')
const disputeDescription = ref('')
const disputeEvidenceText = ref('')
// 使用拆分的 composables
const paymentPolling = usePaymentPolling()
const settlement = useSettlement(order)
// Computed
const isOwner = computed(() => order.value?.owner_id === session.userId)
const isRenter = computed(() => order.value?.renter_id === session.userId)
const orderAmountLabel = computed(() => (isOwner.value ? '我的租金' : '订单金额'))
const canOpenDispute = computed(() => {
if (!order.value || (!isOwner.value && !isRenter.value)) return false
return ![
'completed',
'cancelled',
'closed',
'disputing',
'checkout_disputing',
'abnormal',
].includes(order.value.status)
})
const isCheckoutDisputeStage = computed(() => {
return (
!!order.value &&
['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value.status)
)
})
// Methods
async function loadOrder() {
loading.value = true
try {
order.value = await fetchOrder(String(route.params.id))
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
return order.value
} finally {
loading.value = false
}
}
async function handleCancel() {
if (!order.value) return false
cancelling.value = true
try {
await cancelOrder(order.value.id)
await loadOrder()
return true
} finally {
cancelling.value = false
}
}
async function handlePay() {
if (!order.value) return null
startingPayment.value = true
try {
const payment = await startOrderPayment(order.value.id)
// 开始支付轮询,成功后重新加载订单
paymentPolling.startPaymentPolling(payment, loadOrder)
return payment
} finally {
startingPayment.value = false
}
}
async function handleSubmitHandoff() {
if (!order.value || !handoffContent.value.trim()) return false
handoffing.value = true
try {
await submitHandoff(order.value.id, handoffContent.value.trim())
handoffContent.value = ''
await loadOrder()
return true
} finally {
handoffing.value = false
}
}
async function handleConfirmReceive() {
if (!order.value) return false
confirming.value = true
try {
await confirmReceive(order.value.id)
await loadOrder()
return true
} finally {
confirming.value = false
}
}
async function handleCreateDispute() {
if (!order.value) return false
disputing.value = true
try {
await createDispute(order.value.id, {
type: disputeType.value,
description: disputeDescription.value.trim(),
evidence_urls: linesToList(disputeEvidenceText.value),
})
await loadOrder()
return true
} finally {
disputing.value = false
}
}
async function handleUploadEvidence(file: File) {
uploadingEvidence.value = true
try {
const result = await uploadFile(file, 'evidence')
return result.url
} finally {
uploadingEvidence.value = false
}
}
async function handleOpenChat() {
if (!order.value) return null
openingChat.value = true
try {
const chat = await fetchOrderChat(order.value.id)
return chat
} finally {
openingChat.value = false
}
}
function getOrderStep(status: string) {
const stepMap: Record<string, number> = {
pending_payment: 0,
pending_handoff: 1,
renting: 2,
overdue: 2,
pending_checkout_confirm: 3,
pending_checkout_accept: 3,
completed: 5,
cancelled: 0,
closed: 4,
}
return stepMap[status] ?? 0
}
function linesToList(value: string) {
return value
.split('\n')
.map(line => line.trim())
.filter(Boolean)
}
onMounted(loadOrder)
return {
// States
loading,
cancelling,
startingPayment,
handoffing,
confirming,
disputing,
uploadingEvidence,
openingChat,
// Data
order,
handoffRecords,
handoffContent,
// Dispute form
disputeType,
disputeDescription,
disputeEvidenceText,
// Computed
isOwner,
isRenter,
orderAmountLabel,
canOpenDispute,
isCheckoutDisputeStage,
// Methods
loadOrder,
handleCancel,
handlePay,
handleSubmitHandoff,
handleConfirmReceive,
handleCreateDispute,
handleUploadEvidence,
handleOpenChat,
getOrderStep,
// 从拆分的 composables 导出
...paymentPolling,
...settlement,
}
}
@@ -1,65 +0,0 @@
import { ref, onBeforeUnmount } from 'vue'
import { queryOrderPayment, type PaymentOrder } from '../api/orders'
/**
* 支付轮询 Composable
* 负责轮询查询支付状态,直到支付完成
*/
export function usePaymentPolling() {
const activePayment = ref<PaymentOrder | null>(null)
const checkingPayment = ref(false)
let paymentPollingTimer: number | undefined
/**
* 开始轮询支付状态
*/
function startPaymentPolling(payment: PaymentOrder, onSuccess?: () => void) {
activePayment.value = payment
stopPaymentPolling()
paymentPollingTimer = window.setInterval(() => checkPaymentStatus(onSuccess), 2000)
}
/**
* 停止轮询
*/
function stopPaymentPolling() {
if (paymentPollingTimer !== undefined) {
clearInterval(paymentPollingTimer)
paymentPollingTimer = undefined
}
activePayment.value = null
}
/**
* 检查支付状态
*/
async function checkPaymentStatus(onSuccess?: () => void) {
if (!activePayment.value || checkingPayment.value) return false
checkingPayment.value = true
try {
const updated = await queryOrderPayment(activePayment.value.order_id)
if (updated.paid) {
stopPaymentPolling()
onSuccess?.()
return true
}
} catch {
// 忽略轮询错误,继续下次轮询
} finally {
checkingPayment.value = false
}
return false
}
// 组件卸载时停止轮询
onBeforeUnmount(stopPaymentPolling)
return {
activePayment,
checkingPayment,
startPaymentPolling,
stopPaymentPolling,
checkPaymentStatus,
}
}
@@ -1,178 +0,0 @@
import { ref, type Ref } from 'vue'
import { submitCheckout, acceptCheckout, counterCheckout, confirmCheckout } from '../api/orders'
import type { Order } from '../api/orders'
export interface CheckoutForm {
content: string
consumableAmountYuan: number
coin_consumed_m: number
otherAmountYuan: number
evidenceText: string
}
export interface CounterForm {
consumableAmountYuan: number
coin_consumed_m: number
otherAmountYuan: number
depositDeductAmountYuan: number
reason: string
evidenceText: string
}
/**
* 订单结算 Composable
* 负责处理订单结算流程:提交结算、接受结算、反驳结算、确认结算
*/
export function useSettlement(order: Ref<Order | null>) {
const returning = ref(false)
const acceptingCheckout = ref(false)
const countering = ref(false)
const rejectingCheckout = ref(false)
const completing = ref(false)
const checkoutForm = ref<CheckoutForm>({
content: '',
consumableAmountYuan: 0,
coin_consumed_m: 0,
otherAmountYuan: 0,
evidenceText: '',
})
const counterForm = ref<CounterForm>({
consumableAmountYuan: 0,
coin_consumed_m: 0,
otherAmountYuan: 0,
depositDeductAmountYuan: 0,
reason: '',
evidenceText: '',
})
const resourceUsage = ref<Record<string, number>>({})
/**
* 提交结算单
*/
async function handleSubmitCheckout(onSuccess?: () => void) {
if (!order.value) return false
returning.value = true
try {
await submitCheckout(order.value.id, {
content: checkoutForm.value.content.trim(),
consumableAmountYuan: checkoutForm.value.consumableAmountYuan,
coin_consumed_m: checkoutForm.value.coin_consumed_m,
otherAmountYuan: checkoutForm.value.otherAmountYuan,
evidence_urls: linesToList(checkoutForm.value.evidenceText),
})
onSuccess?.()
return true
} finally {
returning.value = false
}
}
/**
* 接受对方的结算
*/
async function handleAcceptCheckout(onSuccess?: () => void) {
if (!order.value) return false
acceptingCheckout.value = true
try {
await acceptCheckout(order.value.id)
onSuccess?.()
return true
} finally {
acceptingCheckout.value = false
}
}
/**
* 反驳对方的结算
*/
async function handleCounterCheckout(onSuccess?: () => void) {
if (!order.value) return false
countering.value = true
try {
const reasonText = counterForm.value.reason.trim()
await counterCheckout(order.value.id, {
content: reasonText,
consumableAmountYuan: counterForm.value.consumableAmountYuan,
coin_consumed_m: counterForm.value.coin_consumed_m,
otherAmountYuan: counterForm.value.otherAmountYuan,
depositDeductAmountYuan: counterForm.value.depositDeductAmountYuan,
reason: reasonText,
evidence_urls: linesToList(counterForm.value.evidenceText),
})
onSuccess?.()
return true
} finally {
countering.value = false
}
}
/**
* 拒绝对方的结算(简化版反驳)
*/
async function handleRejectCheckout(reason: string, onSuccess?: () => void) {
if (!order.value) return false
rejectingCheckout.value = true
try {
const reasonText = reason.trim()
await counterCheckout(order.value.id, {
content: reasonText,
consumableAmountYuan: 0,
coin_consumed_m: 0,
otherAmountYuan: 0,
depositDeductAmountYuan: 0,
reason: reasonText,
evidence_urls: [],
})
onSuccess?.()
return true
} finally {
rejectingCheckout.value = false
}
}
/**
* 确认最终结算
*/
async function handleConfirmCheckout(onSuccess?: () => void) {
if (!order.value) return false
completing.value = true
try {
await confirmCheckout(order.value.id)
onSuccess?.()
return true
} finally {
completing.value = false
}
}
function linesToList(value: string) {
return value
.split('\n')
.map(line => line.trim())
.filter(Boolean)
}
return {
// Loading states
returning,
acceptingCheckout,
countering,
rejectingCheckout,
completing,
// Forms
checkoutForm,
counterForm,
resourceUsage,
// Methods
handleSubmitCheckout,
handleAcceptCheckout,
handleCounterCheckout,
handleRejectCheckout,
handleConfirmCheckout,
}
}
-3
View File
@@ -1,9 +1,6 @@
// Orders 模块统一导出
export * from './api/orders'
export * from './composables/useOrderDetail'
export * from './composables/useOrderSnapshot'
export * from './composables/usePaymentPolling'
export * from './composables/useSettlement'
export { default as OrderCheckoutSummary } from './components/OrderCheckoutSummary.vue'
export { default as OrderHandoffTimeline } from './components/OrderHandoffTimeline.vue'
export { default as OrderResourceUsageEditor } from './components/OrderResourceUsageEditor.vue'
@@ -1,54 +0,0 @@
import { ref, computed } from 'vue'
import { fetchWalletBalance, fetchWalletLedger } from '../api/wallet'
import type { WalletAccount, WalletLedger } from '../api/wallet'
export function useWallet() {
const balance = ref<WalletAccount | null>(null)
const ledgers = ref<WalletLedger[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const availableBalance = computed(() => balance.value?.available_balance_cent ?? 0)
const frozenBalance = computed(() => balance.value?.frozen_balance_cent ?? 0)
const totalBalance = computed(() => availableBalance.value + frozenBalance.value)
async function loadBalance() {
loading.value = true
error.value = null
try {
balance.value = await fetchWalletBalance()
} catch (err) {
error.value = err instanceof Error ? err.message : '加载余额失败'
throw err
} finally {
loading.value = false
}
}
async function loadLedger(page = 1, pageSize = 20) {
loading.value = true
error.value = null
try {
const result = await fetchWalletLedger(page, pageSize)
ledgers.value = result.items
return result
} catch (err) {
error.value = err instanceof Error ? err.message : '加载账单失败'
throw err
} finally {
loading.value = false
}
}
return {
balance,
ledgers,
loading,
error,
availableBalance,
frozenBalance,
totalBalance,
loadBalance,
loadLedger,
}
}
-1
View File
@@ -1,4 +1,3 @@
// Wallet 模块统一导出
export * from './api/wallet'
export * from './api/withdrawal'
export * from './composables/useWallet'
@@ -1,89 +0,0 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
const props = defineProps<{
source: string
admin?: boolean
}>()
const objectURL = ref('')
const failed = ref(false)
function extractObjectKey(value: string) {
try {
const parsed = new URL(value, window.location.origin)
return parsed.searchParams.get('key') || ''
} catch {
return ''
}
}
function revokeCurrentURL() {
if (!objectURL.value) return
URL.revokeObjectURL(objectURL.value)
objectURL.value = ''
}
async function loadImage() {
revokeCurrentURL()
failed.value = false
if (!props.source) {
failed.value = true
return
}
try {
const key = extractObjectKey(props.source)
const blob =
props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(props.source)
objectURL.value = URL.createObjectURL(blob)
} catch {
failed.value = true
}
}
function openImage() {
if (!objectURL.value) return
window.open(objectURL.value, '_blank')
}
watch(() => [props.source, props.admin] as const, loadImage, { immediate: true })
onBeforeUnmount(revokeCurrentURL)
</script>
<template>
<button v-if="objectURL" class="chat-image-button" type="button" @click="openImage">
<img :src="objectURL" alt="聊天图片" loading="lazy" decoding="async" />
</button>
<span v-else class="chat-image-fallback">{{ failed ? '图片加载失败' : '图片加载中' }}</span>
</template>
<style scoped>
.chat-image-button {
display: block;
max-width: 220px;
padding: 0;
overflow: hidden;
border: 0;
border-radius: 8px;
background: transparent;
cursor: zoom-in;
}
.chat-image-button img {
display: block;
width: 100%;
max-height: 260px;
object-fit: cover;
}
.chat-image-fallback {
display: inline-block;
padding: 8px 10px;
border-radius: 8px;
background: #eef2f7;
color: #6b7280;
font-size: 12px;
}
</style>
@@ -1,137 +0,0 @@
<script setup lang="ts">
import { useRoute, RouterLink } from 'vue-router'
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
const route = useRoute()
const { unreadCount, unreadLabel } = useChatUnreadCount(route)
function isNavActive(path: string) {
if (path === '/m') return route.path === '/m'
return route.path.startsWith(path)
}
</script>
<template>
<nav class="bottom-nav">
<RouterLink to="/m" class="nav-item" :class="{ active: isNavActive('/m') }">
<van-icon name="home-o" :size="22" />
<span>首页</span>
</RouterLink>
<RouterLink to="/m/messages" class="nav-item" :class="{ active: isNavActive('/m/messages') }">
<span class="nav-icon-wrap">
<van-icon name="chat-o" :size="22" />
<em v-if="unreadCount > 0" class="nav-badge">{{ unreadLabel }}</em>
</span>
<span>消息</span>
</RouterLink>
<RouterLink to="/m/seller/listings/create" class="nav-item nav-publish">
<div class="publish-pill">+</div>
<span>发布</span>
</RouterLink>
<RouterLink to="/m/orders" class="nav-item" :class="{ active: isNavActive('/m/orders') }">
<van-icon name="orders-o" :size="22" />
<span>订单</span>
</RouterLink>
<RouterLink to="/m/profile" class="nav-item" :class="{ active: isNavActive('/m/profile') }">
<van-icon name="manager-o" :size="22" />
<span>我的</span>
</RouterLink>
</nav>
</template>
<style scoped>
.bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: -1px;
z-index: 100;
display: flex;
background: #fff;
border-top: 1px solid #eee;
padding-bottom: env(safe-area-inset-bottom);
height: calc(52px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.bottom-nav::after {
position: absolute;
right: 0;
bottom: -18px;
left: 0;
height: 18px;
background: #fff;
content: '';
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: #999;
font-size: 10px;
text-decoration: none;
}
.nav-item.active {
color: #1477ff;
}
.nav-item span {
font-weight: 600;
}
.nav-icon-wrap {
position: relative;
display: grid;
width: 24px;
height: 24px;
place-items: center;
}
.nav-badge {
position: absolute;
top: -5px;
right: -10px;
min-width: 16px;
height: 16px;
padding: 0 4px;
box-sizing: border-box;
border: 2px solid #fff;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-style: normal;
font-weight: 800;
line-height: 12px;
text-align: center;
}
.publish-pill {
width: 36px;
height: 26px;
display: grid;
place-items: center;
border-radius: 13px;
background: #ff6a00;
color: #fff;
font-size: 18px;
font-weight: 900;
}
.nav-publish span {
color: #ff6a00;
}
/* 响应式适配 */
@media (min-width: 520px) {
.bottom-nav {
left: calc((100vw - 430px) / 2);
right: calc((100vw - 430px) / 2);
}
}
</style>
-1
View File
@@ -1,4 +1,3 @@
// 通用 Composables
export { useMoney } from './useMoney'
export { useSmsCountdown } from './useSmsCountdown'
export { usePricingCalculator } from './usePricingCalculator'
@@ -1,5 +0,0 @@
import { formatMoneyWithSymbol } from '@/shared/utils/money'
export function useMoney() {
return (value: number | undefined | null) => formatMoneyWithSymbol(value)
}
File diff suppressed because it is too large Load Diff
-49
View File
@@ -1,49 +0,0 @@
:root {
color: #1f2933;
background: #f6f8fb;
font-family:
Inter,
'PingFang SC',
'Microsoft YaHei',
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
max-width: 100%;
overflow-x: clip;
}
#app {
max-width: 100%;
overflow-x: clip;
}
a {
color: inherit;
text-decoration: none;
}
/* ── Vant Toast 保护规则 ──────────────────────────────────
.van-popup 声明 background: var(--van-popup-background) = #fff (白色)
.van-toast 声明 background: var(--van-toast-background) = rgba(0,0,0,.7) (黑色)
两个类同优先级,但 .van-popup 在 vant/lib/index.css 中后声明 → 覆盖黑色背景
导致 Toast 白底白字看不见。用双类选择器提升优先级强制使用黑色背景。
────────────────────────────────────────────────────────── */
.van-popup.van-toast {
background: var(--van-toast-background) !important;
color: var(--van-toast-text-color) !important;
}
-230
View File
@@ -1,230 +0,0 @@
/* 筛选器相关样式 */
.horizontal-filter-card {
padding: 24px;
border: 1px solid #eef1f5;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(23, 35, 61, 0.03);
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filter-title {
display: flex;
align-items: center;
gap: 12px;
}
.filter-title strong {
font-size: 18px;
font-weight: 800;
color: #17233d;
}
.filter-title span {
padding: 4px 10px;
border-radius: 6px;
background: #f1f5f9;
font-size: 13px;
font-weight: 700;
color: #64748b;
}
.filter-chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 2px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
font-size: 13px;
font-weight: 600;
color: #334155;
cursor: pointer;
transition: all 0.2s;
}
.filter-chip:hover {
border-color: #ff6a00;
background: #fff7ed;
}
.filter-chip.active {
border-color: #ff6a00;
background: #fff7ed;
color: #ff6a00;
}
.filter-chip.wide {
min-width: 140px;
}
/* 筛选器弹窗样式 */
:global(.home-filter-popover) {
padding: 8px !important;
border-radius: 12px !important;
}
.filter-menu,
.range-menu {
display: flex;
flex-direction: column;
gap: 4px;
}
.filter-menu button,
.range-menu button {
padding: 10px 14px;
border: none;
border-radius: 8px;
background: transparent;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #334155;
cursor: pointer;
transition: all 0.15s;
}
.filter-menu button:hover,
.range-menu button:hover {
background: #f1f5f9;
}
.filter-menu button.active,
.range-menu button.active {
background: #fff7ed;
color: #ff6a00;
font-weight: 700;
}
.range-manual {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-top: 1px solid #e2e8f0;
margin-top: 4px;
}
.range-manual :deep(.el-input-number) {
flex: 1;
}
.range-manual span {
color: #94a3b8;
font-weight: 700;
}
/* 皮肤筛选器 */
.skin-filter-menu {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 400px;
overflow-y: auto;
}
.skin-reset {
padding: 10px 14px;
border: none;
border-radius: 8px;
background: transparent;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #334155;
cursor: pointer;
transition: all 0.15s;
}
.skin-reset:hover {
background: #f1f5f9;
}
.skin-reset.active {
background: #fff7ed;
color: #ff6a00;
font-weight: 700;
}
.skin-filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.skin-filter-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
}
.skin-filter-title strong {
font-size: 13px;
font-weight: 700;
color: #17233d;
}
.skin-filter-title button {
padding: 4px 8px;
border: none;
border-radius: 6px;
background: transparent;
font-size: 11px;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.15s;
}
.skin-filter-title button:hover {
background: #f1f5f9;
}
.skin-filter-title button.active {
background: #fff7ed;
color: #ff6a00;
}
.skin-filter-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
}
.skin-filter-options button {
padding: 8px 12px;
border: none;
border-radius: 6px;
background: transparent;
text-align: left;
font-size: 12px;
font-weight: 600;
color: #334155;
cursor: pointer;
transition: all 0.15s;
}
.skin-filter-options button:hover {
background: #f1f5f9;
}
.skin-filter-options button.active {
background: #fff7ed;
color: #ff6a00;
font-weight: 700;
}
File diff suppressed because it is too large Load Diff
@@ -1,36 +0,0 @@
/* 性能优化相关样式 */
.lazy-image {
background: #f1f5f9;
min-height: 180px;
}
.component-loading,
.component-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
color: #94a3b8;
font-size: 14px;
}
.component-error {
color: #ef4444;
}
/* 骨架屏动画 */
@keyframes skeleton-loading {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200px 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
-36
View File
@@ -1,36 +0,0 @@
/* 性能优化相关样式 */
.lazy-image {
background: #f1f5f9;
min-height: 180px;
}
.component-loading,
.component-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
color: #94a3b8;
font-size: 14px;
}
.component-error {
color: #ef4444;
}
/* 骨架屏动画 */
@keyframes skeleton-loading {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200px 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}