找到
181
篇与
站长可乐
相关的结果
- 第 2 页
-
宝塔上怎么删除几百万条文件? 发现服务器有几百万条数据,怎么删除呢? 用以下代码删除的时候,提示错误: [root@S202601217452 ~]# rm -rf /www/wwwroot/123.com/cache/html/* bash: /usr/bin/rm: Argument list too long这是遇到的 Argument list too long 错误,是因为 rm -rf * 会让系统把所有文件名展开成参数传给 rm,而几百万文件的参数数量远超 Linux 系统的限制。使用 rsync 和 find 方案就能解决这个问题的,下面是最快捷的执行步骤: 方案 1:rsync 极速清空(推荐,百万级文件秒清) 直接复制下面的命令一键执行,无需分步操作: # 创建空目录 + 同步清空目标目录 + 删除临时空目录 mkdir -p /tmp/empty && rsync -a --delete /tmp/empty/ /www/wwwroot/bbs.lightweb.vip/cache/html/ && rm -rf /tmp/empty方案 2:find 逐行删除(稳妥,适合怕误操作的场景) 如果想更直观看到删除过程(或分批删除),用这个命令: # 只删除文件(保留目录结构),避免参数溢出 find /www/wwwroot/bbs.lightweb.vip/cache/html -type f -delete这个命令会逐个遍历文件并删除,不会触发参数过长问题; 如果想看到删除进度,可加 -print 参数: find /www/wwwroot/bbs.lightweb.vip/cache/html -type f -print -delete验证是否删除成功 执行完后,用下面的命令检查: # 统计剩余文件数(正常应该返回 0) ls /www/wwwroot/bbs.lightweb.vip/cache/html | wc -l为什么这两个方案能解决问题? rsync:本质是 “用空目录覆盖目标目录”,不遍历文件名,直接清空,效率最高; find -delete:逐行处理文件,每次只传一个文件路径给 rm,不会触发参数数量限制; 而 rm -rf *:会先把所有文件名拼起来当参数,参数长度超系统上限就报错。 总结 优先用 rsync 命令,一键清空百万级文件无压力,速度最快; find -delete 更稳妥,适合需要观察删除过程的场景; 避免再用 rm -rf * 处理超大量文件,必触发参数过长错误。 -
如何分割WPS中某个项目的分数据? 以门店商品列表为例,我们可以通过筛选自提门店字段的数据,对文档进行分割成门店单数据文件。 步骤 1:打开宏编辑器(正确操作) 打开你的订单表格,按 Alt + F11 打开 VBA 编辑器(WPS/Excel 都支持); 左侧「工程资源管理器」→ 右键你的工作簿名称 → 插入 → 模块; 在模块里粘贴下面的 VBA 代码(已适配路径、门店名称、报错处理): Sub SplitOrderByStore_WPS() ' 声明所有变量(WPS兼容版) Dim ws As Worksheet Dim dict As Object Dim lastRow As Long, storeCol As Long, i As Long, j As Long Dim storeName As String, savePath As String Dim newWB As Workbook, newWS As Worksheet Dim rowNum As Long Dim rowArr As Variant Dim keyArr As Variant ' 存储字典所有key的数组 ' 1. 初始化工作表和字典 Set ws = ActiveSheet Set dict = CreateObject("Scripting.Dictionary") dict.CompareMode = 1 ' 忽略大小写 ' 2. 定位「自提门店」列(严格匹配表头) On Error Resume Next storeCol = ws.rows(1).Find(What:="自提门店", LookIn:=xlValues, LookAt:=xlWhole, MatchCase:=False).Column On Error GoTo 0 If storeCol = 0 Then MsgBox "错误:未找到「自提门店」列,请检查表头(无空格/错别字)!", vbExclamation Exit Sub End If ' 3. 检查数据有效性 lastRow = ws.Cells(ws.rows.Count, storeCol).End(xlUp).Row If lastRow < 2 Then MsgBox "错误:没有可拆分的订单数据(数据需从第2行开始)!", vbExclamation Exit Sub End If ' 4. 存储每个门店对应的行号(用字典分组) For i = 2 To lastRow storeName = Trim(ws.Cells(i, storeCol).Value) ' 过滤空门店名 If storeName <> "" Then If Not dict.Exists(storeName) Then dict(storeName) = "" ' 初始化行号字符串 End If dict(storeName) = dict(storeName) & i & "," ' 拼接行号(用逗号分隔) End If Next i ' 5. 若没有有效门店,直接退出 If dict.Count = 0 Then MsgBox "错误:未识别到任何门店名称!", vbExclamation Exit Sub End If ' 6. 选择文件保存文件夹 With Application.FileDialog(msoFileDialogFolderPicker) .Title = "选择拆分文件的保存文件夹" .AllowMultiSelect = False If .Show <> -1 Then Exit Sub ' 用户取消选择则退出 savePath = .SelectedItems(1) & "\" End With ' 7. 把字典Keys转为数组(核心:用下标遍历替代For Each) keyArr = dict.Keys ' 提取所有门店名到数组 ' 8. 批量导出每个门店的文件(纯下标遍历) Application.ScreenUpdating = False ' 关闭屏幕刷新,提速 For j = 0 To UBound(keyArr) ' 遍历门店数组(下标从0开始) storeName = keyArr(j) ' 创建新工作簿和工作表 Set newWB = Workbooks.Add Set newWS = newWB.Sheets(1) newWS.Name = "订单数据" ' 复制表头到新文件 ws.rows(1).Copy newWS.rows(1) ' 拆分该行门店的所有行号,并用下标遍历 rowNum = 2 ' 新表从第2行开始粘贴数据 rowArr = Split(Left(dict(storeName), Len(dict(storeName)) - 1), ",") ' 去掉最后一个逗号 ' 下标遍历行号数组(替代For Each) For i = 0 To UBound(rowArr) ws.rows(CLng(rowArr(i))).Copy newWS.rows(rowNum) rowNum = rowNum + 1 Next i ' 处理门店名特殊字符(避免保存失败) Dim safeName As String safeName = Replace(Replace(Replace(Replace(storeName, "/", ""), "\", ""), ":", ""), "*", "") safeName = Replace(Replace(Replace(Replace(safeName, "?", ""), """", ""), "<", ""), ">", "") safeName = Replace(safeName, "|", "") ' 保存文件(兼容Excel/WPS格式) newWB.SaveAs Filename:=savePath & safeName & ".xlsx", FileFormat:=xlOpenXMLWorkbook newWB.Close SaveChanges:=False ' 关闭新文件 Next j Application.ScreenUpdating = True ' 恢复屏幕刷新 MsgBox "拆分成功!所有文件已保存到:" & vbCrLf & savePath, vbInformation ' 释放对象,避免内存泄漏 Set ws = Nothing Set dict = Nothing Set newWB = Nothing Set newWS = Nothing End Sub步骤 2:点击编辑器顶部的「运行」按钮(▶️),或按F5; 步骤 3:在弹出的对话框中选择空文件夹(建议新建),点击「确定」; 步骤 4:等待几秒,弹出「拆分成功」提示后,去目标文件夹查看即可。 -
智能排版小工具 mm0nfh3e.png图片 本工具旨在帮助您快速整理老师们提交的德育案例、校刊稿件、教学手册等 Word 文档,自动识别标题层级,并允许您对文字样式进行局部调整,最终导出格式规范的 Word 或 PDF 文件。 ✨ 主要功能: 支持上传 ZIP 压缩包(内含 .docx 文件或嵌套 ZIP),自动解压并提取所有文档。自动识别常见的标题序号(如“一、”“(一)”“1.”“1.1”等)及无序号标题(如“背景:”“总结”),并按首次出现顺序分配层级。在预览区可以直接修改文字内容(点击文字即可编辑)。用鼠标选中文字后,可通过操作台单独修改选中部分的字体、字号、颜色。将当前预览内容(含图片、样式)导出为 .doc 文件,可用 Microsoft Word 打开。通过浏览器打印功能生成 PDF 文件,保留所有排版和图片。 <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能校刊排版 · 选中编辑版</title> <style> * { box-sizing: border-box; } body { font-family: 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; background-color: #f5f3f0; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .card { max-width: 1300px; width: 100%; background: white; border-radius: 28px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.3); padding: 35px; } h1 { margin-top: 0; color: #3a2c1b; font-weight: 400; border-left: 6px solid #b78c5a; padding-left: 20px; font-size: 2.2rem; } .sub { color: #6b5a48; margin-bottom: 30px; font-style: italic; } .upload-area { border: 2px dashed #c9b69c; border-radius: 32px; padding: 40px 20px; text-align: center; background: #fefcf9; transition: background 0.2s; cursor: pointer; } .upload-area:hover { background: #f9f3ea; } .upload-icon { font-size: 48px; margin-bottom: 10px; color: #b78c5a; } /* 操作台样式 */ .toolbar { background: #f0e9e0; border-radius: 50px; padding: 15px 25px; margin: 20px 0; display: flex; flex-wrap: wrap; gap: 15px; align-items: center; justify-content: center; } .tool-item { display: flex; align-items: center; gap: 8px; } .tool-item label { font-weight: 500; color: #5e4b36; font-size: 0.9rem; } .tool-item select, .tool-item input[type="number"], .tool-item input[type="color"] { padding: 6px 10px; border: 1px solid #dcd0c0; border-radius: 30px; background: white; font-size: 0.9rem; } .tool-item input[type="number"] { width: 70px; } .style-btn { background: #b78c5a; color: white; border: none; padding: 6px 18px; border-radius: 30px; cursor: pointer; font-weight: 500; font-size: 0.9rem; margin-left: 5px; } .style-btn:hover { background: #a07344; } .btn-group { display: flex; gap: 15px; flex-wrap: wrap; margin: 20px 0 20px; justify-content: center; } button { background: #b78c5a; border: none; color: white; padding: 12px 30px; border-radius: 40px; font-size: 1.1rem; font-weight: 500; cursor: pointer; box-shadow: 0 8px 16px -4px rgba(140,90,40,0.3); transition: all 0.2s; border: 1px solid transparent; min-width: 140px; } button:disabled { opacity: 0.4; pointer-events: none; filter: grayscale(0.5); } button.secondary { background: #ece3d9; color: #5e4b36; box-shadow: none; } button.secondary:hover { background: #dfd2c2; } button:hover { background: #a07344; transform: translateY(-2px); } .preview { background: #fefcf9; border-radius: 20px; border: 1px solid #e7dccc; padding: 25px; margin-top: 20px; } .preview h3 { margin-top: 0; color: #4e3e2b; font-weight: 400; display: flex; align-items: center; gap: 8px; } /* 预览容器 */ .preview-content { background: white; border-radius: 16px; padding: 2cm 2cm 2cm 2cm; max-height: 600px; overflow-y: auto; box-shadow: inset 0 0 0 1px #eee7de; font-family: '宋体', 'SimSun', serif; font-size: 10.5pt; line-height: 1.5; outline: none; } /* 标题样式(基础结构,字体大小固定) */ .preview-content .chapter-title { font-family: '黑体', 'Hei', sans-serif; font-size: 18pt; font-weight: bold; text-align: center; margin: 24pt 0 18pt 0; } .preview-content .h1 { font-family: '黑体', 'Hei', sans-serif; font-size: 16pt; font-weight: bold; margin: 18pt 0 12pt 0; } .preview-content .h2 { font-family: '黑体', 'Hei', sans-serif; font-size: 14pt; font-weight: bold; margin: 12pt 0 6pt 0; } .preview-content .h3 { font-family: '黑体', 'Hei', sans-serif; font-size: 12pt; font-weight: bold; margin: 6pt 0 3pt 0; } .preview-content .h4 { font-family: '黑体', 'Hei', sans-serif; font-size: 11pt; font-weight: bold; margin: 3pt 0 3pt 0; } .preview-content .h5 { font-family: '黑体', 'Hei', sans-serif; font-size: 10.5pt; font-weight: bold; margin: 3pt 0 3pt 0; } .preview-content p { font-family: '宋体', 'SimSun', serif; font-size: 10.5pt; line-height: 1.5; margin: 0 0 0 0; text-indent: 2em; text-align: justify; } .preview-content .image-container { text-align: center; margin: 12pt 0; } .preview-content .image-container img { max-width: 100%; height: auto; display: block; margin: 0 auto; } .status { margin: 15px 0; padding: 12px; border-radius: 40px; background: #f0e9e0; color: #3f352b; text-align: center; font-weight: 500; min-height: 60px; display: flex; align-items: center; justify-content: center; word-break: break-word; } .footer-note { margin-top: 20px; font-size: 0.8rem; color: #9b8c7a; text-align: center; } hr { border: none; border-top: 1px solid #e2d7cb; margin: 20px 0; } @media print { body * { visibility: hidden; } .printable-area, .printable-area * { visibility: visible; } .printable-area { position: absolute; left: 0; top: 0; width: 100%; background: white; padding: 2cm 2cm 2cm 2cm !important; margin: 0; box-shadow: none; max-height: none; overflow: visible; } .toolbar, .btn-group, .upload-area, .preview h3, .status, .footer-note, hr { display: none !important; } } </style> <!-- 核心库 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <script>if (typeof JSZip === 'undefined') document.write('<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"><\/script>');</script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script> <script>if (typeof mammoth === 'undefined') document.write('<script src="https://unpkg.com/mammoth@1.6.0/mammoth.browser.min.js"><\/script>');</script> <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> <script>if (typeof saveAs === 'undefined') document.write('<script src="https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js"><\/script>');</script> </head> <body> <div class="card"> <h1>📚 智能校刊排版 · 选中编辑版</h1> <div class="sub">先选中文字,再点击操作台按钮修改选中内容的样式</div> <!-- 上传区域 --> <div class="upload-area" id="dropZone"> <div class="upload-icon">📂</div> <div><strong>点击选择 或 拖拽ZIP到此</strong></div> <div style="font-size:0.9rem; margin-top:8px; color:#7f6e5b;">支持多级标题,图片自动居中</div> <input type="file" id="fileInput" accept=".zip,application/zip" style="display:none;"> </div> <!-- 操作台(选中编辑版) --> <div class="toolbar" id="toolbar"> <div class="tool-item"> <label>字体</label> <select id="fontSelect"> <option value="宋体">宋体</option> <option value="黑体">黑体</option> <option value="楷体">楷体</option> <option value="仿宋">仿宋</option> <option value="微软雅黑">微软雅黑</option> </select> <button class="style-btn" id="applyFontBtn">应用</button> </div> <div class="tool-item"> <label>字号</label> <select id="fontSizeSelect"> <option value="1">8pt (极小)</option> <option value="2">10pt (小)</option> <option value="3" selected>12pt (正常)</option> <option value="4">14pt (中)</option> <option value="5">18pt (大)</option> <option value="6">24pt (很大)</option> <option value="7">36pt (极大)</option> </select> <button class="style-btn" id="applySizeBtn">应用</button> </div> <div class="tool-item"> <label>字色</label> <input type="color" id="colorInput" value="#000000"> <button class="style-btn" id="applyColorBtn">应用</button> </div> </div> <!-- 状态和按钮组 --> <div class="status" id="statusMsg">⏳ 等待上传压缩包</div> <div class="btn-group"> <button id="processBtn" disabled>🧠 智能顺序定级</button> <button id="downloadWordBtn" disabled class="secondary">📄 导出 Word (.doc)</button> <button id="printPdfBtn" disabled class="secondary">🖨️ 打印 / PDF</button> </div> <!-- 预览区域 (可编辑) --> <div class="preview"> <h3>🔍 可编辑预览 (选中文字后修改样式)</h3> <div class="preview-content" id="previewContent" contenteditable="true"> <p style="color:#b7a48e; text-align:center;">上传并解析后点击「智能顺序定级」生成预览</p> </div> </div> <div class="footer-note">※ 选中文字后点击操作台按钮,可单独修改选中部分的样式</div> </div> <script> (function() { // DOM 元素 const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const statusMsg = document.getElementById('statusMsg'); const processBtn = document.getElementById('processBtn'); const downloadWordBtn = document.getElementById('downloadWordBtn'); const printPdfBtn = document.getElementById('printPdfBtn'); const previewContent = document.getElementById('previewContent'); // 操作台按钮 const applyFontBtn = document.getElementById('applyFontBtn'); const applySizeBtn = document.getElementById('applySizeBtn'); const applyColorBtn = document.getElementById('applyColorBtn'); const fontSelect = document.getElementById('fontSelect'); const fontSizeSelect = document.getElementById('fontSizeSelect'); const colorInput = document.getElementById('colorInput'); // 全局数据 let chapters = []; let hasError = false; // ---------- 选中编辑功能 ---------- // 使用 mousedown 而不是 click,防止按钮点击导致选区丢失 applyFontBtn.addEventListener('mousedown', (e) => { e.preventDefault(); // 阻止默认行为,避免失去焦点 const font = fontSelect.value; document.execCommand('fontName', false, font); }); applySizeBtn.addEventListener('mousedown', (e) => { e.preventDefault(); const size = fontSizeSelect.value; // 1-7 的数字 document.execCommand('fontSize', false, size); }); applyColorBtn.addEventListener('mousedown', (e) => { e.preventDefault(); const color = colorInput.value; document.execCommand('foreColor', false, color); }); // ---------- 状态更新 ---------- function setStatus(msg, isError = false, isWarning = false) { statusMsg.innerText = msg; if (isError) { statusMsg.style.background = '#ffded9'; statusMsg.style.color = '#b33c2f'; } else if (isWarning) { statusMsg.style.background = '#fff3d6'; statusMsg.style.color = '#8a6d3b'; } else { statusMsg.style.background = '#f0e9e0'; statusMsg.style.color = '#3f352b'; } } function escapeHtml(unsafe) { return unsafe.replace(/[&<>"']/g, function(m) { if(m === '&') return '&'; if(m === '<') return '<'; if(m === '>') return '>'; if(m === '"') return '"'; return '''; }); } function extractTitleFromFilename(path) { let fileName = path.split('/').pop(); fileName = fileName.replace(/\.docx$/i, '').replace(/[_-]/g, ' '); return fileName || '未命名案例'; } // ---------- 递归解压ZIP ---------- async function extractAllFiles(zipFile) { let fileEntries = []; try { const rootZip = await JSZip.loadAsync(zipFile); const queue = [{ zip: rootZip, pathPrefix: '' }]; while (queue.length > 0) { const { zip, pathPrefix } = queue.shift(); const entries = Object.keys(zip.files); for (const relPath of entries) { const zipEntry = zip.files[relPath]; if (zipEntry.dir) continue; const fullPath = pathPrefix ? `${pathPrefix}/${relPath}` : relPath; try { if (relPath.toLowerCase().endsWith('.zip')) { const data = await zipEntry.async('arraybuffer'); const nestedZip = await JSZip.loadAsync(data); queue.push({ zip: nestedZip, pathPrefix: fullPath.replace(/\.zip$/i, '') }); } else { const data = await zipEntry.async('arraybuffer'); fileEntries.push({ path: fullPath, data: data }); } } catch (innerErr) { console.warn('跳过文件:', fullPath, innerErr); setStatus(`⚠️ 文件 ${fullPath} 解压失败,已跳过`, false, true); } } } } catch (err) { throw new Error('解压失败: ' + err.message); } return fileEntries; } // 提取 HTML(含图片) async function extractHtmlFromDocx(arrayBuffer) { const result = await mammoth.convertToHtml({ arrayBuffer }); return result.value; } // 上传处理 async function handleFileUpload(file) { setStatus('📦 正在解压...'); hasError = false; try { const allFiles = await extractAllFiles(file); const docxFiles = allFiles.filter(f => f.path.toLowerCase().endsWith('.docx')); if (docxFiles.length === 0) { setStatus('❌ 未找到 .docx 文件', true); return false; } setStatus(`🔍 找到 ${docxFiles.length} 个文档,提取中...`); docxFiles.sort((a, b) => a.path.localeCompare(b.path, 'zh')); const chapterPromises = docxFiles.map(async (file) => { try { const html = await extractHtmlFromDocx(file.data); const plainText = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); const paragraphs = plainText.split(/\n/).filter(p => p.trim().length > 0); return { title: extractTitleFromFilename(file.path), html: html, paragraphs: paragraphs }; } catch (err) { console.error('解析失败:', file.path, err); hasError = true; return { title: extractTitleFromFilename(file.path), html: '<p>文档解析错误</p>', paragraphs: ['[文档解析错误]'] }; } }); chapters = await Promise.all(chapterPromises); if (hasError) { setStatus('✅ 解析完成(部分错误),可点击「智能顺序定级」', false, true); } else { setStatus(`✅ 解析完成,共 ${chapters.length} 章`); } return true; } catch (err) { setStatus('❌ 解压失败:' + err.message, true); return false; } } // ---------- 标题识别 ---------- const headingKeywords = [ '摘要','引言','前言','背景','目的','意义','对象','方法','过程','结果', '分析','讨论','结论','建议','反思','总结','附录','参考文献','注','说明', '关键词','概述','缘由','思路','举措','成效','思考','问题','对策','缘由', '基本情况','主要做法','取得成效','存在不足','下一步打算','案例描述', '案例反思','案例背景','案例过程','案例分析','案例启示','教育建议', '教育对策','教育反思','教育叙事','教育案例','德育案例','管理案例' ]; function detectTitlePattern(text) { const trimmed = text.trim(); if (!trimmed || trimmed.length > 80 || trimmed.length < 2) return null; const patterns = [ { name: 'chinese_num', regex: /^\s*[一二三四五六七八九十百千]+[、..]/ }, { name: 'paren_chinese', regex: /^\s*[((][一二三四五六七八九十百千]+[))]/ }, { name: 'arabic_num', regex: /^\s*\d+[、..]/ }, { name: 'paren_arabic', regex: /^\s*[((]\d+[))]/ }, { name: 'multi_arabic', regex: /^\s*\d+(\.\d+)+[\s\.::]?/ } ]; for (let p of patterns) if (p.regex.test(trimmed)) return p.name; if (/[。!?;]$/.test(trimmed)) return null; if (/[::]/.test(trimmed)) { let parts = trimmed.split(/[::]/); if (parts[0].length < 25) return 'plain_heading'; } if (headingKeywords.includes(trimmed)) return 'plain_heading'; for (let kw of headingKeywords) if (trimmed.startsWith(kw) && trimmed.length < 30) return 'plain_heading'; return null; } function enhanceHeadingDetection(paragraphs) { let candidates = []; paragraphs.forEach((para, idx) => { let pattern = detectTitlePattern(para); if (pattern) candidates.push({ idx, pattern }); }); let order = []; let seen = new Set(); candidates.forEach(c => { if (!seen.has(c.pattern)) { seen.add(c.pattern); order.push(c.pattern); } }); const levelMap = {}; order.forEach((p, i) => { if (i === 0) levelMap[p] = 'h1'; else if (i === 1) levelMap[p] = 'h2'; else if (i === 2) levelMap[p] = 'h3'; else if (i === 3) levelMap[p] = 'h4'; else levelMap[p] = 'h5'; }); let classified = paragraphs.map((text, idx) => { let cand = candidates.find(c => c.idx === idx); return { text, level: cand ? levelMap[cand.pattern] : 'body' }; }); for (let i = 0; i < classified.length; i++) { if (classified[i].level === 'body') { let t = classified[i].text; if (t.length < 40 && !/[。!?;]$/.test(t)) { let prevLevel = i > 0 ? classified[i-1].level : null; let nextLevel = i < classified.length-1 ? classified[i+1].level : null; if (prevLevel && prevLevel.startsWith('h')) { classified[i].level = prevLevel; } else if (nextLevel && nextLevel.startsWith('h')) { classified[i].level = nextLevel; } } } } return classified; } // 构建预览HTML function buildPreviewHtmlFromChapters() { if (!chapters.length) return '<p style="color:#b7a48e;">请先上传并解析文档</p>'; let html = ''; for (const ch of chapters) { if (!ch.classified) continue; html += `<div class="chapter-title">${escapeHtml(ch.title)}</div>`; const tempDiv = document.createElement('div'); tempDiv.innerHTML = ch.html; const children = Array.from(tempDiv.children); if (children.length === ch.classified.length) { for (let i = 0; i < children.length; i++) { const elem = children[i]; const level = ch.classified[i].level; if (level.startsWith('h')) { const wrapper = document.createElement('div'); wrapper.className = level; wrapper.innerHTML = elem.innerHTML; html += wrapper.outerHTML; } else { html += elem.outerHTML; } } } else { let content = ch.html .replace(/<h1>/g, '<div class="h1">').replace(/<\/h1>/g, '</div>') .replace(/<h2>/g, '<div class="h2">').replace(/<\/h2>/g, '</div>') .replace(/<h3>/g, '<div class="h3">').replace(/<\/h3>/g, '</div>') .replace(/<h4>/g, '<div class="h4">').replace(/<\/h4>/g, '</div>') .replace(/<h5>/g, '<div class="h5">').replace(/<\/h5>/g, '</div>') .replace(/<h6>/g, '<div class="h5">').replace(/<\/h6>/g, '</div>') .replace(/<img([^>]+)>/g, '<div class="image-container"><img$1></div>'); html += content; } html += `<div style="page-break-before: always; margin: 0;"></div>`; } return html; } function updatePreview() { let html = buildPreviewHtmlFromChapters(); previewContent.innerHTML = html; } // 下载函数 function downloadBlob(blob, filename) { return new Promise((resolve, reject) => { try { if (typeof window.saveAs === 'function') { window.saveAs(blob, filename); resolve(); } else { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); resolve(); }, 100); } } catch (err) { reject(err); } }); } // 事件绑定 dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => e.preventDefault()); dropZone.addEventListener('drop', async (e) => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file && (file.type === 'application/zip' || file.name.endsWith('.zip'))) { const success = await handleFileUpload(file); if (success) { processBtn.disabled = false; downloadWordBtn.disabled = true; printPdfBtn.disabled = true; } } else { setStatus('请上传ZIP压缩包', true); } }); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { const success = await handleFileUpload(file); if (success) { processBtn.disabled = false; downloadWordBtn.disabled = true; printPdfBtn.disabled = true; } } }); processBtn.addEventListener('click', () => { if (!chapters.length) { setStatus('请先上传有效压缩包', true); return; } try { for (let i = 0; i < chapters.length; i++) { chapters[i].classified = enhanceHeadingDetection(chapters[i].paragraphs); } updatePreview(); downloadWordBtn.disabled = false; printPdfBtn.disabled = false; setStatus('🎯 标题识别完成,可选中文字修改样式'); } catch (err) { setStatus('❌ 处理失败: ' + err.message, true); } }); // 导出 Word downloadWordBtn.addEventListener('click', async () => { if (!chapters.length) { setStatus('没有可导出的内容', true); return; } setStatus('📄 正在准备导出 Word 文件...'); try { const fullHtml = previewContent.innerHTML; const styles = document.querySelector('style').innerHTML; const fullDocument = `<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> ${styles} .preview-content { padding: 2cm; max-height: none; overflow: visible; } body { margin: 0; background: white; } </style> </head> <body> <div class="preview-content"> ${fullHtml} </div> </body> </html>`; const blob = new Blob([fullDocument], { type: 'application/msword' }); await downloadBlob(blob, '校刊排版.doc'); setStatus('✅ 文档已导出为 .doc 格式'); } catch (err) { console.error(err); setStatus('❌ 导出失败: ' + err.message, true); } }); // 打印 PDF printPdfBtn.addEventListener('click', () => { if (!chapters.length) { setStatus('没有可打印的内容', true); return; } previewContent.classList.add('printable-area'); setTimeout(() => window.print(), 50); window.addEventListener('afterprint', function onAfterPrint() { previewContent.classList.remove('printable-area'); window.removeEventListener('afterprint', onAfterPrint); }); }); // 初始预览 previewContent.innerHTML = '<p style="color:#b7a48e; text-align:center;">上传并解析后点击「智能顺序定级」预览</p>'; })(); </script> </body> </html> -
分享一个网站维护中的网页源码 用豆包帮忙写的,分享给大家! mkwvf5xm.png图片 源码完整版: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>网站升级维护中 - 敬请期待</title> <!-- 引入 Tailwind CSS --> <script src="https://cdn.tailwindcss.com"></script> <!-- 引入 Font Awesome 图标 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <!-- 自定义 Tailwind 配置 --> <script> tailwind.config = { theme: { extend: { colors: { primary: '#3b82f6', // 主色调:蓝色 secondary: '#f97316', // 强调色:橙色 neutral: '#1f2937', // 中性色:深灰 }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], }, } } } </script> <!-- 自定义工具类 --> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .text-shadow { text-shadow: 0 2px 4px rgba(0,0,0,0.1); } .bg-gradient { background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); } } </style> </head> <body class="min-h-screen bg-gradient flex flex-col items-center justify-center px-4 py-8 font-sans text-neutral"> <!-- 页面容器 --> <div class="max-w-lg w-full bg-white rounded-2xl shadow-xl p-8 md:p-10 transform transition-all duration-300 hover:shadow-2xl"> <!-- 图标区域 --> <div class="flex justify-center mb-6"> <div class="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center"> <i class="fa-solid fa-gears text-5xl text-primary animate-spin [animation-duration:3s]"></i> </div> </div> <!-- 标题和描述 --> <div class="text-center mb-8"> <h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-neutral mb-4 text-shadow">网站升级维护中</h1> <p class="text-gray-600 text-lg leading-relaxed mb-6"> 为了给你提供更好的服务体验,我们正在对网站进行升级优化。 升级期间网站暂时无法访问,给你带来不便,敬请谅解。 </p> <!-- 预计恢复时间 --> <div class="bg-secondary/10 rounded-lg p-4 inline-block mb-4"> <p class="text-secondary font-semibold"> <i class="fa-solid fa-clock mr-2"></i> 预计恢复时间:2026年02月01日 08:00 </p> </div> </div> <!-- 联系方式 --> <div class="border-t border-gray-200 pt-6 text-center"> <h2 class="font-semibold text-lg mb-4">如有紧急需求,请通过以下方式联系我们</h2> <div class="flex flex-col md:flex-row justify-center gap-4"> <a href="tel:4008888888" class="flex items-center justify-center gap-2 bg-primary/5 hover:bg-primary/10 transition-colors rounded-lg px-6 py-3"> <i class="fa-solid fa-phone text-primary"></i> <span class="text-primary font-medium">400-888-8888</span> </a> <a href="mailto:support@example.com" class="flex items-center justify-center gap-2 bg-primary/5 hover:bg-primary/10 transition-colors rounded-lg px-6 py-3"> <i class="fa-solid fa-envelope text-primary"></i> <span class="text-primary font-medium">support@example.com</span> </a> </div> </div> <!-- 页脚 --> <div class="mt-8 text-center text-gray-500 text-sm"> <p>© 2026 你的公司名称 版权所有</p> </div> </div> <!-- 页面加载动画(可选) --> <script> // 页面加载完成后的简单动画效果 document.addEventListener('DOMContentLoaded', () => { const container = document.querySelector('.max-w-lg'); container.classList.add('opacity-100'); container.classList.remove('opacity-0'); }); </script> </body> </html> -
2025年输入法集体“变难用”:政策、技术与商业的三重博弈 mk2ru3qt.png图片 2025年,不少用户发现手机和电脑里的输入法突然“不听话”了:熟悉的方言联想频频出错,输入短句却弹出冗长的AI文案,打字间隙的广告弹窗防不胜防,甚至卸载后仍会收到推广推送。从搜狗、百度到讯飞,曾经精准高效的输入工具仿佛集体“失准”,这场全行业的体验滑坡并非偶然,而是政策监管收紧、技术迭代失衡与商业变现焦虑三重因素叠加的必然结果。 政策合规压力的集中传导,是输入法体验变形的直接导火索。2025年,三部门联合强化《信息技术产品国家通用语言文字使用管理规定》的实施,要求输入法等信息技术产品必须通过国家通用语言文字使用符合性认证。与此同时,修订后的《国家通用语言文字法》正式落地,明确将App等线上产品纳入规范管理,公共服务场景优先使用普通话和规范汉字,对方言、繁体用字的使用场景做出严格限制。为应对合规要求,各输入法厂商紧急调整词库与识别逻辑,大幅弱化方言识别能力——原本能精准识别粤语“早晨”“饮茶”的联想功能,如今要么直接屏蔽,要么错译为普通话表述;部分地区用户习惯的特色俚语更是从词库中消失,导致输入效率断崖式下降。更值得注意的是,基层执行中“宁紧不松”的倾向,让厂商在功能调整上过度保守,甚至连正常的个性化词库同步都受到限制,原本适配个人输入习惯的“智能”属性大打折扣。 AI大模型的仓促落地,让输入法陷入“为智能而智能”的功能陷阱。2025年成为输入法行业的“AI军备竞赛年”,搜狗、百度、讯飞等头部厂商纷纷将大模型技术嵌入产品核心功能,试图从“输入工具”向“智能交互中枢”转型。但技术迭代的速度远超用户适应周期,更出现了明显的“水土不服”:百度输入法的“超会写”功能,在用户输入“拒绝”二字时,直接生成三段不同语气的完整文案,反而打乱正常输入节奏;讯飞输入法的端侧大模型虽号称离线识别率达98%,但连续输入2000字后就会出现内存占用激增、输入卡顿的问题,在中低端设备上表现尤为糟糕。更关键的是,大模型让输入法的核心逻辑从“猜词”变成“懂话”,但这种转变并未匹配用户需求——用户需要的是精准补全,而非意图解读;期待的是效率提升,而非额外的文案生成操作。技术升级的方向与用户核心需求的错位,让“智能增强”最终沦为“体验负担”。 商业变现的焦虑式扩张,则彻底击穿了输入法的体验底线。随着市场格局趋于稳定,搜狗、百度、讯飞等厂商占据84.4%的市场份额,形成寡占竞争格局,但增速分化加剧了盈利压力——讯飞以11.2%的增速领跑,而搜狗仅维持3.4%的增长,市场份额持续被蚕食。在这种背景下,广告成为厂商增收的核心抓手,且推送方式愈发激进。搜狗输入法成为重灾区,不仅在输入界面频繁弹出皮肤推荐、游戏广告,更被曝光通过后台模块篡改浏览器配置、卸载后仍弹窗推送的“流氓操作”,多位网络大V公开吐槽其“把用户电脑当菜市场”。即便是主打技术优势的百度和讯飞,也未能幸免:百度输入法在游戏模式下植入RGB键盘皮肤推广,讯飞则在语音转文字界面夹带办公软件广告。据新浪发起的投票显示,95.86%的用户无法接受输入法插入广告,而这种“流量变现优先于用户体验”的策略,直接导致了用户对输入法“难用”的集体感知。 此外,行业竞争的差异化跑偏,让输入法陷入“功能堆砌”的怪圈。为抢占细分场景,厂商纷纷推出针对性功能:搜狗主打专业词库,却忽略了普通用户的基础需求;百度深耕游戏与年轻用户市场,导致专业场景术语联想准确率低下,曾将“缔约过失”错改为“钓鱼方式”;讯飞聚焦语音输入效率,却在内存管理上出现明显短板。这种“为差异化而差异化”的竞争逻辑,让输入法从“全而精”变成“偏而杂”,多数用户用不上的专业功能占用大量系统资源,而核心的输入体验却被忽视。 2025年输入法的集体“变难用”,本质上是行业发展节奏与用户需求的严重错位。政策合规的初衷是规范语言使用、降低沟通成本,技术升级的目标是提升效率,商业变现更是企业生存的必然需求,但当这三者都以牺牲用户核心体验为代价时,体验滑坡便不可避免。对于用户而言,输入法的核心价值始终是“精准、高效、简洁”;对于厂商来说,如何在政策红线、技术创新与商业利益之间找到平衡,让功能升级回归用户需求本质,才是走出当前体验困境的关键。毕竟,再先进的大模型、再丰富的商业场景,都不能替代“打字不费力、联想够精准”的基础体验——这正是2025年输入法行业最该反思的核心命题。 -
从“管道”到“智能中枢”:AI原生网络正在重构数字世界 如果说前几年我们还在惊叹5G带来的高速下载体验,那现在科技圈的焦点已经悄悄转向了一个更具颠覆性的概念——AI原生网络。可能很多人对这个词还很陌生,甚至会把它和“AI赋能网络”混为一谈,但只要你留意身边的智能设备协同、远程工业控制这些场景,就能发现它已经在悄然落地。和传统网络只负责数据传输的“管道”角色不同,AI原生网络是把人工智能深度植入网络架构的每一个环节,让网络自己具备感知、决策和优化能力,从被动响应变成主动服务,这波技术变革的影响力,可能远超我们想象。 先说说最直观的区别:以前的网络升级,大多是“修管道”——要么加宽带宽,要么降低时延,本质上还是在提升数据传输的基础能力。但AI原生网络是直接把“大脑”装在了管道里。比如在智慧工厂里,上百个工业机器人要实时协同作业,传统网络很难精准匹配每个机器人的动态带宽需求,偶尔会出现指令延迟的情况。而AI原生网络能提前预判机器人的作业节奏,自动分配网络资源,甚至在故障发生前就主动排查隐患。中国电信之前做的IMS网络自智示范应用,就把故障处置效率提升了30%,这就是AI原生网络的核心优势:不是事后补救,而是事前预判、动态适配。 mk2rpxyf.png图片 可能有人会问,这种技术离我们普通人远吗?其实一点都不远。现在很多人家里有智能音箱、扫地机器人、智能门锁等一堆智能设备,经常会遇到设备之间“互不兼容”“响应卡顿”的问题,这本质上是传统网络无法高效支撑多智能设备协同的短板。而AI原生网络构建的“云-边-端”三级协同架构,能让数据处理更贴近设备终端——比如扫地机器人的路径规划数据不用再传到遥远的云端,在家庭边缘节点就能完成计算,响应速度会快很多;同时云端还能统一协调所有设备的网络资源,避免高峰期卡顿。未来我们畅想的“人车家全场景一体化”智慧生活,靠的就是AI原生网络这个核心支撑。 更值得关注的是,AI原生网络正在和6G、算力网络这些前沿技术深度绑定,形成“双向赋能”的格局。一方面,AI原生网络能为6G的空天地一体化网络提供智能调度能力——未来成千上万颗低轨卫星组成的全球网络,需要精准协调星地之间的通信资源,这只能靠AI原生技术实现;另一方面,6G的高速率、广覆盖特性,又能为AI原生网络提供更广阔的应用舞台,让智能调度能力延伸到海洋、沙漠等传统网络覆盖不到的区域。中国电信提出的云网融合2035规划,就明确了先通过AI驱动网络优化,再逐步建成AI原生云网基础设施的路径,这也和6G的商用时间表完美衔接。 当然,这个技术现在还处在发展初期,还有很多难题要攻克。比如算法的伦理风险——网络自主决策会不会出现误判?不同厂商的AI原生设备如何实现标准统一?这些都是行业需要共同解决的问题。但不可否认的是,AI原生网络已经从实验室走向了产业应用的关键阶段,中国电信、中国联通等企业已经在工业互联网、智慧园区等场景实现了规模化试点。 回过头看,从1G到5G,我们解决的是“能不能连、连得快不快”的问题;而AI原生网络加上6G,要解决的是“能不能智能连接、能不能赋能万物”的问题。当网络不再只是冰冷的管道,而是变成有感知、有智慧的中枢,它能催生的不仅是更流畅的个人体验,更是工业制造、远程医疗、低空经济等千行百业的新业态。这大概就是AI原生网络最吸引人的地方——它不是对现有技术的小修小补,而是对数字世界底层架构的一次重构。 -
如何调整电脑上各个软件媒体声音大小? 不知道大家有没有这样的场景,就是在使用电脑播放媒体资料时,会发现,如果两个或者多个平台同时播放的时候,会导致声音冲突,会比较混乱,但是无法单独关闭某个平台的音频开关和声音大小,那我们怎么去解决这个问题呢? 其实电脑上有很多实用的系统设置,是可以直接单独控制平台媒体声音大小的,比如我们在挂网课的时候,也可以同时查阅相关资料。 话不多说,跟着可乐操作。 首先打开电脑右下角的声音按钮: mhj7sshj.png图片 点击右下角的“ 选择声音输出 ” mhj7sy70.png图片 点击后,往下翻,找到音量合成器,直接在这里调整软件的声音大小,会发现,每个软件的声音大小都可以调节控制。 mhj7t980.png图片 感谢大家的观看,我已经很久没有跟大家做技术分享了,希望以后能继续给大家分享更多实用高效的办公经验。 -
-
-
发现一个有趣的便签墙网站 发现一个不错的网站,这个网站作者是:https://github.com/uninto/notes,纯html、css、js实现。 有人说这个类似之前的许愿墙,我觉得也可以把这个元素加进去,毕竟我群里很多宅男腐女,最喜欢这个玩意儿了,晚些我直接上线到我的工具箱里 mhgb3msq.png图片 因为这个只有一个前端页面,代码如下,我就准备做个后端出来,到时候重新开个帖子,供大家使用。 <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>便签墙</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background-image: linear-gradient(0deg, #eee 1px, transparent 0), linear-gradient(90deg, #eee 1px, transparent 0); background-size: 30px 30px; color: #333; min-height: 100dvh; overflow: hidden; } body.has-maximized-card { overflow: hidden; } body.is-mobile { overflow-y: auto; } #board { position: relative; width: 100vw; height: 100dvh; overflow: hidden; } body.is-mobile #board { height: auto; min-height: 100dvh; } .card { position: absolute; width: 220px; border-radius: 12px; box-shadow: 0 16px 35px rgba(0, 0, 0, 0.2); background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); overflow: hidden; opacity: 0; transform-origin: center; transition: transform 0.35s ease, opacity 0.35s ease, left 0.35s ease, top 0.35s ease, width 0.35s ease, height 0.35s ease, border-radius 0.35s ease; } .card.dragging { transition: none; box-shadow: 0 22px 45px rgba(0, 0, 0, 0.35); } .card.maximized { position: fixed; inset: 0; width: 100vw; height: 100vh; height: 100dvh; border-radius: 0; box-shadow: 0 28px 60px rgba(0, 0, 0, 0.4); } .card-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; background: rgba(255, 255, 255, 0.7); cursor: grab; user-select: none; touch-action: pan-y; } .card-header.dragging { cursor: grabbing; } .window-controls { display: flex; align-items: center; gap: 6px; } .window-controls .control { position: relative; width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.08); background: #ccc; cursor: pointer; outline: none; padding: 0; display: inline-flex; align-items: center; justify-content: center; } .window-controls .control.close { background: #ff5f57; border-color: #e0443e; } .window-controls .control.minimize { background: #febb2e; border-color: #dea123; } .window-controls .control.maximize { background: #28c840; border-color: #1aab2c; } .window-controls .control::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); opacity: 0; transition: opacity 0.2s ease; } .card-header:hover .window-controls .control::after { opacity: 0.8; } .window-controls .control.close::after { content: '×'; width: auto; height: auto; background: none; font-size: 10px; line-height: 1; font-weight: 700; color: rgba(0, 0, 0, 0.7); } .window-controls .control.minimize::after { width: 6px; height: 2px; background: rgba(0, 0, 0, 0.6); } .window-controls .control.maximize::after { width: 6px; height: 6px; background: linear-gradient( 45deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.6) 45%, transparent 45%, transparent 55%, rgba(0, 0, 0, 0.6) 55%, rgba(0, 0, 0, 0.6) 100% ); } .card-title { font-size: 13px; font-weight: 600; color: rgba(0, 0, 0, 0.55); padding-left: 10px; flex: 1; } .card-body { padding: 16px; font-size: 16px; line-height: 1.4; font-weight: 600; color: rgba(0, 0, 0, 0.72); word-break: break-word; overflow-wrap: anywhere; white-space: normal; } .card.maximized { display: flex; flex-direction: column; } .card.maximized .card-title { display: none; } .card.maximized .card-body { flex: 1; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; text-align: center; padding: clamp(32px, min(10vw, 10vh), 128px); padding-top: clamp(72px, min(14vw, 14vh), 192px); font-size: clamp(48px, min(18vw, 18vh), 200px); line-height: 1.05; } @media (max-width: 768px) { .card { width: 180px; border-radius: 10px; } .card-body { padding: 14px; font-size: 14px; } .card-title { font-size: 12px; } } </style> </head> <body> <div id="board"></div> <script> const board = document.getElementById('board') const messages = [ '保持好心情', '多喝水哦', '今天辛苦啦', '早点休息', '记得吃水果', '加油,你可以的', '祝你顺利', '保持微笑呀', '愿所有烦恼都消失', '期待下一次见面', '梦想总会实现', '天气冷了,多穿衣服', '记得给自己放松', '每天都要元气满满', '今天也要好好爱自己', '适当休息一下' ] const colors = [ '#ffe0e3', '#c7f0ff', '#ffd8a8', '#d9f2d9', '#e5d7ff', '#f9f7d9', '#d2f0f8', '#ffd4f5' ] const cardStates = new WeakMap() // Reserve a very high层级给全屏卡片,避免被后续元素覆盖 const MAXIMIZED_LAYER = 1000000 let activeMaximizedCard = null const pointerMediaQuery = window.matchMedia('(pointer: coarse)') let isMobile = pointerMediaQuery.matches || window.innerWidth <= 768 let maxCards = isMobile ? 120 : 180 // 限制 DOM 节点数量,减轻移动端压力 const initialCardCount = isMobile ? 18 : 30 let spawnInterval = isMobile ? 700 : 400 let zIndexCursor = 200 let spawnTimer = null document.body.classList.toggle('is-mobile', isMobile) function randomFrom(array) { return array[Math.floor(Math.random() * array.length)] } function clamp(value, min, max) { return Math.min(Math.max(value, min), max) } function applyTransform(card, state) { const scale = state.scale ?? 1 const angle = state.angle ?? 0 card.style.transform = `scale(${scale}) rotate(${angle}deg)` } function bringToFront(card) { if (card === activeMaximizedCard) { card.style.zIndex = MAXIMIZED_LAYER return } zIndexCursor += 1 if (activeMaximizedCard && zIndexCursor >= MAXIMIZED_LAYER) { zIndexCursor = MAXIMIZED_LAYER - 1 } card.style.zIndex = zIndexCursor } function updateBodyMaximizedState() { document.body.classList.toggle( 'has-maximized-card', Boolean(activeMaximizedCard) ) } function scheduleNextSpawn() { clearTimeout(spawnTimer) spawnTimer = setTimeout(() => { if (!document.hidden) { createCard() } scheduleNextSpawn() }, spawnInterval) } function syncMobileMode() { const nextIsMobile = pointerMediaQuery.matches || window.innerWidth <= 768 if (nextIsMobile === isMobile) return isMobile = nextIsMobile maxCards = isMobile ? 120 : 180 spawnInterval = isMobile ? 700 : 400 document.body.classList.toggle('is-mobile', isMobile) scheduleNextSpawn() } function handleBoardClick(event) { const control = event.target.closest('.control') if (!control) return const card = control.closest('.card') if (!card || !board.contains(card)) return event.preventDefault() if (control.classList.contains('close')) { closeCard(card) } else if (control.classList.contains('minimize')) { minimizeCard(card) } else if (control.classList.contains('maximize')) { toggleMaximize(card) } } function handleBoardPointerDown(event) { const card = event.target.closest('.card') if (!card || !board.contains(card)) return const control = event.target.closest('.control') const header = event.target.closest('.card-header') const pointerType = event.pointerType || 'mouse' const isPrimaryPointer = event.isPrimary !== false if ( header && !control && pointerType !== 'touch' && isPrimaryPointer ) { startDrag(event, card) return } bringToFront(card) } function handleBoardDoubleClick(event) { const header = event.target.closest('.card-header') if (!header || event.target.closest('.control')) return const card = header.closest('.card') if (!card || !board.contains(card)) return toggleMaximize(card) } board.addEventListener('click', handleBoardClick) board.addEventListener('pointerdown', handleBoardPointerDown) board.addEventListener('dblclick', handleBoardDoubleClick) function closeCard(card) { const state = cardStates.get(card) if (!state || state.closing) return if (card === activeMaximizedCard) { activeMaximizedCard = null updateBodyMaximizedState() } state.closing = true state.scale = 0.1 card.style.opacity = '0' applyTransform(card, state) const handleTransitionEnd = event => { if (event.propertyName === 'opacity') { card.removeEventListener('transitionend', handleTransitionEnd) card.remove() } } card.addEventListener('transitionend', handleTransitionEnd) } function minimizeCard(card) { const state = cardStates.get(card) if (!state || state.closing) return // 最小化动画:缩小并淡出到底部,结束时移除节点释放内存 const runMinimize = () => { state.closing = true bringToFront(card) const bottom = Math.max(window.innerHeight - 24, 0) const targetLeft = clamp( state.left, 16, Math.max(window.innerWidth - card.offsetWidth - 16, 16) ) state.left = targetLeft state.top = bottom state.scale = 0.1 state.angle = 0 card.style.left = `${targetLeft}px` card.style.top = `${bottom}px` card.style.opacity = '0.35' applyTransform(card, state) const handleTransitionEnd = event => { if (event.propertyName === 'transform') { card.removeEventListener('transitionend', handleTransitionEnd) card.remove() } } card.addEventListener('transitionend', handleTransitionEnd) } if (state.maximized) { restoreFromMaximize(card, state) requestAnimationFrame(() => { requestAnimationFrame(runMinimize) }) return } runMinimize() } function toggleMaximize(card) { const state = cardStates.get(card) if (!state || state.closing) return if (state.maximized) { restoreFromMaximize(card, state) } else { maximizeCard(card, state) } } function maximizeCard(card, state) { if (activeMaximizedCard && activeMaximizedCard !== card) { const activeState = cardStates.get(activeMaximizedCard) if (activeState) { restoreFromMaximize(activeMaximizedCard, activeState) } } state.beforeMaximize = { left: state.left, top: state.top, scale: state.scale ?? 1, angle: state.angle ?? 0, width: card.offsetWidth, height: card.offsetHeight, inlinePosition: card.style.position } card.classList.add('maximized') card.style.position = 'fixed' card.style.left = '0px' card.style.top = '0px' card.style.width = '100vw' card.style.height = '100dvh' card.style.borderRadius = '0' state.left = 0 state.top = 0 state.scale = 1 state.angle = 0 applyTransform(card, state) activeMaximizedCard = card bringToFront(card) state.maximized = true updateBodyMaximizedState() } function restoreFromMaximize(card, state) { const previous = state.beforeMaximize if (!previous) return card.classList.remove('maximized') card.style.position = previous.inlinePosition || 'absolute' card.style.left = `${previous.left}px` card.style.top = `${previous.top}px` card.style.width = `${previous.width}px` card.style.height = `${previous.height}px` card.style.borderRadius = '12px' state.left = previous.left state.top = previous.top state.scale = previous.scale ?? 1 state.angle = previous.angle ?? state.angle ?? 0 applyTransform(card, state) state.maximized = false if (activeMaximizedCard === card) { activeMaximizedCard = null updateBodyMaximizedState() } bringToFront(card) setTimeout(() => { if (!state.maximized) { card.style.width = '' card.style.height = '' card.style.borderRadius = '' if (previous.inlinePosition) { card.style.position = previous.inlinePosition } else { card.style.position = '' } state.beforeMaximize = null } }, 360) } function startDrag(event, card) { const control = event.target.closest('.control') if (control) return const state = cardStates.get(card) if (!state || state.closing || state.maximized) return // 鼠标拖拽使用 rAF 节流,避免频繁触发布局计算 event.preventDefault() bringToFront(card) const header = card.querySelector('.card-header') card.classList.add('dragging') header.classList.add('dragging') state.dragging = true state.dragOffsetX = event.clientX - state.left state.dragOffsetY = event.clientY - state.top let dragFrame = null let pendingLeft = state.left let pendingTop = state.top const commitDrag = () => { dragFrame = null const maxLeft = Math.max(window.innerWidth - card.offsetWidth, 0) const maxTop = Math.max(window.innerHeight - card.offsetHeight, 0) state.left = clamp(pendingLeft, -card.offsetWidth * 0.4, maxLeft) state.top = clamp(pendingTop, -card.offsetHeight * 0.4, maxTop) card.style.left = `${state.left}px` card.style.top = `${state.top}px` } const handlePointerMove = moveEvent => { if (!state.dragging) return pendingLeft = moveEvent.clientX - state.dragOffsetX pendingTop = moveEvent.clientY - state.dragOffsetY if (dragFrame === null) { dragFrame = requestAnimationFrame(commitDrag) } } const handlePointerUp = () => { state.dragging = false card.classList.remove('dragging') header.classList.remove('dragging') if (dragFrame !== null) { cancelAnimationFrame(dragFrame) commitDrag() } document.removeEventListener('pointermove', handlePointerMove) document.removeEventListener('pointerup', handlePointerUp) } document.addEventListener('pointermove', handlePointerMove) document.addEventListener('pointerup', handlePointerUp) } function createCard() { const card = document.createElement('div') card.className = 'card' const color = randomFrom(colors) const angleRange = isMobile ? 6 : 10 const angle = (Math.random() - 0.5) * angleRange const entryScale = isMobile ? 0.8 : 0.65 const cardWidth = isMobile ? 180 : 220 const cardHeight = isMobile ? 130 : 140 const horizontalMargin = isMobile ? 12 : 16 const verticalMargin = isMobile ? 12 : 20 const left = horizontalMargin + Math.random() * Math.max(window.innerWidth - cardWidth - horizontalMargin * 2, 0) const top = verticalMargin + Math.random() * Math.max(window.innerHeight - cardHeight - verticalMargin * 2, 0) card.style.background = color card.style.left = `${left}px` card.style.top = `${top}px` card.style.opacity = '0' if (activeMaximizedCard && zIndexCursor >= MAXIMIZED_LAYER - 2) { zIndexCursor = MAXIMIZED_LAYER - 2 } card.style.zIndex = ++zIndexCursor card.innerHTML = ` <div class="card-header"> <div class="window-controls"> <button class="control close" type="button" aria-label="关闭"></button> <button class="control minimize" type="button" aria-label="最小化"></button> <button class="control maximize" type="button" aria-label="最大化"></button> </div> <div class="card-title">温馨提示</div> </div> <div class="card-body">${randomFrom(messages)}</div> ` const state = { angle, scale: entryScale, left, top, maximized: false, closing: false } cardStates.set(card, state) applyTransform(card, state) board.appendChild(card) requestAnimationFrame(() => { requestAnimationFrame(() => { state.scale = 1 applyTransform(card, state) card.style.opacity = '1' }) }) if (board.children.length > maxCards) { const oldest = board.firstElementChild if (oldest && oldest !== card) { oldest.remove() } } } for (let i = 0; i < initialCardCount; i++) { setTimeout(createCard, i * (isMobile ? 60 : 40)) } scheduleNextSpawn() document.addEventListener('visibilitychange', () => { if (!document.hidden) { scheduleNextSpawn() } }) if (typeof pointerMediaQuery.addEventListener === 'function') { pointerMediaQuery.addEventListener('change', syncMobileMode) } else if (typeof pointerMediaQuery.addListener === 'function') { pointerMediaQuery.addListener(syncMobileMode) } window.addEventListener('resize', syncMobileMode) </script> </body> </html>