feat: Features架构迁移 - P0和P1部分完成
## 完成的工作 ### P0: 基础设施准备 - 创建 features/ 和 shared/ 目录结构 - 迁移共享资源:API基础设施、工具函数、类型定义 - 迁移通用composables:useMoney, useSmsCountdown, usePricingCalculator - 迁移全局样式文件 - 建立模块化导出系统 ### P1.1: 钱包模块 (wallet) - 迁移 API: wallet.ts - 迁移 Views: WalletView.vue - 新增 Composable: useWallet.ts (封装钱包状态管理) - 更新导入路径到 shared/ ### P1.2: 聊天模块 (chats) - 迁移 API: chats.ts - 迁移 Views: ChatView, MessagesView (桌面+移动) - 迁移 Composables: useChatSSE.ts - 迁移 Components: ChatAttachmentImage.vue - 更新导入路径到 shared/ ## 技术改进 - 修复 shared/composables 导出问题 (default → 命名导出) - 修复 shared/api/client.ts 类型导入路径 - 建立清晰的模块边界和导出规范 ## 文档 - 添加完整的迁移计划文档 - 添加进度跟踪文档 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
10acca637e
commit
b5903a169f
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/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>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, RouterLink } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
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') }"
|
||||
>
|
||||
<van-icon name="chat-o" :size="22" />
|
||||
<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: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
height: calc(50px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user