Initial commit

master
gz 1 month ago
commit 1fd4c94097

Binary file not shown.

Binary file not shown.

@ -0,0 +1,99 @@
[2026-05-22 12:59:36] SYSTEM::zgz 加入聊天室
[2026-05-22 12:59:38] SYSTEM::gz 加入聊天室
[2026-05-22 13:00:32] SYSTEM::gz 离开聊天室
[2026-05-22 13:00:32] SYSTEM::zgz 离开聊天室
[2026-05-22 13:00:36] SYSTEM::zgz 加入聊天室
[2026-05-22 13:00:37] SYSTEM::gz 加入聊天室
[2026-05-22 13:01:06] SYSTEM::zgz 离开聊天室
[2026-05-22 13:01:09] SYSTEM::zgz 加入聊天室
[2026-05-22 13:01:15] TEXT::zgz::?试一下
[2026-05-22 13:01:18] TEXT::zgz::????
[2026-05-22 13:01:27] TEXT::zgz::123123123123、
[2026-05-22 13:05:09] SYSTEM::zgz 离开聊天室
[2026-05-22 13:05:09] SYSTEM::gz 离开聊天室
[2026-05-22 13:05:14] SYSTEM::zgz 加入聊天室
[2026-05-22 13:05:14] SYSTEM::zgz 离开聊天室
[2026-05-22 13:05:18] SYSTEM::zgz 加入聊天室
[2026-05-22 13:05:21] SYSTEM::gz 加入聊天室
[2026-05-22 13:12:59] SYSTEM::gz 离开聊天室
[2026-05-22 13:12:59] SYSTEM::zgz 离开聊天室
[2026-05-22 13:13:04] SYSTEM::zgz 加入聊天室
[2026-05-22 13:13:05] SYSTEM::gz 加入聊天室
[2026-05-22 13:16:58] SYSTEM::gz 离开聊天室
[2026-05-22 13:16:58] SYSTEM::zgz 离开聊天室
[2026-05-22 13:17:03] SYSTEM::zgz 加入聊天室
[2026-05-22 13:17:04] SYSTEM::gz 加入聊天室
[2026-05-22 13:18:06] SYSTEM::gz 离开聊天室
[2026-05-22 13:18:06] SYSTEM::zgz 离开聊天室
[2026-05-22 13:18:12] SYSTEM::zgz 加入聊天室
[2026-05-22 13:18:14] SYSTEM::gz 加入聊天室
[2026-05-22 13:19:14] SYSTEM::zgz 离开聊天室
[2026-05-22 13:19:17] SYSTEM::zgz 加入聊天室
[2026-05-22 13:19:18] TEXT::zgz::?
[2026-05-22 13:19:23] TEXT::zgz::你好
[2026-05-22 13:19:26] TEXT::zgz::你好
[2026-05-22 13:19:27] TEXT::zgz::显示
[2026-05-22 13:19:31] SYSTEM::zgz 离开聊天室
[2026-05-22 13:19:33] SYSTEM::zgz 加入聊天室
[2026-05-22 13:19:35] TEXT::zgz::你好
[2026-05-22 13:20:14] TEXT::zgz::???
[2026-05-22 13:20:19] SYSTEM::zgz 离开聊天室
[2026-05-22 13:20:21] SYSTEM::zgz 加入聊天室
[2026-05-22 13:20:22] SYSTEM::zgz 离开聊天室
[2026-05-22 13:20:27] SYSTEM::gz 离开聊天室
[2026-05-22 13:20:30] SYSTEM::gz 加入聊天室
[2026-05-22 13:20:35] SYSTEM::zgz 加入聊天室
[2026-05-22 13:20:45] TEXT::gz::你好
[2026-05-22 13:20:48] TEXT::gz::???
[2026-05-22 13:20:55] TEXT::zgz::????
[2026-05-22 13:28:15] SYSTEM::zgz 离开聊天室
[2026-05-22 13:28:18] SYSTEM::zgz 加入聊天室
[2026-05-22 13:28:19] SYSTEM::zgz 离开聊天室
[2026-05-22 13:28:21] SYSTEM::zgz 加入聊天室
[2026-05-22 13:30:49] SYSTEM::zgz 离开聊天室
[2026-05-22 13:30:49] SYSTEM::gz 离开聊天室
[2026-05-22 14:04:27] SYSTEM::gz 加入聊天室
[2026-05-22 14:04:31] SYSTEM::zgz 加入聊天室
[2026-05-22 14:04:32] SYSTEM::zgz 离开聊天室
[2026-05-22 14:04:34] SYSTEM::zgz 加入聊天室
[2026-05-22 14:04:37] TEXT::zgz::123
[2026-05-22 14:04:38] TEXT::zgz::123
[2026-05-22 14:04:38] TEXT::zgz::12
[2026-05-22 14:04:38] TEXT::zgz::3
[2026-05-22 14:04:38] TEXT::zgz::21
[2026-05-22 14:04:38] TEXT::zgz::321
[2026-05-22 14:04:39] TEXT::zgz::31
[2026-05-22 14:04:41] SYSTEM::zgz 离开聊天室
[2026-05-22 14:04:43] SYSTEM::zgz 加入聊天室
[2026-05-22 14:04:52] TEXT::zgz::12313
[2026-05-22 14:04:54] TEXT::zgz::1111
[2026-05-22 14:11:15] SYSTEM::zgz 离开聊天室
[2026-05-22 14:11:17] SYSTEM::zgz 加入聊天室
[2026-05-22 14:11:19] TEXT::zgz::123
[2026-05-22 14:11:20] TEXT::zgz::123
[2026-05-22 14:11:22] TEXT::zgz::qwe
[2026-05-22 14:11:22] TEXT::zgz::qwe
[2026-05-22 14:11:25] SYSTEM::zgz 离开聊天室
[2026-05-22 14:11:27] SYSTEM::zgz 加入聊天室
[2026-05-22 14:12:30] SYSTEM::zgz 离开聊天室
[2026-05-22 14:12:30] SYSTEM::gz 离开聊天室
[2026-05-22 14:13:41] SYSTEM::zgz 加入聊天室
[2026-05-22 14:13:42] SYSTEM::gz 加入聊天室
[2026-05-22 14:13:55] SYSTEM::zgz 离开聊天室
[2026-05-22 14:13:58] SYSTEM::zgz 加入聊天室
[2026-05-22 14:13:58] TEXT::zgz::1111
[2026-05-22 14:13:59] TEXT::zgz::2222
[2026-05-22 14:14:00] TEXT::zgz::3333
[2026-05-22 14:14:01] TEXT::zgz::444
[2026-05-22 14:14:03] SYSTEM::zgz 离开聊天室
[2026-05-22 14:14:05] SYSTEM::zgz 加入聊天室
[2026-05-22 14:30:26] SYSTEM::zgz 离开聊天室
[2026-05-22 14:30:26] SYSTEM::gz 离开聊天室
[2026-05-22 14:30:31] SYSTEM::gz 加入聊天室
[2026-05-22 14:30:31] SYSTEM::zgz 加入聊天室
[2026-05-22 14:32:44] SYSTEM::测试机器人 加入聊天室
[2026-05-22 14:32:44] TEXT::测试机器人::你好世界
[2026-05-22 14:32:44] SYSTEM::匿名用户 离开聊天室
[2026-05-22 14:32:48] SYSTEM::测试机器人2 加入聊天室
[2026-05-22 14:32:48] TEXT::测试机器人2::这是一条实时测试消息!
[2026-05-22 14:32:48] SYSTEM::测试机器人2 离开聊天室

@ -0,0 +1,2 @@
[2026-05-22 14:32:59] SYSTEM::nginx测试 加入聊天室
[2026-05-22 14:32:59] SYSTEM::nginx测试 离开聊天室

@ -0,0 +1,131 @@
[2026-05-22 14:36:47] SYSTEM::gz 离开聊天室
[2026-05-22 14:36:50] SYSTEM::gz 加入聊天室
[2026-05-22 14:36:54] TEXT::gz::测试一下
[2026-05-22 14:36:58] TEXT::gz::发送聊天记录
[2026-05-22 14:37:19] SYSTEM::zgz 离开聊天室
[2026-05-22 14:37:21] SYSTEM::zgz 加入聊天室
[2026-05-22 14:37:25] TEXT::zgz::可以,还不错
[2026-05-22 14:37:28] TEXT::zgz::哪里
[2026-05-22 14:37:33] TEXT::zgz::什么啊
[2026-05-22 14:37:47] TEXT::gz::你是谁
[2026-05-22 14:40:58] SYSTEM::zgz 离开聊天室
[2026-05-22 14:41:00] SYSTEM::zgz 加入聊天室
[2026-05-22 14:41:09] TEXT::zgz::你好
[2026-05-22 14:41:12] TEXT::zgz::什么
[2026-05-22 14:41:15] TEXT::zgz::这是哪里的消息.
[2026-05-22 14:41:22] TEXT::zgz::可以啊
[2026-05-22 14:41:57] TEXT::zgz::刚刚发送了图片,是不是没显示成功
[2026-05-22 14:42:12] TEXT::zgz::还是说文件太大了
[2026-05-22 14:43:47] SYSTEM::zgz 离开聊天室
[2026-05-22 14:43:47] SYSTEM::gz 离开聊天室
[2026-05-22 14:43:57] SYSTEM::zgz 加入聊天室
[2026-05-22 14:43:57] SYSTEM::gz 加入聊天室
[2026-05-22 14:44:00] SYSTEM::gz 离开聊天室
[2026-05-22 14:44:00] SYSTEM::zgz 离开聊天室
[2026-05-22 14:44:05] SYSTEM::gz 加入聊天室
[2026-05-22 14:44:05] SYSTEM::zgz 加入聊天室
[2026-05-22 14:44:47] SYSTEM::zgz 离开聊天室
[2026-05-22 14:44:49] SYSTEM::zgz 加入聊天室
[2026-05-22 14:45:15] TEXT::zgz::还没有成功?
[2026-05-22 14:47:35] SYSTEM::gz 离开聊天室
[2026-05-22 14:47:37] SYSTEM::gz 加入聊天室
[2026-05-22 14:47:41] TEXT::gz::我再试一下
[2026-05-22 14:48:22] TEXT::gz::?
[2026-05-22 14:48:43] SYSTEM::gz 离开聊天室
[2026-05-22 14:48:46] SYSTEM::gz 加入聊天室
[2026-05-22 14:53:04] SYSTEM::zgz 离开聊天室
[2026-05-22 14:53:04] SYSTEM::gz 离开聊天室
[2026-05-22 14:53:09] SYSTEM::gz 加入聊天室
[2026-05-22 14:53:10] SYSTEM::zgz 加入聊天室
[2026-05-22 14:53:10] SYSTEM::gz 离开聊天室
[2026-05-22 14:53:10] SYSTEM::zgz 离开聊天室
[2026-05-22 14:53:15] SYSTEM::gz 加入聊天室
[2026-05-22 14:53:15] SYSTEM::zgz 加入聊天室
[2026-05-22 14:53:25] SYSTEM::测试用户 加入聊天室
[2026-05-22 14:53:25] SYSTEM::测试用户 离开聊天室
[2026-05-22 14:53:41] SYSTEM::gz 离开聊天室
[2026-05-22 14:53:45] SYSTEM::gz 加入聊天室
[2026-05-22 14:53:49] TEXT::gz::123
[2026-05-22 14:54:12] TEXT::gz::这次真可以了.
[2026-05-22 14:56:29] SYSTEM::怀旧用户 加入聊天室
[2026-05-22 14:56:29] TEXT::怀旧用户::你好,老聊天室的感觉!
[2026-05-22 14:56:29] SYSTEM::怀旧用户 离开聊天室
[2026-05-22 14:56:57] SYSTEM::gz 离开聊天室
[2026-05-22 14:57:00] SYSTEM::gz 加入聊天室
[2026-05-22 14:57:15] TEXT::gz::你好
[2026-05-22 14:57:22] TEXT::gz::测试一下
[2026-05-22 14:57:39] SYSTEM::zgz 离开聊天室
[2026-05-22 14:57:41] SYSTEM::zgz 加入聊天室
[2026-05-22 14:57:43] TEXT::zgz::还可以啊
[2026-05-22 14:57:56] TEXT::zgz::继续
[2026-05-22 14:57:59] TEXT::zgz::测试
[2026-05-22 14:59:09] SYSTEM::ts 加入聊天室
[2026-05-22 14:59:32] TEXT::ts::什么?
[2026-05-22 14:59:46] TEXT::ts::东东
[2026-05-22 15:00:22] SYSTEM::ts 离开聊天室
[2026-05-22 15:03:16] SYSTEM::TG用户 加入聊天室
[2026-05-22 15:03:16] TEXT::TG用户::Telegram风格测试
[2026-05-22 15:03:16] SYSTEM::TG用户 离开聊天室
[2026-05-22 15:03:32] SYSTEM::zgz 离开聊天室
[2026-05-22 15:03:35] SYSTEM::zgz 加入聊天室
[2026-05-22 15:09:15] SYSTEM::设计测试 加入聊天室
[2026-05-22 15:09:15] TEXT::设计测试::对齐布局测试
[2026-05-22 15:09:15] SYSTEM::设计测试 离开聊天室
[2026-05-22 15:09:52] SYSTEM::gz 离开聊天室
[2026-05-22 15:09:54] SYSTEM::gz 加入聊天室
[2026-05-22 15:12:39] TEXT::gz::这是
[2026-05-22 15:12:44] TEXT::gz::什么内容.
[2026-05-22 15:14:58] SYSTEM::群聊测试 加入聊天室
[2026-05-22 15:14:58] TEXT::群聊测试::所有人都在左侧
[2026-05-22 15:14:58] SYSTEM::群聊测试 离开聊天室
[2026-05-22 15:15:41] SYSTEM::gz 离开聊天室
[2026-05-22 15:15:44] SYSTEM::gz 加入聊天室
[2026-05-22 15:15:49] TEXT::gz::这能区分出来哪个是我吗
[2026-05-22 15:15:53] TEXT::gz::哦
[2026-05-22 15:15:56] TEXT::gz::好像可以
[2026-05-22 15:16:07] SYSTEM::zgz 离开聊天室
[2026-05-22 15:16:09] SYSTEM::zgz 加入聊天室
[2026-05-22 15:16:10] TEXT::zgz::试一下
[2026-05-22 15:16:12] TEXT::zgz::这个是我
[2026-05-22 15:16:22] SYSTEM::zgz 离开聊天室
[2026-05-22 15:16:27] SYSTEM::gz 离开聊天室
[2026-05-22 15:16:29] SYSTEM::gz 加入聊天室
[2026-05-22 15:16:43] TEXT::gz::好玩的.
[2026-05-22 15:16:47] TEXT::gz::test
[2026-05-22 15:16:47] TEXT::gz::test
[2026-05-22 15:20:08] SYSTEM::无痕测试 加入聊天室
[2026-05-22 15:20:08] TEXT::无痕测试::退出重进就看不见了
[2026-05-22 15:20:08] SYSTEM::无痕测试 离开聊天室
[2026-05-22 15:20:35] SYSTEM::gz 离开聊天室
[2026-05-22 15:20:37] SYSTEM::gz 加入聊天室
[2026-05-22 15:20:51] TEXT::gz::你好,这是一条新的消息.
[2026-05-22 15:20:56] TEXT::gz::开始测试.
[2026-05-22 15:21:08] SYSTEM::gz 加入聊天室
[2026-05-22 15:21:13] SYSTEM::gz 离开聊天室
[2026-05-22 15:21:28] SYSTEM::ts 加入聊天室
[2026-05-22 15:21:36] TEXT::ts::hello
[2026-05-22 15:21:36] TEXT::ts::hello
[2026-05-22 15:21:40] TEXT::ts::可以吗
[2026-05-22 15:21:43] TEXT::ts::历史
[2026-05-22 15:22:03] SYSTEM::ts 离开聊天室
[2026-05-22 15:22:25] TEXT::gz::退出再进消息就没了
[2026-05-22 16:47:10] SYSTEM::ts 加入聊天室
[2026-05-22 16:47:11] SYSTEM::ts 离开聊天室
[2026-05-22 16:47:13] SYSTEM::ts 加入聊天室
[2026-05-22 16:49:26] SYSTEM::ts 离开聊天室
[2026-05-22 17:59:32] SYSTEM::gz 离开聊天室
[2026-05-22 17:59:37] SYSTEM::gz 加入聊天室
[2026-05-22 18:00:16] SYSTEM::gz 离开聊天室
[2026-05-22 18:01:20] SYSTEM::gz 加入聊天室
[2026-05-22 18:02:20] SYSTEM::gz 离开聊天室
[2026-05-22 18:18:47] SYSTEM::gz 加入聊天室
[2026-05-22 18:22:10] SYSTEM::gz 离开聊天室
[2026-05-22 18:38:35] SYSTEM::gz 加入聊天室
[2026-05-22 18:40:16] SYSTEM::gz 离开聊天室
[2026-05-22 18:57:14] SYSTEM::gz 加入聊天室
[2026-05-22 21:05:21] SYSTEM::gz 离开聊天室
[2026-05-22 21:05:26] SYSTEM::gz 加入聊天室
[2026-05-22 23:10:35] SYSTEM::gz 离开聊天室
[2026-05-22 23:15:01] SYSTEM::gz 加入聊天室
[2026-05-22 23:22:08] SYSTEM::gz 离开聊天室
[2026-05-22 23:26:34] SYSTEM::gz 加入聊天室

@ -0,0 +1,8 @@
[2026-05-23 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-23 03:01:10] SYSTEM::gz 加入聊天室
[2026-05-23 09:16:55] SYSTEM::gz 离开聊天室
[2026-05-23 09:24:58] SYSTEM::gz 加入聊天室
[2026-05-23 13:21:44] SYSTEM::gz 离开聊天室
[2026-05-23 13:26:48] SYSTEM::gz 加入聊天室
[2026-05-23 16:47:08] SYSTEM::gz 离开聊天室
[2026-05-23 16:47:13] SYSTEM::gz 加入聊天室

@ -0,0 +1,6 @@
[2026-05-24 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-24 03:01:11] SYSTEM::gz 加入聊天室
[2026-05-24 21:55:39] SYSTEM::gz 离开聊天室
[2026-05-24 22:00:06] SYSTEM::gz 加入聊天室
[2026-05-24 22:02:28] SYSTEM::gz 离开聊天室
[2026-05-24 22:07:18] SYSTEM::gz 加入聊天室

@ -0,0 +1,20 @@
[2026-05-25 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-25 03:01:14] SYSTEM::gz 加入聊天室
[2026-05-25 09:18:50] SYSTEM::gz 离开聊天室
[2026-05-25 09:21:12] SYSTEM::gz 加入聊天室
[2026-05-25 13:31:34] SYSTEM::gz 离开聊天室
[2026-05-25 13:38:33] SYSTEM::gz 加入聊天室
[2026-05-25 14:30:06] SYSTEM::gz 加入聊天室
[2026-05-25 14:31:56] SYSTEM::匿名6524 加入聊天室
[2026-05-25 14:32:07] SYSTEM::匿名6524 离开聊天室
[2026-05-25 14:35:01] SYSTEM::gz 离开聊天室
[2026-05-25 14:35:40] SYSTEM::gz 离开聊天室
[2026-05-25 14:35:41] SYSTEM::gz 加入聊天室
[2026-05-25 14:35:53] SYSTEM::zgz 加入聊天室
[2026-05-25 14:35:55] SYSTEM::zgz 离开聊天室
[2026-05-25 18:12:17] SYSTEM::gz 离开聊天室
[2026-05-25 18:12:20] SYSTEM::gz 加入聊天室
[2026-05-25 18:12:22] SYSTEM::gz 离开聊天室
[2026-05-25 18:12:24] SYSTEM::gz 加入聊天室
[2026-05-25 18:13:04] SYSTEM::gz 离开聊天室
[2026-05-25 18:32:44] SYSTEM::gz 加入聊天室

@ -0,0 +1,26 @@
[2026-05-26 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-26 03:02:58] SYSTEM::gz 加入聊天室
[2026-05-26 04:00:38] SYSTEM::gz 离开聊天室
[2026-05-26 04:05:03] SYSTEM::gz 加入聊天室
[2026-05-26 09:00:25] SYSTEM::gz 离开聊天室
[2026-05-26 09:01:05] SYSTEM::gz 加入聊天室
[2026-05-26 18:04:52] SYSTEM::gz 离开聊天室
[2026-05-26 18:04:55] SYSTEM::gz 加入聊天室
[2026-05-26 18:05:35] SYSTEM::gz 离开聊天室
[2026-05-26 18:20:09] SYSTEM::gz 加入聊天室
[2026-05-26 18:24:00] SYSTEM::gz 离开聊天室
[2026-05-26 18:40:12] SYSTEM::gz 加入聊天室
[2026-05-26 19:47:40] SYSTEM::gz 离开聊天室
[2026-05-26 20:04:10] SYSTEM::gz 加入聊天室
[2026-05-26 20:08:33] SYSTEM::gz 离开聊天室
[2026-05-26 20:25:13] SYSTEM::gz 加入聊天室
[2026-05-26 20:26:53] SYSTEM::gz 离开聊天室
[2026-05-26 20:41:49] SYSTEM::gz 加入聊天室
[2026-05-26 20:52:57] SYSTEM::gz 离开聊天室
[2026-05-26 21:09:09] SYSTEM::gz 加入聊天室
[2026-05-26 21:11:10] SYSTEM::gz 离开聊天室
[2026-05-26 21:26:00] SYSTEM::gz 加入聊天室
[2026-05-26 21:27:20] SYSTEM::gz 离开聊天室
[2026-05-26 21:44:19] SYSTEM::gz 加入聊天室
[2026-05-26 21:45:39] SYSTEM::gz 离开聊天室
[2026-05-26 22:00:53] SYSTEM::gz 加入聊天室

@ -0,0 +1,39 @@
[2026-05-27 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-27 03:01:45] SYSTEM::gz 加入聊天室
[2026-05-27 04:00:25] SYSTEM::gz 离开聊天室
[2026-05-27 04:04:50] SYSTEM::gz 加入聊天室
[2026-05-27 07:59:08] SYSTEM::gz 离开聊天室
[2026-05-27 08:18:30] SYSTEM::gz 加入聊天室
[2026-05-27 08:20:11] SYSTEM::gz 离开聊天室
[2026-05-27 08:36:08] SYSTEM::gz 加入聊天室
[2026-05-27 08:42:13] SYSTEM::gz 离开聊天室
[2026-05-27 08:42:14] SYSTEM::gz 加入聊天室
[2026-05-27 08:42:42] SYSTEM::张卫贤 加入聊天室
[2026-05-27 08:42:45] TEXT::张卫贤::1
[2026-05-27 08:42:47] SYSTEM::匿名9712 加入聊天室
[2026-05-27 08:42:48] TEXT::gz::?
[2026-05-27 08:42:49] SYSTEM::匿名9712 离开聊天室
[2026-05-27 08:42:54] SYSTEM::gz 离开聊天室
[2026-05-27 08:42:56] SYSTEM::gz 加入聊天室
[2026-05-27 08:42:59] TEXT::gz::哈
[2026-05-27 08:45:15] SYSTEM::匿名6572 加入聊天室
[2026-05-27 08:45:17] SYSTEM::匿名6572 离开聊天室
[2026-05-27 08:46:16] SYSTEM::张卫贤 加入聊天室
[2026-05-27 08:46:31] SYSTEM::张卫贤 离开聊天室
[2026-05-27 08:56:47] SYSTEM::张卫贤 离开聊天室
[2026-05-27 10:15:46] SYSTEM::匿名3899 加入聊天室
[2026-05-27 10:15:47] SYSTEM::匿名3899 离开聊天室
[2026-05-27 16:18:34] SYSTEM::gz 离开聊天室
[2026-05-27 16:18:44] SYSTEM::gz 加入聊天室
[2026-05-27 18:01:59] SYSTEM::gz 离开聊天室
[2026-05-27 18:02:02] SYSTEM::gz 加入聊天室
[2026-05-27 18:02:41] SYSTEM::gz 离开聊天室
[2026-05-27 18:17:23] SYSTEM::gz 加入聊天室
[2026-05-27 18:18:43] SYSTEM::gz 离开聊天室
[2026-05-27 18:34:51] SYSTEM::gz 加入聊天室
[2026-05-27 20:32:01] SYSTEM::gz 离开聊天室
[2026-05-27 20:42:44] SYSTEM::gz 加入聊天室
[2026-05-27 23:06:33] SYSTEM::gz 离开聊天室
[2026-05-27 23:06:39] SYSTEM::gz 加入聊天室
[2026-05-27 23:44:03] SYSTEM::gz 加入聊天室
[2026-05-27 23:44:19] SYSTEM::gz 离开聊天室

@ -0,0 +1,21 @@
[2026-05-28 03:00:06] SYSTEM::gz 离开聊天室
[2026-05-28 03:01:42] SYSTEM::gz 加入聊天室
[2026-05-28 11:15:18] SYSTEM::gz 离开聊天室
[2026-05-28 11:18:26] SYSTEM::gz 加入聊天室
[2026-05-28 11:32:23] SYSTEM::gz 离开聊天室
[2026-05-28 11:34:51] SYSTEM::gz 加入聊天室
[2026-05-28 11:37:14] SYSTEM::gz 离开聊天室
[2026-05-28 11:37:18] SYSTEM::gz 加入聊天室
[2026-05-28 12:42:05] SYSTEM::gz 离开聊天室
[2026-05-28 12:42:09] SYSTEM::gz 加入聊天室
[2026-05-28 13:08:21] SYSTEM::gz 离开聊天室
[2026-05-28 13:09:56] SYSTEM::gz 加入聊天室
[2026-05-28 13:14:47] SYSTEM::gz 离开聊天室
[2026-05-28 13:14:51] SYSTEM::gz 加入聊天室
[2026-05-28 14:04:28] SYSTEM::ts 加入聊天室
[2026-05-28 14:05:34] SYSTEM::ts 离开聊天室
[2026-05-28 14:14:02] SYSTEM::ts 加入聊天室
[2026-05-28 14:17:20] SYSTEM::ts 离开聊天室
[2026-05-28 14:20:23] SYSTEM::gz 离开聊天室
[2026-05-28 14:48:35] SYSTEM::ts 加入聊天室
[2026-05-28 14:50:54] SYSTEM::ts 离开聊天室

@ -0,0 +1,300 @@
"""
ChatHub - 实时聊天室
FastAPI + WebSocket
"""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, Form, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from typing import List, Dict, Tuple
import os
import uuid
import json
import asyncio
import shutil
import time
import collections
from datetime import datetime
app = FastAPI()
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
BACKUP_DIR = os.path.join(BASE_DIR, "backups")
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(BACKUP_DIR, exist_ok=True)
CHAT_LOG_FILE = os.path.join(BASE_DIR, "chat_history.txt")
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB per file
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp", "heic"}
ALLOWED_VIDEO_EXTENSIONS = {"mp4", "mov", "wmv", "avi", "m4v", "mpg", "mpeg", "flv", "mkv", "3gp", "webm"}
templates = Jinja2Templates(directory=TEMPLATE_DIR)
app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
# ── Global State ────────────────────────────────────────────
connections: Dict[WebSocket, str] = {} # ws -> username
glock = asyncio.Lock()
# ── Rate Limiting ─────────────────────────────────────────
# Upload rate limit: {ip: [timestamp, ...]}
upload_history: Dict[str, List[float]] = {}
UPLOAD_COOLDOWN = 3.0 # seconds between uploads
# Message rate limit: per-connection message timestamps
msg_history: Dict[WebSocket, List[float]] = {}
MSG_BURST = 8 # max messages
MSG_WINDOW = 3.0 # per this many seconds
def get_ext(fn: str) -> str:
return fn.rsplit(".", 1)[-1].lower()
def is_image(ext: str) -> bool:
return ext in ALLOWED_IMAGE_EXTENSIONS
def is_video(ext: str) -> bool:
return ext in ALLOWED_VIDEO_EXTENSIONS
def log_to_file(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
with open(CHAT_LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{ts}] {msg}\n")
except Exception:
pass
def load_recent_history(n: int = 200) -> List[str]:
if not os.path.exists(CHAT_LOG_FILE):
return []
try:
with open(CHAT_LOG_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
return [l.strip() for l in lines[-n:]]
except Exception:
return []
# ── HTTP Endpoints ──────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
history = load_recent_history(200)
return templates.TemplateResponse("chat.html", {"request": request, "history": history})
@app.get("/members")
async def get_members():
"""Return current online member list."""
async with glock:
names = list(connections.values())
return JSONResponse({"members": names})
# ── Rate Limiter Helpers ────────────────────────────────
async def _check_upload_rate(request: Request) -> Tuple[bool, str]:
"""Check upload rate limit per client IP.
Returns (allowed, reason).
"""
ip = request.client.host if request.client else "unknown"
now = time.time()
async with glock:
times = upload_history.get(ip, [])
# Clean old entries
times = [t for t in times if now - t < UPLOAD_COOLDOWN]
if times:
remaining = int(UPLOAD_COOLDOWN - (now - times[-1]))
return False, f"上传太频繁,请 {remaining} 秒后再试"
times.append(now)
upload_history[ip] = times
return True, ""
async def _check_msg_rate(ws: WebSocket) -> bool:
"""Check message send rate per WebSocket connection."""
now = time.time()
async with glock:
times = msg_history.get(ws, [])
times = [t for t in times if now - t < MSG_WINDOW]
if len(times) >= MSG_BURST:
return False
times.append(now)
msg_history[ws] = times
return True
# ── HTTP Endpoints ──────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
history = load_recent_history(200)
return templates.TemplateResponse("chat.html", {"request": request, "history": history})
@app.get("/members")
async def get_members():
"""Return current online member list."""
async with glock:
names = list(connections.values())
return JSONResponse({"members": names})
@app.post("/upload")
async def upload_file(file: UploadFile = File(...), request: Request = None, sender: str = Form("匿名")):
# Rate limit check
allowed, reason = await _check_upload_rate(request)
if not allowed:
return JSONResponse({"error": reason}, status_code=429)
# File type check
ext = get_ext(file.filename)
if not (is_image(ext) or is_video(ext)):
return JSONResponse({"error": "不支持的文件类型,仅接受图片和视频"}, status_code=400)
# Read and check size
data = await file.read()
if len(data) > MAX_FILE_SIZE:
size_mb = len(data) / (1024 * 1024)
return JSONResponse({
"error": f"文件过大({size_mb:.1f}MB最大允许 10MB"
}, status_code=413)
# Save and broadcast
fn = f"{uuid.uuid4().hex}.{ext}"
path = os.path.join(UPLOAD_DIR, fn)
with open(path, "wb") as f:
f.write(data)
url = f"/uploads/{fn}"
# Format: TYPE::sender::url — frontend parses two colons
msg = f"IMG::{sender}::{url}" if is_image(ext) else f"VIDEO::{sender}::{url}"
asyncio.create_task(_broadcast(msg))
return {"url": url}
# ── Broadcast ───────────────────────────────────────────────
async def _broadcast(message: str):
"""Send message to all connected clients.
Uses snapshot-then-send pattern: grab targets under lock, then send concurrently without lock.
"""
async with glock:
targets = list(connections.items()) # [(ws, name), ...]
tasks = []
for ws, _ in targets:
tasks.append(_safe_send(ws, message))
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
async def _safe_send(ws: WebSocket, message: str):
"""Send text to one WebSocket, remove on failure."""
try:
await ws.send_text(message)
except Exception:
async with glock:
connections.pop(ws, None)
async def _broadcast_members():
"""Broadcast JSON member list and count to all clients."""
async with glock:
names = list(connections.values())
count = len(names)
payload = json.dumps({"type": "members", "data": names})
await _broadcast(f"MEMBERS::{payload}")
await _broadcast(f"COUNT::{count}")
# ── WebSocket Endpoint ──────────────────────────────────────
@app.websocket("/wschat")
async def ws_endpoint(ws: WebSocket):
await ws.accept()
# ── 1) Receive username as first text frame ──
try:
raw = await asyncio.wait_for(ws.receive_text(), timeout=30)
except Exception:
await ws.close(1008)
return
username = (raw.strip() or f"匿名{id(ws) % 10000}")[:20]
async with glock:
connections[ws] = username
join_msg = f"SYSTEM::{username} 加入聊天室"
log_to_file(join_msg)
asyncio.create_task(_broadcast(join_msg))
asyncio.create_task(_broadcast_members())
# ── 2) Message loop ──
try:
while True:
text = await ws.receive_text()
if not text or not text.strip():
continue
# Rate limit check
if not await _check_msg_rate(ws):
asyncio.create_task(_safe_send(ws, "SYSTEM::⚠️ 发送太频繁,请稍后再试"))
continue
msg = f"TEXT::{username}::{text.strip()}"
log_to_file(msg)
asyncio.create_task(_broadcast(msg))
except (WebSocketDisconnect, Exception):
pass
finally:
async with glock:
name = connections.pop(ws, None) or "匿名用户"
leave_msg = f"SYSTEM::{name} 离开聊天室"
log_to_file(leave_msg)
asyncio.create_task(_broadcast(leave_msg))
asyncio.create_task(_broadcast_members())
# ── Nightly Cleanup ─────────────────────────────────────────
@app.post("/api/nightly-cleanup")
async def nightly_cleanup():
"""Backup chat history to backups/ dir, then clear the log.
Also purge uploaded files older than 24h.
Called by cron / systemd timer.
"""
now = datetime.now()
backup_name = f"chat_history_{now.strftime('%Y%m%d_%H%M%S')}.txt"
backup_path = os.path.join(BACKUP_DIR, backup_name)
if os.path.exists(CHAT_LOG_FILE) and os.path.getsize(CHAT_LOG_FILE) > 0:
shutil.copy2(CHAT_LOG_FILE, backup_path)
# Clear log
open(CHAT_LOG_FILE, "w", encoding="utf-8").close()
# Clean old uploads (> 24h)
now_ts = time.time()
cleaned = 0
for fname in os.listdir(UPLOAD_DIR):
fpath = os.path.join(UPLOAD_DIR, fname)
if os.path.isfile(fpath) and now_ts - os.path.getmtime(fpath) > 86400:
try:
os.remove(fpath)
cleaned += 1
except Exception:
pass
return {"status": "ok", "backup": backup_name, "cleaned_uploads": cleaned}
# ── Main ────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run("chat:app", host="0.0.0.0", port=8202, reload=False, log_level="info")

@ -0,0 +1,76 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
import uvicorn
app = FastAPI()
# 挂载模板目录
templates = Jinja2Templates(directory="templates")
# 活跃连接列表
active_connections = []
# WebSocket 与昵称映射表
usernames = {}
# 首页路由
@app.get("/")
async def get(request: Request):
return templates.TemplateResponse("chat.html", {"request": request})
# WebSocket 路由
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
active_connections.append(websocket)
await broadcast_online_count() # 一连上就广播在线人数
try:
# 接收第一个消息作为昵称
username = await websocket.receive_text()
usernames[websocket] = username
await broadcast(f"[系统]{username} 加入了聊天室")
await broadcast_online_count()
while True:
data = await websocket.receive_text()
await broadcast(f"{username}{data}")
except WebSocketDisconnect:
active_connections.remove(websocket)
left_username = usernames.pop(websocket, "匿名用户")
await broadcast(f"[系统]{left_username} 离开了聊天室")
await broadcast_online_count()
# 广播消息给所有连接
async def broadcast(message: str):
to_remove = []
for connection in active_connections:
try:
await connection.send_text(message)
except:
to_remove.append(connection)
for conn in to_remove:
active_connections.remove(conn)
usernames.pop(conn, None)
# 广播在线人数
async def broadcast_online_count():
message = f"[人数]{len(active_connections)}"
to_remove = []
for conn in active_connections:
try:
await conn.send_text(message)
except:
to_remove.append(conn)
for conn in to_remove:
active_connections.remove(conn)
usernames.pop(conn, None)
if __name__ == "__main__":
uvicorn.run("chat:app", host="0.0.0.0", port=8202)

@ -0,0 +1,160 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import os
import shutil
import uuid
import uvicorn
app = FastAPI()
# =========================
# 基础配置
# =========================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
ALLOWED_VIDEO_EXTENSIONS = {"mp4", "webm"}
# =========================
# 目录初始化
# =========================
os.makedirs(UPLOAD_DIR, exist_ok=True)
templates = Jinja2Templates(directory=TEMPLATE_DIR)
app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
# =========================
# WebSocket 状态
# =========================
active_connections: list[WebSocket] = []
usernames: dict[WebSocket, str] = {}
# =========================
# 工具函数
# =========================
def get_ext(filename: str) -> str:
return filename.rsplit(".", 1)[-1].lower()
def is_image(ext: str) -> bool:
return ext in ALLOWED_IMAGE_EXTENSIONS
def is_video(ext: str) -> bool:
return ext in ALLOWED_VIDEO_EXTENSIONS
# =========================
# 页面路由
# =========================
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("chat.html", {"request": request})
# =========================
# 文件上传接口
# =========================
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
ext = get_ext(file.filename)
if not (is_image(ext) or is_video(ext)):
return {"error": "不支持的文件类型"}
contents = await file.read()
if len(contents) > MAX_FILE_SIZE:
return {"error": "文件过大(最大 10MB"}
filename = f"{uuid.uuid4().hex}.{ext}"
save_path = os.path.join(UPLOAD_DIR, filename)
with open(save_path, "wb") as f:
f.write(contents)
file_url = f"/uploads/{filename}"
if is_image(ext):
msg = f"IMG::{file_url}"
elif is_video(ext):
msg = f"VIDEO::{file_url}"
else:
msg = f"FILE::{file_url}"
await broadcast(msg)
return {"url": file_url}
# =========================
# WebSocket 聊天
# =========================
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
active_connections.append(ws)
await broadcast_online_count()
try:
# 首条消息作为用户名
username = await ws.receive_text()
usernames[ws] = username
await broadcast(f"SYSTEM::{username} 加入聊天室")
await broadcast_online_count()
while True:
text = await ws.receive_text()
await broadcast(f"TEXT::{username}::{text}")
except WebSocketDisconnect:
pass
finally:
if ws in active_connections:
active_connections.remove(ws)
name = usernames.pop(ws, "匿名用户")
await broadcast(f"SYSTEM::{name} 离开聊天室")
await broadcast_online_count()
# =========================
# 广播工具
# =========================
async def broadcast(message: str):
dead = []
for ws in active_connections:
try:
await ws.send_text(message)
except Exception:
dead.append(ws)
for ws in dead:
active_connections.remove(ws)
usernames.pop(ws, None)
async def broadcast_online_count():
msg = f"COUNT::{len(active_connections)}"
await broadcast(msg)
# =========================
# 启动
# =========================
if __name__ == "__main__":
uvicorn.run(
"chat:app",
host="0.0.0.0",
port=8202,
reload=True
)

@ -0,0 +1,21 @@
[2026-05-23 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260523_030001.txt","cleaned_uploads":0}
[2026-05-23 03:00:01] Cleanup completed.
[2026-05-24 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260524_030001.txt","cleaned_uploads":7}
[2026-05-24 03:00:01] Cleanup completed.
[2026-05-25 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260525_030001.txt","cleaned_uploads":0}
[2026-05-25 03:00:01] Cleanup completed.
[2026-05-26 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260526_030001.txt","cleaned_uploads":0}
[2026-05-26 03:00:01] Cleanup completed.
[2026-05-27 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260527_030001.txt","cleaned_uploads":0}
[2026-05-27 03:00:01] Cleanup completed.
[2026-05-28 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260528_030001.txt","cleaned_uploads":0}
[2026-05-28 03:00:01] Cleanup completed.
[2026-05-29 03:00:01] Starting nightly cleanup...
{"status":"ok","backup":"chat_history_20260529_030001.txt","cleaned_uploads":0}
[2026-05-29 03:00:01] Cleanup completed.

@ -0,0 +1,15 @@
#!/bin/bash
# Nightly cleanup for ChatHub
# Backups chat history, clears log, removes old uploads
# Called by cron at 03:00 daily
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/nightly_cleanup.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting nightly cleanup..." >> "$LOG_FILE"
# Call the API endpoint
curl -s -X POST http://localhost:8202/api/nightly-cleanup >> "$LOG_FILE" 2>&1
echo "" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Cleanup completed." >> "$LOG_FILE"

@ -0,0 +1,3 @@
fastapi
uvicorn
jinja2

@ -0,0 +1,821 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ChatHub</title>
<style>
/* ── Reset & Base ─────────────────────────────── */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
font-size: 15px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ── Theme Variables ──────────────────────────── */
:root {
/* Dark (Telegram Dark) — default */
--bg-app: #0e1621;
--sidebar-bg: #17212b;
--sidebar-hover: #202b36;
--sidebar-header-bg: #17212b;
--sidebar-header-border: rgba(255,255,255,0.04);
--chat-bg: #0e1621;
--header-bg: #17212b;
--header-border: rgba(255,255,255,0.04);
--bubble-self: #2b5278;
--bubble-self-text: #e8e8e8;
--bubble-other: #182533;
--bubble-other-text: #e8e8e8;
--input-bg: #17212b;
--input-border: rgba(255,255,255,0.04);
--input-focus-border: #5f9cea;
--input-inner: #242f3d;
--text-primary: #e8e8e8;
--text-secondary: #858e99;
--text-muted: #5f6773;
--accent: #5f9cea;
--accent-hover: #7bafed;
--online-green: #4fae4f;
--red: #e9575f;
--shadow: none;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--sidebar-width: 300px;
--header-height: 48px;
--footer-height: 52px;
--transition: 0.15s ease;
}
/* Light theme */
[data-theme="light"] {
--bg-app: #e6ebee;
--sidebar-bg: #fff;
--sidebar-hover: #f2f6fa;
--sidebar-header-bg: #5682a3;
--sidebar-header-border: #d9dee1;
--chat-bg: #e6ebee;
--header-bg: #fff;
--header-border: #d9dee1;
--bubble-self: #effdde;
--bubble-self-text: #000;
--bubble-other: #fff;
--bubble-other-text: #000;
--input-bg: #fff;
--input-border: #d9dee1;
--input-focus-border: #5b9bd5;
--input-inner: #f2f6fa;
--text-primary: #222;
--text-secondary: #888;
--text-muted: #aaa;
--accent: #5b9bd5;
--accent-hover: #4a8bc5;
--online-green: #3dd68c;
--red: #f0566a;
--shadow: 0 1px 2px rgba(0,0,0,0.04);
}
body {
background: var(--bg-app);
color: var(--text-primary);
}
/* ── App Layout ───────────────────────────────── */
#app {
display: flex;
height: 100vh;
width: 100%;
overflow: hidden;
background: var(--bg-app);
}
/* ═══════════════ Sidebar ═══════════════ */
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
border-right: 1px solid var(--sidebar-header-border);
}
/* Sidebar header — height aligned with chat header */
.sidebar-header {
height: var(--header-height);
min-height: var(--header-height);
background: var(--sidebar-header-bg);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid var(--sidebar-header-border);
flex-shrink: 0;
}
.sidebar-title {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 6px;
}
.sidebar-title-icon {
font-size: 18px;
}
.sidebar-badge {
font-size: 11px;
color: var(--text-secondary);
background: rgba(255,255,255,0.06);
padding: 2px 10px;
border-radius: 10px;
}
.sidebar-search {
padding: 8px 12px;
flex-shrink: 0;
border-bottom: 1px solid var(--sidebar-header-border);
}
.search-input {
width: 100%;
background: var(--input-inner);
border: none;
border-radius: var(--radius-sm);
padding: 7px 12px;
font-size: 13px;
color: var(--text-primary);
outline: none;
pointer-events: none;
}
.search-input::placeholder { color: var(--text-muted); }
.sidebar-section {
font-size: 11px;
text-transform: uppercase;
color: var(--text-secondary);
padding: 10px 16px 4px;
letter-spacing: 0.6px;
font-weight: 600;
flex-shrink: 0;
}
.member-list {
flex: 1;
overflow-y: auto;
}
.member-list::-webkit-scrollbar { width: 4px; }
.member-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 4px; }
[data-theme="light"] .member-list::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.08); }
.member-item {
display: flex;
align-items: center;
gap: 11px;
padding: 8px 16px;
cursor: pointer;
transition: background var(--transition);
}
.member-item:hover { background: var(--sidebar-hover); }
.member-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 600;
color: #fff;
position: relative;
}
.member-avatar-dot {
position: absolute;
bottom: 1px;
right: -1px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--online-green);
border: 2px solid var(--sidebar-bg);
}
.member-info { flex: 1; min-width: 0; }
.member-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-sub {
font-size: 11px;
color: var(--text-secondary);
margin-top: 1px;
}
.member-tag {
font-size: 11px;
color: var(--accent);
background: rgba(95,156,234,0.12);
padding: 1px 8px;
border-radius: 8px;
font-weight: 500;
}
[data-theme="light"] .member-tag { background: #e8f0f7; }
.sidebar-empty {
padding: 30px 16px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/* Sidebar footer — height aligned with input area */
.sidebar-footer {
height: var(--footer-height);
min-height: var(--footer-height);
display: flex;
align-items: center;
gap: 6px;
padding: 0 16px;
border-top: 1px solid var(--sidebar-header-border);
font-size: 12px;
color: var(--text-secondary);
flex-shrink: 0;
}
.footer-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.footer-dot.online { background: var(--online-green); }
.footer-dot.offline { background: var(--red); }
.footer-dot.connecting { background: #f59e0b; animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* ═══════════════ Main (Chat) ═══════════════ */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: var(--chat-bg);
position: relative;
}
/* Chat header — height aligned with sidebar header */
.chat-header {
height: var(--header-height);
min-height: var(--header-height);
background: var(--header-bg);
border-bottom: 1px solid var(--header-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
flex-shrink: 0;
}
.chat-header-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.chat-header-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--accent);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.chat-header-info { line-height: 1.2; min-width: 0; }
.chat-header-name {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-header-status {
font-size: 11px;
color: var(--text-secondary);
}
.chat-header-right {
display: flex;
gap: 6px;
align-items: center;
flex-shrink: 0;
}
.header-btn {
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--text-secondary);
font-size: 17px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background var(--transition);
}
.header-btn:hover {
background: var(--sidebar-hover);
color: var(--text-primary);
}
.header-btn:active { transform: scale(0.9); }
/* ── Messages ── */
#chatLog {
flex: 1;
overflow-y: auto;
padding: 12px 16px 4px;
display: flex;
flex-direction: column;
gap: 3px;
}
#chatLog::-webkit-scrollbar { width: 5px; }
#chatLog::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 5px; }
[data-theme="light"] #chatLog::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.06); }
#emptyTip {
text-align: center;
color: var(--text-muted);
padding: 80px 0;
user-select: none;
}
#emptyTip .empty-icon { font-size: 34px; margin-bottom: 8px; opacity: 0.25; }
#emptyTip .empty-t1 { font-size: 14px; }
#emptyTip .empty-t2 { font-size: 12px; margin-top: 4px; opacity: 0.6; }
/* ── Bubbles ── */
.msg-item {
display: flex;
flex-direction: column;
max-width: 80%;
animation: inMsg 0.15s ease;
}
@keyframes inMsg { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
.msg-item.self { align-self: flex-start; align-items: flex-start; }
.msg-item.other { align-self: flex-start; align-items: flex-start; }
.msg-item.system { align-self: center; align-items: center; max-width: 94%; }
.msg-row {
display: flex;
align-items: flex-end;
gap: 6px;
max-width: 100%;
}
.msg-item.self .msg-row,
.msg-item.other .msg-row { flex-direction: row; }
.msg-avatar {
width: 26px;
height: 26px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
color: #fff;
opacity: 0.9;
}
.msg-body { min-width: 0; }
.msg-sender {
font-size: 11px;
font-weight: 600;
color: var(--accent);
margin-bottom: 1px;
padding-left: 2px;
}
.msg-item.self .msg-sender { display: block; }
.msg-bubble {
padding: 6px 12px;
border-radius: var(--radius-lg);
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
box-shadow: var(--shadow);
}
.msg-item.self .msg-bubble {
background: var(--bubble-other);
color: var(--bubble-other-text);
border-bottom-left-radius: 3px;
border-left: 3px solid var(--accent);
}
.msg-item.other .msg-bubble {
background: var(--bubble-other);
color: var(--bubble-other-text);
border-bottom-left-radius: 3px;
}
.msg-item.system .msg-bubble {
background: transparent;
color: var(--text-muted);
font-size: 12px;
text-align: center;
padding: 1px 6px;
box-shadow: none;
}
.msg-time {
font-size: 10px;
color: var(--text-muted);
margin-top: 1px;
padding: 0 4px;
}
.msg-item.self .msg-time,
.msg-item.other .msg-time { text-align: left; }
/* Media */
.msg-media {
max-width: 200px;
max-height: 230px;
border-radius: var(--radius-md);
cursor: pointer;
display: block;
transition: opacity 0.15s;
margin: -1px 0;
}
.msg-media:hover { opacity: 0.92; }
video.msg-media { cursor: default; }
video.msg-media:hover { opacity: 1; }
.preview-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.9);
z-index: 9999;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
animation: ovIn 0.15s ease;
}
@keyframes ovIn { from{opacity:0} to{opacity:1} }
.preview-overlay img, .preview-overlay video {
max-width: 92vw; max-height: 92vh;
object-fit: contain;
border-radius: var(--radius-md);
}
/* ── Input Area — height aligned with sidebar footer ── */
.input-area {
height: var(--footer-height);
min-height: var(--footer-height);
background: var(--input-bg);
border-top: 1px solid var(--header-border);
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
flex-shrink: 0;
}
.input-wrap {
flex: 1;
display: flex;
align-items: center;
background: var(--input-inner);
border: 1px solid transparent;
border-radius: var(--radius-lg);
padding: 0 2px 0 0;
transition: border-color var(--transition), background var(--transition);
height: 36px;
}
.input-wrap:focus-within {
border-color: var(--input-focus-border);
background: var(--input-bg);
}
#messageInput {
flex: 1;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 14px;
padding: 0 12px;
outline: none;
font-family: inherit;
line-height: 36px;
height: 36px;
overflow: hidden;
resize: none;
}
#messageInput::placeholder { color: var(--text-muted); }
.btn {
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 17px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: var(--text-secondary);
transition: all var(--transition);
flex-shrink: 0;
}
.btn:hover { background: var(--sidebar-hover); color: var(--text-primary); }
.btn:active { transform: scale(0.88); }
.btn-send {
background: var(--accent);
color: #fff;
font-size: 16px;
}
.btn-send:hover { background: var(--accent-hover); color: #fff; }
.btn-send:active { transform: scale(0.88); }
#fileInput { display: none; }
.upload-progress {
height: 2px;
background: transparent;
display: none;
flex-shrink: 0;
}
.upload-progress.active { display: block; }
.upload-progress-bar {
height: 100%;
width: 0%;
background: var(--accent);
transition: width 0.3s;
}
/* ── Responsive ── */
@media (max-width: 700px) {
.sidebar { display: none; }
.msg-item { max-width: 92%; }
.msg-media { max-width: 140px; max-height: 170px; }
#chatLog { padding: 8px 8px 2px; }
}
</style>
</head>
<body>
<div id="app">
<!-- ═══ Sidebar ═══ -->
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<span class="sidebar-title-icon">💬</span>ChatHub
</div>
<span class="sidebar-badge" id="sidebarBadge">0</span>
</div>
<div class="sidebar-search">
<input class="search-input" placeholder="搜索在线成员..." readonly>
</div>
<div class="sidebar-section">在线成员</div>
<div class="member-list" id="memberList">
<div class="sidebar-empty" id="memberEmpty">🫧 暂无在线成员</div>
</div>
<div class="sidebar-footer">
<span class="footer-dot offline" id="footerDot"></span>
<span id="footerText">连接中...</span>
</div>
</div>
<!-- ═══ Main ═══ -->
<div class="main">
<!-- Chat Header -->
<div class="chat-header">
<div class="chat-header-left">
<div class="chat-header-avatar">💬</div>
<div class="chat-header-info">
<div class="chat-header-name">无痕聊天室</div>
<div class="chat-header-status" id="headerStatus">🟡 连接中...</div>
</div>
</div>
<div class="chat-header-right">
<span class="header-btn" id="themeToggle" title="切换主题">🌙</span>
</div>
</div>
<!-- Messages -->
<div id="chatLog">
<div id="emptyTip">
<div class="empty-icon">🫧</div>
<div class="empty-t1">聊天记录为空</div>
<div class="empty-t2">发出一条消息开始聊天吧</div>
</div>
</div>
<!-- Upload Progress -->
<div class="upload-progress" id="uploadProgress">
<div class="upload-progress-bar" id="uploadProgressBar"></div>
</div>
<!-- Input -->
<div class="input-area">
<input type="file" id="fileInput" accept="image/*,video/*">
<div class="input-wrap">
<textarea id="messageInput" placeholder="输入消息..." rows="1" maxlength="2000"></textarea>
</div>
<button class="btn" id="uploadBtn" title="发送图片/视频">📎</button>
<button class="btn btn-send" id="sendBtn"></button>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ── Username ────────────────────────────────────────
var username = localStorage.getItem('chat_username');
if (!username) {
username = prompt('请输入你的昵称:') || ('匿名' + Math.floor(Math.random() * 9999));
username = username.trim().slice(0, 20);
localStorage.setItem('chat_username', username);
}
// ── Theme ───────────────────────────────────────────
var themeToggle = document.getElementById('themeToggle');
var currentTheme = localStorage.getItem('chat_theme') || 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
themeToggle.textContent = currentTheme === 'dark' ? '🌙' : '☀️';
themeToggle.onclick = function() {
var next = currentTheme === 'dark' ? 'light' : 'dark';
currentTheme = next;
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('chat_theme', next);
themeToggle.textContent = next === 'dark' ? '🌙' : '☀️';
};
// ── DOM ─────────────────────────────────────────────
var chatLog = document.getElementById('chatLog');
var emptyTip = document.getElementById('emptyTip');
var memberList = document.getElementById('memberList');
var emptyMsg = document.getElementById('memberEmpty');
var sidebarBadge = document.getElementById('sidebarBadge');
var footerTxt = document.getElementById('footerText');
var footerDot = document.getElementById('footerDot');
var headerSt = document.getElementById('headerStatus');
var msgInput = document.getElementById('messageInput');
var sendBtn = document.getElementById('sendBtn');
var upBtn = document.getElementById('uploadBtn');
var fileInp = document.getElementById('fileInput');
var progEl = document.getElementById('uploadProgress');
var progBar = document.getElementById('uploadProgressBar');
// ── Helpers ─────────────────────────────────────────
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function setStatus(state) {
if (state === 'connected') {
headerSt.textContent = '🟢 已连接';
footerDot.className = 'footer-dot online';
footerTxt.textContent = '已连接';
} else if (state === 'connecting') {
headerSt.textContent = '🟡 连接中...';
footerDot.className = 'footer-dot connecting';
footerTxt.textContent = '连接中...';
} else {
headerSt.textContent = '🔴 已断开';
footerDot.className = 'footer-dot offline';
footerTxt.textContent = '已断开,重连中...';
}
}
// ── Append message ─────────────────────────────────
function appendMsg(cls, sender, html, ts) {
if (emptyTip && emptyTip.parentNode) emptyTip.style.display = 'none';
var d = document.createElement('div');
d.className = 'msg-item ' + cls;
var t = ts ? '<div class="msg-time">' + esc(ts) + '</div>' : '';
if (cls === 'system') {
d.innerHTML = '<div class="msg-bubble">' + html + '</div>';
} else {
var nameTag = cls === 'self' ? '<span class="member-tag" style="font-size:10px;margin-left:4px;vertical-align:middle;"></span>' : '';
d.innerHTML =
'<div class="msg-row">' +
'<div class="msg-avatar" style="background:' + avColor(sender) + '">' + avL(sender) + '</div>' +
'<div class="msg-body">' +
'<div class="msg-sender">' + esc(sender) + nameTag + '</div>' +
'<div class="msg-bubble">' + html + '</div>' + t +
'</div>' +
'</div>';
}
chatLog.appendChild(d);
chatLog.scrollTop = chatLog.scrollHeight;
}
function avL(n) { return (n&&n[0])?n[0].toUpperCase():'?'; }
var _colors = ['#5f9cea','#e17076','#7bc862','#e5c441','#65aadd','#a695e7','#ee7a6f','#6bc9b6','#f0906f','#b0a0e0'];
function avColor(n) {
var h=0; for(var i=0;i<n.length;i++) h=((h<<5)-h)+n.charCodeAt(i)|0;
return _colors[Math.abs(h)%_colors.length];
}
// ── WebSocket ───────────────────────────────────────
var ws = null, rt = null, wsc = false;
function connect() {
if(ws&&(ws.readyState===1||ws.readyState===0)) return;
setStatus('connecting');
var p = location.protocol==='https:'?'wss':'ws';
ws = new WebSocket(p+'://'+location.host+'/wschat');
ws.onopen = function(){ ws.send(username); setStatus('connected'); wsc=true; refresh(); };
ws.onmessage = function(e){ handle(e.data); };
ws.onclose = function(){ wsc=false; setStatus('offline'); if(rt)clearTimeout(rt); rt=setTimeout(connect,2000); };
}
function handle(msg) {
if(msg.indexOf('COUNT::')===0) {
var c = parseInt(msg.substring(6),10)||0;
sidebarBadge.textContent = c;
return;
}
if(msg.indexOf('MEMBERS::')===0) {
try { var p=JSON.parse(msg.substring(9)); if(p.type==='members') render(p.data||[]); } catch(e){}
return;
}
var c1=msg.indexOf('::'); if(c1<0)return;
var tp=msg.substring(0,c1), bd=msg.substring(c1+2);
if(tp==='SYSTEM') { appendMsg('system','',esc(bd),''); refresh(); }
else if(tp==='TEXT') { var c2=bd.indexOf('::'); if(c2<0)return; appendMsg(bd.substring(0,c2)===username?'self':'other',bd.substring(0,c2),esc(bd.substring(c2+2)),''); }
else if(tp==='IMG') { var c2=bd.indexOf('::'); if(c2<0)return; appendMsg(bd.substring(0,c2)===username?'self':'other',bd.substring(0,c2),'<img class="msg-media" src="'+bd.substring(c2+2)+'" onclick="previewMedia(this.src)" loading="lazy">',''); }
else if(tp==='VIDEO') { var c2=bd.indexOf('::'); if(c2<0)return; appendMsg(bd.substring(0,c2)===username?'self':'other',bd.substring(0,c2),'<video class="msg-media" src="'+bd.substring(c2+2)+'" controls preload="metadata"></video>',''); }
}
// ── Member list ─────────────────────────────────────
function render(members) {
memberList.innerHTML = '';
if(!members||!members.length) {
memberList.innerHTML = '<div class="sidebar-empty">🫧 暂无在线成员</div>';
sidebarBadge.textContent = '0';
return;
}
sidebarBadge.textContent = members.length;
var s = members.slice().sort(function(a,b){ if(a===username)return-1;if(b===username)return1;return a.localeCompare(b); });
for(var i=0;i<s.length;i++) {
var n=s[i], me=n===username;
var d=document.createElement('div'); d.className='member-item';
d.innerHTML =
'<div class="member-avatar" style="background:'+avColor(n)+'">'+
avL(n)+'<span class="member-avatar-dot"></span></div>'+
'<div class="member-info">'+
'<div class="member-name">'+esc(n)+'</div>'+
'<div class="member-sub">'+(me?'在线 (我)':'在线')+'</div></div>'+
(me?'<span class="member-tag"></span>':'');
memberList.appendChild(d);
}
}
var f = false;
function refresh(){ if(f)return; f=true; fetch('/members').then(function(r){return r.ok?r.json():Promise.reject();}).then(function(d){render(d.members||[]);}).catch(function(){}).finally(function(){f=false;}); }
setInterval(function(){if(wsc)refresh();},5000);
// ── Send ────────────────────────────────────────────
function send(t){ if(!t||!t.trim())return false; if(!ws||ws.readyState!==1){appendMsg('system','','⚠️ 连接已断开','');return false;} ws.send(t.trim()); return true; }
sendBtn.onclick = function(){ var v=msgInput.value; if(send(v)){msgInput.value='';msgInput.style.height='36px';} };
msgInput.addEventListener('keydown',function(e){
if(e.key==='Enter'&&!e.shiftKey){ e.preventDefault(); var v=msgInput.value; if(send(v)){msgInput.value='';msgInput.style.height='36px';} }
});
// ── Upload ──────────────────────────────────────────
upBtn.onclick = function(){ fileInp.click(); };
fileInp.addEventListener('change',function(){
var f=this.files[0]; if(!f)return;
if(f.size>10*1024*1024){ appendMsg('system','','⚠️ 文件过大('+(f.size/1024/1024).toFixed(1)+'MB最大 10MB',''); fileInp.value=''; return; }
var fd=new FormData(); fd.append('file',f); fd.append('sender',username);
progEl.classList.add('active'); progBar.style.width='25%';
fetch('/upload',{method:'POST',body:fd})
.then(function(r){ if(!r.ok)return r.json().then(function(d){throw new Error(d.error||'上传失败');}); return r.json(); })
.then(function(){ progBar.style.width='100%'; })
.catch(function(e){ progBar.style.width='100%'; appendMsg('system','','⚠️ '+esc(e.message),''); })
.finally(function(){ setTimeout(function(){progEl.classList.remove('active');progBar.style.width='0%';},600); fileInp.value=''; });
});
// ── Preview ─────────────────────────────────────────
window.previewMedia = function(s){
var o=document.createElement('div'); o.className='preview-overlay';
var e=s.match(/\.(png|jpg|jpeg|gif|webp|bmp)(\?|$)/i)?(function(){var i=new Image();i.src=s;return i;})():(function(){var v=document.createElement('video');v.src=s;v.controls=true;v.autoplay=true;return v;})();
o.appendChild(e); o.onclick=function(){document.body.removeChild(o);}; document.body.appendChild(o);
};
// ── Init ────────────────────────────────────────────
connect();
refresh();
})();
</script>
</body>
</html>

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html>
<head>
<title>纵有千古、横有八荒</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
}
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
}
#chatLog {
flex: 1;
padding: 16px;
overflow-y: auto;
background: #f9f9fb;
display: flex;
flex-direction: column;
gap: 8px;
}
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.self {
align-self: flex-end;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border-bottom-right-radius: 4px;
}
.other {
align-self: flex-start;
background: #e5e5ea;
color: #333;
border-bottom-left-radius: 4px;
}
#input-container {
padding: 12px;
background-color: #f4f6f8;
display: flex;
gap: 10px;
border-top: 1px solid #e0e0e0;
align-items: flex-end;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
font-size: 14px;
resize: none;
min-height: 42px;
max-height: 150px;
background: #fff;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
#messageInput:focus {
border-color: #999;
outline: none;
}
#sendButton {
padding: 10px 18px;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#sendButton:hover {
background: linear-gradient(135deg, #66b1ff, #85c1ff);
}
/* 手机适配 */
@media screen and (max-width: 600px) {
#chat-container {
max-width: 100%;
border-radius: 0;
}
.message {
font-size: 15px;
padding: 10px 14px;
}
#messageInput {
font-size: 16px;
padding: 10px 14px;
}
#sendButton {
padding: 10px 14px;
font-size: 15px;
}
#input-container {
padding: 10px;
gap: 8px;
}
}
</style>
</head>
<body>
<div id="chat-container">
<div id="chatLog"></div>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..." rows="1"></textarea>
<button id="sendButton" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:");
if (!username) {
username = "匿名用户";
}
localStorage.setItem("chat_username", username);
}
let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
let ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
ws.onmessage = function(event) {
const chatLog = document.getElementById("chatLog");
const messageDiv = document.createElement("div");
// 判断是否是自己发的
if (event.data.startsWith(username + "")) {
messageDiv.className = "message self";
messageDiv.textContent = event.data.replace(username + "", "我:");
} else {
messageDiv.className = "message other";
messageDiv.textContent = event.data;
}
chatLog.appendChild(messageDiv);
chatLog.scrollTop = chatLog.scrollHeight;
};
function sendMessage() {
const input = document.getElementById("messageInput");
if (input.value.trim() !== "") {
const message = username + "" + input.value.trim();
ws.send(message);
input.value = "";
adjustInputHeight();
}
}
document.getElementById("messageInput").addEventListener("keydown", function(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
document.getElementById("messageInput").addEventListener("input", adjustInputHeight);
function adjustInputHeight() {
const input = document.getElementById("messageInput");
input.style.height = "auto";
input.style.height = (input.scrollHeight) + "px";
}
</script>
</body>
</html>

@ -0,0 +1,354 @@
<!DOCTYPE html>
<head>
<title>无痕聊天室</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
transition: background 0.3s, color 0.3s;
}
body.dark {
background: #1e1e1e;
color: #ccc;
}
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
position: relative;
}
body.dark #chat-container {
background: #2c2c2ccc;
}
#chatLog {
flex: 1;
padding: 16px;
overflow-y: auto;
background: #f9f9fb;
display: flex;
flex-direction: column;
gap: 8px;
}
body.dark #chatLog {
background: #2a2a2a;
}
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
display: flex;
align-items: center;
justify-content: space-between;
}
.self {
align-self: flex-end;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border-bottom-right-radius: 4px;
}
.other {
align-self: flex-start;
background: #e5e5ea;
color: #333;
border-bottom-left-radius: 4px;
}
.system {
align-self: center;
background: transparent;
color: #999;
font-size: 13px;
padding: 4px 8px;
border-radius: 8px;
}
#input-container {
padding: 12px;
background-color: #f4f6f8;
display: flex;
gap: 10px;
border-top: 1px solid #e0e0e0;
align-items: flex-end;
}
body.dark #input-container {
background-color: #333;
border-color: #444;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
font-size: 14px;
resize: none;
min-height: 42px;
max-height: 150px;
background: #fff;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
body.dark #messageInput {
background: #444;
color: #ccc;
border: 1px solid #666;
}
#sendButton {
padding: 10px 18px;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#sendButton:hover {
background: linear-gradient(135deg, #66b1ff, #85c1ff);
}
#onlineCount {
position: absolute;
top: 8px;
right: 12px;
font-size: 13px;
color: #666;
background: #eef;
padding: 4px 8px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
body.dark #onlineCount {
background: #444;
color: #ccc;
}
#toggleDark {
position: absolute;
top: 8px;
left: 12px;
font-size: 13px;
background: #eef;
padding: 4px 8px;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
body.dark #toggleDark {
background: #444;
color: #ccc;
}
.copy-btn {
margin-left: 8px;
background: none;
border: none;
cursor: pointer;
font-size: 13px;
color: #999;
}
.copy-btn:hover {
color: #007aff;
}
@media screen and (max-width: 600px) {
#chat-container {
max-width: 100%;
border-radius: 0;
}
}
/* 新增状态提示样式 */
#status {
position: absolute;
top: 40px;
left: 12px;
font-size: 12px;
color: #999;
}
body.dark #status {
color: #ccc;
}
</style>
</head>
<body>
<div id="chat-container">
<div id="toggleDark" onclick="toggleDarkMode()">🌙 夜间</div>
<div id="onlineCount">在线人数0</div>
<div id="status">🟢 已连接</div>
<div id="chatLog"></div>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..." rows="1"></textarea>
<button id="sendButton" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:");
if (!username) {
username = "匿名用户";
}
localStorage.setItem("chat_username", username);
}
let ws;
let reconnectInterval = 2000;
let reconnectAttempts = 0;
function connectWebSocket() {
let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
ws.onopen = function () {
ws.send(username);
updateStatus("🟢 已连接");
document.getElementById("sendButton").disabled = false;
reconnectAttempts = 0;
};
ws.onmessage = function (event) {
const chatLog = document.getElementById("chatLog");
const msg = event.data;
if (msg.startsWith("[人数]")) {
const count = msg.replace("[人数]", "");
document.getElementById("onlineCount").textContent = "在线人数:" + count;
return;
}
const messageDiv = document.createElement("div");
const contentSpan = document.createElement("span");
let contentText = "";
if (msg.startsWith("[系统]")) {
messageDiv.className = "message system";
contentText = msg;
contentSpan.textContent = contentText;
} else {
const splitIndex = msg.indexOf("");
const sender = msg.substring(0, splitIndex);
const content = msg.substring(splitIndex + 1);
const nameSpan = document.createElement("span");
nameSpan.style.fontWeight = "bold";
nameSpan.style.marginRight = "6px";
if (sender === username) {
messageDiv.className = "message self";
nameSpan.textContent = "我:";
} else {
messageDiv.className = "message other";
nameSpan.textContent = sender + "";
}
messageDiv.appendChild(nameSpan);
contentText = content;
contentSpan.textContent = contentText;
const copyBtn = document.createElement("button");
copyBtn.textContent = "📋";
copyBtn.className = "copy-btn";
copyBtn.onclick = () => {
navigator.clipboard.writeText(contentText).then(() => {
copyBtn.textContent = "✅";
setTimeout(() => copyBtn.textContent = "📋", 1000);
});
};
messageDiv.appendChild(contentSpan);
messageDiv.appendChild(copyBtn);
}
if (msg.startsWith("[系统]")) {
messageDiv.appendChild(contentSpan);
}
chatLog.appendChild(messageDiv);
chatLog.scrollTop = chatLog.scrollHeight;
};
ws.onclose = function () {
updateStatus(`🔴 断开,尝试重连(${++reconnectAttempts})...`);
document.getElementById("sendButton").disabled = true;
setTimeout(connectWebSocket, reconnectInterval);
};
ws.onerror = function () {
ws.close();
};
}
connectWebSocket();
function sendMessage() {
const input = document.getElementById("messageInput");
if (input.value.trim() !== "" && ws.readyState === WebSocket.OPEN) {
ws.send(input.value.trim());
input.value = "";
adjustInputHeight();
}
}
document.getElementById("messageInput").addEventListener("keydown", function (event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
document.getElementById("messageInput").addEventListener("input", adjustInputHeight);
function adjustInputHeight() {
const input = document.getElementById("messageInput");
input.style.height = "auto";
input.style.height = (input.scrollHeight) + "px";
}
function toggleDarkMode() {
document.body.classList.toggle("dark");
const btn = document.getElementById("toggleDark");
if (document.body.classList.contains("dark")) {
btn.textContent = "☀️ 日间";
} else {
btn.textContent = "🌙 夜间";
}
}
function updateStatus(text) {
document.getElementById("status").textContent = text;
}
</script>
</body>
</html>

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<title>纵有千古、横有八荒</title>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
}
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
}
#chatLog {
flex: 1;
padding: 16px;
border: none;
resize: none;
background: #f9f9fb;
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
color: #333;
border-bottom: 1px solid #e0e0e0;
}
#chatLog:focus {
outline: none;
}
#input-container {
padding: 14px;
background-color: #f4f6f8;
display: flex;
gap: 10px;
border-top: 1px solid #e0e0e0;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
font-size: 14px;
resize: none;
min-height: 42px;
background: #fff;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
#messageInput:focus {
border-color: #999;
outline: none;
}
#sendButton {
padding: 0 20px;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#sendButton:hover {
background: linear-gradient(135deg, #66b1ff, #85c1ff);
}
</style>
</head>
<body>
<div id="chat-container">
<textarea id="chatLog" readonly></textarea>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..." rows="1"></textarea>
<button id="sendButton" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:");
if (!username) {
username = "匿名用户";
}
localStorage.setItem("chat_username", username);
}
let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
let ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
ws.onmessage = function(event) {
const chatLog = document.getElementById("chatLog");
chatLog.value += event.data + "\n";
chatLog.scrollTop = chatLog.scrollHeight;
};
function sendMessage() {
const input = document.getElementById("messageInput");
if (input.value.trim() !== "") {
const message = username + "" + input.value.trim();
ws.send(message);
input.value = "";
adjustInputHeight();
}
}
document.getElementById("messageInput").addEventListener("keydown", function(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
document.getElementById("messageInput").addEventListener("input", adjustInputHeight);
function adjustInputHeight() {
const input = document.getElementById("messageInput");
input.style.height = "auto";
input.style.height = (input.scrollHeight) + "px";
}
</script>
</body>
</html>

@ -0,0 +1,246 @@
<!DOCTYPE html>
<head>
<title>无痕聊天室</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- ================= 原有样式,未做任何删改 ================= -->
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
}
body.dark { background: #1e1e1e; color: #ccc; }
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
position: relative;
}
body.dark #chat-container { background: #2c2c2ccc; }
#chatLog {
flex: 1;
padding: 16px;
overflow-y: auto;
background: #f9f9fb;
display: flex;
flex-direction: column;
gap: 8px;
}
body.dark #chatLog { background: #2a2a2a; }
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.self {
align-self: flex-end;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border-bottom-right-radius: 4px;
}
.other {
align-self: flex-start;
background: #e5e5ea;
color: #333;
border-bottom-left-radius: 4px;
}
.system {
align-self: center;
background: transparent;
color: #999;
font-size: 13px;
}
#input-container {
padding: 12px;
background-color: #f4f6f8;
display: flex;
gap: 8px;
border-top: 1px solid #e0e0e0;
align-items: flex-end;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
resize: none;
min-height: 42px;
}
#sendButton, #uploadButton {
padding: 10px 14px;
border: none;
border-radius: 8px;
cursor: pointer;
background: #409eff;
color: #fff;
}
#onlineCount, #toggleDark, #status {
position: absolute;
font-size: 12px;
padding: 4px 8px;
background: #eef;
border-radius: 12px;
}
#onlineCount { top: 8px; right: 12px; }
#toggleDark { top: 8px; left: 12px; cursor: pointer; }
#status { top: 40px; left: 12px; }
</style>
</head>
<body>
<div id="chat-container">
<div id="toggleDark" onclick="toggleDarkMode()">🌙 夜间</div>
<div id="onlineCount">在线人数0</div>
<div id="status">🟢 已连接</div>
<div id="chatLog"></div>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..."></textarea>
<input type="file" id="fileInput" style="display:none">
<button id="uploadButton">📎</button>
<button id="sendButton">发送</button>
</div>
</div>
<script>
/* ================= 用户名 ================= */
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:") || "匿名用户";
localStorage.setItem("chat_username", username);
}
/* ================= WebSocket ================= */
let ws;
function connectWS() {
const scheme = location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(`${scheme}://${location.host}/ws`);
ws.onopen = () => {
ws.send(username);
setStatus("🟢 已连接");
};
ws.onmessage = e => renderMessage(e.data);
ws.onclose = () => {
setStatus("🔴 断开,重连中...");
setTimeout(connectWS, 2000);
};
}
connectWS();
/* ================= 渲染消息 ================= */
function renderMessage(msg) {
const log = document.getElementById("chatLog");
const div = document.createElement("div");
if (msg.startsWith("COUNT::")) {
document.getElementById("onlineCount").textContent =
"在线人数:" + msg.replace("COUNT::", "");
return;
}
if (msg.startsWith("SYSTEM::")) {
div.className = "message system";
div.textContent = msg.replace("SYSTEM::", "");
}
else if (msg.startsWith("IMG::")) {
div.className = "message other";
const img = document.createElement("img");
img.src = msg.replace("IMG::", "");
img.style.maxWidth = "240px";
img.style.borderRadius = "8px";
div.appendChild(img);
}
else if (msg.startsWith("VIDEO::")) {
div.className = "message other";
const v = document.createElement("video");
v.src = msg.replace("VIDEO::", "");
v.controls = true;
v.style.maxWidth = "260px";
div.appendChild(v);
}
else if (msg.startsWith("TEXT::")) {
const [, sender, text] = msg.split("::", 3);
div.className = "message " + (sender === username ? "self" : "other");
div.textContent = `${sender}${text}`;
}
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
/* ================= 发送文本 ================= */
document.getElementById("sendButton").onclick = () => {
const input = document.getElementById("messageInput");
if (input.value.trim()) {
ws.send(input.value.trim());
input.value = "";
}
}
document.getElementById("messageInput").addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault(); // 防止换行
const input = event.target;
if (input.value.trim()) {
ws.send(input.value.trim());
input.value = "";
}
}
});
/* ================= 上传附件 ================= */
document.getElementById("uploadButton").onclick = () =>
document.getElementById("fileInput").click();
document.getElementById("fileInput").onchange = async () => {
const file = fileInput.files[0];
if (!file) return;
setStatus("📤 上传中...");
const form = new FormData();
form.append("file", file);
const res = await fetch("/upload", { method: "POST", body: form });
const data = await res.json();
if (data.error) alert(data.error);
setStatus("🟢 已连接");
fileInput.value = "";
};
/* ================= UI ================= */
function toggleDarkMode() {
document.body.classList.toggle("dark");
}
function setStatus(t) {
document.getElementById("status").textContent = t;
}
</script>
</body>
</html>

@ -0,0 +1,246 @@
<!DOCTYPE html>
<head>
<title>无痕聊天室</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- ================= 原有样式,未做任何删改 ================= -->
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
}
body.dark { background: #1e1e1e; color: #ccc; }
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
position: relative;
}
body.dark #chat-container { background: #2c2c2ccc; }
#chatLog {
flex: 1;
padding: 16px;
overflow-y: auto;
background: #f9f9fb;
display: flex;
flex-direction: column;
gap: 8px;
}
body.dark #chatLog { background: #2a2a2a; }
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.self {
align-self: flex-end;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border-bottom-right-radius: 4px;
}
.other {
align-self: flex-start;
background: #e5e5ea;
color: #333;
border-bottom-left-radius: 4px;
}
.system {
align-self: center;
background: transparent;
color: #999;
font-size: 13px;
}
#input-container {
padding: 12px;
background-color: #f4f6f8;
display: flex;
gap: 8px;
border-top: 1px solid #e0e0e0;
align-items: flex-end;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
resize: none;
min-height: 42px;
}
#sendButton, #uploadButton {
padding: 10px 14px;
border: none;
border-radius: 8px;
cursor: pointer;
background: #409eff;
color: #fff;
}
#onlineCount, #toggleDark, #status {
position: absolute;
font-size: 12px;
padding: 4px 8px;
background: #eef;
border-radius: 12px;
}
#onlineCount { top: 8px; right: 12px; }
#toggleDark { top: 8px; left: 12px; cursor: pointer; }
#status { top: 40px; left: 12px; }
</style>
</head>
<body>
<div id="chat-container">
<div id="toggleDark" onclick="toggleDarkMode()">🌙 夜间</div>
<div id="onlineCount">在线人数0</div>
<div id="status">🟢 已连接</div>
<div id="chatLog"></div>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..."></textarea>
<input type="file" id="fileInput" style="display:none">
<button id="uploadButton">📎</button>
<button id="sendButton">发送</button>
</div>
</div>
<script>
/* ================= 用户名 ================= */
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:") || "匿名用户";
localStorage.setItem("chat_username", username);
}
/* ================= WebSocket ================= */
let ws;
function connectWS() {
const scheme = location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(`${scheme}://${location.host}/ws`);
ws.onopen = () => {
ws.send(username);
setStatus("🟢 已连接");
};
ws.onmessage = e => renderMessage(e.data);
ws.onclose = () => {
setStatus("🔴 断开,重连中...");
setTimeout(connectWS, 2000);
};
}
connectWS();
/* ================= 渲染消息 ================= */
function renderMessage(msg) {
const log = document.getElementById("chatLog");
const div = document.createElement("div");
if (msg.startsWith("COUNT::")) {
document.getElementById("onlineCount").textContent =
"在线人数:" + msg.replace("COUNT::", "");
return;
}
if (msg.startsWith("SYSTEM::")) {
div.className = "message system";
div.textContent = msg.replace("SYSTEM::", "");
}
else if (msg.startsWith("IMG::")) {
div.className = "message other";
const img = document.createElement("img");
img.src = msg.replace("IMG::", "");
img.style.maxWidth = "240px";
img.style.borderRadius = "8px";
div.appendChild(img);
}
else if (msg.startsWith("VIDEO::")) {
div.className = "message other";
const v = document.createElement("video");
v.src = msg.replace("VIDEO::", "");
v.controls = true;
v.style.maxWidth = "260px";
div.appendChild(v);
}
else if (msg.startsWith("TEXT::")) {
const [, sender, text] = msg.split("::", 3);
div.className = "message " + (sender === username ? "self" : "other");
div.textContent = `${sender}${text}`;
}
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
/* ================= 发送文本 ================= */
document.getElementById("sendButton").onclick = () => {
const input = document.getElementById("messageInput");
if (input.value.trim()) {
ws.send(input.value.trim());
input.value = "";
}
}
document.getElementById("messageInput").addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault(); // 防止换行
const input = event.target;
if (input.value.trim()) {
ws.send(input.value.trim());
input.value = "";
}
}
});
/* ================= 上传附件 ================= */
document.getElementById("uploadButton").onclick = () =>
document.getElementById("fileInput").click();
document.getElementById("fileInput").onchange = async () => {
const file = fileInput.files[0];
if (!file) return;
setStatus("📤 上传中...");
const form = new FormData();
form.append("file", file);
const res = await fetch("/upload", { method: "POST", body: form });
const data = await res.json();
if (data.error) alert(data.error);
setStatus("🟢 已连接");
fileInput.value = "";
};
/* ================= UI ================= */
function toggleDarkMode() {
document.body.classList.toggle("dark");
}
function setStatus(t) {
document.getElementById("status").textContent = t;
}
</script>
</body>
</html>

@ -0,0 +1,234 @@
<!DOCTYPE html>
<head>
<title>无痕聊天室</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- ================= 原有样式,未做任何删改 ================= -->
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #eceff4, #f5f7fa);
}
body.dark { background: #1e1e1e; color: #ccc; }
#chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #ffffffcc;
backdrop-filter: blur(8px);
border-radius: 12px;
overflow: hidden;
position: relative;
}
body.dark #chat-container { background: #2c2c2ccc; }
#chatLog {
flex: 1;
padding: 16px;
overflow-y: auto;
background: #f9f9fb;
display: flex;
flex-direction: column;
gap: 8px;
}
body.dark #chatLog { background: #2a2a2a; }
.message {
max-width: 75%;
padding: 10px 14px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.self {
align-self: flex-end;
background: linear-gradient(135deg, #409eff, #66b1ff);
color: #fff;
border-bottom-right-radius: 4px;
}
.other {
align-self: flex-start;
background: #e5e5ea;
color: #333;
border-bottom-left-radius: 4px;
}
.system {
align-self: center;
background: transparent;
color: #999;
font-size: 13px;
}
#input-container {
padding: 12px;
background-color: #f4f6f8;
display: flex;
gap: 8px;
border-top: 1px solid #e0e0e0;
align-items: flex-end;
}
#messageInput {
flex: 1;
padding: 10px 14px;
border: 1px solid #ccc;
border-radius: 10px;
resize: none;
min-height: 42px;
}
#sendButton, #uploadButton {
padding: 10px 14px;
border: none;
border-radius: 8px;
cursor: pointer;
background: #409eff;
color: #fff;
}
#onlineCount, #toggleDark, #status {
position: absolute;
font-size: 12px;
padding: 4px 8px;
background: #eef;
border-radius: 12px;
}
#onlineCount { top: 8px; right: 12px; }
#toggleDark { top: 8px; left: 12px; cursor: pointer; }
#status { top: 40px; left: 12px; }
</style>
</head>
<body>
<div id="chat-container">
<div id="toggleDark" onclick="toggleDarkMode()">🌙 夜间</div>
<div id="onlineCount">在线人数0</div>
<div id="status">🟢 已连接</div>
<div id="chatLog"></div>
<div id="input-container">
<textarea id="messageInput" placeholder="来说点什么吧..."></textarea>
<input type="file" id="fileInput" style="display:none">
<button id="uploadButton">📎</button>
<button id="sendButton">发送</button>
</div>
</div>
<script>
/* ================= 用户名 ================= */
let username = localStorage.getItem("chat_username");
if (!username) {
username = prompt("请输入你的昵称:") || "匿名用户";
localStorage.setItem("chat_username", username);
}
/* ================= WebSocket ================= */
let ws;
function connectWS() {
const scheme = location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(`${scheme}://${location.host}/ws`);
ws.onopen = () => {
ws.send(username);
setStatus("🟢 已连接");
};
ws.onmessage = e => renderMessage(e.data);
ws.onclose = () => {
setStatus("🔴 断开,重连中...");
setTimeout(connectWS, 2000);
};
}
connectWS();
/* ================= 渲染消息 ================= */
function renderMessage(msg) {
const log = document.getElementById("chatLog");
const div = document.createElement("div");
if (msg.startsWith("COUNT::")) {
document.getElementById("onlineCount").textContent =
"在线人数:" + msg.replace("COUNT::", "");
return;
}
if (msg.startsWith("SYSTEM::")) {
div.className = "message system";
div.textContent = msg.replace("SYSTEM::", "");
}
else if (msg.startsWith("IMG::")) {
div.className = "message other";
const img = document.createElement("img");
img.src = msg.replace("IMG::", "");
img.style.maxWidth = "240px";
img.style.borderRadius = "8px";
div.appendChild(img);
}
else if (msg.startsWith("VIDEO::")) {
div.className = "message other";
const v = document.createElement("video");
v.src = msg.replace("VIDEO::", "");
v.controls = true;
v.style.maxWidth = "260px";
div.appendChild(v);
}
else if (msg.startsWith("TEXT::")) {
const [, sender, text] = msg.split("::", 3);
div.className = "message " + (sender === username ? "self" : "other");
div.textContent = `${sender}${text}`;
}
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
/* ================= 发送文本 ================= */
document.getElementById("sendButton").onclick = () => {
const input = document.getElementById("messageInput");
if (input.value.trim()) {
ws.send(input.value.trim());
input.value = "";
}
};
/* ================= 上传附件 ================= */
document.getElementById("uploadButton").onclick = () =>
document.getElementById("fileInput").click();
document.getElementById("fileInput").onchange = async () => {
const file = fileInput.files[0];
if (!file) return;
setStatus("📤 上传中...");
const form = new FormData();
form.append("file", file);
const res = await fetch("/upload", { method: "POST", body: form });
const data = await res.json();
if (data.error) alert(data.error);
setStatus("🟢 已连接");
fileInput.value = "";
};
/* ================= UI ================= */
function toggleDarkMode() {
document.body.classList.toggle("dark");
}
function setStatus(t) {
document.getElementById("status").textContent = t;
}
</script>
</body>
</html>

@ -0,0 +1,30 @@
[uwsgi]
uid = uwsgi
gid = uwsgi
# 启动服务监听的地址和端口
http-socket = 0.0.0.0:8202
# 指定虚拟环境路径
virtualenv = /opt/service/python_prj/pictoHub.env
# 指定 Flask 应用文件的路径
wsgi-file = /opt/service/python_prj/chatHub/chat.py
# 设置 Flask 的应用实例
callable = app
# 设置静态文件目录映射
static-map = /static=/opt/service/python_prj/chatHub/static/
# 日志文件
logto = /var/log/uwsgi/chat-project.log
# 设置进程数
processes = 4
# 启动时的 Python 环境路径
home = /opt/service/python_prj/pictoHub.env
# 确保应用正常启动
touch-reload = /opt/service/python_prj/chatHub/chat.py
Loading…
Cancel
Save