Update file upload limits to 2GB in backend and frontend components; refactor avatar utility functions for improved name handling.

This commit is contained in:
liu
2026-01-31 01:17:51 +08:00
parent 25196410c2
commit f18bf32d33
7 changed files with 80 additions and 66 deletions

View File

@@ -39,7 +39,7 @@
/>
</svg>
拖拽文件到此处或点击 / Ctrl+V 粘贴
<span class="text-slate-400">· 单文件最大 50MB</span>
<span class="text-slate-400">· 单文件最大 2GB</span>
</span>
</div>

View File

@@ -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;
/** 小图直发阈值doc07200KB 以下直接发送 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}`,
};
}

View File

@@ -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] ?? '?';
}

View 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] : '?';
}

View File

@@ -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();

View File

@@ -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 },