diff --git a/components.d.ts b/components.d.ts
index a2be2a7..01127df 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -13,10 +13,26 @@ declare module 'vue' {
AppLoading: typeof import('./src/components/common/AppLoading.vue')['default']
AppLogos: typeof import('./src/components/AppLogos.vue')['default']
AppNavBar: typeof import('./src/components/common/AppNavBar.vue')['default']
+ AppTabBar: typeof import('./src/components/common/AppTabBar.vue')['default']
+ CallWindow: typeof import('./src/components/call/CallWindow.vue')['default']
+ GroupCallBanner: typeof import('./src/components/call/GroupCallBanner.vue')['default']
+ GroupCallIncoming: typeof import('./src/components/call/GroupCallIncoming.vue')['default']
+ GroupCallModal: typeof import('./src/components/call/GroupCallModal.vue')['default']
+ GroupCallWindow: typeof import('./src/components/call/GroupCallWindow.vue')['default']
+ ImagePreview: typeof import('./src/components/common/ImagePreview.vue')['default']
InputEntry: typeof import('./src/components/InputEntry.vue')['default']
+ MentionPicker: typeof import('./src/components/chat/MentionPicker.vue')['default']
MessageBubble: typeof import('./src/components/chat/MessageBubble.vue')['default']
MessageInput: typeof import('./src/components/chat/MessageInput.vue')['default']
MomentCard: typeof import('./src/components/moment/MomentCard.vue')['default']
+ MomentCommentInput: typeof import('./src/components/moment/MomentCommentInput.vue')['default']
+ MomentCommentList: typeof import('./src/components/moment/MomentCommentList.vue')['default']
+ MomentNotificationPanel: typeof import('./src/components/moment/MomentNotificationPanel.vue')['default']
+ SelectContactsModal: typeof import('./src/components/chat/SelectContactsModal.vue')['default']
+ UserInfoCard: typeof import('./src/components/common/UserInfoCard.vue')['default']
+ UserMomentsModal: typeof import('./src/components/moment/UserMomentsModal.vue')['default']
+ VideoPlayer: typeof import('./src/components/common/VideoPlayer.vue')['default']
+ VisibilitySelector: typeof import('./src/components/moment/VisibilitySelector.vue')['default']
WdActionSheet: typeof import('wot-design-uni/components/wd-action-sheet/wd-action-sheet.vue')['default']
WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']
diff --git a/package.json b/package.json
index add7302..baf50e9 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@uni-helper/unh": "^0.2.7",
"@uni-helper/uni-types": "^1.0.0-alpha.7",
"@uni-helper/vite-plugin-uni-components": "^0.2.3",
+ "@uni-ku/root": "^1.4.1",
"@vue/runtime-core": "3.4.21",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7929ec5..582370e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
'@uni-helper/vite-plugin-uni-components':
specifier: ^0.2.3
version: 0.2.3(rollup@4.53.3)
+ '@uni-ku/root':
+ specifier: ^1.4.1
+ version: 1.4.1(vite@5.2.8(@types/node@24.10.1)(sass@1.64.2)(terser@5.44.1))
'@vue/runtime-core':
specifier: 3.4.21
version: 3.4.21
@@ -1825,6 +1828,11 @@ packages:
'@uni-helper/vite-plugin-uni-components@0.2.3':
resolution: {integrity: sha512-in7jRYQnPkLg6W13dzz8mzHDuZC+OFc3qb8MrnmDQgniIxHfxJw8vaibL1au6gvq7ij3h6EHvhHaRytIJ2PkYw==}
+ '@uni-ku/root@1.4.1':
+ resolution: {integrity: sha512-DI9LLbWmPXjqJVwZTC84GuvSz9vzOgSUjbFrOE1y3kivttEReNVqwWM3n/kzFoNuS3KKm/DissOph4rFs912Zw==}
+ peerDependencies:
+ vite: ^5.0.0
+
'@vitejs/plugin-legacy@5.3.2':
resolution: {integrity: sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -8291,6 +8299,13 @@ snapshots:
- rollup
- supports-color
+ '@uni-ku/root@1.4.1(vite@5.2.8(@types/node@24.10.1)(sass@1.64.2)(terser@5.44.1))':
+ dependencies:
+ '@vue/compiler-sfc': 3.4.21
+ chokidar: 3.6.0
+ jsonc-parser: 3.3.1
+ vite: 5.2.8(@types/node@24.10.1)(sass@1.64.2)(terser@5.44.1)
+
'@vitejs/plugin-legacy@5.3.2(terser@5.44.1)(vite@5.2.8(@types/node@24.10.1)(sass@1.64.2)(terser@5.44.1))':
dependencies:
'@babel/core': 7.25.2
diff --git a/src/App.ku.vue b/src/App.ku.vue
new file mode 100644
index 0000000..d5c4033
--- /dev/null
+++ b/src/App.ku.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/src/App.vue b/src/App.vue
index 792b6a3..97d11f5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,10 +1,38 @@
@@ -39,7 +72,10 @@ page {
}
/* 暗色模式下的 wot-design-uni 组件适配 */
-page.dark {
+page.dark,
+.dark,
+html.dark,
+body.dark {
// Cell
--wot-cell-bg-color: var(--bg-content);
--wot-cell-title-color: var(--text-primary);
diff --git a/src/api/modules/room.ts b/src/api/modules/room.ts
index b6f3dd1..8d6c2a3 100644
--- a/src/api/modules/room.ts
+++ b/src/api/modules/room.ts
@@ -149,3 +149,8 @@ export function getGroupSettings(roomId: string) {
updated_at: string
}>(`/groups/${roomId}/settings`)
}
+
+// 更新我在群里的昵称
+export function updateMyNickname(roomId: string, nickname: string) {
+ return request.post(`/groups/${roomId}/nickname`, { nickname })
+}
\ No newline at end of file
diff --git a/src/components/call/CallWindow.vue b/src/components/call/CallWindow.vue
new file mode 100644
index 0000000..dd5388f
--- /dev/null
+++ b/src/components/call/CallWindow.vue
@@ -0,0 +1,325 @@
+
+
+
+
+
+
+ {{ formatDuration(call.duration) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ call.callerName || '对方' }}
+
+
+
+
+
+
+
+
+
+ {{ call.callerName || '对方' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前平台暂不支持音视频通话
+ 关闭
+
+
+
+
+
+
+
+
diff --git a/src/components/call/GroupCallBanner.vue b/src/components/call/GroupCallBanner.vue
new file mode 100644
index 0000000..dddb7a0
--- /dev/null
+++ b/src/components/call/GroupCallBanner.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 群通话进行中
+ {{ participantsCount }}人
+
+
+
+
+
+
+
+
+ +{{ participantsCount - 5 }}
+
+
+
+
+
+
+
+ 加入
+
+
+
+
+
+
+
+
diff --git a/src/components/call/GroupCallIncoming.vue b/src/components/call/GroupCallIncoming.vue
new file mode 100644
index 0000000..d1c76a4
--- /dev/null
+++ b/src/components/call/GroupCallIncoming.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ callState.inviterName }}
+
+ 邀请你加入群{{ callState.type === 'video' ? '视频' : '语音' }}通话
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 拒绝
+
+
+
+
+
+
+
+ 接听
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/call/GroupCallModal.vue b/src/components/call/GroupCallModal.vue
new file mode 100644
index 0000000..4e413c1
--- /dev/null
+++ b/src/components/call/GroupCallModal.vue
@@ -0,0 +1,346 @@
+
+
+
+
+
+
+
+
+
+
+ {{ isAllSelected ? '取消全选' : '全选' }}
+
+
+
+
+
+
+
+ 未找到成员
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ member.remark_name || member.user?.name || '未知成员' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/call/GroupCallWindow.vue b/src/components/call/GroupCallWindow.vue
new file mode 100644
index 0000000..450033b
--- /dev/null
+++ b/src/components/call/GroupCallWindow.vue
@@ -0,0 +1,474 @@
+
+
+
+
+
+
+
+
+ {{ formatDuration(callState.duration) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 摄像头已关闭
+
+
+
+ 我
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.name }}
+
+ {{ user.status === 'connecting' ? '连接中...' : user.status === 'rejected' ? '已拒绝' : '摄像头关闭' }}
+
+
+
+
+ {{ user.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ callState.isSelfMuted ? '已静音' : '静音' }}
+
+
+
+
+
+
+
+ 挂断
+
+
+
+
+
+
+
+ {{ callState.isSelfCamOff ? '已关闭' : '摄像头' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat/MentionPicker.vue b/src/components/chat/MentionPicker.vue
new file mode 100644
index 0000000..6eecf45
--- /dev/null
+++ b/src/components/chat/MentionPicker.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+ 暂无匹配用户
+
+
+
+
+ {{ user.name || user.id }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat/MessageBubble.vue b/src/components/chat/MessageBubble.vue
index 560c2e8..01f49ba 100644
--- a/src/components/chat/MessageBubble.vue
+++ b/src/components/chat/MessageBubble.vue
@@ -105,3 +105,10 @@ const bubbleClass = computed(() => {
}
}
+
+
+
+
+
+
+
diff --git a/src/components/chat/MessageInput.vue b/src/components/chat/MessageInput.vue
index 1d0643b..262617c 100644
--- a/src/components/chat/MessageInput.vue
+++ b/src/components/chat/MessageInput.vue
@@ -1,10 +1,37 @@
+
+
+
+
+
+
+
+ {{ member.nickname || member.user?.name || '未知' }}
+
+
+
+
+
+
@@ -16,6 +43,7 @@
:rows="1"
placeholder="输入消息..."
no-border
+ @input="onInput"
@confirm="onSend"
/>
@@ -41,13 +69,19 @@
+
+
+
diff --git a/src/components/common/AppAvatar.vue b/src/components/common/AppAvatar.vue
index 6fe44df..8da69d2 100644
--- a/src/components/common/AppAvatar.vue
+++ b/src/components/common/AppAvatar.vue
@@ -1,8 +1,8 @@
import { computed } from 'vue'
import { generateColor } from '@/utils/format'
+import { resolveImageUrl } from '@/utils/image'
interface Props {
src?: string
@@ -54,6 +55,23 @@ const props = withDefaults(defineProps(), {
bgColor: ''
})
+// 判断是否为有效图片 URL
+function isValidImageUrl(url?: string): boolean {
+ if (!url) return false
+ // 有效 URL: http/https/data URI 或以 /uploads/ 开头
+ return url.startsWith('http://') ||
+ url.startsWith('https://') ||
+ url.startsWith('data:') ||
+ url.startsWith('/uploads/')
+}
+
+// 处理图片 URL,添加域名前缀,并验证是否为有效图片 URL
+const resolvedSrc = computed(() => {
+ // 先检查原始 src 是否为有效图片路径
+ if (!isValidImageUrl(props.src)) return ''
+ return resolveImageUrl(props.src)
+})
+
const emit = defineEmits<{
(e: 'click'): void
}>()
@@ -113,3 +131,10 @@ function onClick() {
border: 2rpx solid var(--bg-content);
}
+
+
+
+
+
+
+
diff --git a/src/components/common/AppEmpty.vue b/src/components/common/AppEmpty.vue
index 8df6f0c..ee669f7 100644
--- a/src/components/common/AppEmpty.vue
+++ b/src/components/common/AppEmpty.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/src/components/common/AppLoading.vue b/src/components/common/AppLoading.vue
index 03e4e67..f2894fe 100644
--- a/src/components/common/AppLoading.vue
+++ b/src/components/common/AppLoading.vue
@@ -48,3 +48,10 @@ withDefaults(defineProps(), {
color: var(--text-tertiary);
}
+
+
+
+
+
+
+
diff --git a/src/components/common/AppNavBar.vue b/src/components/common/AppNavBar.vue
index 00112a4..f9f2a2d 100644
--- a/src/components/common/AppNavBar.vue
+++ b/src/components/common/AppNavBar.vue
@@ -1,136 +1,76 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ avatarText }}
-
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/src/components/common/AppTabBar.vue b/src/components/common/AppTabBar.vue
new file mode 100644
index 0000000..584c54f
--- /dev/null
+++ b/src/components/common/AppTabBar.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ unreadCount > 99 ? '99+' : unreadCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
diff --git a/src/components/common/ImagePreview.vue b/src/components/common/ImagePreview.vue
new file mode 100644
index 0000000..11e312f
--- /dev/null
+++ b/src/components/common/ImagePreview.vue
@@ -0,0 +1,385 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentIndex + 1 }} / {{ images.length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ Math.round(scale * 100) }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/common/UserInfoCard.vue b/src/components/common/UserInfoCard.vue
new file mode 100644
index 0000000..f3500d5
--- /dev/null
+++ b/src/components/common/UserInfoCard.vue
@@ -0,0 +1,541 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userMoments[0].content }}
+
+
+
+
+
+
+ 暂无朋友圈动态
+
+
+
+
+
+
+
+
+ 发消息
+
+
+
+ 语音
+
+
+
+ 视频
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/common/VideoPlayer.vue b/src/components/common/VideoPlayer.vue
new file mode 100644
index 0000000..d2471e9
--- /dev/null
+++ b/src/components/common/VideoPlayer.vue
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(currentTime) }} / {{ formatTime(duration) }}
+
+
+
+
+
+
+ {{ playbackRate }}x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/index.ts b/src/components/index.ts
index 9ac185b..6765cca 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -3,10 +3,34 @@ export { default as AppNavBar } from './common/AppNavBar.vue'
export { default as AppAvatar } from './common/AppAvatar.vue'
export { default as AppEmpty } from './common/AppEmpty.vue'
export { default as AppLoading } from './common/AppLoading.vue'
+export { default as UserInfoCard } from './common/UserInfoCard.vue'
+export { default as VideoPlayer } from './common/VideoPlayer.vue'
+export { default as ImagePreview } from './common/ImagePreview.vue'
// 聊天组件
export { default as MessageBubble } from './chat/MessageBubble.vue'
export { default as MessageInput } from './chat/MessageInput.vue'
+export { default as SelectContactsModal } from './chat/SelectContactsModal.vue'
+export { default as MentionPicker } from './chat/MentionPicker.vue'
+
+// 通话组件
+export { default as CallWindow } from './call/CallWindow.vue'
+export { default as GroupCallBanner } from './call/GroupCallBanner.vue'
+export { default as GroupCallIncoming } from './call/GroupCallIncoming.vue'
+export { default as GroupCallModal } from './call/GroupCallModal.vue'
+export { default as GroupCallWindow } from './call/GroupCallWindow.vue'
// 朋友圈组件
export { default as MomentCard } from './moment/MomentCard.vue'
+export { default as UserMomentsModal } from './moment/UserMomentsModal.vue'
+export { default as VisibilitySelector } from './moment/VisibilitySelector.vue'
+export { default as MomentCommentInput } from './moment/MomentCommentInput.vue'
+export { default as MomentCommentList } from './moment/MomentCommentList.vue'
+export { default as MomentNotificationPanel } from './moment/MomentNotificationPanel.vue'
+
+
+
+
+
+
+
diff --git a/src/components/moment/MomentCard.vue b/src/components/moment/MomentCard.vue
index fbfe429..d2233c8 100644
--- a/src/components/moment/MomentCard.vue
+++ b/src/components/moment/MomentCard.vue
@@ -25,20 +25,20 @@
+
+
+
+
+
+
+ 新朋友
+
+
+
+
+
+
+ 群通知
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ 加载中...
-
-
-
-
- 加载中...
-
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+ {{ contact.remark_name || contact.user?.name || '未知' }}
+ {{ contact.user?.desc || '' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ contact.remark_name || contact.user?.name || '未知' }}
+ {{ contact.user?.desc || '' }}
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
- {{ contact.user?.name?.charAt(0) || '?' }}
+
+ {{ contact.remark_name || contact.user?.name || '未知' }}
+ {{ contact.user?.desc || '' }}
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.room_name || '未命名群聊' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.room_name || '未命名群聊' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.room_name || '未命名群聊' }}
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
@@ -135,27 +297,67 @@ import { onShow } from '@dcloudio/uni-app'
import { useContactStore, useChatStore } from '@/stores'
import { useTheme } from '@/composables/useTheme'
import { generateColor } from '@/utils/format'
+import { resolveImageUrl } from '@/utils/image'
import * as contactApi from '@/api/modules/contact'
-import type { Contact } from '@/types/api'
+import * as roomApi from '@/api/modules/room'
+import AppAvatar from '@/components/common/AppAvatar.vue'
+import AppTabBar from '@/components/common/AppTabBar.vue'
+import type { Contact, ContactGroup } from '@/types/api'
+
+interface GroupChat {
+ room_id: string
+ room_type: string
+ room_name: string
+ room_avatar: string
+ owner_id: string
+ creator_id: string
+ category: 'joined' | 'created' | 'managed'
+ role: number
+ last_message_time?: string
+ last_message?: string
+ created_at: string
+ updated_at: string
+}
const contactStore = useContactStore()
const chatStore = useChatStore()
const { isDark } = useTheme()
+// Tab 配置
+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([])
+const groupChats = ref([])
// 计算属性
const contacts = computed(() => chatStore.contacts)
const friendRequestCount = computed(() => contactStore.friendRequests.filter(r => r.status === 'pending').length)
+// 未分组联系人
+const ungroupedContacts = computed(() => {
+ return 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 groupedContacts = computed(() => {
+const alphabetGroupedContacts = computed(() => {
const groups: Record = {}
contacts.value.forEach(contact => {
@@ -181,6 +383,11 @@ const groupedContacts = computed(() => {
}))
})
+// 群聊分组
+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'))
+
// 生命周期
onMounted(() => {
loadData()
@@ -194,10 +401,18 @@ onShow(() => {
async function loadData() {
loading.value = true
try {
- const list = await contactApi.getContacts()
- chatStore.setContacts(list)
+ // 并行加载联系人、分组和群聊
+ const [contactList, groups, chats] = await Promise.all([
+ contactApi.getContacts(),
+ contactApi.getGroups(),
+ roomApi.getUserGroups()
+ ])
+
+ chatStore.setContacts(contactList)
+ contactGroups.value = groups
+ groupChats.value = chats || []
} catch (error) {
- console.error('加载联系人失败:', error)
+ console.error('加载数据失败:', error)
} finally {
loading.value = false
}
@@ -219,6 +434,15 @@ async function onRefresh() {
refreshing.value = false
}
+function toggleCollapse(id: number | string) {
+ const idx = collapsedIds.value.indexOf(id)
+ if (idx === -1) {
+ collapsedIds.value.push(id)
+ } else {
+ collapsedIds.value.splice(idx, 1)
+ }
+}
+
function goSearch() {
uni.navigateTo({ url: '/pages/search/index' })
}
@@ -231,10 +455,6 @@ function goFriendRequests() {
uni.navigateTo({ url: '/pages/contact/requests' })
}
-function goGroups() {
- uni.navigateTo({ url: '/pages/contact/groups' })
-}
-
function goGroupNotify() {
uni.navigateTo({ url: '/pages/group/notify' })
}
@@ -243,6 +463,12 @@ 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 scrollToLetter(letter: string) {
// TODO: 实现滚动到指定字母
console.log('Scroll to:', letter)
@@ -295,54 +521,147 @@ function scrollToLetter(letter: string) {
background: var(--nav-bg);
}
-// 内容滚动区域
-.content-scroll {
+// 功能入口行
+.function-row {
position: fixed;
top: calc(88rpx + 80rpx + var(--status-bar-height, 0));
left: 0;
- right: 40rpx;
- bottom: 0;
+ right: 0;
+ z-index: 98;
+ display: flex;
+ padding: 20rpx 30rpx;
+ background: var(--bg-content);
+ border-bottom: 1rpx solid var(--divider-color);
+ gap: 40rpx;
}
-// 功能入口
-.function-section {
- background: var(--bg-content);
- margin-bottom: 20rpx;
+.function-item {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ position: relative;
}
.function-icon {
- width: 72rpx;
- height: 72rpx;
+ width: 56rpx;
+ height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
- margin-right: 24rpx;
&.icon-friend {
background: #fa9d3b;
}
- &.icon-group {
- background: #07c160;
- }
-
&.icon-notify {
background: #10aeff;
}
}
-// 联系人列表
-.contact-section {
- background: var(--bg-content);
+.function-text {
+ font-size: 28rpx;
+ color: var(--text-primary);
}
+.function-badge {
+ position: absolute;
+ top: -8rpx;
+ left: 40rpx;
+}
+
+// Tab 切换
+.tab-bar {
+ position: fixed;
+ top: calc(88rpx + 80rpx + 96rpx + var(--status-bar-height, 0));
+ left: 0;
+ right: 0;
+ z-index: 97;
+ display: flex;
+ background: var(--bg-content);
+ border-bottom: 1rpx solid var(--divider-color);
+}
+
+.tab-item {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 80rpx;
+ font-size: 28rpx;
+ color: var(--text-secondary);
+ position: relative;
+ transition: all 0.2s;
+
+ &.active {
+ color: var(--color-primary);
+ font-weight: 600;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60rpx;
+ height: 4rpx;
+ background: var(--color-primary);
+ border-radius: 2rpx;
+ }
+ }
+}
+
+// 内容滚动区域
+.content-scroll {
+ position: fixed;
+ top: calc(88rpx + 80rpx + 96rpx + 80rpx + 20rpx + var(--status-bar-height, 0));
+ left: 0;
+ right: 40rpx;
+ bottom: calc(100rpx + env(safe-area-inset-bottom));
+}
+
+// 联系人分组
.contact-group {
- .group-header {
- padding: 12rpx 30rpx;
- font-size: 26rpx;
- color: var(--text-tertiary);
+ background: var(--bg-content);
+ margin-bottom: 16rpx;
+}
+
+.group-header {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 30rpx;
+ gap: 12rpx;
+
+ &.letter-header {
background: var(--bg-page);
+ padding: 12rpx 30rpx;
+ }
+}
+
+.group-name {
+ font-size: 26rpx;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.group-count {
+ font-size: 24rpx;
+ color: var(--text-tertiary);
+}
+
+.group-contacts {
+ padding-left: 16rpx;
+}
+
+// 联系人项
+.contact-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 30rpx;
+ gap: 20rpx;
+
+ &:active {
+ background: var(--bg-hover);
}
}
@@ -353,10 +672,37 @@ function scrollToLetter(letter: string) {
align-items: center;
justify-content: center;
border-radius: 8rpx;
- margin-right: 24rpx;
color: #fff;
font-size: 32rpx;
font-weight: 600;
+
+ &.avatar-group {
+ background: var(--color-primary);
+ }
+}
+
+.contact-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.contact-name {
+ display: block;
+ font-size: 30rpx;
+ color: var(--text-primary);
+ margin-bottom: 4rpx;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.contact-desc {
+ display: block;
+ font-size: 24rpx;
+ color: var(--text-tertiary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
// 字母索引
@@ -392,4 +738,9 @@ function scrollToLetter(letter: string) {
color: var(--text-tertiary);
gap: 20rpx;
}
+
+// 底部安全区
+.bottom-safe-area {
+ height: calc(40rpx + env(safe-area-inset-bottom));
+}
diff --git a/src/pages/contact/requests.vue b/src/pages/contact/requests.vue
index 1568658..1b76cc0 100644
--- a/src/pages/contact/requests.vue
+++ b/src/pages/contact/requests.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
加载中...
@@ -13,16 +16,12 @@
:key="item.id"
class="request-item"
>
-
-
- {{ item.from_user?.name?.charAt(0) || '?' }}
-
{{ item.from_user?.name || '未知用户' }}
@@ -50,11 +49,15 @@ import { ref, onMounted } from 'vue'
import { useContactStore } from '@/stores'
import * as contactApi from '@/api/modules/contact'
import { generateColor } from '@/utils/format'
+import { resolveImageUrl } from '@/utils/image'
import { useToast } from 'wot-design-uni'
+import { useTheme } from '@/composables/useTheme'
+import AppAvatar from '@/components/common/AppAvatar.vue'
import type { FriendRequest } from '@/types/api'
const contactStore = useContactStore()
const toast = useToast()
+const { isDark } = useTheme()
const loading = ref(true)
const requests = ref([])
diff --git a/src/pages/group/create.vue b/src/pages/group/create.vue
index dee0113..6469149 100644
--- a/src/pages/group/create.vue
+++ b/src/pages/group/create.vue
@@ -1,5 +1,8 @@
+
+
+
-
-
- {{ contact.user?.name?.charAt(0) || '?' }}
-
{{ contact.remark_name || contact.user?.name || '未知' }}
@@ -72,6 +71,7 @@ import * as contactApi from '@/api/modules/contact'
import * as roomApi from '@/api/modules/room'
import { generateColor } from '@/utils/format'
import { useToast } from 'wot-design-uni'
+import AppAvatar from '@/components/common/AppAvatar.vue'
import type { Contact } from '@/types/api'
const toast = useToast()
diff --git a/src/pages/group/info.vue b/src/pages/group/info.vue
index 95d1935..dd7ec26 100644
--- a/src/pages/group/info.vue
+++ b/src/pages/group/info.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
加载中...
@@ -8,57 +11,121 @@
+
+
+
+
+
+
+
+
+ 通话
+
+
+
+
+
+
+
+ 邀请
+
+
+
+
+
+
+
+ 公告
+
+
+
+
+
+
+
+ 管理员
+
+
+
+
+
+
+
+ 设置
+
+
+
+
+
+
+
+ 群公告
+
+ {{ announcement }}
-
-
-
-
- {{ member.user?.name?.charAt(0) || '?' }}
-
- {{ member.nickname || member.user?.name || '未知' }}
-
-
+
+
+
邀请
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ member.nickname || member.user?.name || '未知' }}
+
+
-
-
-
+
@@ -69,14 +136,117 @@
+
-
-
- 退出群聊
+
+
+ 退出群聊
+
+ 转让群主
+ 解散群聊
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ member.nickname || member.user?.name || '未知' }}
+ {{ member.role === 1 ? '当前管理员' : '普通成员' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ member.nickname || member.user?.name || '未知' }}
+ 管理员
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -84,15 +254,25 @@
diff --git a/src/pages/group/members.vue b/src/pages/group/members.vue
index 81e382f..680d55e 100644
--- a/src/pages/group/members.vue
+++ b/src/pages/group/members.vue
@@ -1,5 +1,8 @@
+
+
+
-
-
- {{ member.user?.name?.charAt(0) || '?' }}
+
+
@@ -48,6 +49,8 @@ import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import * as roomApi from '@/api/modules/room'
import { generateColor } from '@/utils/format'
+import { resolveImageUrl } from '@/utils/image'
+import AppAvatar from '@/components/common/AppAvatar.vue'
import type { GroupMember } from '@/types/api'
const loading = ref(true)
@@ -86,6 +89,11 @@ async function loadMembers() {
loading.value = false
}
}
+
+function goMemberDetail(member: GroupMember) {
+ // 跳转到成员详情页
+ uni.navigateTo({ url: `/pages/contact/detail?userId=${member.user_id}` })
+}
diff --git a/src/pages/group/notify.vue b/src/pages/group/notify.vue
index 792604e..ab295fa 100644
--- a/src/pages/group/notify.vue
+++ b/src/pages/group/notify.vue
@@ -1,5 +1,8 @@
+
+
+
加载中...
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 684681d..da7ca1d 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -5,20 +5,16 @@
-
-
- {{ user?.name?.charAt(0) || '?' }}
-
- 消息
+ {{ user?.name || '消息' }}
@@ -61,24 +57,14 @@
-
-
- {{ item.name?.charAt(0) || '?' }}
-
-
-
-
@@ -120,21 +106,18 @@
@@ -143,11 +126,7 @@
@@ -160,6 +139,9 @@
+
+
+
@@ -169,12 +151,26 @@ import { onShow } from '@dcloudio/uni-app'
import { useAuthStore, useConversationStore } from '@/stores'
import { useTheme } from '@/composables/useTheme'
import { formatMessageTime, generateColor } from '@/utils/format'
+import { resolveImageUrl } from '@/utils/image'
import { useToast, useMessage } from 'wot-design-uni'
+import AppAvatar from '@/components/common/AppAvatar.vue'
+import AppTabBar from '@/components/common/AppTabBar.vue'
import type { Conversation } from '@/types/conversation'
const authStore = useAuthStore()
const conversationStore = useConversationStore()
-const { isDark, toggleTheme } = useTheme()
+const { isDark } = useTheme()
+
+// 主题显示文本
+const themeText = computed(() => {
+ const mode = uni.getStorageSync('nl_im_theme_mode') || 'system'
+ const texts: Record = {
+ system: '跟随系统',
+ light: '浅色模式',
+ dark: '深色模式'
+ }
+ return texts[mode] || '跟随系统'
+})
const toast = useToast()
const messageBox = useMessage()
@@ -227,7 +223,7 @@ function goSearch() {
function goChat(item: Conversation) {
conversationStore.clearUnread(item.target_id)
uni.navigateTo({
- url: `/pages/chat/index?roomId=${item.room_id || ''}&targetId=${item.target_id}&name=${encodeURIComponent(item.name || '')}`
+ url: `/pages/chat/index?roomId=${item.room_id || ''}&targetId=${item.target_id}&name=${encodeURIComponent(item.name || '')}&avatar=${encodeURIComponent(item.avatar || '')}`
})
}
@@ -284,6 +280,11 @@ function goSettings() {
uni.navigateTo({ url: '/pages/settings/index' })
}
+function goTheme() {
+ showDrawer.value = false
+ uni.navigateTo({ url: '/pages/settings/theme' })
+}
+
async function logout() {
try {
await messageBox.confirm({
@@ -354,7 +355,7 @@ async function logout() {
top: calc(88rpx + 80rpx + var(--status-bar-height, 0));
left: 0;
right: 0;
- bottom: 0;
+ bottom: calc(100rpx + env(safe-area-inset-bottom));
background: var(--bg-content);
}
@@ -492,13 +493,15 @@ async function logout() {
display: flex;
flex-direction: column;
background: var(--bg-page);
+ padding-bottom: env(safe-area-inset-bottom);
}
.drawer-header {
display: flex;
flex-direction: column;
align-items: center;
- padding: 80rpx 30rpx 40rpx;
+ padding: 40rpx 30rpx 40rpx;
+ padding-top: calc(40rpx + var(--status-bar-height, 44px));
background: var(--bg-content);
}
diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue
index 32213d4..18994e2 100644
--- a/src/pages/login/index.vue
+++ b/src/pages/login/index.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
@@ -75,10 +78,12 @@
import { ref, computed } from 'vue'
import { useAuthStore } from '@/stores'
import { useToast, useMessage } from 'wot-design-uni'
+import { useTheme } from '@/composables/useTheme'
const authStore = useAuthStore()
const toast = useToast()
const message = useMessage()
+const { isDark } = useTheme()
// 表单数据
const form = ref({
@@ -107,7 +112,7 @@ async function handleLogin() {
})
toast.success('登录成功')
setTimeout(() => {
- uni.switchTab({ url: '/pages/index/index' })
+ uni.reLaunch({ url: '/pages/index/index' })
}, 1000)
} catch (error: any) {
toast.error(error.message || '登录失败')
diff --git a/src/pages/moment/detail.vue b/src/pages/moment/detail.vue
index c92c236..6be26db 100644
--- a/src/pages/moment/detail.vue
+++ b/src/pages/moment/detail.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
加载中...
@@ -122,9 +125,12 @@ import { ref, onMounted } from 'vue'
import { useMomentStore } from '@/stores'
import { formatTime } from '@/utils/format'
import { parseMediaUrls } from '@/types/moment'
+import { useTheme } from '@/composables/useTheme'
+import AppAvatar from '@/components/common/AppAvatar.vue'
import type { Moment } from '@/types/moment'
const momentStore = useMomentStore()
+const { isDark } = useTheme()
const loading = ref(true)
const moment = ref(null)
@@ -185,7 +191,7 @@ async function sendComment() {
+
+
+
+
+
+
+
diff --git a/src/pages/profile/edit.vue b/src/pages/profile/edit.vue
index 6103b10..c9cc21d 100644
--- a/src/pages/profile/edit.vue
+++ b/src/pages/profile/edit.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
import { ref, onMounted } from 'vue'
+import { useTheme } from '@/composables/useTheme'
+const { isDark } = useTheme()
const field = ref('')
const value = ref('')
const placeholder = ref('')
@@ -65,7 +70,7 @@ function save() {
diff --git a/src/pages/settings/index.vue b/src/pages/settings/index.vue
index ab085cf..ca0d348 100644
--- a/src/pages/settings/index.vue
+++ b/src/pages/settings/index.vue
@@ -1,5 +1,8 @@
-
+
+
+
+
@@ -8,11 +11,7 @@
-
-
-
-
-
+
@@ -33,18 +32,29 @@
+
+
+
diff --git a/src/static/icons/chat-active.png b/src/static/icons/chat-active.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/chat-active.png
+++ b/src/static/icons/chat-active.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/icons/chat.png b/src/static/icons/chat.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/chat.png
+++ b/src/static/icons/chat.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/icons/contact-active.png b/src/static/icons/contact-active.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/contact-active.png
+++ b/src/static/icons/contact-active.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/icons/contact.png b/src/static/icons/contact.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/contact.png
+++ b/src/static/icons/contact.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/icons/moment-active.png b/src/static/icons/moment-active.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/moment-active.png
+++ b/src/static/icons/moment-active.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/icons/moment.png b/src/static/icons/moment.png
index b3a4252..eb0703e 100644
--- a/src/static/icons/moment.png
+++ b/src/static/icons/moment.png
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/static/images/moment-cover.jpg b/src/static/images/moment-cover.jpg
index b3a4252..eb0703e 100644
--- a/src/static/images/moment-cover.jpg
+++ b/src/static/images/moment-cover.jpg
@@ -1 +1,7 @@
-placeholder
\ No newline at end of file
+placeholder
+
+
+
+
+
+
diff --git a/src/styles/common.scss b/src/styles/common.scss
index 8d7e33b..3423efb 100644
--- a/src/styles/common.scss
+++ b/src/styles/common.scss
@@ -179,3 +179,10 @@
box-shadow: 0 4rpx 20rpx rgba(7, 193, 96, 0.4);
z-index: 99;
}
+
+
+
+
+
+
+
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 61f2466..e05e1a5 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -61,8 +61,11 @@ page {
--switch-off: #e5e5e5;
}
-// 暗色主题
-page.dark {
+// 暗色主题 (支持多种选择器以兼容 H5)
+page.dark,
+.dark,
+html.dark,
+body.dark {
// 背景色
--bg-page: #111111;
--bg-content: #1e1e1e;
@@ -99,3 +102,10 @@ page.dark {
// 开关
--switch-off: #444444;
}
+
+
+
+
+
+
+
diff --git a/src/utils/image.ts b/src/utils/image.ts
new file mode 100644
index 0000000..1206dc9
--- /dev/null
+++ b/src/utils/image.ts
@@ -0,0 +1,36 @@
+/**
+ * 图片 URL 处理工具函数
+ */
+
+// 图片资源基础域名
+const IMAGE_BASE_URL = 'http://127.0.0.1:12080'
+
+/**
+ * 解析图片 URL,为相对路径添加域名前缀
+ * @param url 图片路径(可能是相对路径或完整 URL)
+ * @returns 完整的图片 URL
+ */
+export function resolveImageUrl(url?: string): string {
+ if (!url) return ''
+
+ // 已经是完整 URL(http/https/data URI)
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) {
+ return url
+ }
+
+ // 仅处理 /upload/ 开头的路径
+ if (url.startsWith('/uploads/')) {
+ return IMAGE_BASE_URL + url
+ }
+
+ // 其他情况直接返回
+ return url
+}
+
+/**
+ * 获取图片基础 URL
+ */
+export function getImageBaseUrl(): string {
+ return IMAGE_BASE_URL
+}
+
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 3ece7d8..2e383ed 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -5,3 +5,4 @@ export * from './storage'
export * from './format'
export * from './message'
export * from './platform'
+export * from './image'
diff --git a/vite.config.ts b/vite.config.ts
index c758eb5..49c451a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,6 +3,7 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import Components from '@uni-helper/vite-plugin-uni-components'
import { WotResolver } from '@uni-helper/vite-plugin-uni-components/resolvers'
+import UniKuRoot from '@uni-ku/root'
import Uni from '@uni-helper/plugin-uni'
export default defineConfig({
@@ -17,6 +18,8 @@ export default defineConfig({
dts: true,
resolvers: [WotResolver()]
}),
+ // https://github.com/niceyou/uni-ku-root - 全局共享 ConfigProvider
+ UniKuRoot(),
// https://uni-helper.js.org/plugin-uni
Uni(),
],