出发前保存

This commit is contained in:
heiye111
2026-05-27 13:36:35 +08:00
parent 205b492be9
commit 0509104ed1
19 changed files with 1115 additions and 121 deletions

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Vite App</title>
</head>
<body>

32
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@microsoft/signalr-protocol-msgpack": "^10.0.0",
"mitt": "^3.0.1",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"update": "^0.7.4",
"vant": "^4.9.24",
"vue": "beta",
@@ -6095,6 +6096,12 @@
"node": ">=0.10.0"
}
},
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/delimiter-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/delimiter-regex/-/delimiter-regex-2.0.0.tgz",
@@ -11400,6 +11407,31 @@
}
}
},
"node_modules/pinia-plugin-persistedstate": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
"integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
"license": "MIT",
"dependencies": {
"defu": "^6.1.4"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"@pinia/nuxt": ">=0.10.0",
"pinia": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@pinia/nuxt": {
"optional": true
},
"pinia": {
"optional": true
}
}
},
"node_modules/pkg-store": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/pkg-store/-/pkg-store-0.2.2.tgz",

View File

@@ -19,6 +19,7 @@
"@microsoft/signalr-protocol-msgpack": "^10.0.0",
"mitt": "^3.0.1",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"update": "^0.7.4",
"vant": "^4.9.24",
"vue": "beta",

View File

@@ -1,11 +1,140 @@
<template>
<div id="app">
<router-view /> <!-- 这里会根据路由显示不同页面 -->
<!-- 页面内容 -->
<router-view class="page-content" />
<!-- 底部导航栏 -->
<van-tabbar v-if="!$route.meta.hideTabbar" v-model="active" fixed>
<van-tabbar-item icon="chat" :badge="unreadMessages" @click="$router.push('/')">
消息
</van-tabbar-item>
<van-tabbar-item icon="friends" :badge="unreadContacts" @click="$router.push('/contacts')">
联系人
</van-tabbar-item>
<van-tabbar-item icon="user" @click="$router.push('/profile')">
我的
</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { emitter } from '@/network/signalr'
import { useMessagesStore } from '@/stores/messages'
import { useUserStore } from '@/stores/user'
import { useFriendStore } from '@/stores/friend'
const usefriendStore = useFriendStore()
const userStore = useUserStore()
const currentUserId = userStore.Id
const currentChatId = usefriendStore.currentFriend.userid
const active = ref(1)
const unreadMessages = ref(5) // 消息未读数
const unreadContacts = ref(2) // 联系人未读数
const messagesStore = useMessagesStore()
onMounted(() => {
console.log('App.vue mounted, currentUserId:', currentUserId, 'currentChatId:', currentChatId)
const accessToken = localStorage.getItem('accessToken')
if (!accessToken) {
if (window.location.pathname !== '/login') {
// 没有访问令牌,跳转到登录页面
window.location.href = '/login'
}
}
emitter.on('UserStatusChanged', ({ userId, isOnline }) => {
console.log('用户状态变化:', userId, isOnline)
// 更新消息列表中的用户状态
//messagesStore.updateUserStatus(userId, isOnline)
const friend = usefriendStore.friends.find(f => f.userid === Number(userId))
if (friend) {
friend.isonline = isOnline
}
})
emitter.on("SendPrivateMessage", (resp) => {
// 根据 senderId / receiverId 判断属于哪个会话
const friendId = resp.SenderId === currentUserId ? resp.ReceiverId : resp.SenderId
console.log('收到消息:', resp.Message, '来自用户ID:', resp.SenderId, "friendId:", friendId)
if (resp.IsMine) {
messagesStore.addMessage(friendId, {
id: resp.MessageId,
from: 'me',
text: resp.Message,
isRead: false,
avatarurl: resp.AvatarUrl,
nickname: resp.Nickname
})
} else {
messagesStore.addMessage(friendId, {
id: resp.MessageId,
from: 'other',
text: resp.Message,
isRead: false,
avatarurl: resp.AvatarUrl,
nickname: resp.Nickname
})
}
// // 已读回执
// if (friendId === currentChatId) {
// connection.send("SendReadReceipt", resp.messageId)
// }
})
// 监听已读回执事件
emitter.on("SendReadReceipt", (messageId) => {
messagesStore.markRead(currentChatId, messageId)
})
})
onUnmounted(() => {
console.log('App.vue unmounted')
})
</script>
<style>
html,
body,
#app {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.page-content {
min-height: calc(100vh - 50px);
/* 扣掉底部导航栏高度 */
width: 100%;
box-sizing: border-box;
}
/* 调整图标大小 */
.van-tabbar-item__icon .van-icon {
font-size: 24px;
}
/* 修改背景颜色 */
.van-tabbar {
background-color: #f5f5f5 !important;
border-top: 1px solid #e0e0e0;
/* 在最上方加一条浅灰色边框 */
}
.van-tabbar-item--active {
background-color: #f5f5f5 !important;
/* 改成你想要的浅灰色或其他颜色 */
}
</style>

View File

@@ -73,30 +73,30 @@ const messages = computed(() => chatMessages.value[currentChat.value.id] || [])
const newMessage = ref("")
onMounted(() => {
emitter.on('SendPrivateMessage', ({ success, message, userid, messageId }) => {
if (success) {
if (!chatMessages.value[userid]) {
chatMessages.value[userid] = []
emitter.on('SendPrivateMessage', (ResultResponse) => {
if (ResultResponse.success) {
if (!chatMessages.value[ResultResponse.senderId]) {
chatMessages.value[ResultResponse.senderId] = []
}
chatMessages.value[userid].push({
id: messageId,
from: chats.value.find(c => c.id === userid)?.name || "未知用户",
text: message,
chatMessages.value[ResultResponse.senderId].push({
id: ResultResponse.messageId,
from: chats.value.find(c => c.id === ResultResponse.senderId)?.name || "未知用户",
text: ResultResponse.message,
isRead: false
})
currentChat.value.lastMessage = message
if (currentChat.value.id === userid) {
sendIsRead(messageId)
console.log("收到消息,已发送已读回执:", messageId)
currentChat.value.lastMessage = ResultResponse.message
if (currentChat.value.id === ResultResponse.senderId) {
sendIsRead(ResultResponse.messageId)
console.log("收到消息,已发送已读回执:", ResultResponse.messageId)
}
} else {
chatMessages.value[currentChat.value.id].push({
id: messageId,
id: ResultResponse.messageId,
from: "我",
text: message,
text: ResultResponse.message,
isRead: false
})
currentChat.value.lastMessage = message
currentChat.value.lastMessage = ResultResponse.message
}
})
emitter.on('SendReadReceipt', (messageId) => {

View File

@@ -5,23 +5,21 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 引入 Vant 组件
import { Search, Cell, List, NavBar, Card, Field, Button } from 'vant'
import Vant from 'vant'
import 'vant/lib/index.css'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 注册 Vant 组件
app.use(Search)
app.use(Cell)
app.use(List)
app.use(NavBar)
app.use(Card)
app.use(Field)
app.use(Button)
app.use(Vant)
app.use(pinia)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -20,20 +20,17 @@ function registerHandlers(conn) {
emitter.emit('GroupCount', { groupName, count })
})
conn.on('RegisterResult', (success, message) => {
emitter.emit('RegisterResult', { success, message })
})
conn.on('LoginResult', async (success, accessToken, refreshToken) => {
emitter.emit('LoginResult', { success, accessToken, refreshToken })
})
conn.on('SendPrivateMessage', (success, message, userid, messageId) => {
emitter.emit('SendPrivateMessage', { success, message, userid, messageId })
conn.on('SendPrivateMessage', (ResultResponse) => {
emitter.emit('SendPrivateMessage', ResultResponse)
})
conn.on('SendReadReceipt', (messageId) => {
emitter.emit('SendReadReceipt', messageId)
})
// 监听用户状态变化
connection.on("UserStatusChanged", (userId, isOnline) => {
emitter.emit('UserStatusChanged', { userId, isOnline })
})
}
// 初始连接(不带 token
@@ -99,4 +96,4 @@ async function reconnectWithToken() {
await RefreshAccessToken() // 尝试刷新 token 并重连
})()
export { connection, reconnectWithToken }
export { connection, reconnectWithToken, createConnection }

View File

@@ -4,23 +4,54 @@ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
path: '/', // 消息页面
name: 'home',
component: () => import('@/views/LoginTest.vue'),
component: () => import('@/views/IMessage.vue'),
},
{
path: '/chat',
path: '/chat', //旧的测试聊天页面
name: 'chat',
component: () => import('@/components/HelloWorld.vue'),
},
{
path: '/chattest',
path: '/chattest', //新的测试聊天页面
name: 'chattest',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('@/components/ChatTest.vue'),
},
{
path: '/contacts', //联系人页面
name: 'contacts',
component: () => import('@/views/IContacts.vue'),
},
{
path: '/frienddetail', //好友详情页面
name: 'friendDetail',
component: () => import('@/views/Users/FriendDetail.vue'),
},
{
path: '/searchcontacts', //搜索联系人页面
name: 'searchContacts',
component: () => import('@/views/Users/SearchContacts.vue'),
},
{
path: '/profile', //个人中心页面
name: 'profile',
component: () => import('@/views/IProfile.vue'),
},
{
path: '/login', //登录页面
name: 'login',
component: () => import('@/views/ILogin.vue'),
},
{
path: '/friendmessage', //好友消息页面
name: 'friendMessage',
component: () => import('@/views/Message/FriendMessage.vue'),
meta: { hideTabbar: true }
}
],
})

27
src/stores/friend.js Normal file
View File

@@ -0,0 +1,27 @@
// stores/friend.js
import { defineStore } from 'pinia'
export const useFriendStore = defineStore('friend', {
state: () => ({
currentFriend: {},
friends: []
}),
actions: {
setFriend(friend) {
this.currentFriend = friend
},
clearFriend() {
this.currentFriend = null
}
},
persist: {
enabled: true,
strategies: [
{
key: 'friend',
storage: localStorage
}
]
}
})

57
src/stores/messages.js Normal file
View File

@@ -0,0 +1,57 @@
// stores/messages.js
import { defineStore } from 'pinia'
import { useFriendStore } from './friend'
export const useMessagesStore = defineStore('messages', {
state: () => ({
chats: {}, // { friendId: [消息数组] }
}),
actions: {
addMessage(friendId, msg) {
if (!this.chats[friendId]) this.chats[friendId] = []
const exists = this.chats[friendId].some((m) => m.id === msg.id)
if (!exists) {
this.chats[friendId].push(msg)
}
},
setMessages(friendId, msgs) {
const id = Number(friendId)
const existing = this.chats[id] || []
const merged = [...existing]
msgs.forEach((msg) => {
if (!merged.some((m) => m.id === msg.id)) {
merged.push(msg)
}
})
// 按消息 id 或时间排序,保证顺序正确
this.chats[id] = merged.sort((a, b) => a.id - b.id)
},
markRead(friendId, messageId) {
const msg = this.chats[friendId]?.find((m) => m.id === messageId)
if (msg) msg.isRead = true
},
},
getters: {
chatList: (state) => {
const friendStore = useFriendStore()
return Object.keys(state.chats).map((friendId) => {
const msgs = state.chats[friendId]
const lastMsg = msgs[msgs.length - 1]
const unreadCount = msgs.filter((m) => !m.isRead && m.from === 'other').length
const friend = friendStore.friends.find(f => f.userid === Number(friendId)) || {}
// 直接返回引用 + 补充字段
return {
...friend, // ✅ 保持响应式引用
lastMessage: lastMsg ? lastMsg.text : '',
unreadCount
}
})
},
},
})

46
src/stores/user.js Normal file
View File

@@ -0,0 +1,46 @@
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
AccessToken: '',
RefreshToken: '',
Nickname: '',
Id: 0,
AvatarUrl: '',
Signature: '',
IsOnline: false,
Success: false
}),
actions: {
setUser(data) {
this.AccessToken = data.AccessToken
this.RefreshToken = data.RefreshToken
this.Nickname = data.Nickname
this.Id = data.Id
this.AvatarUrl = data.AvatarUrl
this.Signature = data.Signature
this.IsOnline = data.IsOnline
this.Success = data.Success
},
clearUser() {
this.AccessToken = ''
this.RefreshToken = ''
this.Nickname = ''
this.Id = 0
this.AvatarUrl = ''
this.Signature = ''
this.IsOnline = false
this.Success = false
}
},
persist: {
enabled: true, // 开启持久化
strategies: [
{
key: 'user', // localStorage 的 key
storage: localStorage // 默认就是 localStorage
}
]
}
})

View File

@@ -1,44 +0,0 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

98
src/views/IContacts.vue Normal file
View File

@@ -0,0 +1,98 @@
<template>
<div class="contacts-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="联系人" left-text="返回" left-arrow @click-left="$router.back()">
<template #right>
<van-icon name="search" size="18" @click="goSearch" />
</template>
</van-nav-bar>
<!-- 子路由出口 -->
<router-view />
<!-- 搜索框 -->
<van-search v-model="searchValue" placeholder="搜索联系人" @search="searchContacts" />
<!-- 联系人列表 -->
<van-cell-group>
<van-cell v-for="contact in contacts" :key="contact.userid" :title="contact.nickname" :label="contact.signature"
:description="contact.signature" :is-online="contact.isonline" is-link @click="openContact(contact)">
<template #icon>
<van-image :src="contact.avatarurl" width="40" height="40" fit="cover" />
</template>
<van-tag v-if="contact.isonline" type="success">在线</van-tag>
<van-tag v-else type="danger">离线</van-tag>
</van-cell>
</van-cell-group>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { connection } from '@/network/signalr'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useFriendStore } from '@/stores/friend'
const router = useRouter()
const userStore = useUserStore()
const friendStore = useFriendStore()
onMounted( async () => {
console.log('用户ID:', userStore.Id)
await getContacts(userStore.Id);
})
async function getContacts(userid)
{
await connection.invoke("GetFriendsAsync", userid)
.then(result => {
console.log('获取联系人列表成功:', result)
contacts.value.push(...result); // 将获取到的联系人列表赋值给contacts
friendStore.friends = result
})
.catch(err => console.error("获取联系人列表失败:", err));
}
function goSearch() {
router.push('/searchcontacts')
}
const searchValue = ref('')
const contacts = ref([])
async function searchContacts() {
// 这里可以添加搜索逻辑例如调用API获取搜索结果
console.log('搜索联系人:', searchValue.value)
await connection.invoke("SearchFriends", searchValue.value)
.then(result => {
console.log('搜索结果:', result)
contacts.value.push(result);
// 这里可以将result赋值给contacts来更新联系人列表
})
.catch(err => console.error("搜索联系人失败:", err));
}
function openContact(contact) {
console.log('进入联系人详情:', contact)
// 这里可以用 $router.push('/contact/' + contact.id) 跳转详情页
friendStore.setFriend(contact)
router.push(`/frienddetail`)
}
</script>
<style scoped>
.contacts-page {
height: 100vh;
width: 100vw;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -5,8 +5,8 @@
<form @submit.prevent="handleSubmit">
<div v-if="isLogin" class="form-group">
<label>邮箱</label>
<input v-model="email" type="email" required />
<label>用户名或邮箱</label>
<input v-model="username" type="text" required />
</div>
<div v-if="isLogin" class="form-group">
@@ -37,51 +37,50 @@
</template>
<script setup>
import { ref, onMounted } from "vue";
import { connection, reconnectWithToken, emitter } from '@/network/signalr'
import { ref } from "vue";
import { connection, reconnectWithToken } from '@/network/signalr'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
const isLogin = ref(true);
const email = ref("");
const username = ref("");
const password = ref("");
const confirmPassword = ref("");
const router = useRouter()
const userStore = useUserStore()
onMounted(async () => {
// SignalR
//await startConnection();
emitter.on("RegisterResult", ({ success, message }) => {
alert(message);
if (success) {
isLogin.value = true; //
console.log("注册成功:", message);
return
}
console.error("注册失败:", message);
});
emitter.on("LoginResult", async ({ success, accessToken, refreshToken }) => {
if (success) {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
console.log("登录成功:", accessToken, "=========", refreshToken);
await reconnectWithToken();
router.push("/chattest"); //
return
}
console.error("登录失败:", accessToken); // accessToken
});
});
function RegisterRequest() {
connection.send("Register")
async function RegisterRequest() {
const result = await connection.invoke("Register")
.catch(err => console.error("注册请求失败:", err));
if (result.Success) {
isLogin.value = true; //
console.log("注册成功:", result.Username); // result.message
await LoginRequest(result.Username, result.Password); //
return
}
console.error("注册失败:", result.Success); // result.message
}
function LoginRequest(email, password) {
connection.send("LoginAuthentication", email, password)
async function LoginRequest(username, password) {
// 访
const ua = navigator.userAgent //
const platform = navigator.platform //
console.log("ua:", ua, "platform:", platform)
const result = await connection.invoke("LoginAuthentication", username, password, ua + platform)
.catch(err => console.error("登录请求失败:", err));
if (result.Success) {
localStorage.setItem("accessToken", result.AccessToken);
localStorage.setItem("refreshToken", result.RefreshToken);
console.log("登录成功:", result.AccessToken, "=========", result.RefreshToken);
userStore.setUser(result);
await reconnectWithToken();
router.push("/"); //
return
}
console.error("登录失败:", "用户名或密码错误!"); // result.message
}
function handleSubmit() {
@@ -90,9 +89,9 @@ function handleSubmit() {
return;
}
if (isLogin.value) {
LoginRequest(email.value, password.value);
LoginRequest(username.value, password.value);
} else {
RegisterRequest(email.value, password.value);
RegisterRequest(username.value, password.value);
}
}

92
src/views/IMessage.vue Normal file
View File

@@ -0,0 +1,92 @@
<template>
<div class="message-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="消息" />
<h1>消息</h1>
<!-- 消息列表 -->
<van-list>
<van-cell v-for="chat in messagesStore.chatList" :key="chat.userid" clickable @click="openChat(chat)">
<!-- 左侧头像 -->
<template #icon>
<van-image :src="chat.avatarurl" width="40" height="40" />
</template>
<!-- 中间昵称 + 最新消息 -->
<template #title>
<div class="nickname">{{ chat.nickname }}</div>
<div class="last-message">{{ chat.lastMessage }}</div>
</template>
<!-- 右侧未读数 -->
<template #right-icon v-if="chat.unreadCount > 0">
<van-badge :content="chat.unreadCount" class="unread-badge" />
</template>
</van-cell>
</van-list>
</div>
</template>
<script setup>
import { useMessagesStore } from '@/stores/messages'
import { useFriendStore } from '@/stores/friend'
import { useRouter } from 'vue-router'
const router = useRouter()
const friendStore = useFriendStore()
const messagesStore = useMessagesStore()
function openChat(chat) {
// 跳转到聊天详情页
console.log('打开聊天:', chat.userid, chat)
const friend = friendStore.friends.find(f => f.userid === Number(chat.userid)) || {}
friendStore.setFriend(friend)
router.push(`/friendmessage`)
}
</script>
<style scoped>
.message-page {
height: 100vh;
background-color: #f5f5f5;
}
/* 昵称加粗 */
.nickname {
font-weight: bold;
font-size: 16px;
}
/* 最新消息灰色小字 */
.last-message {
font-size: 14px;
color: #999;
margin-top: 4px;
}
.unread-badge {
/* 放大角标 */
font-size: 16px;
/* 字体更大 */
min-width: 28px;
/* 宽度更大 */
height: 28px;
/* 高度更大 */
line-height: 28px;
/* 垂直居中 */
/* 调整位置到右侧中间 */
position: absolute;
right: 16px;
/* 距离右边 */
top: 50%;
/* 垂直居中 */
transform: translateY(-50%);
}
</style>

116
src/views/IProfile.vue Normal file
View File

@@ -0,0 +1,116 @@
<template>
<div class="my-page">
<!-- 顶部个人信息 -->
<div class="profile-box">
<van-row>
<van-col span="6">
<van-image
:src="userStore.AvatarUrl"
width="80"
height="80"
fit="cover"
@click="showAvatar = true"
/>
</van-col>
<van-col span="18" class="profile-info">
<div class="nickname" @click="goDetail">{{ userStore.Nickname }}</div>
<div class="userid" @click="goDetail">UID: {{ userStore.Id }}</div>
<van-tag v-if="isOnline" type="success">在线</van-tag>
<van-tag v-else type="danger">离线</van-tag>
</van-col>
</van-row>
<van-button @click="isOnline = !isOnline">切换状态</van-button>
</div>
<!-- 功能列表 -->
<van-cell-group>
<van-cell title="个人资料" is-link />
<van-cell title="我的订单" is-link />
<van-cell title="消息通知" is-link />
<van-cell title="设置" is-link />
</van-cell-group>
<!-- 底部退出按钮 -->
<div class="logout-box">
<van-button type="danger" block @click="logout">退出登录</van-button>
</div>
<!-- 全屏头像预览 -->
<van-image-preview v-model:show="showAvatar" :images="[userStore.AvatarUrl]" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const showAvatar = ref(false)
const isOnline = ref(true)
onMounted(() => {
console.log('isOnline:', userStore.IsOnline)
// 初始化时检查用户是否在线
isOnline.value = userStore.IsOnline
})
function goDetail() {
// 跳转详情页逻辑(功能为空)
console.log('进入详情页')
}
// 这里可以添加退出登录的逻辑,例如清除用户信息和访问令牌
function logout() {
// 清除用户信息和访问令牌
userStore.clearUser()
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
// 跳转到登录页面
window.location.href = '/login'
}
</script>
<style scoped>
/* 外层容器铺满全屏 */
.my-page {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 顶部信息卡片 */
.profile-box {
padding: 30px;
background-color: #ffffff;
border-bottom: 1px solid #eee;
}
/* 右侧文字布局 */
.profile-info {
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 12px;
}
.nickname {
font-size: 20px;
font-weight: bold;
margin-bottom: 6px;
}
.userid {
font-size: 14px;
color: #666;
}
.logout-box {
margin: 20px;
margin-top: auto 1; /* 保证按钮在底部 */
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<div class="send-message-page">
<!-- 顶部导航栏 -->
<van-nav-bar :title="friend.nickname" left-text="返回" left-arrow @click-left="$router.back()">
<template #right>
<van-tag type="success" v-if="friend.isonline">在线</van-tag>
<van-tag type="danger" v-else>离线</van-tag>
</template>
</van-nav-bar>
<!-- 消息窗口 -->
<div class="messages" ref="messagesBox">
<div v-for="msg in messages" :key="msg.id" :class="['message', msg.from === 'me' ? 'me' : 'other']">
<div class="bubble">
{{ msg.text }}
</div>
<!-- 已读回执 -->
<div v-if="msg.from === 'me'" class="receipt">
<van-icon :name="msg.isRead ? 'success' : 'clock-o'" :color="msg.isRead ? '#4a90e2' : '#999'" size="14" />
<span :class="msg.isRead ? 'read' : 'unread'">
{{ msg.isRead ? '已读' : '未读' }}
</span>
</div>
</div>
</div>
<!-- 输入框和发送按钮 -->
<div class="input-box">
<van-field v-model="newMessage" placeholder="输入消息..." @keyup.enter="sendMessage" />
<van-button type="primary" @click="sendMessage">发送</van-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, computed, watch } from 'vue'
import { useFriendStore } from '@/stores/friend'
import { connection } from '@/network/signalr'
import { useMessagesStore } from '@/stores/messages'
// import { route } from '@/router'
const friendStore = useFriendStore()
const friend = ref({})
const newMessage = ref('')
const messagesBox = ref(null)
const messagesStore = useMessagesStore()
//const friend = computed(() => friendStore.currentFriend)
onMounted(() => {
friend.value = friendStore.currentFriend
getChatRecord(Number(friend.value.userid))
// 进入聊天详情页时,把该好友的未读消息全部标记为已读
const msgs = messagesStore.chats[friend.value.userid] || []
msgs.forEach(msg => {
if (!msg.isRead && msg.from === 'other') {
msg.isRead = true
connection.send("SendReadReceipt", msg.id)
}
})
})
// 只取当前好友的消息
const messages = computed(() => messagesStore.chats[friend.value.userid] || [])
watch(
() => messages.value.length,
(newLen, oldLen) => {
if (newLen > oldLen) {
const lastMsg = messages.value[newLen - 1]
if (lastMsg && lastMsg.from === 'other' && !lastMsg.isRead) {
lastMsg.isRead = true
connection.send("SendReadReceipt", lastMsg.id)
}
scrollToBottom()
}
}
)
//
async function getChatRecord(friendid) {
try {
const res = await connection.invoke("GetChatRecord", friendid)
console.log('获取聊天记录:', res)
// 一次性转换并存储
const msgs = res.map(msg => ({
id: msg.MessageId,
from: msg.IsMine ? 'me' : 'other',
text: msg.Message,
isRead: msg.IsRead
}))
messagesStore.setMessages(friendid, msgs)
} catch (err) {
console.error('获取聊天记录失败:', err)
}
}
function sendMessage() {
if (!newMessage.value.trim()) return
connection.send("SendPrivateMessage", friend.value.userid.toString(), newMessage.value)
//messages.value.push({ from: 'me', text: newMessage.value, id: Date.now(), isRead: false })
console.log('发送消息:', newMessage.value)
newMessage.value = ''
scrollToBottom()
}
function scrollToBottom() {
nextTick(() => {
if (messagesBox.value) {
messagesBox.value.scrollTop = messagesBox.value.scrollHeight
}
})
}
</script>
<style scoped>
.send-message-page {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
/* 消息窗口 */
.messages {
flex: 1;
padding: 10px;
overflow-y: auto;
}
.message {
margin: 8px 0;
display: flex;
flex-direction: column;
}
.message.me {
align-items: flex-end;
}
.message.other {
align-items: flex-start;
}
.bubble {
max-width: 70%;
padding: 8px 12px;
border-radius: 16px;
font-size: 14px;
word-break: break-word;
}
.message.me .bubble {
background-color: #4a90e2;
/* 蓝色气泡 */
color: #fff;
}
.message.other .bubble {
background-color: #e5e5ea;
/* 灰色气泡 */
color: #000;
}
.receipt {
font-size: 12px;
margin-top: 2px;
display: flex;
align-items: center;
}
.read {
color: #4a90e2;
/* 蓝色已读 */
margin-left: 4px;
}
.unread {
color: #999;
/* 灰色未读 */
margin-left: 4px;
}
.input-box {
display: flex;
padding: 10px;
background-color: #fff;
}
.van-field {
flex: 1;
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="friend-detail-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="好友详情" left-text="返回" left-arrow @click-left="$router.back()" />
<!-- 好友信息卡片 -->
<div class="profile-box">
<van-image :src="friend.avatarurl" width="100" height="100" round fit="cover" />
<div class="info">
<div class="nickname">{{ friend.nickname }}</div>
<div class="username">@{{ friend.username }}</div>
<div class="signature">{{ friend.signature }}</div>
<div class="userid">UID: {{ friend.userid }}</div>
<div class="status">状态: {{ friend.isonline ? '在线' : '离线' }}</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="actions">
<van-button type="primary" block @click="sendMessage">发送消息</van-button>
<div class="top"></div>
<van-button type="success" block @click="addFriend">加为好友</van-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useFriendStore } from '@/stores/friend'
import { connection } from '@/network/signalr'
import { useRouter } from 'vue-router'
const router = useRouter()
const friendStore = useFriendStore()
onMounted(() => {
console.log('好友详情页加载,好友信息:', friendStore.currentFriend)
friend.value = friendStore.currentFriend
})
// 模拟好友数据(实际应从后端接口获取)
const friend = ref({
userid: 3, username: '王五', nickname: '13800000003', avatarurl: 'https://auth.zotv.ru/webapp/9666.webp',
signature: '这是王五的个性签名', isonline: false
})
function sendMessage() {
console.log('跳转到聊天页面:', friend)
// $router.push('/chat/' + friend.value.id)
//friendStore.setFriend(friend.value)
router.push(`/friendmessage`)
}
async function addFriend() {
console.log('添加好友:', friend.value)
// 这里可以添加添加好友的逻辑例如调用API发送好友请求
await connection.invoke("AddFriend", friend.value.userid)
.then(result => {
console.log('添加好友结果:', result)
// 这里可以根据result来更新UI例如显示添加成功的提示
})
.catch(err => console.error("添加好友失败:", err));
}
</script>
<style scoped>
.friend-detail-page {
height: 100vh;
width: 100vw;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.profile-box {
background-color: #fff;
padding: 20px;
text-align: center;
}
.info {
margin-top: 15px;
}
.nickname {
font-size: 20px;
font-weight: bold;
}
.username {
font-size: 16px;
color: #888;
margin-top: 5px;
}
.signature {
font-size: 14px;
color: #666;
margin-top: 10px;
}
.userid {
font-size: 12px;
color: #aaa;
margin-top: 5px;
}
.actions {
margin: 30px;
}
.top {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="add-friend-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="添加好友" left-text="返回" left-arrow @click-left="$router.back()" />
<!-- 搜索框 -->
<van-search v-model="searchValue" placeholder="输入昵称或ID搜索" @search="onSearch" />
<!-- 搜索结果列表 -->
<van-empty v-if="results.length === 0" description="暂无搜索结果" />
<van-cell-group v-else>
<van-cell v-for="user in results" :key="user.id" is-link @click="openUser(user)">
<template #title>
<div class="nickname">{{ user.nickname }}</div>
<div class="username">@{{ user.username }}</div>
</template>
<template #label>
<div class="signature">{{ user.signature }}</div>
<div class="userid">UID: {{ user.userid }}</div>
</template>
<template #icon>
<van-image :src="user.avatarurl" width="40" height="40" round fit="cover" />
</template>
</van-cell>
</van-cell-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { connection } from '@/network/signalr'
import { useFriendStore } from '@/stores/friend'
import { useRouter } from 'vue-router'
const router = useRouter()
const friendStore = useFriendStore()
const searchValue = ref('')
const results = ref([])
// 模拟搜索
async function onSearch() {
// 这里可以添加搜索逻辑例如调用API获取搜索结果
console.log('搜索联系人:', searchValue.value)
await connection.invoke("SearchFriends", searchValue.value)
.then(result => {
console.log('搜索结果:', result)
if (result.success) {
results.value = []
results.value.push(result)
}else{
results.value = []
}
// 这里可以将result赋值给results来更新搜索结果列表
})
.catch(err => console.error("搜索联系人失败:", err));
}
function openUser(user) {
console.log('进入用户详情:', user)
// 这里可以用 $router.push('/user/' + user.id) 跳转详情页
friendStore.setFriend(user)
router.push(`/frienddetail`)
}
</script>
<style scoped>
.add-friend-page {
height: 100vh;
width: 100vw;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.nickname {
font-size: 16px;
font-weight: bold;
}
.username {
font-size: 14px;
color: #888;
}
.signature {
font-size: 14px;
color: #666;
}
.addfriend {
margin-left: 70%;
margin-top: -10%;
/* margin-right: 100px; */
}
.userid {
font-size: 12px;
color: #aaa;
}
</style>