diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6890f65..844564d 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -8,8 +8,8 @@ spring: name: datatool-backend servlet: multipart: - max-file-size: 100MB - max-request-size: 100MB + max-file-size: 2GB + max-request-size: 2GB # WebSocket / STOMP 基础配置占位,后续可扩展 datatool: @@ -20,7 +20,7 @@ datatool: # 文件上传存储与限制(大文件走 HTTP 上传/下载,避免 WebSocket 断连) transfer: upload-dir: ./data/uploads - max-file-size: 104857600 # 100MB + max-file-size: 2147483648 # 2GB room-expire-hours: 24 cleanup-interval-ms: 3600000 # 定时过期清理间隔(毫秒),默认 1 小时 diff --git a/frontend/src/components/FileDropZone.vue b/frontend/src/components/FileDropZone.vue index 44d708a..693349e 100644 --- a/frontend/src/components/FileDropZone.vue +++ b/frontend/src/components/FileDropZone.vue @@ -39,7 +39,7 @@ /> 拖拽文件到此处,或点击 / Ctrl+V 粘贴 - · 单文件最大 50MB + · 单文件最大 2GB diff --git a/frontend/src/stores/wsStore.ts b/frontend/src/stores/wsStore.ts index 26242cb..03c7a19 100644 --- a/frontend/src/stores/wsStore.ts +++ b/frontend/src/stores/wsStore.ts @@ -6,6 +6,7 @@ import type { RoomMessagePayload, SessionInfo } from '@/types/room'; import { isImageMimeType } from '@/types/room'; import { uploadRoomFile } from '@/api/room'; import { mergeChunksToBlob } from '@/utils/fileChunker'; +import { generateChineseName } from '@/utils/chineseName'; const HISTORY_KEY_PREFIX = 'DataTool-history-'; /** 历史记录最大条数,超出则淘汰最旧(doc10 容量) */ @@ -19,8 +20,8 @@ const CHUNK_SIZE = 32 * 1024; const CHUNK_CACHE_TTL = 60 * 1000; /** 小图直发阈值(doc07:200KB 以下直接发送 base64) */ const IMAGE_INLINE_THRESHOLD = 200 * 1024; -/** 单文件大小上限(100MB),与后端 transfer.max-file-size 一致 */ -const MAX_FILE_SIZE = 100 * 1024 * 1024; +/** 单文件大小上限(2GB),与后端 transfer.max-file-size 一致 */ +const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; interface JoinRoomPayload { roomCode: string; @@ -67,7 +68,7 @@ export const useWsStore = defineStore('ws', () => { const status = ref('disconnected'); const client = ref(null); const myUserId = ref(randomUserId()); - const myNickname = ref('匿名用户'); + const myNickname = ref(generateChineseName()); const currentRoomCode = ref(null); const userList = ref([]); const roomMessages = ref([]); @@ -478,9 +479,12 @@ export const useWsStore = defineStore('ws', () => { if (!client.value) return { ok: false, error: '未连接' }; if (file.size > MAX_FILE_SIZE) { const limitMB = MAX_FILE_SIZE / (1024 * 1024); + const limitStr = limitMB >= 1024 ? `${limitMB / 1024}GB` : `${limitMB}MB`; + const sizeMB = file.size / (1024 * 1024); + const sizeStr = sizeMB >= 1024 ? `${(sizeMB / 1024).toFixed(1)}GB` : `${sizeMB.toFixed(1)}MB`; return { ok: false, - error: `文件过大(${(file.size / (1024 * 1024)).toFixed(1)}MB),当前最大支持 ${limitMB}MB`, + error: `文件过大(${sizeStr}),当前最大支持 ${limitStr}`, }; } diff --git a/frontend/src/utils/avatar.ts b/frontend/src/utils/avatar.ts index dd0406b..419c40b 100644 --- a/frontend/src/utils/avatar.ts +++ b/frontend/src/utils/avatar.ts @@ -1,6 +1,6 @@ /** * 根据显示名生成头像文字与背景色,用于消息/用户列表等。 - * IP 时取最后一段(如 192.168.100.166 → 166)作为头像与显示名。 + * 取名字第一个字作为头像。 */ const AVATAR_COLORS = [ @@ -22,62 +22,19 @@ function hashString(s: string): number { return n; } -/** 简单判断是否为 IPv4 字符串(如 192.168.100.166) */ -export function isIPv4(s: string): boolean { - if (!s || typeof s !== 'string') return false; - const trimmed = s.trim(); - return /^\d{1,3}(\.\d{1,3}){3}$/.test(trimmed); -} - -/** 匹配纯 IPv4 或带后缀的 IPv4(如 192.168.100.166-2) */ -const IP_OPTIONAL_SUFFIX = /^(\d{1,3}(?:\.\d{1,3}){3})(-\d+)?$/; - -/** 取 IPv4 最后一段,如 192.168.100.166 → 166;非 IP 返回原字符串 */ -export function getLastOctet(name: string): string { - const trimmed = (name ?? '').trim(); - if (!trimmed) return trimmed; - const m = trimmed.match(IP_OPTIONAL_SUFFIX); - if (m) { - const parts = m[1]!.split('.'); - return parts[parts.length - 1] ?? trimmed; - } - if (isIPv4(trimmed)) { - const parts = trimmed.split('.'); - return parts[parts.length - 1] ?? trimmed; - } - return trimmed; -} - -/** 是否为 IP 或 IP-后缀 形式 */ -function isIPOrWithSuffix(s: string): boolean { - return IP_OPTIONAL_SUFFIX.test((s ?? '').trim()); -} - /** - * 显示用短名:IP 只显示最后一段(166、128),带后缀则 166-2;非 IP 原样。 + * 获取短显示名(直接返回原名称) */ export function getShortDisplayName(name: string): string { - const trimmed = (name ?? '').trim(); - if (!trimmed) return trimmed; - const m = trimmed.match(IP_OPTIONAL_SUFFIX); - if (m) { - const octet = m[1]!.split('.').pop() ?? m[1]; - return m[2] ? `${octet}${m[2]}` : octet; - } - if (isIPv4(trimmed)) return getLastOctet(trimmed); - return trimmed; + return (name ?? '').trim() || '未知'; } /** - * 头像文字:IP 取最后一段(166、128),否则取首字。 + * 头像文字:取名字第一个字 */ export function getAvatarLetter(name: string): string { const trimmed = (name ?? '').trim(); if (!trimmed) return '?'; - if (isIPOrWithSuffix(trimmed)) { - const octet = getLastOctet(trimmed); - return octet.length > 0 ? octet : '?'; - } return trimmed[0] ?? '?'; } diff --git a/frontend/src/utils/chineseName.ts b/frontend/src/utils/chineseName.ts new file mode 100644 index 0000000..036662a --- /dev/null +++ b/frontend/src/utils/chineseName.ts @@ -0,0 +1,57 @@ +/** + * 中国人名随机生成器 + * 从常见姓氏和名字中随机组合生成中国人名 + */ + +// 常见姓氏(百家姓高频姓氏) +const SURNAMES = [ + '王', '李', '张', '刘', '陈', '杨', '黄', '赵', '吴', '周', + '徐', '孙', '马', '朱', '胡', '郭', '何', '林', '高', '罗', + '郑', '梁', '谢', '宋', '唐', '许', '邓', '冯', '韩', '曹', + '曾', '彭', '萧', '蔡', '潘', '田', '董', '袁', '于', '余', + '叶', '蒋', '杜', '苏', '魏', '程', '吕', '丁', '沈', '任', + '姚', '卢', '傅', '钟', '姜', '崔', '谭', '廖', '范', '汪', + '陆', '金', '石', '戴', '贾', '韦', '夏', '邱', '方', '侯', + '邹', '熊', '孟', '秦', '白', '江', '阎', '薛', '尹', '段', + '雷', '黎', '史', '龙', '陶', '贺', '顾', '毛', '郝', '龚', + '邵', '万', '钱', '严', '赖', '覃', '洪', '武', '莫', '孔', +]; + +// 常见名字用字(男女通用/偏中性) +const NAME_CHARS = [ + '伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', + '勇', '艳', '杰', '娟', '涛', '明', '超', '秀', '英', '华', + '慧', '巧', '美', '雪', '倩', '玲', '红', '春', '辉', '霞', + '浩', '建', '平', '刚', '峰', '鹏', '宇', '飞', '林', '波', + '文', '云', '龙', '凤', '琳', '萍', '晨', '晓', '阳', '婷', + '欣', '怡', '佳', '嘉', '瑞', '祥', '博', '俊', '航', '鑫', + '昊', '轩', '睿', '泽', '豪', '子', '梓', '一', '宁', '乐', + '天', '雨', '诗', '琪', '雯', '萱', '颖', '悦', '淼', '然', + '思', '远', '哲', '皓', '逸', '安', '宏', '志', '国', '正', + '新', '海', '旭', '亮', '清', '冰', '健', '蕾', '燕', '菲', +]; + +/** + * 生成随机中国人名 + * @returns 随机生成的中国人名(2-3个字) + */ +export function generateChineseName(): string { + const surname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)]; + // 50% 概率生成单字名,50% 概率生成双字名 + const nameLength = Math.random() < 0.5 ? 1 : 2; + let name = ''; + for (let i = 0; i < nameLength; i++) { + name += NAME_CHARS[Math.floor(Math.random() * NAME_CHARS.length)]; + } + return surname + name; +} + +/** + * 获取姓名的第一个字(用于头像显示) + * @param name 姓名 + * @returns 第一个字 + */ +export function getFirstChar(name: string): string { + const trimmed = (name ?? '').trim(); + return trimmed.length > 0 ? trimmed[0] : '?'; +} diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 3008a04..046feb5 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -80,7 +80,7 @@ import axios from 'axios'; import BaseButton from '@/components/ui/BaseButton.vue'; // 开发时用空字符串走同源,由 Vite 代理到后端,便于其它设备通过本机 IP:5173 访问 -const API_BASE = import.meta.env.VITE_API_BASE ?? (import.meta.env.DEV ? '' : 'http://localhost:8080'); +const API_BASE = import.meta.env.VITE_API_BASE ?? ''; const router = useRouter(); diff --git a/frontend/src/views/RoomView.vue b/frontend/src/views/RoomView.vue index d410131..08b25a1 100644 --- a/frontend/src/views/RoomView.vue +++ b/frontend/src/views/RoomView.vue @@ -95,13 +95,13 @@ import MessageItem from '@/components/MessageItem.vue'; import FileMessage from '@/components/FileMessage.vue'; import ImageMessage from '@/components/ImageMessage.vue'; import MessageInput from '@/components/MessageInput.vue'; -import { getFileDownloadUrl, downloadWithProgress, getMyIp } from '@/api/room'; +import { getFileDownloadUrl, downloadWithProgress } from '@/api/room'; import { useWsStore } from '@/stores/wsStore'; import type { RoomMessagePayload } from '@/types/room'; import { isImageMimeType } from '@/types/room'; -// 开发时用空字符串走同源,由 Vite 代理到后端,便于其它设备通过本机 IP:5173 访问 -const API_BASE = import.meta.env.VITE_API_BASE ?? (import.meta.env.DEV ? '' : 'http://localhost:8080'); +// 未设置 VITE_API_BASE 时使用同源:开发时走 Vite 代理,Docker/生产时与页面同 host:port,避免硬编码 localhost 导致部署后连不上 +const API_BASE = import.meta.env.VITE_API_BASE ?? ''; const route = useRoute(); const router = useRouter(); @@ -128,14 +128,10 @@ onMounted(() => { watch( () => wsStore.status, - async (status) => { + (status) => { if (status === 'connected' && roomCode) { - try { - const ip = await getMyIp(API_BASE); - wsStore.joinRoom({ roomCode, nickname: ip?.trim() || '访客' }); - } catch { - wsStore.joinRoom({ roomCode, nickname: '访客' }); - } + // 使用 store 中已生成的随机中国人名 + wsStore.joinRoom({ roomCode, nickname: wsStore.myNickname }); } }, { immediate: true },