Files
productcanvas/js/utils.js
史悦 d74fbce137 现在支持的格式
-  ```svg<svg... 
  -  ```svg <svg... 
  -  ```xml<svg... 
  -  ```xml <svg... 
  -  ``` xml<svg... 
  -  ``` svg<svg... 
  -  ```<svg... (无语言标识)

  这样无论 LLM 输出哪种格式,都能正确实时绘制和复制图片到剪贴板了。
2026-02-03 10:55:17 +08:00

354 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 工具函数集合
*/
// HTML转义防止XSS攻击
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 滚动到指定元素的底部
function scrollToBottom(element) {
if (element) {
element.scrollTop = element.scrollHeight;
}
}
// 生成唯一ID
function generateId(prefix = 'id') {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// 解析SVG响应提取SVG内容和前后文本容错缺失的结束反引号
function parseSVGResponse(response = '') {
const content = typeof response === 'string' ? response : String(response || '');
// 兼容 ```svg、```xml、``` xml、``` svg 等格式
const svgFenceRegex = /```\s*(?:svg|xml)?\s*([\s\S]*?)```/i;
const fenceMatch = content.match(svgFenceRegex);
if (fenceMatch) {
const svgBody = fenceMatch[1].trim();
const beforeText = content.substring(0, fenceMatch.index).trim();
let afterText = content.substring(fenceMatch.index + fenceMatch[0].length).trim();
afterText = afterText.replace(/^\s*```/, '').trim();
return {
svgContent: svgBody,
beforeText,
afterText
};
}
// 兼容缺失结束反引号的情况(同样兼容 xml 格式)
const svgStartRegex = /```\s*(?:svg|xml)?\s*<svg[\s\S]*$/i;
const startMatch = content.match(svgStartRegex);
if (startMatch) {
const startIndex = startMatch.index;
const beforeText = content.substring(0, startIndex).trim();
let svgSection = content.substring(startIndex).replace(/```\s*(?:svg|xml)?\s*/i, '').trim();
// 去掉尾部残留的反引号
svgSection = svgSection.replace(/```$/, '').trim();
// 拆分 SVG 正文与额外文本
let afterText = '';
const svgEndIndex = svgSection.lastIndexOf('</svg>');
if (svgEndIndex !== -1) {
afterText = svgSection.substring(svgEndIndex + 6).replace(/```/, '').trim();
svgSection = svgSection.substring(0, svgEndIndex + 6).trim();
}
// 补齐缺失的结束标签
if (svgSection && !svgSection.endsWith('</svg>')) {
svgSection += '\n</svg>';
}
return {
svgContent: svgSection || null,
beforeText,
afterText
};
}
return {
svgContent: null,
beforeText: content.trim(),
afterText: ''
};
}
// 下载文件
function downloadFile(content, filename, mimeType = 'text/plain') {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 显示状态信息
function showStatus(element, message, type = 'info') {
if (!element) return;
element.classList.remove('hidden');
element.textContent = message;
// 移除所有状态类
element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
element.classList.remove('border-green-500', 'bg-green-50', 'text-green-700');
element.classList.remove('border-red-500', 'bg-red-50', 'text-red-700');
element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
// 根据类型添加相应的样式类
switch (type) {
case 'success':
element.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
break;
case 'error':
element.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
break;
case 'loading':
element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
break;
default:
element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
}
}
// 本地存储操作
const storage = {
// 保存数据到本地存储
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('保存到本地存储失败:', error);
return false;
}
},
// 从本地存储获取数据
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('从本地存储获取数据失败:', error);
return defaultValue;
}
},
// 删除本地存储中的数据
remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch (error) {
console.error('删除本地存储数据失败:', error);
return false;
}
}
};
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 格式化日期时间
function formatDateTime(date = new Date()) {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// 深拷贝对象
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => deepClone(item));
if (typeof obj === 'object') {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
}
// 检查对象是否为空
function isEmpty(obj) {
if (obj == null) return true;
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
if (typeof obj === 'object') return Object.keys(obj).length === 0;
return false;
}
// 自动调整文本域高度
function autoResizeTextarea(textarea) {
if (!textarea) return;
// 重置高度以获取正确的scrollHeight
textarea.style.height = 'auto';
// 计算新高度,限制最大高度
const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px约5行
textarea.style.height = newHeight + 'px';
}
// 流式文本处理
class StreamProcessor {
constructor(onChunk, onComplete) {
this.onChunk = onChunk;
this.onComplete = onComplete;
this.buffer = '';
this.completed = false;
}
complete(info = {}) {
if (this.completed) return;
this.completed = true;
if (typeof this.onComplete === 'function') {
this.onComplete(info);
}
}
// 处理数据块
processChunk(chunk) {
this.buffer += chunk;
// 尝试解析完整的JSON行
const lines = this.buffer.split('\n');
this.buffer = lines.pop() || ''; // 保留不完整的行
for (const line of lines) {
if (line.trim()) {
try {
// 处理SSE格式
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
this.complete({ aborted: false });
return;
}
const parsed = JSON.parse(data);
this.onChunk(parsed);
}
} catch (error) {
console.warn('解析流数据失败:', error, line);
}
}
}
}
}
// 创建流式请求
function createStreamRequest(url, options, onChunk, onComplete) {
const processor = new StreamProcessor(onChunk, onComplete);
const controller = new AbortController();
const fetchPromise = (async () => {
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...options.headers,
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
processor.processChunk(chunk);
if (processor.completed) {
break;
}
}
if (!processor.completed) {
processor.complete({ aborted: false });
}
} catch (error) {
if (error.name === 'AbortError') {
processor.complete({ aborted: true });
return;
}
throw error;
}
})();
return {
cancel: () => controller.abort(),
finished: fetchPromise
};
}
// 导出工具函数
window.Utils = {
escapeHtml,
scrollToBottom,
generateId,
parseSVGResponse,
downloadFile,
showStatus,
storage,
debounce,
throttle,
formatDateTime,
deepClone,
isEmpty,
autoResizeTextarea,
StreamProcessor,
createStreamRequest
};