Update file upload limits to 2GB in backend and frontend components; refactor avatar utility functions for improved name handling.
This commit is contained in:
@@ -8,8 +8,8 @@ spring:
|
|||||||
name: datatool-backend
|
name: datatool-backend
|
||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
max-file-size: 100MB
|
max-file-size: 2GB
|
||||||
max-request-size: 100MB
|
max-request-size: 2GB
|
||||||
|
|
||||||
# WebSocket / STOMP 基础配置占位,后续可扩展
|
# WebSocket / STOMP 基础配置占位,后续可扩展
|
||||||
datatool:
|
datatool:
|
||||||
@@ -20,7 +20,7 @@ datatool:
|
|||||||
# 文件上传存储与限制(大文件走 HTTP 上传/下载,避免 WebSocket 断连)
|
# 文件上传存储与限制(大文件走 HTTP 上传/下载,避免 WebSocket 断连)
|
||||||
transfer:
|
transfer:
|
||||||
upload-dir: ./data/uploads
|
upload-dir: ./data/uploads
|
||||||
max-file-size: 104857600 # 100MB
|
max-file-size: 2147483648 # 2GB
|
||||||
room-expire-hours: 24
|
room-expire-hours: 24
|
||||||
cleanup-interval-ms: 3600000 # 定时过期清理间隔(毫秒),默认 1 小时
|
cleanup-interval-ms: 3600000 # 定时过期清理间隔(毫秒),默认 1 小时
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
拖拽文件到此处,或点击 / Ctrl+V 粘贴
|
拖拽文件到此处,或点击 / Ctrl+V 粘贴
|
||||||
<span class="text-slate-400">· 单文件最大 50MB</span>
|
<span class="text-slate-400">· 单文件最大 2GB</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { RoomMessagePayload, SessionInfo } from '@/types/room';
|
|||||||
import { isImageMimeType } from '@/types/room';
|
import { isImageMimeType } from '@/types/room';
|
||||||
import { uploadRoomFile } from '@/api/room';
|
import { uploadRoomFile } from '@/api/room';
|
||||||
import { mergeChunksToBlob } from '@/utils/fileChunker';
|
import { mergeChunksToBlob } from '@/utils/fileChunker';
|
||||||
|
import { generateChineseName } from '@/utils/chineseName';
|
||||||
|
|
||||||
const HISTORY_KEY_PREFIX = 'DataTool-history-';
|
const HISTORY_KEY_PREFIX = 'DataTool-history-';
|
||||||
/** 历史记录最大条数,超出则淘汰最旧(doc10 容量) */
|
/** 历史记录最大条数,超出则淘汰最旧(doc10 容量) */
|
||||||
@@ -19,8 +20,8 @@ const CHUNK_SIZE = 32 * 1024;
|
|||||||
const CHUNK_CACHE_TTL = 60 * 1000;
|
const CHUNK_CACHE_TTL = 60 * 1000;
|
||||||
/** 小图直发阈值(doc07:200KB 以下直接发送 base64) */
|
/** 小图直发阈值(doc07:200KB 以下直接发送 base64) */
|
||||||
const IMAGE_INLINE_THRESHOLD = 200 * 1024;
|
const IMAGE_INLINE_THRESHOLD = 200 * 1024;
|
||||||
/** 单文件大小上限(100MB),与后端 transfer.max-file-size 一致 */
|
/** 单文件大小上限(2GB),与后端 transfer.max-file-size 一致 */
|
||||||
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
interface JoinRoomPayload {
|
interface JoinRoomPayload {
|
||||||
roomCode: string;
|
roomCode: string;
|
||||||
@@ -67,7 +68,7 @@ export const useWsStore = defineStore('ws', () => {
|
|||||||
const status = ref<ConnectionStatus>('disconnected');
|
const status = ref<ConnectionStatus>('disconnected');
|
||||||
const client = ref<RoomWsClient | null>(null);
|
const client = ref<RoomWsClient | null>(null);
|
||||||
const myUserId = ref<string>(randomUserId());
|
const myUserId = ref<string>(randomUserId());
|
||||||
const myNickname = ref<string>('匿名用户');
|
const myNickname = ref<string>(generateChineseName());
|
||||||
const currentRoomCode = ref<string | null>(null);
|
const currentRoomCode = ref<string | null>(null);
|
||||||
const userList = ref<SessionInfo[]>([]);
|
const userList = ref<SessionInfo[]>([]);
|
||||||
const roomMessages = ref<RoomMessagePayload[]>([]);
|
const roomMessages = ref<RoomMessagePayload[]>([]);
|
||||||
@@ -478,9 +479,12 @@ export const useWsStore = defineStore('ws', () => {
|
|||||||
if (!client.value) return { ok: false, error: '未连接' };
|
if (!client.value) return { ok: false, error: '未连接' };
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
const limitMB = MAX_FILE_SIZE / (1024 * 1024);
|
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 {
|
return {
|
||||||
ok: false,
|
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 = [
|
const AVATAR_COLORS = [
|
||||||
@@ -22,62 +22,19 @@ function hashString(s: string): number {
|
|||||||
return n;
|
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 {
|
export function getShortDisplayName(name: string): string {
|
||||||
const trimmed = (name ?? '').trim();
|
return (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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 头像文字:IP 取最后一段(166、128),否则取首字。
|
* 头像文字:取名字第一个字
|
||||||
*/
|
*/
|
||||||
export function getAvatarLetter(name: string): string {
|
export function getAvatarLetter(name: string): string {
|
||||||
const trimmed = (name ?? '').trim();
|
const trimmed = (name ?? '').trim();
|
||||||
if (!trimmed) return '?';
|
if (!trimmed) return '?';
|
||||||
if (isIPOrWithSuffix(trimmed)) {
|
|
||||||
const octet = getLastOctet(trimmed);
|
|
||||||
return octet.length > 0 ? octet : '?';
|
|
||||||
}
|
|
||||||
return trimmed[0] ?? '?';
|
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';
|
import BaseButton from '@/components/ui/BaseButton.vue';
|
||||||
|
|
||||||
// 开发时用空字符串走同源,由 Vite 代理到后端,便于其它设备通过本机 IP:5173 访问
|
// 开发时用空字符串走同源,由 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();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -95,13 +95,13 @@ import MessageItem from '@/components/MessageItem.vue';
|
|||||||
import FileMessage from '@/components/FileMessage.vue';
|
import FileMessage from '@/components/FileMessage.vue';
|
||||||
import ImageMessage from '@/components/ImageMessage.vue';
|
import ImageMessage from '@/components/ImageMessage.vue';
|
||||||
import MessageInput from '@/components/MessageInput.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 { useWsStore } from '@/stores/wsStore';
|
||||||
import type { RoomMessagePayload } from '@/types/room';
|
import type { RoomMessagePayload } from '@/types/room';
|
||||||
import { isImageMimeType } from '@/types/room';
|
import { isImageMimeType } from '@/types/room';
|
||||||
|
|
||||||
// 开发时用空字符串走同源,由 Vite 代理到后端,便于其它设备通过本机 IP:5173 访问
|
// 未设置 VITE_API_BASE 时使用同源:开发时走 Vite 代理,Docker/生产时与页面同 host:port,避免硬编码 localhost 导致部署后连不上
|
||||||
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 route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -128,14 +128,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => wsStore.status,
|
() => wsStore.status,
|
||||||
async (status) => {
|
(status) => {
|
||||||
if (status === 'connected' && roomCode) {
|
if (status === 'connected' && roomCode) {
|
||||||
try {
|
// 使用 store 中已生成的随机中国人名
|
||||||
const ip = await getMyIp(API_BASE);
|
wsStore.joinRoom({ roomCode, nickname: wsStore.myNickname });
|
||||||
wsStore.joinRoom({ roomCode, nickname: ip?.trim() || '访客' });
|
|
||||||
} catch {
|
|
||||||
wsStore.joinRoom({ roomCode, nickname: '访客' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user