This commit is contained in:
2025-12-11 21:02:05 +08:00
parent 3f0860b405
commit 408fe9294a
16 changed files with 872 additions and 713 deletions

View File

@@ -281,6 +281,7 @@ watch(() => props.remoteStream, (stream) => {
position: absolute; position: absolute;
inset: 0; inset: 0;
overflow: hidden; overflow: hidden;
background: #0f172a; // 设计稿: 来电背景
.blur-img { .blur-img {
width: 100%; width: 100%;
@@ -294,7 +295,35 @@ watch(() => props.remoteStream, (stream) => {
.blur-overlay { .blur-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, transparent 30%, rgba(0,0,0,0.9) 100%); background: rgba(15, 23, 42, 0.8); // 设计稿: bg-[#0f172a]/80
backdrop-filter: blur(48px);
-webkit-backdrop-filter: blur(48px);
}
// 设计稿: 呼吸光球动画
&::before, &::after {
content: '';
position: absolute;
width: 600rpx;
height: 600rpx;
border-radius: 50%;
filter: blur(160rpx);
opacity: 0.4;
animation: glow-breathe 8s ease-in-out infinite;
}
&::before {
top: 10%;
left: -100rpx;
background: #9333ea; // purple-600
animation-delay: 0s;
}
&::after {
bottom: 10%;
right: -100rpx;
background: #2563eb; // blue-600
animation-delay: 4s;
} }
} }
@@ -366,23 +395,36 @@ watch(() => props.remoteStream, (stream) => {
position: relative; position: relative;
margin-bottom: 48rpx; margin-bottom: 48rpx;
// 设计稿: 头像波纹效果
.avatar-ring { .avatar-ring {
position: absolute; position: absolute;
inset: -20rpx;
border-radius: 50%; border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.1); border: 2rpx solid rgba(255, 255, 255, 0.3); // 设计稿: border-white/30
&.ring-1 {
animation: ripple 2s cubic-bezier(0, 0, 0.2, 1) infinite;
}
&.ring-2 {
animation: ripple 2s cubic-bezier(0, 0, 0.2, 1) infinite;
animation-delay: 1s;
}
&.ring-ping { &.ring-ping {
inset: -4rpx; inset: -4rpx;
border: 2rpx solid rgba(255, 255, 255, 0.2); border: 2rpx solid rgba(255, 255, 255, 0.3);
animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
} }
} }
.avatar-container { .avatar-container {
position: relative;
border: 4rpx solid rgba(255, 255, 255, 0.1); border: 4rpx solid rgba(255, 255, 255, 0.1);
border-radius: 50%; border-radius: 50%;
padding: 4rpx; padding: 4rpx;
box-shadow: 0 40rpx 100rpx rgba(0, 0, 0, 0.4); box-shadow: 0 40rpx 100rpx rgba(0, 0, 0, 0.4);
z-index: 1;
} }
} }
@@ -527,4 +569,16 @@ watch(() => props.remoteStream, (stream) => {
from { transform: translateY(40rpx); opacity: 0; } from { transform: translateY(40rpx); opacity: 0; }
to { transform: translateY(0); opacity: 1; } to { transform: translateY(0); opacity: 1; }
} }
// 设计稿: 呼吸光球动画
@keyframes glow-breathe {
0%, 100% { transform: scale(1); opacity: 0.3; }
50% { transform: scale(1.2); opacity: 0.5; }
}
// 设计稿: 头像波纹动画
@keyframes ripple {
0% { transform: scale(1); opacity: 0.4; }
100% { transform: scale(2.5); opacity: 0; }
}
</style> </style>

View File

@@ -15,15 +15,12 @@
<!-- 头部用户信息 --> <!-- 头部用户信息 -->
<view class="drawer-header"> <view class="drawer-header">
<view class="avatar-wrap"> <view class="avatar-wrap">
<image <app-avatar
v-if="user?.avatar" :src="user?.avatar"
class="avatar" :name="user?.name"
:src="resolveImageUrl(user.avatar)" :size="120"
mode="aspectFill" round
/> />
<view v-else class="avatar avatar-placeholder">
{{ (user?.name || '?').charAt(0) }}
</view>
</view> </view>
<text class="username">{{ user?.name || '请登录' }}</text> <text class="username">{{ user?.name || '请登录' }}</text>
<text class="bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text> <text class="bio">{{ user?.desc || '编辑个签,展示你的独特个性' }}</text>
@@ -66,6 +63,7 @@ import { computed } from 'vue'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { resolveImageUrl } from '@/utils/image' import { resolveImageUrl } from '@/utils/image'
import AppAvatar from '@/components/common/AppAvatar.vue'
interface Props { interface Props {
modelValue: boolean modelValue: boolean

View File

@@ -50,6 +50,18 @@
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/> <polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
</svg> </svg>
</template> </template>
<!-- 我的图标 (user) -->
<template v-else-if="tab.name === 'profile'">
<svg v-if="current === tab.name" class="icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<svg v-else class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</template>
</view> </view>
<text class="tabbar-text">{{ tab.label }}</text> <text class="tabbar-text">{{ tab.label }}</text>
</view> </view>
@@ -74,11 +86,12 @@ const { isDark } = useTheme()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const unreadCount = computed(() => conversationStore.totalUnread) const unreadCount = computed(() => conversationStore.totalUnread)
// 底部导航只有3个: 消息/联系人/发现 (无"我的") // 底部导航4个: 消息/联系人/发现/我的
const tabs = [ const tabs = [
{ name: 'messages', label: '消息', path: '/pages/index/index' }, { name: 'messages', label: '消息', path: '/pages/index/index' },
{ name: 'contacts', label: '联系人', path: '/pages/contact/index' }, { name: 'contacts', label: '联系人', path: '/pages/contact/index' },
{ name: 'moments', label: '发现', path: '/pages/moment/index' } { name: 'moments', label: '发现', path: '/pages/moment/index' },
{ name: 'profile', label: '我的', path: '/pages/profile/index' }
] ]
function switchTab(name: string) { function switchTab(name: string) {

View File

@@ -75,15 +75,12 @@
<view v-else class="msg-row" :class="{ 'is-self': msg.isSelf }"> <view v-else class="msg-row" :class="{ 'is-self': msg.isSelf }">
<!-- 头像 - 设计稿: w-10 h-10 rounded-xl shrink-0 --> <!-- 头像 - 设计稿: w-10 h-10 rounded-xl shrink-0 -->
<view class="avatar-wrap" @click="onAvatarClick(msg)"> <view class="avatar-wrap" @click="onAvatarClick(msg)">
<image <app-avatar
v-if="msg.isSelf ? currentUser?.avatar : getMessageSenderAvatar(msg)" :src="msg.isSelf ? currentUser?.avatar : getMessageSenderAvatar(msg)"
:src="msg.isSelf ? resolveImageUrl(currentUser?.avatar || '') : getMessageSenderAvatar(msg)" :name="msg.isSelf ? currentUser?.name : getMessageSenderName(msg)"
class="avatar-img" :size="80"
mode="aspectFill" radius="24rpx"
/> />
<view v-else class="avatar-placeholder">
{{ msg.isSelf ? (currentUser?.name?.charAt(0) || '?') : (getMessageSenderName(msg)?.charAt(0) || '?') }}
</view>
</view> </view>
<!-- 内容区 - 设计稿: flex flex-col max-w-[75%] --> <!-- 内容区 - 设计稿: flex flex-col max-w-[75%] -->

File diff suppressed because it is too large Load Diff

View File

@@ -191,25 +191,33 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面容器 - 浅色模式 // 页面容器 - 浅色模式 (与设计稿完全一致)
.detail-container { .detail-container {
--bg-base: #f7f8fa; --bg-base: #ffffff; // 白色页面背景
--bg-card: #ffffff; --bg-card: #ffffff; // 卡片背景
--text-main: #1d1d1f; --bg-secondary: #f3f4f6; // gray-100 (次要背景/按钮)
--text-sub: #6b7280; --text-main: #1f2937; // gray-900 (名称)
--color-brand: #4F46E5; --text-sub: #6b7280; // gray-500 (签名/标签)
--text-tertiary: #9ca3af; // gray-400 (值)
--color-brand: #4F46E5; // indigo-600
--border-color: #f9fafb; // gray-50
--gradient-to: #ffffff; // 渐变终点色
min-height: 100vh; min-height: 100vh;
background: var(--bg-base); background: var(--bg-base);
position: relative; position: relative;
// 深色模式 - Warm Stone // 深色模式 - Warm Stone (与设计稿完全一致)
&.theme-dark { &.theme-dark {
--bg-base: #1c1917; --bg-base: #0c0a09; // warm-950 (极深背景)
--bg-card: #292524; --bg-card: #1c1917; // warm-900
--text-main: #f5f5f4; --bg-secondary: #292524; // warm-800
--text-sub: #78716c; --text-main: #f5f5f4; // warm-100
--color-brand: #f97316; --text-sub: #a8a29e; // warm-400
--text-tertiary: #78716c; // warm-500
--color-brand: #f97316; // orange-500
--border-color: rgba(41, 37, 36, 0.5); // warm-800/50
--gradient-to: #0c0a09; // warm-950
} }
} }
@@ -263,7 +271,7 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
.gradient-mask { .gradient-mask {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 85%); background: linear-gradient(to bottom, transparent 0%, var(--gradient-to) 85%);
} }
.nav-back { .nav-back {
@@ -341,14 +349,10 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
gap: 8rpx; gap: 8rpx;
font-size: 24rpx; font-size: 24rpx;
color: var(--text-sub); color: var(--text-sub);
background: rgba(0, 0, 0, 0.04); background: var(--bg-secondary);
padding: 10rpx 20rpx; padding: 10rpx 20rpx;
border-radius: 20rpx; border-radius: 20rpx;
.theme-dark & {
background: rgba(255, 255, 255, 0.08);
}
svg { svg {
width: 24rpx; width: 24rpx;
height: 24rpx; height: 24rpx;
@@ -515,12 +519,8 @@ async function deleteFriend() { try { await messageBox.confirm({ title: '提示'
&.secondary { &.secondary {
flex: 1; flex: 1;
background: rgba(0, 0, 0, 0.04); background: var(--bg-secondary);
color: var(--text-main); color: var(--text-main);
.theme-dark & {
background: rgba(255, 255, 255, 0.08);
}
} }
} }
} }

View File

@@ -20,16 +20,12 @@
@click="goChat(group)" @click="goChat(group)"
> >
<view class="card-left"> <view class="card-left">
<wd-img <app-avatar
v-if="group.room_avatar" :src="group.room_avatar"
:src="group.room_avatar" :name="group.room_name"
width="100rpx" :size="100"
height="100rpx" radius="20rpx"
radius="20rpx"
/> />
<view v-else class="avatar-placeholder" :style="{ background: generateColor(group.room_name || '') }">
{{ group.room_name?.charAt(0) || '群' }}
</view>
</view> </view>
<view class="card-right"> <view class="card-right">
@@ -59,6 +55,7 @@ import { ref, onMounted } from 'vue'
import * as roomApi from '@/api/modules/room' import * as roomApi from '@/api/modules/room'
import { generateColor } from '@/utils/format' import { generateColor } from '@/utils/format'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import AppAvatar from '@/components/common/AppAvatar.vue'
// --- 逻辑保持不变 --- // --- 逻辑保持不变 ---
interface GroupItem { room_id: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed' } interface GroupItem { room_id: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed' }

View File

@@ -50,6 +50,8 @@
<scroll-view <scroll-view
class="main-scroll custom-scrollbar" class="main-scroll custom-scrollbar"
scroll-y scroll-y
:scroll-into-view="scrollIntoViewId"
:scroll-with-animation="true"
:refresher-enabled="true" :refresher-enabled="true"
:refresher-triggered="refreshing" :refresher-triggered="refreshing"
@refresherrefresh="onRefresh" @refresherrefresh="onRefresh"
@@ -292,7 +294,7 @@ import type { Contact, ContactGroup } from '@/types/api'
// --- 逻辑完全不变 --- // --- 逻辑完全不变 ---
interface GroupChat { room_id: string; room_type: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed'; } interface GroupChat { room_id: string; room_type: string; room_name: string; room_avatar: string; category: 'joined' | 'created' | 'managed'; }
const contactStore = useContactStore(); const chatStore = useChatStore(); const { isDark } = useTheme(); const toast = useToast(); const messageBox = useMessage(); const contactStore = useContactStore(); const chatStore = useChatStore(); const { isDark } = useTheme(); const toast = useToast(); const messageBox = useMessage();
const tabs = [{ key: 'groups', label: '分组' }, { key: 'friends', label: '好友' }, { key: 'chats', label: '群聊' }]; const activeTab = ref('groups'); const searchKeyword = ref(''); const refreshing = ref(false); const loading = ref(false); const collapsedIds = ref<(number | string)[]>([]); const contactGroups = ref<ContactGroup[]>([]); const groupChats = ref<GroupChat[]>([]); const showContactActions = ref(false); const selectedContact = ref<Contact | null>(null); const showMoveGroupModal = ref(false); const showRemarkModal = ref(false); const remarkName = ref(''); const savingRemark = ref(false); const showGroupActions = ref(false); const selectedGroup = ref<ContactGroup | null>(null); const showCreateGroupModal = ref(false); const showRenameGroupModal = ref(false); const newGroupName = ref(''); const renameGroupName = ref(''); const creating = ref(false); const renaming = ref(false); const tabs = [{ key: 'groups', label: '分组' }, { key: 'friends', label: '好友' }, { key: 'chats', label: '群聊' }]; const activeTab = ref('groups'); const searchKeyword = ref(''); const refreshing = ref(false); const loading = ref(false); const collapsedIds = ref<(number | string)[]>([]); const contactGroups = ref<ContactGroup[]>([]); const groupChats = ref<GroupChat[]>([]); const showContactActions = ref(false); const selectedContact = ref<Contact | null>(null); const showMoveGroupModal = ref(false); const showRemarkModal = ref(false); const remarkName = ref(''); const savingRemark = ref(false); const showGroupActions = ref(false); const selectedGroup = ref<ContactGroup | null>(null); const showCreateGroupModal = ref(false); const showRenameGroupModal = ref(false); const newGroupName = ref(''); const renameGroupName = ref(''); const creating = ref(false); const renaming = ref(false); const scrollIntoViewId = ref('');
const contacts = computed(() => chatStore.contacts); const friendRequestCount = computed(() => contactStore.friendRequests.filter(r => r.status === 'pending').length); const ungroupedContacts = computed(() => contacts.value.filter(c => !c.group_id || c.group_id === 0)); function getGroupContacts(groupId: number): Contact[] { return contacts.value.filter(c => c.group_id === groupId) } const contacts = computed(() => chatStore.contacts); const friendRequestCount = computed(() => contactStore.friendRequests.filter(r => r.status === 'pending').length); const ungroupedContacts = computed(() => contacts.value.filter(c => !c.group_id || c.group_id === 0)); function getGroupContacts(groupId: number): Contact[] { return contacts.value.filter(c => c.group_id === groupId) }
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#']; const alphabetGroupedContacts = computed(() => { const groups: Record<string, Contact[]> = {}; contacts.value.forEach(contact => { const name = contact.remark_name || contact.user?.name || ''; const firstChar = name.charAt(0).toUpperCase(); const letter = /[A-Z]/.test(firstChar) ? firstChar : '#'; if (!groups[letter]) groups[letter] = []; groups[letter].push(contact) }); return Object.keys(groups).sort((a, b) => (a === '#' ? 1 : b === '#' ? -1 : a.localeCompare(b))).map(letter => ({ letter, contacts: groups[letter] })) }); const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#']; const alphabetGroupedContacts = computed(() => { const groups: Record<string, Contact[]> = {}; contacts.value.forEach(contact => { const name = contact.remark_name || contact.user?.name || ''; const firstChar = name.charAt(0).toUpperCase(); const letter = /[A-Z]/.test(firstChar) ? firstChar : '#'; if (!groups[letter]) groups[letter] = []; groups[letter].push(contact) }); return Object.keys(groups).sort((a, b) => (a === '#' ? 1 : b === '#' ? -1 : a.localeCompare(b))).map(letter => ({ letter, contacts: groups[letter] })) });
const createdGroups = computed(() => groupChats.value.filter(g => g.category === 'created')); const managedGroups = computed(() => groupChats.value.filter(g => g.category === 'managed')); const joinedGroups = computed(() => groupChats.value.filter(g => g.category === 'joined')); const createdGroups = computed(() => groupChats.value.filter(g => g.category === 'created')); const managedGroups = computed(() => groupChats.value.filter(g => g.category === 'managed')); const joinedGroups = computed(() => groupChats.value.filter(g => g.category === 'joined'));
@@ -312,7 +314,17 @@ function goFriendRequests() { uni.navigateTo({ url: '/pages/contact/requests' })
function goGroupNotify() { uni.navigateTo({ url: '/pages/group/notify' }) } function goGroupNotify() { uni.navigateTo({ url: '/pages/group/notify' }) }
function goContactDetail(contact: Contact) { uni.navigateTo({ url: `/pages/contact/detail?id=${contact.id}` }) } function goContactDetail(contact: Contact) { uni.navigateTo({ url: `/pages/contact/detail?id=${contact.id}` }) }
function goGroupChat(group: GroupChat) { uni.navigateTo({ url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name || '群聊')}` }) } function goGroupChat(group: GroupChat) { uni.navigateTo({ url: `/pages/chat/index?roomId=${group.room_id}&name=${encodeURIComponent(group.room_name || '群聊')}` }) }
function scrollToLetter(letter: string) { console.log(letter) } function scrollToLetter(letter: string) {
// 检查是否有该字母的分组
const hasGroup = alphabetGroupedContacts.value.some(g => g.letter === letter)
if (hasGroup) {
// 清空后再设置,确保重复点击同一字母也能触发滚动
scrollIntoViewId.value = ''
setTimeout(() => {
scrollIntoViewId.value = `letter-${letter}`
}, 10)
}
}
function handleContactLongPress(c: Contact) { selectedContact.value = c; remarkName.value = c.remark_name || ''; showContactActions.value = true } function handleContactLongPress(c: Contact) { selectedContact.value = c; remarkName.value = c.remark_name || ''; showContactActions.value = true }
function onContactActionSelect(a: { value: string }) { showContactActions.value = false; if(a.value === 'remark') showRemarkModal.value = true; else if(a.value === 'move') showMoveGroupModal.value = true; else if(a.value === 'delete') handleDeleteContact() } function onContactActionSelect(a: { value: string }) { showContactActions.value = false; if(a.value === 'remark') showRemarkModal.value = true; else if(a.value === 'move') showMoveGroupModal.value = true; else if(a.value === 'delete') handleDeleteContact() }
async function handleSaveRemark() { if(!selectedContact.value) return; savingRemark.value=true; try { await contactApi.updateContact(selectedContact.value.id.toString(), {remark_name: remarkName.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].remark_name = remarkName.value; showRemarkModal.value=false; toast.success('备注已修改') } catch(e){toast.error('失败')} finally {savingRemark.value=false} } async function handleSaveRemark() { if(!selectedContact.value) return; savingRemark.value=true; try { await contactApi.updateContact(selectedContact.value.id.toString(), {remark_name: remarkName.value}); const idx = chatStore.contacts.findIndex(c => c.id === selectedContact.value?.id); if(idx>-1) chatStore.contacts[idx].remark_name = remarkName.value; showRemarkModal.value=false; toast.success('备注已修改') } catch(e){toast.error('失败')} finally {savingRemark.value=false} }
@@ -330,18 +342,20 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
// 页面容器 - 浅色模式 (与设计稿一致) // 页面容器 - 浅色模式 (与设计稿一致)
// ========================================== // ==========================================
.page-container { .page-container {
--bg-page: #ffffff; --bg-page: #ffffff; // 页面背景
--bg-surface: #ffffff; --bg-surface: #ffffff; // 卡片背景
--bg-highlight: #f5f5f5; --bg-highlight: #f9fafb; // gray-50 (hover)
--bg-group: rgba(0, 0, 0, 0.02); --bg-group: #f9fafb; // gray-50 (分组容器)
--text-primary: #1f2937; --text-primary: #1f2937; // gray-900
--text-secondary: #6b7280; --text-secondary: #6b7280; // gray-500
--text-tertiary: #9ca3af; --text-tertiary: #9ca3af; // gray-400
--border-color: rgba(0, 0, 0, 0.05); --border-color: #f3f4f6; // gray-100
--color-brand: #4F46E5; --color-brand: #4F46E5; // indigo-600
--tab-bg: #f1f1f1; --tab-bg: #f3f4f6; // gray-100
--tab-active-bg: #ffffff; --tab-active-bg: #ffffff; // white + shadow-sm
--search-bg: #f1f1f1; --search-bg: #f3f4f6; // gray-100
--index-bg: rgba(243, 244, 246, 0.8); // gray-100/80
--index-hover: rgba(79, 70, 229, 0.1); // indigo-100
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
@@ -352,18 +366,20 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
// 深色模式 - Warm Stone (与设计稿一致) // 深色模式 - Warm Stone (与设计稿一致)
// ========================================== // ==========================================
.page-container.dark { .page-container.dark {
--bg-page: #1c1917; --bg-page: #1c1917; // warm-900
--bg-surface: #1c1917; --bg-surface: #1c1917; // warm-900
--bg-highlight: #44403c; --bg-highlight: #292524; // warm-800
--bg-group: rgba(255, 255, 255, 0.03); --bg-group: rgba(41, 37, 36, 0.4); // warm-800/40
--text-primary: #f5f5f4; --text-primary: #f5f5f4; // warm-100
--text-secondary: #e7e5e4; --text-secondary: #a8a29e; // warm-400
--text-tertiary: #78716c; --text-tertiary: #78716c; // warm-500
--border-color: #44403c; --border-color: rgba(41, 37, 36, 0.5); // warm-800/50
--color-brand: #f97316; --color-brand: #f97316; // orange-500
--tab-bg: #292524; --tab-bg: #292524; // warm-800
--tab-active-bg: #44403c; --tab-active-bg: #44403c; // warm-700
--search-bg: #292524; --search-bg: #292524; // warm-800
--index-bg: rgba(41, 37, 36, 0.8); // warm-800/80
--index-hover: rgba(154, 52, 18, 0.3); // orange-900/30
} }
// 动画 // 动画
@@ -754,23 +770,22 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
overflow: hidden; overflow: hidden;
} }
// A-Z索引条 // A-Z索引条 (设计稿: 带模糊背景)
.index-bar { .index-bar {
position: fixed; position: fixed;
right: 16rpx; right: 20rpx; // 设计稿: right-[20px] = 40rpx/2
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
background: rgba(0, 0, 0, 0.04); background: var(--index-bg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 20rpx; border-radius: 20rpx;
padding: 16rpx 8rpx; padding: 16rpx 8rpx;
z-index: 101; z-index: 101;
border: 1rpx solid var(--border-color);
.dark & {
background: rgba(255, 255, 255, 0.08);
}
.index-item { .index-item {
font-size: 20rpx; font-size: 20rpx; // 设计稿: text-[10px]
font-weight: 600; font-weight: 600;
width: 32rpx; width: 32rpx;
height: 32rpx; height: 32rpx;
@@ -783,11 +798,7 @@ async function handleDeleteGroup() { try { await messageBox.confirm({title:'提
&:active { &:active {
color: var(--color-brand); color: var(--color-brand);
background: rgba(79, 70, 229, 0.1); background: var(--index-hover);
.dark & {
background: rgba(249, 115, 22, 0.15);
}
} }
} }
} }

View File

@@ -9,15 +9,12 @@
<!-- 左侧: 头像 + 标题 --> <!-- 左侧: 头像 + 标题 -->
<view class="nav-left" @click="openDrawer"> <view class="nav-left" @click="openDrawer">
<view class="avatar-container"> <view class="avatar-container">
<image <app-avatar
v-if="user?.avatar" :src="user?.avatar"
class="nav-avatar" :name="user?.name"
:src="resolveImageUrl(user.avatar)" :size="72"
mode="aspectFill" radius="16rpx"
/> />
<view v-else class="nav-avatar avatar-placeholder">
{{ (user?.name || '?').charAt(0) }}
</view>
<view class="online-dot" v-if="!loading"></view> <view class="online-dot" v-if="!loading"></view>
</view> </view>
<text class="nav-title">消息</text> <text class="nav-title">消息</text>
@@ -307,20 +304,21 @@ async function logout() {
<style lang="scss" scoped> <style lang="scss" scoped>
// ========================================== // ==========================================
// 页面容器 - 浅色模式变量 // 页面容器 - 浅色模式变量 (与设计稿完全一致)
// ========================================== // ==========================================
.page-container { .page-container {
--bg-page: #ffffff; --bg-page: #ffffff; // 页面背景
--bg-surface: #ffffff; --bg-surface: #ffffff; // 卡片背景
--bg-hover: #f5f5f5; --bg-hover: #f9fafb; // gray-50 (设计稿: 会话项悬浮)
--bg-top: rgba(0, 0, 0, 0.02); --bg-top: rgba(249, 250, 251, 0.7); // gray-50/70 (设计稿: 会话置顶背景)
--text-primary: #1f2937; --text-primary: #1f2937; // gray-900 (设计稿: 会话名称)
--text-secondary: #6b7280; --text-secondary: #6b7280; // gray-500 (设计稿: 会话消息)
--text-tertiary: #9ca3af; --text-tertiary: #9ca3af; // gray-400 (设计稿: 会话时间)
--text-placeholder: #9ca3af; // gray-400 (设计稿: 搜索placeholder)
--border-color: rgba(0, 0, 0, 0.05); --border-color: rgba(0, 0, 0, 0.05);
--nav-bg: rgba(255, 255, 255, 0.9); --nav-bg: rgba(255, 255, 255, 0.9);
--color-brand: #4F46E5; --color-brand: #4F46E5; // indigo-600
--search-bg: #f1f1f1; --search-bg: #f1f1f1; // gray-100
--search-focus: rgba(79, 70, 229, 0.1); --search-focus: rgba(79, 70, 229, 0.1);
min-height: 100vh; min-height: 100vh;
@@ -329,20 +327,21 @@ async function logout() {
} }
// ========================================== // ==========================================
// 深色模式 - Warm Stone 暖色调 // 深色模式 - Warm Stone 暖色调 (与设计稿完全一致)
// ========================================== // ==========================================
.page-container.dark { .page-container.dark {
--bg-page: #1c1917; --bg-page: #1c1917; // warm-900
--bg-surface: #1c1917; --bg-surface: #1c1917; // warm-900
--bg-hover: #44403c; --bg-hover: #292524; // warm-800 (设计稿: 会话项悬浮)
--bg-top: rgba(255, 255, 255, 0.03); --bg-top: rgba(41, 37, 36, 0.3); // warm-800/30 (设计稿: 会话置顶背景)
--text-primary: #f5f5f4; --text-primary: #f5f5f4; // warm-100 (设计稿: 会话名称)
--text-secondary: #e7e5e4; --text-secondary: #a8a29e; // warm-400 (设计稿: 会话消息)
--text-tertiary: #78716c; --text-tertiary: #78716c; // warm-500 (设计稿: 会话时间)
--border-color: #44403c; --text-placeholder: #57534e; // warm-600 (设计稿: 搜索placeholder)
--border-color: #44403c; // warm-700
--nav-bg: rgba(28, 25, 23, 0.9); --nav-bg: rgba(28, 25, 23, 0.9);
--color-brand: #f97316; --color-brand: #f97316; // orange-500
--search-bg: #292524; --search-bg: #292524; // warm-800
--search-focus: rgba(249, 115, 22, 0.1); --search-focus: rgba(249, 115, 22, 0.1);
} }
@@ -527,7 +526,7 @@ async function logout() {
margin-left: 16rpx; margin-left: 16rpx;
&::placeholder { &::placeholder {
color: var(--text-tertiary); color: var(--text-placeholder);
} }
} }

View File

@@ -11,15 +11,12 @@
<view class="moment-content"> <view class="moment-content">
<!-- 用户信息 --> <!-- 用户信息 -->
<view class="header"> <view class="header">
<image <app-avatar
v-if="moment.user?.avatar" :src="moment.user?.avatar"
class="avatar" :name="moment.user?.name"
:src="resolveImageUrl(moment.user.avatar)" :size="80"
mode="aspectFill" round
/> />
<view v-else class="avatar avatar-placeholder">
{{ moment.user?.name?.charAt(0) || '?' }}
</view>
<view class="user-info"> <view class="user-info">
<text class="name">{{ moment.user?.name || '未知' }}</text> <text class="name">{{ moment.user?.name || '未知' }}</text>
<text class="time">{{ formatTime(moment.created_at) }}</text> <text class="time">{{ formatTime(moment.created_at) }}</text>
@@ -79,15 +76,12 @@
:key="comment.id" :key="comment.id"
class="comment-item" class="comment-item"
> >
<image <app-avatar
v-if="comment.user?.avatar" :src="comment.user?.avatar"
class="avatar" :name="comment.user?.name"
:src="resolveImageUrl(comment.user.avatar)" :size="64"
mode="aspectFill" round
/> />
<view v-else class="avatar avatar-placeholder">
{{ comment.user?.name?.charAt(0) || '?' }}
</view>
<view class="comment-content"> <view class="comment-content">
<view class="comment-header"> <view class="comment-header">
<text class="name">{{ comment.user?.name || '未知' }}</text> <text class="name">{{ comment.user?.name || '未知' }}</text>

View File

@@ -308,30 +308,32 @@ function getImageSize(moment: Moment): string {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面容器 - 浅色模式 // 页面容器 - 浅色模式 (与设计稿完全一致)
.moments-page { .moments-page {
--bg-page: #ffffff; --bg-page: #ffffff; // 页面背景
--bg-nav-rgb: 255, 255, 255; --bg-nav-rgb: 255, 255, 255; // 导航栏背景 RGB
--bg-detail: #f7f7f7; --bg-detail: #f3f4f6; // gray-100 (评论区背景)
--text-main: #1d1d1f; --text-main: #1f2937; // gray-900 (动态内容)
--text-blue: #576b95; --text-secondary: #9ca3af; // gray-400 (时间)
--color-brand: #4F46E5; --text-blue: #576b95; // 链接色 (作者名、点赞、评论)
--border-color: rgba(0, 0, 0, 0.05); --color-brand: #4F46E5; // indigo-600
--border-color: #f3f4f6; // gray-100
height: 100vh; height: 100vh;
background: var(--bg-page); background: var(--bg-page);
position: relative; position: relative;
} }
// 深色模式 - Warm Stone // 深色模式 - Warm Stone (与设计稿完全一致)
.moments-page.dark { .moments-page.dark {
--bg-page: #1c1917; --bg-page: #1c1917; // warm-900
--bg-nav-rgb: 28, 25, 23; --bg-nav-rgb: 28, 25, 23; // warm-900 RGB
--bg-detail: #292524; --bg-detail: rgba(41, 37, 36, 0.6); // warm-800/60 (评论区背景)
--text-main: #f5f5f4; --text-main: #f5f5f4; // warm-100 (动态内容)
--text-blue: #7e95c5; --text-secondary: #78716c; // warm-500 (时间)
--color-brand: #f97316; --text-blue: #7e95c5; // blue-400 (链接色)
--border-color: rgba(255, 255, 255, 0.1); --color-brand: #f97316; // orange-500
--border-color: #44403c; // warm-700
} }
// 动画 // 动画
@@ -577,8 +579,7 @@ function getImageSize(moment: Moment): string {
.time { .time {
font-size: 24rpx; font-size: 24rpx;
color: var(--text-blue); color: var(--text-secondary);
opacity: 0.6;
} }
.action-btn { .action-btn {
@@ -619,7 +620,11 @@ function getImageSize(moment: Moment): string {
height: 0; height: 0;
border-left: 12rpx solid transparent; border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent; border-right: 12rpx solid transparent;
border-bottom: 12rpx solid var(--bg-detail); border-bottom: 12rpx solid #f3f4f6; // gray-100
.dark & {
border-bottom-color: #292524; // 设计稿: warm-800
}
} }
.likes-list { .likes-list {

View File

@@ -23,16 +23,12 @@
:class="{ unread: !item.is_read }" :class="{ unread: !item.is_read }"
@click="goMomentDetail(item)" @click="goMomentDetail(item)"
> >
<wd-img <app-avatar
v-if="item.from_user?.avatar" :src="item.from_user?.avatar"
:src="resolveImageUrl(item.from_user.avatar)" :name="item.from_user?.name"
width="80rpx" :size="80"
height="80rpx"
radius="8rpx" radius="8rpx"
/> />
<view v-else class="avatar-placeholder" :style="{ background: generateColor(item.from_user?.name || '') }">
{{ item.from_user?.name?.charAt(0) || '?' }}
</view>
<view class="notify-content"> <view class="notify-content">
<view class="notify-header"> <view class="notify-header">

View File

@@ -49,8 +49,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { useAuthStore } from '@/stores'
import * as userApi from '@/api/modules/user'
const { isDark } = useTheme() const { isDark } = useTheme()
const authStore = useAuthStore()
const saving = ref(false)
const field = ref('') const field = ref('')
const value = ref('') const value = ref('')
const placeholder = ref('') const placeholder = ref('')
@@ -89,13 +93,42 @@ onMounted(() => {
} }
}) })
function save() { async function save() {
if (!value.value.trim()) { if (!value.value.trim()) {
uni.showToast({ title: '内容不能为空', icon: 'none' }) uni.showToast({ title: '内容不能为空', icon: 'none' })
return return
} }
uni.showToast({ title: '保存成功', icon: 'success' }) if (saving.value) return
setTimeout(() => { uni.navigateBack() }, 1500) saving.value = true
try {
const userId = authStore.user?.id
if (!userId) {
uni.showToast({ title: '用户信息异常', icon: 'none' })
return
}
// 构建更新数据
const updates: Record<string, string> = {}
updates[field.value] = value.value.trim()
// 调用 API 更新用户信息
const updatedUser = await userApi.updateUser({ id: userId, updates })
// 更新本地 store
if (authStore.user) {
const newUserInfo = { ...authStore.user, ...updates }
authStore.updateUserInfo(newUserInfo)
}
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => { uni.navigateBack() }, 1500)
} catch (error: any) {
console.error('保存失败:', error)
uni.showToast({ title: error.message || '保存失败', icon: 'none' })
} finally {
saving.value = false
}
} }
</script> </script>

View File

@@ -5,12 +5,7 @@
<!-- 头部大卡片 --> <!-- 头部大卡片 -->
<view class="profile-header"> <view class="profile-header">
<view class="header-top"> <view class="header-top">
<view class="back-btn" @click="goBack"> <text class="page-title">我的</text>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
</view>
<text class="page-title">个人资料</text>
<view class="icon-btn" @click="uni.navigateTo({ url: '/pages/settings/index' })"> <view class="icon-btn" @click="uni.navigateTo({ url: '/pages/settings/index' })">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
@@ -93,6 +88,7 @@
</view> </view>
</view> </view>
<app-tab-bar current="profile" />
</view> </view>
</wd-config-provider> </wd-config-provider>
</template> </template>
@@ -102,11 +98,11 @@ import { computed } from 'vue'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import AppAvatar from '@/components/common/AppAvatar.vue' import AppAvatar from '@/components/common/AppAvatar.vue'
import AppTabBar from '@/components/common/AppTabBar.vue'
const { isDark } = useTheme() const { isDark } = useTheme()
const authStore = useAuthStore() const authStore = useAuthStore()
const userInfo = computed(() => authStore.userInfo || {}) const userInfo = computed(() => authStore.userInfo || {})
function goBack() { uni.navigateBack() }
function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) } function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
</script> </script>
@@ -123,7 +119,7 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
padding-bottom: 80rpx; padding-bottom: 200rpx;
} }
// 深色模式 - Warm Stone // 深色模式 - Warm Stone
@@ -169,30 +165,11 @@ function goProfileEdit() { uni.navigateTo({ url: '/pages/profile/edit' }) }
align-items: center; align-items: center;
margin-bottom: 48rpx; margin-bottom: 48rpx;
.back-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.15s;
svg {
width: 40rpx;
height: 40rpx;
color: var(--text-primary);
}
&:active {
background: var(--border-color);
}
}
.page-title { .page-title {
font-size: 36rpx; font-size: 42rpx;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
letter-spacing: -0.5rpx;
} }
.icon-btn { .icon-btn {

View File

@@ -165,7 +165,7 @@ function clearHistory() {
function goBack() { uni.navigateBack() } function goBack() { uni.navigateBack() }
function goDetail(item: User) { function goDetail(item: User) {
uni.navigateTo({ url: `/pages/contact/detail?id=${item.id}` }) uni.navigateTo({ url: `/pages/contact/detail?userId=${item.id}` })
} }
// 高亮匹配文字 // 高亮匹配文字

View File

@@ -185,27 +185,27 @@ function handleClearCache() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面容器 - 浅色模式 // 页面容器 - 浅色模式 (与设计稿完全一致)
.settings-page { .settings-page {
--bg-page: #f7f8fa; --bg-page: #F5F7FA; // 设计稿: bg-[#F5F7FA]
--bg-surface: #ffffff; --bg-surface: #ffffff; // 卡片背景
--text-primary: #1d1d1f; --text-primary: #1f2937; // gray-800 (列表项文字)
--text-secondary: #6b7280; --text-secondary: #9ca3af; // gray-400 (分组标题)
--text-tertiary: #9ca3af; --text-tertiary: #9ca3af; // gray-400 (值/箭头)
--border-color: rgba(0, 0, 0, 0.05); --border-color: #f9fafb; // gray-50
min-height: 100vh; min-height: 100vh;
background: var(--bg-page); background: var(--bg-page);
} }
// 深色模式 - Warm Stone // 深色模式 - Warm Stone (与设计稿完全一致)
.settings-page.dark { .settings-page.dark {
--bg-page: #1c1917; --bg-page: #121212; // 设计稿: bg-[#121212]
--bg-surface: #292524; --bg-surface: #1c1917; // warm-900 (卡片背景)
--text-primary: #f5f5f4; --text-primary: #e7e5e4; // warm-200 (列表项文字)
--text-secondary: #e7e5e4; --text-secondary: #78716c; // warm-500 (分组标题)
--text-tertiary: #78716c; --text-tertiary: #9ca3af; // gray-400 (值/箭头)
--border-color: rgba(255, 255, 255, 0.1); --border-color: rgba(41, 37, 36, 0.5); // warm-800/50
} }
// 动画 // 动画
@@ -300,7 +300,11 @@ function handleClearCache() {
} }
&:active { &:active {
background: var(--border-color); background: #f9fafb; // gray-50
.dark & {
background: #292524; // warm-800
}
} }
.icon-box { .icon-box {