Update file upload limits to 2GB in backend and frontend components; refactor avatar utility functions for improved name handling.
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
/>
|
||||
</svg>
|
||||
拖拽文件到此处,或点击 / Ctrl+V 粘贴
|
||||
<span class="text-slate-400">· 单文件最大 50MB</span>
|
||||
<span class="text-slate-400">· 单文件最大 2GB</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<ConnectionStatus>('disconnected');
|
||||
const client = ref<RoomWsClient | null>(null);
|
||||
const myUserId = ref<string>(randomUserId());
|
||||
const myNickname = ref<string>('匿名用户');
|
||||
const myNickname = ref<string>(generateChineseName());
|
||||
const currentRoomCode = ref<string | null>(null);
|
||||
const userList = ref<SessionInfo[]>([]);
|
||||
const roomMessages = ref<RoomMessagePayload[]>([]);
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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] ?? '?';
|
||||
}
|
||||
|
||||
|
||||
57
frontend/src/utils/chineseName.ts
Normal file
57
frontend/src/utils/chineseName.ts
Normal file
@@ -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] : '?';
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user