前端死代码清理
This commit is contained in:
@@ -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,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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user