Codex/merge feature nxdev #70
@@ -14,7 +14,11 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
|
<<<<<<< HEAD
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+eb8455e141ea496a2134ad7c7d9b759b6029dd75")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+eb8455e141ea496a2134ad7c7d9b759b6029dd75")]
|
||||||
|
=======
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f7223dc5904286fdf8e117b56c37f58dc431d68b")]
|
||||||
|
>>>>>>> eb8455e141ea496a2134ad7c7d9b759b6029dd75
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
546570633bb9288fc2957cbb29a807a45f3e48ba127ec13bc3956d28f5e6ed5b
|
546570633bb9288fc2957cbb29a807a45f3e48ba127ec13bc3956d28f5e6ed5b
|
||||||
|
=======
|
||||||
|
3c0a5335719f892c65744a9df7eae9fd7b12de9067131f5c9f4cb65f2b27b8d4
|
||||||
|
>>>>>>> eb8455e141ea496a2134ad7c7d9b759b6029dd75
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ namespace IM_API.Application.EventHandlers.FriendAddHandler
|
|||||||
var usersList = new List<string> {
|
var usersList = new List<string> {
|
||||||
@event.RequestUserId.ToString(), @event.ResponseUserId.ToString()
|
@event.RequestUserId.ToString(), @event.ResponseUserId.ToString()
|
||||||
};
|
};
|
||||||
var res = new HubResponse<MessageBaseDto>("Event", new MessageBaseDto()
|
var res = new HubResponse<MessageBaseDto>(HubResponseType.FriendAccepted, new MessageBaseDto()
|
||||||
{
|
{
|
||||||
ChatType = ChatType.PRIVATE,
|
ChatType = ChatType.PRIVATE,
|
||||||
Content = "您有新的好友关系已添加",
|
Content = "您有新的好友关系已添加",
|
||||||
|
|||||||
@@ -39,11 +39,44 @@ namespace IM_API.Dtos
|
|||||||
Message = codeDefine.Message;
|
Message = codeDefine.Message;
|
||||||
Data = data;
|
Data = data;
|
||||||
}
|
}
|
||||||
|
public HubResponse(HubResponseType type, T? data)
|
||||||
|
{
|
||||||
|
Code = CodeDefine.SUCCESS.Code;
|
||||||
|
Method = "Event";
|
||||||
|
Type = type;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public enum HubResponseType
|
public enum HubResponseType
|
||||||
{
|
{
|
||||||
ChatMsg = 1, // 聊天内容
|
// --- 基础聊天 (10-19) ---
|
||||||
SystemNotice = 2, // 系统通知(如:申请好友成功)
|
ChatMsg = 1, // 普通消息
|
||||||
ActionStatus = 3 // 状态变更(如:对方正在输入、已读回执)
|
|
||||||
|
// --- 系统与通知 (20-29) ---
|
||||||
|
SystemNotice = 2, // 通用系统通知
|
||||||
|
FriendRequest = 21, // 好友申请消息
|
||||||
|
FriendAccepted = 22, // 好友通过通知
|
||||||
|
UserInLine = 23, // 好友上线/下线通知
|
||||||
|
|
||||||
|
// --- 状态变更与交互 (30-39) ---
|
||||||
|
ActionStatus = 3, // 通用状态(正在输入等)
|
||||||
|
MsgReadReceipt = 31, // 已读回执
|
||||||
|
MsgRevoke = 32, // 消息撤回
|
||||||
|
MsgEdit = 33, // 消息二次编辑更新
|
||||||
|
|
||||||
|
// --- 群组管理 (40-49) ---
|
||||||
|
GroupInvited = 41, // 被邀请入群
|
||||||
|
GroupMemberUpdate = 42, // 群成员变动(进群/退群)
|
||||||
|
GroupAnnouncement = 43, // 群公告更新
|
||||||
|
GroupDismissed = 44, // 群组解散
|
||||||
|
|
||||||
|
// --- 实时通信控制 (50-59) ---
|
||||||
|
RTC_CallRequest = 51, // 音视频通话邀请
|
||||||
|
RTC_CallHandled = 52, // 音视频接听/挂断/取消状态
|
||||||
|
|
||||||
|
// --- 异常与安全 (90-99) ---
|
||||||
|
ErrorInternal = 91, // 服务器内部错误
|
||||||
|
TokenExpired = 92, // 登录过期,强制下线
|
||||||
|
KickedOut = 93 // 被挤下线(异地登录)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ namespace IM_API.Services
|
|||||||
|
|
||||||
public async Task MakeConversationAsync(int userAId, int userBId, ChatType chatType)
|
public async Task MakeConversationAsync(int userAId, int userBId, ChatType chatType)
|
||||||
{
|
{
|
||||||
var userAcExist = await _context.Conversations.AnyAsync(x => x.UserId == userAId && x.TargetId == userBId);
|
var userAcExist = await _context.Conversations.AnyAsync(
|
||||||
|
x => x.UserId == userAId && x.TargetId == userBId && x.ChatType == chatType);
|
||||||
if (userAcExist) return;
|
if (userAcExist) return;
|
||||||
var streamKey = chatType == ChatType.PRIVATE ?
|
var streamKey = chatType == ChatType.PRIVATE ?
|
||||||
StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId);
|
StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId);
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
# VITE_API_BASE_URL = http://localhost:5202/api
|
VITE_API_BASE_URL = http://localhost:5202/api
|
||||||
# VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
|
VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
|
||||||
# VITE_API_BASE_URL = https://im.test.nxsir.cn/api
|
# VITE_API_BASE_URL = https://im.test.nxsir.cn/api
|
||||||
# VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
|
# VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
|
||||||
|
|
||||||
|
|
||||||
VITE_API_BASE_URL = http://192.168.5.116:7070/api
|
# VITE_API_BASE_URL = http://192.168.5.116:7070/api
|
||||||
VITE_SIGNALR_BASE_URL = http://192.168.5.116:7070/chat/
|
# VITE_SIGNALR_BASE_URL = http://192.168.5.116:7070/chat/
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function createWindow() {
|
|||||||
createTry(mainWindow);
|
createTry(mainWindow);
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow.show()
|
// mainWindow.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function registerWindowHandler() {
|
|||||||
changeSize: () => {
|
changeSize: () => {
|
||||||
win.setSize(data.width, data.height, true)
|
win.setSize(data.width, data.height, true)
|
||||||
win.setResizable(data.resizable)
|
win.setResizable(data.resizable)
|
||||||
}
|
win.center()
|
||||||
|
},
|
||||||
|
show: () => win.show()
|
||||||
}
|
}
|
||||||
actions[action]?.()
|
actions[action]?.()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const api = {
|
|||||||
maximize: () => ipcRenderer.send('window-action', 'maximize'),
|
maximize: () => ipcRenderer.send('window-action', 'maximize'),
|
||||||
close: () => ipcRenderer.send('window-action', 'close'),
|
close: () => ipcRenderer.send('window-action', 'close'),
|
||||||
closeThis: () => ipcRenderer.send('window-action', 'closeThis'),
|
closeThis: () => ipcRenderer.send('window-action', 'closeThis'),
|
||||||
|
show: () => ipcRenderer.send('window-action', 'show'),
|
||||||
isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'),
|
isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'),
|
||||||
newWindow: (route, data, width, height) => ipcRenderer.send('window-new', { route, data, width, height }),
|
newWindow: (route, data, width, height) => ipcRenderer.send('window-new', { route, data, width, height }),
|
||||||
getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId),
|
getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function toggleMaximize() {
|
|||||||
function close() {
|
function close() {
|
||||||
window.api.window.close()
|
window.api.window.close()
|
||||||
emits('close')
|
emits('close')
|
||||||
|
emits('close')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const NOTIFICATION_TYPE = Object.freeze({
|
||||||
|
// --- 基础聊天 (10-19) ---
|
||||||
|
ChatMsg: 1,
|
||||||
|
FileMsg: 11,
|
||||||
|
EmojiSticker: 12,
|
||||||
|
ForwardMsg: 13,
|
||||||
|
|
||||||
|
// --- 系统与通知 (20-29) ---
|
||||||
|
SystemNotice: 2,
|
||||||
|
FriendRequest: 21,
|
||||||
|
FriendAccepted: 22,
|
||||||
|
UserInLine: 23,
|
||||||
|
|
||||||
|
// --- 状态变更与交互 (30-39) ---
|
||||||
|
ActionStatus: 3,
|
||||||
|
MsgReadReceipt: 31,
|
||||||
|
MsgRevoke: 32,
|
||||||
|
MsgEdit: 33,
|
||||||
|
|
||||||
|
// --- 群组管理 (40-49) ---
|
||||||
|
GroupInvited: 41,
|
||||||
|
GroupMemberUpdate: 42,
|
||||||
|
GroupAnnouncement: 43,
|
||||||
|
GroupDismissed: 44,
|
||||||
|
|
||||||
|
// --- 实时通信控制 (50-59) ---
|
||||||
|
RTC_CallRequest: 51,
|
||||||
|
RTC_CallHandled: 52,
|
||||||
|
|
||||||
|
// --- 异常与安全 (90-99) ---
|
||||||
|
ErrorInternal: 91,
|
||||||
|
TokenExpired: 92,
|
||||||
|
KickedOut: 93
|
||||||
|
})
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
import { useBrowserNotification } from "@/services/useBrowserNotification";
|
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
||||||
|
|
||||||
import { useChatStore } from "@/stores/chat";
|
import { useChatStore } from '@/stores/chat'
|
||||||
|
|
||||||
import { messageHandler } from "@/handler/messageHandler";
|
import { messageHandler } from '@/handler/messageHandler'
|
||||||
import { generateSessionId } from "../sessionIdTools";
|
import { generateSessionId } from '../sessionIdTools'
|
||||||
import { useConversationStore } from "@/stores/conversation";
|
import { useConversationStore } from '@/stores/conversation'
|
||||||
import { MESSAGE_TYPE } from "@/constants/MessageType";
|
import { MESSAGE_TYPE } from '@/constants/MessageType'
|
||||||
|
import { NOTIFICATION_TYPE } from '../../constants/notificationType'
|
||||||
|
|
||||||
export const SignalRMessageHandler = (data) => {
|
export const SignalRMessageHandler = (data) => {
|
||||||
const msg = data.data;
|
const msg = data.data
|
||||||
const chatStore = useChatStore()
|
const type = data.type
|
||||||
const browserNotification = useBrowserNotification();
|
const chatStore = useChatStore()
|
||||||
const sessionId = generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP);
|
const browserNotification = useBrowserNotification()
|
||||||
messageHandler(msg);
|
const sessionId = generateSessionId(
|
||||||
chatStore.pushAndSortMessagesAsync([msg], sessionId);
|
msg.senderId,
|
||||||
const conversation = useConversationStore().conversations.find(x => x.targetId == msg.senderId);
|
msg.receiverId,
|
||||||
browserNotification.send(`${conversation.targetName}发来一条消息`, {
|
msg.chatType == MESSAGE_TYPE.GROUP
|
||||||
body: msg.content,
|
)
|
||||||
icon: conversation.targetAvatar
|
messageHandler(msg)
|
||||||
});
|
chatStore.pushAndSortMessagesAsync([msg], sessionId)
|
||||||
}
|
const conversation = useConversationStore().conversations.find((x) => x.targetId == msg.senderId)
|
||||||
|
browserNotification.send(`${conversation.targetName}发来一条消息`, {
|
||||||
|
body: msg.content,
|
||||||
|
icon: conversation.targetAvatar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="im-container">
|
<div class="im-container">
|
||||||
<nav class="nav-sidebar">
|
<nav class="nav-sidebar">
|
||||||
<div class="user-self">
|
<div class="user-self">
|
||||||
<async-image :raw-url="myInfo?.avatar" class="avatar-std"/>
|
<async-image :raw-url="myInfo?.avatar" class="avatar-std" />
|
||||||
<!-- <img :src="myInfo?.avatar ?? defaultAvatar" class="avatar-std" /> -->
|
<!-- <img :src="myInfo?.avatar ?? defaultAvatar" class="avatar-std" /> -->
|
||||||
</div>
|
</div>
|
||||||
<router-link class="nav-item" to="/messages" active-class="active">
|
<router-link class="nav-item" to="/messages" active-class="active">
|
||||||
@@ -26,6 +26,7 @@ import { useAuthStore } from '@/stores/auth';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
import AsyncImage from '../components/AsyncImage.vue';
|
import AsyncImage from '../components/AsyncImage.vue';
|
||||||
|
import { isElectron } from '../utils/electronHelper';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -46,11 +47,14 @@ function handleStartChat(contact) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.api.window.setMainSize(900, 670)
|
if (isElectron()) {
|
||||||
|
window.api.window.setMainSize(900, 670)
|
||||||
|
window.api.window.show()
|
||||||
|
}
|
||||||
const { useSignalRStore } = await import('../stores/signalr');
|
const { useSignalRStore } = await import('../stores/signalr');
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const signalRStore = useSignalRStore();
|
const signalRStore = useSignalRStore();
|
||||||
if(authStore.token){
|
if (authStore.token) {
|
||||||
signalRStore.initSignalR();
|
signalRStore.initSignalR();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -66,7 +70,7 @@ onMounted(async () => {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 12px 48px rgba(0,0,0,0.1);
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 导航栏 */
|
/* 导航栏 */
|
||||||
@@ -90,8 +94,11 @@ onMounted(async () => {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-self { margin-bottom: 10px;/* 允许拖动整个窗口 */
|
.user-self {
|
||||||
-webkit-app-region:no-drag; }
|
margin-bottom: 10px;
|
||||||
|
/* 允许拖动整个窗口 */
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
/* 2. 列表区修复 */
|
/* 2. 列表区修复 */
|
||||||
.list-panel {
|
.list-panel {
|
||||||
@@ -107,6 +114,7 @@ onMounted(async () => {
|
|||||||
.search-section {
|
.search-section {
|
||||||
padding: 20px 12px 10px 12px;
|
padding: 20px 12px 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -115,7 +123,12 @@ onMounted(async () => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
.search-icon { font-size: 12px; color: #666; }
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.search-box input {
|
.search-box input {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -124,7 +137,10 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-area { flex: 1; overflow-y: auto; }
|
.scroll-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* 3. 聊天主面板修复 */
|
/* 3. 聊天主面板修复 */
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
@@ -159,7 +175,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 别人发的:默认靠左 */
|
/* 别人发的:默认靠左 */
|
||||||
.msg.other { flex-direction: row; }
|
.msg.other {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
/* 本人发的:翻转排列方向,靠右显示 */
|
/* 本人发的:翻转排列方向,靠右显示 */
|
||||||
.msg.mine {
|
.msg.mine {
|
||||||
@@ -184,11 +202,18 @@ onMounted(async () => {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.other .bubble { background: #fff; color: #333; }
|
.other .bubble {
|
||||||
.mine .bubble { background: #95ec69; color: #000; }
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine .bubble {
|
||||||
|
background: #95ec69;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.msg-time {
|
.msg-time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -197,11 +222,26 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 头像样式统一 */
|
/* 头像样式统一 */
|
||||||
:deep(.avatar-std) { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
:deep(.avatar-std) {
|
||||||
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-chat {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 未读气泡 */
|
/* 未读气泡 */
|
||||||
.avatar-container { position: relative; }
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.unread-badge {
|
.unread-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -5px;
|
top: -5px;
|
||||||
@@ -229,8 +269,20 @@ onMounted(async () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar { display: flex; gap: 12px; margin-bottom: 5px; font-size: 20px; color: #666; }
|
.toolbar {
|
||||||
.toolbar button { background: none; border: none; cursor: pointer; opacity: 0.7; }
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -242,7 +294,11 @@ textarea {
|
|||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-row { display: flex; justify-content: flex-end; }
|
.send-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.send-btn {
|
.send-btn {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #07c160;
|
color: #07c160;
|
||||||
@@ -252,18 +308,67 @@ textarea {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.send-btn:hover { background: #e2e2e2; }
|
|
||||||
|
.send-btn:hover {
|
||||||
|
background: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
/* 列表美化 */
|
/* 列表美化 */
|
||||||
.list-item { display: flex; padding: 12px; gap: 12px; cursor: pointer; }
|
.list-item {
|
||||||
.list-item.active { background: #c6c6c6; }
|
display: flex;
|
||||||
.list-item:hover:not(.active) { background: #ddd; }
|
padding: 12px;
|
||||||
.info { flex: 1; overflow: hidden; }
|
gap: 12px;
|
||||||
.name-row { display: flex; justify-content: space-between; align-items: center; }
|
cursor: pointer;
|
||||||
.name { font-size: 14px; font-weight: 500; }
|
}
|
||||||
.time { font-size: 11px; color: #999; }
|
|
||||||
.last-msg { font-size: 12px; color: #888; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
|
|
||||||
.nav-item { font-size: 24px; cursor: pointer; opacity: 0.5; background-color: #fff; border-radius: 5px; text-decoration: none;}
|
.list-item.active {
|
||||||
.nav-item.active { opacity: 1; }
|
background: #c6c6c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:hover:not(.active) {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-msg {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
|
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<WindowControls :resizable="false"/>
|
<WindowControls :resizable="false" />
|
||||||
<div class="window-header">
|
<div class="window-header">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="logo-dot"></span>
|
<span class="logo-dot"></span>
|
||||||
@@ -20,19 +20,9 @@
|
|||||||
|
|
||||||
<form @submit.prevent="handleLogin" class="form-body">
|
<form @submit.prevent="handleLogin" class="form-body">
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<input
|
<input type="text" v-model="form.username" placeholder="账号/邮箱" class="classic-input" />
|
||||||
type="text"
|
|
||||||
v-model="form.username"
|
|
||||||
placeholder="账号/邮箱"
|
|
||||||
class="classic-input"
|
|
||||||
/>
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<input
|
<input type="password" v-model="form.password" placeholder="密码" class="classic-input" />
|
||||||
type="password"
|
|
||||||
v-model="form.password"
|
|
||||||
placeholder="密码"
|
|
||||||
class="classic-input"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
@@ -67,6 +57,7 @@ import useVuelidate from '@vuelidate/core'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useSignalRStore } from '@/stores/signalr'
|
import { useSignalRStore } from '@/stores/signalr'
|
||||||
import WindowControls from '../../components/WindowControls.vue'
|
import WindowControls from '../../components/WindowControls.vue'
|
||||||
|
import { isElectron } from '../../utils/electronHelper'
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -80,48 +71,52 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
username:{
|
username: {
|
||||||
required:helpers.withMessage('用户名不能为空', required),
|
required: helpers.withMessage('用户名不能为空', required),
|
||||||
maxLength:helpers.withMessage('用户名最大20字符', maxLength(20))
|
maxLength: helpers.withMessage('用户名最大20字符', maxLength(20))
|
||||||
},
|
},
|
||||||
password:{
|
password: {
|
||||||
required:helpers.withMessage('密码不能为空', required),
|
required: helpers.withMessage('密码不能为空', required),
|
||||||
maxLength:helpers.withMessage('密码最大50字符', maxLength(50))
|
maxLength: helpers.withMessage('密码最大50字符', maxLength(50))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const v$ = useVuelidate(rules,form);
|
const v$ = useVuelidate(rules, form);
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
const isFormCorrect = await v$.value.$validate()
|
const isFormCorrect = await v$.value.$validate()
|
||||||
if (!isFormCorrect) {
|
if (!isFormCorrect) {
|
||||||
if (v$.value.$errors.length > 0) {
|
if (v$.value.$errors.length > 0) {
|
||||||
message.error(v$.value.$errors[0].$message)
|
message.error(v$.value.$errors[0].$message)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try{
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await authService.login(form);
|
const res = await authService.login(form);
|
||||||
if(res.code === 0){ // Assuming 0 is success
|
if (res.code === 0) { // Assuming 0 is success
|
||||||
message.success('登录成功')
|
message.success('登录成功')
|
||||||
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
|
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
|
||||||
signalRStore.initSignalR();
|
signalRStore.initSignalR();
|
||||||
router.push('/messages')
|
router.push('/messages')
|
||||||
}else{
|
} else {
|
||||||
message.error(res.message || '登录失败')
|
message.error(res.message || '登录失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally{
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.api.window.setMainSize(360, 532, false)
|
if (isElectron()) {
|
||||||
|
window.api.window.setMainSize(360, 532, false)
|
||||||
|
window.api.window.show()
|
||||||
|
}
|
||||||
feather.replace()
|
feather.replace()
|
||||||
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -140,29 +135,37 @@ onMounted(() => {
|
|||||||
/* 2. 核心登录框:双端适配的关键 */
|
/* 2. 核心登录框:双端适配的关键 */
|
||||||
.login-box {
|
.login-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px; /* Web 端限制最大宽度 */
|
max-width: 420px;
|
||||||
|
/* Web 端限制最大宽度 */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 540px; /* Web 端限制最大高度 */
|
max-height: 540px;
|
||||||
|
/* Web 端限制最大高度 */
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.05); /* Web 端浮动感 */
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.05);
|
||||||
|
/* Web 端浮动感 */
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. 桌面端(Electron)适配:当窗口较小时(即客户端模式) */
|
/* 3. 桌面端(Electron)适配:当窗口较小时(即客户端模式) */
|
||||||
@media (max-width: 480px) or (max-height: 580px) {
|
@media (max-width: 480px) or (max-height: 580px) {
|
||||||
.login-container {
|
.login-container {
|
||||||
background-color: #fff; /* 客户端通常不需要容器背景 */
|
background-color: #fff;
|
||||||
|
/* 客户端通常不需要容器背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-box {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
box-shadow: none; /* 客户端全屏模式不需要投影 */
|
box-shadow: none;
|
||||||
|
/* 客户端全屏模式不需要投影 */
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-header {
|
.window-header {
|
||||||
display: flex; /* 客户端显示拖拽区 */
|
display: flex;
|
||||||
|
/* 客户端显示拖拽区 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +173,8 @@ onMounted(() => {
|
|||||||
.window-header {
|
.window-header {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: none; /* Web 端默认隐藏 */
|
display: none;
|
||||||
|
/* Web 端默认隐藏 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
@@ -208,7 +212,8 @@ onMounted(() => {
|
|||||||
height: 72px;
|
height: 72px;
|
||||||
background: #f0f7ff;
|
background: #f0f7ff;
|
||||||
color: #0099ff;
|
color: #0099ff;
|
||||||
border-radius: 20px; /* 稍微方圆一点更现代 */
|
border-radius: 20px;
|
||||||
|
/* 稍微方圆一点更现代 */
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user