Google AI Ultra 拼车, 175/人/月
今天刚开,先开三个月,到期考虑换号重续优惠
拉微信群月付,先上车后付费 有需求请私信或联系: https://t.me/ikcisczh
要求美区账号,禁止反代
帖子还在就是有位置
xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。
今天刚开,先开三个月,到期考虑换号重续优惠
拉微信群月付,先上车后付费 有需求请私信或联系: https://t.me/ikcisczh
要求美区账号,禁止反代
帖子还在就是有位置
本文档系统性总结前端开发中颜色透明度处理的完整方案,涵盖 JavaScript 转换函数、CSS 原生特性、性能优化策略及工程化实践,为开发者提供从基础到进阶的技术参考。 CSS 场景 1:菜单选中状态背景 场景 2:卡片背景 场景 3:搜索高亮 场景 4:按钮悬停效果 场景 5:渐变背景 使用 8 位十六进制颜色值,最后两位表示透明度(00-FF)。 直接使用 rgba 颜色值,最直观的方式。 如需支持旧版浏览器,建议使用 JavaScript 方法或 rgba()。 兼容性较好,可放心使用。概述
一、JavaScript 颜色转换函数
1.1 hexToRGBA - 基础转换函数
/**
* 将十六进制颜色转换为 RGBA 格式
* @param hex 十六进制颜色值,如 '#1890ff'
* @param alpha 透明度,范围 0-1
* @returns rgba 格式的颜色字符串
*/
export const hexToRGBA = (hex: string, alpha: number) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
// 使用示例
const color = hexToRGBA('#1890ff', 0.5); // 'rgba(24, 144, 255, 0.5)'1.2 addColorOpacity - 带容错的转换函数
/**
* 添加颜色透明度(带容错处理)
* @param hexColor 十六进制颜色值,如 '#FF5733'
* @param opacity 透明度,范围 0-1
* @returns rgba 格式的颜色字符串
*/
export const addColorOpacity = (hexColor: string, opacity: number): string => {
if (!hexColor) return 'rgba(0, 0, 0, 0.1)';
// 移除 # 号
const hex = hexColor.replace('#', '');
// 解析RGB值
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
// 返回rgba格式
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
// 使用示例
const color1 = addColorOpacity('#FF5733', 0.8); // 'rgba(255, 87, 51, 0.8)'
const color2 = addColorOpacity('', 0.5); // 'rgba(0, 0, 0, 0.1)' (容错)二、CSS 原生方法
2.1 color-mix() - 现代 CSS 方法
color-mix() 函数可以混合两种颜色,常用于创建半透明效果。基础语法
color-mix(in srgb, 颜色1 百分比, 颜色2)使用场景
.menu-item-selected {
color: var(--primary-color);
background-color: color-mix(in srgb, var(--primary-color) 5%, transparent);
}const StyledCard = styled.div`
padding: 10px;
background-color: color-mix(in srgb, var(--primary-color) 5%, transparent);
border-radius: 4px;
`;const highlightText = (text: string, keyword: string) => {
return text.replace(
new RegExp(keyword, 'g'),
`<span style="color: var(--primary-color); background: color-mix(in srgb, var(--primary-color) 10%, transparent)">${keyword}</span>`,
);
};const Button = styled.button<{ $themeColor: string }>`
background-color: ${(props) => `color-mix(in srgb, ${props.$themeColor} 10%, transparent)`};
&:hover {
background-color: ${(props) => `color-mix(in srgb, ${props.$themeColor} 15%, transparent)`};
}
`;const GradientBox = styled.div<{ $color: string }>`
background: linear-gradient(
90deg,
${(props) => `color-mix(in srgb, ${props.$color} 10%, transparent)`} 0%,
rgba(255, 255, 255, 0) 100%
);
`;2.2 十六进制 8 位格式 (#RRGGBBAA)
透明度对照表
透明度 十六进制 示例 常见用途 100% (1.0) FF #1890ffFF 完全不透明 90% (0.9) E6 #1890ffE6 主要内容 80% (0.8) CC #1890ffCC 次要内容 70% (0.7) B3 #1890ffB3 辅助信息 60% (0.6) 99 #1890ff99 遮罩层 50% (0.5) 80 #1890ff80 半透明效果 40% (0.4) 66 #1890ff66 禁用状态 30% (0.3) 4D #1890ff4D 水印、占位符 20% (0.2) 33 #1890ff33 分割线 10% (0.1) 1A #1890ff1A 背景色、悬停效果 5% (0.05) 0D #1890ff0D 极浅背景 0% (0.0) 00 #1890ff00 完全透明 使用示例
// 按钮背景
const Button = styled.button`
background: #ffffff99; // 白色 60% 透明度
`;
// 文本颜色
<span style={{ color: '#626060ff' }}>提示文本</span>
// 遮罩层
const Overlay = styled.div`
background: #00000080; // 黑色 50% 透明度
`;转换函数
/**
* 将透明度(0-1)转换为十六进制(00-FF)
* @param alpha 透明度,范围 0-1
* @returns 两位十六进制字符串
*/
const alphaToHex = (alpha: number): string => {
const value = Math.round(Math.min(1, Math.max(0, alpha)) * 255);
return value.toString(16).padStart(2, '0').toUpperCase();
};
// 使用示例
const color1 = `#1890ff${alphaToHex(0.5)}`; // '#1890ff80'
const color2 = `#1890ff${alphaToHex(0.6)}`; // '#1890ff99'
/**
* 将十六进制颜色添加透明度
* @param hex 十六进制颜色,如 '#1890ff'
* @param alpha 透明度,范围 0-1
* @returns 8位十六进制颜色
*/
const addAlphaToHex = (hex: string, alpha: number): string => {
const cleanHex = hex.replace('#', '');
return `#${cleanHex}${alphaToHex(alpha)}`;
};
// 使用示例
const transparentColor = addAlphaToHex('#1890ff', 0.5); // '#1890ff80'2.3 rgba() - 传统方法
// 在 styled-components 中使用
const StyledDiv = styled.div`
background-color: rgba(24, 144, 255, 0.1);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
`;
// 在内联样式中使用
<div style={{ backgroundColor: 'rgba(24, 144, 255, 0.1)' }}>内容</div>三、实际应用场景
3.1 从图片提取主色并设置透明度
/**
* 从图片提取主色
* @param url 图片地址
* @returns rgba 格式的颜色字符串
*/
const extractColor = (url: string): Promise<string> => {
return new Promise((resolve) => {
try {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = url;
img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return resolve('rgba(0,0,0,0.2)');
const w = 20;
const h = 20;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
let r = 0,
g = 0,
b = 0,
count = 0;
for (let i = 0; i < data.length; i += 4) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
count++;
}
r = Math.round(r / count);
g = Math.round(g / count);
b = Math.round(b / count);
// 返回带透明度的颜色
resolve(`rgba(${r}, ${g}, ${b}, 0.6)`);
} catch {
resolve('rgba(0,0,0,0.2)');
}
};
img.onerror = () => resolve('rgba(0,0,0,0.2)');
} catch {
resolve('rgba(0,0,0,0.2)');
}
});
};
// 动态调整透明度
const adjustOpacity = (color: string): string => {
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (match) {
const r = Number(match[1]);
const g = Number(match[2]);
const b = Number(match[3]);
const a = match[4] !== undefined ? Math.min(1, Number(match[4]) + 0.4) : 0.4;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return 'rgba(0,0,0,0.2)';
};3.2 主题色透明度处理
// 根据主题色生成不同透明度的颜色
const ThemeCard = styled.div<{ $color?: string }>`
background-color: ${(props) => {
if (props.$color) {
const hex = props.$color;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.05)`;
}
return 'rgba(24, 144, 255, 0.05)';
}};
border-radius: 8px;
padding: 16px;
`;3.3 条件透明度
// 根据状态设置不同透明度
const StatusBadge = styled.div<{ active: boolean }>`
background-color: ${({ active }) => (active ? 'rgba(24, 144, 255, 0.1)' : 'rgba(0, 0, 0, 0.04)')};
padding: 4px 8px;
border-radius: 4px;
`;3.4 悬停效果
const InteractiveButton = styled.button`
background-color: rgba(0, 0, 0, 0.04);
border: none;
&:hover {
background-color: rgba(0, 0, 0, 0.06);
}
`;四、方法对比
方法 优点 缺点 适用场景 hexToRGBA()简单直接,兼容性好 需要手动计算 动态颜色转换 addColorOpacity()带容错处理 代码稍多 需要容错的场景 color-mix()CSS 原生,性能好 浏览器兼容性要求高 现代浏览器项目 #RRGGBBAA简洁,性能最好 透明度不直观 固定透明度值 rgba()最直观 不够灵活 固定颜色值 五、最佳实践
5.1 选择合适的方法
// ✅ 推荐:动态颜色使用 hexToRGBA
const dynamicColor = hexToRGBA(userColor, 0.5);
// ✅ 推荐:CSS 变量使用 color-mix
background-color: color-mix(in srgb, var(--primary-color) 10%, transparent);
// ✅ 推荐:固定颜色使用 8 位十六进制(性能最好)
background-color: #1890ff1A; // 10% 透明度
// ✅ 推荐:需要直观表达时使用 rgba
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);5.2 透明度值规范
// 常用透明度值
const OPACITY = {
HOVER: 0.06, // 悬停效果
BACKGROUND: 0.05, // 背景色
DISABLED: 0.4, // 禁用状态
SHADOW: 0.1, // 阴影
OVERLAY: 0.6, // 遮罩层
};
// 使用示例
const hoverColor = hexToRGBA('#1890ff', OPACITY.HOVER);5.3 性能优化
// ❌ 避免:频繁计算
const Component = () => {
return <div style={{ backgroundColor: hexToRGBA('#1890ff', 0.1) }}>内容</div>;
};
// ✅ 推荐:缓存计算结果
const COLORS = {
primaryBg: hexToRGBA('#1890ff', 0.1),
primaryHover: hexToRGBA('#1890ff', 0.15),
};
const Component = () => {
return <div style={{ backgroundColor: COLORS.primaryBg }}>内容</div>;
};5.4 类型安全
// 定义颜色类型
type HexColor = `#${string}`;
type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
// 类型安全的转换函数
export const hexToRGBA = (hex: HexColor, alpha: number): RGBAColor => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})` as RGBAColor;
};六、浏览器兼容性
color-mix() 兼容性
RRGGBBAA 兼容性
七、参考资源
👉 访问地址:免费 SSL 证书签发 兄弟们,最近运维圈有个大事:2026 年 3 月 15 日起,行业新规正式生效。原本能管一年的付费证书,最长有效期也被强行砍到了 200 天。 这就非常尴尬了:以前很多人花钱买证书是为了“一年才折腾一次”,现在既然连付费的都要半年折腾一回,那跟 90 天有效期的 Let's Encrypt 相比,手动更新的性价比已经彻底没了。 既然横竖都要频繁折腾,为什么不直接搞个“全自动续期”?上周我因为忘记续期导致子域名报不安全,被吐槽了一顿,痛定思痛,我自己撸了一个自动化工具分享给大家。 目前市面上的工具要么太笨重(要装各种 Certbot 依赖、跑命令行),要么太封闭(云厂商工具只管自家的,跨云就不灵了)。 我的需求很简单: 这个小工具目前部署在 AWS 上,还是 MVP 阶段,界面走极简风格。大家如果正好有证书快到期的,欢迎来“救急”测试,反馈 Bug: 👉 访问地址:免费 SSL 证书签发 PS: 暂时只做了基础功能。如果大家有需要增加其他解析商(如华为云、Cloudflare)支持的,可以在评论区留言,我在线蹲一个反馈! 标签: #SSL证书 #HTTPS #自动化运维 #阿里云 #腾讯云 #acme.sh #后端开发前言
为什么要搞这个?
功能亮点


操作流程
yourdomain.com。最后
HTML5 Drag & Drop API 是浏览器原生提供的拖拽功能,允许用户通过鼠标拖动元素并将其放置到目标位置。 在 在 由于 HTML5 Drag & Drop API 支持在同源的不同窗口/标签页之间拖拽: HTML5 Drag & Drop API 支持多种标准 MIME 类型: 格式规范: 使用示例: 从浏览器地址栏拖拽: 多个 URI 的格式: 浏览器支持情况: 常见使用场景: 注意事项: 在 Vue 2 中,推荐使用组件状态而不是 dataTransfer: 在 Vue 3 中,同样推荐使用组件状态而不是 dataTransfer: 只有以下场景才需要使用 dataTransfer: 原因: 没有在 dragover 中调用 preventDefault() 原因: dataTransfer 只能传递字符串 解决方案: 使用 setDragImage 自定义 解决方案: 需要额外处理 touch 事件或使用第三方库 解决方案: 只能在同源窗口间拖拽,跨域需要其他方案(如 postMessage)一、基础概念
核心角色
二、完整的拖拽流程
2.1 事件流程图
用户按下鼠标并开始拖动
↓
dragstart 事件 (在拖拽源触发,只触发一次)
↓
drag 事件 (在拖拽源持续触发,拖动过程中)
↓
dragenter 事件 (鼠标进入拖放目标时触发)
↓
dragover 事件 (鼠标在拖放目标上方时持续触发)
↓
dragleave 事件 (鼠标离开拖放目标时触发)
↓
drop 事件 (在拖放目标释放鼠标时触发)
↓
dragend 事件 (在拖拽源触发,拖拽结束)2.2 事件详解
事件名称 触发位置 触发时机 触发频率 dragstart拖拽源 开始拖动时 一次 drag拖拽源 拖动过程中 持续触发 dragenter拖放目标 进入目标区域 一次 dragover拖放目标 在目标区域上方 持续触发 dragleave拖放目标 离开目标区域 一次 drop拖放目标 释放鼠标 一次 dragend拖拽源 拖拽结束 一次 三、基础用法示例
3.1 最简单的拖拽实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML5 拖拽示例</title>
<style>
.drag-source {
width: 200px;
padding: 20px;
background: #4CAF50;
color: white;
cursor: move;
margin: 20px;
}
.drop-target {
width: 300px;
height: 200px;
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
.drop-target.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
</style>
</head>
<body>
<!-- 拖拽源 -->
<div class="drag-source" draggable="true" id="dragItem">
拖动我
</div>
<!-- 拖放目标 -->
<div class="drop-target" id="dropZone">
拖放到这里
</div>
<script>
const dragItem = document.getElementById('dragItem');
const dropZone = document.getElementById('dropZone');
// 1. 拖拽开始
dragItem.addEventListener('dragstart', function(e) {
console.log('dragstart: 开始拖拽');
// 设置拖拽数据
e.dataTransfer.setData('text/plain', '这是拖拽的数据');
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'copy';
});
// 2. 拖拽结束
dragItem.addEventListener('dragend', function(e) {
console.log('dragend: 拖拽结束');
});
// 3. 进入目标区域
dropZone.addEventListener('dragenter', function(e) {
console.log('dragenter: 进入目标区域');
e.preventDefault();
this.classList.add('drag-over');
});
// 4. 在目标区域上方(必须阻止默认行为)
dropZone.addEventListener('dragover', function(e) {
e.preventDefault(); // 必须调用,否则 drop 事件不会触发
e.dataTransfer.dropEffect = 'copy';
});
// 5. 离开目标区域
dropZone.addEventListener('dragleave', function(e) {
console.log('dragleave: 离开目标区域');
this.classList.remove('drag-over');
});
// 6. 放置到目标区域
dropZone.addEventListener('drop', function(e) {
e.preventDefault(); // 阻止默认行为(如打开链接)
this.classList.remove('drag-over');
// 获取拖拽数据
const data = e.dataTransfer.getData('text/plain');
console.log('drop: 接收到数据:', data);
// 显示数据
this.innerHTML = `<p>接收到: ${data}</p>`;
});
</script>
</body>
</html>3.2 关键点说明
dragover 和 drop 中必须调用,否则拖放无效四、DataTransfer 对象详解
4.1 核心属性
属性 说明 设置位置 effectAllowed允许的拖放效果 拖拽源(dragstart) dropEffect实际的拖放效果 拖放目标(dragover) files拖拽的文件列表 只读 types数据类型列表 只读 items数据项列表 只读 4.2 核心方法
// 设置数据
e.dataTransfer.setData(format, data);
// 获取数据
const data = e.dataTransfer.getData(format);
// 清除数据
e.dataTransfer.clearData(format);
// 设置拖拽图像
e.dataTransfer.setDragImage(element, xOffset, yOffset);五、鼠标效果控制
5.1 effectAllowed(拖拽源设置)
dragstart 事件中设置,定义允许的操作类型:dragItem.addEventListener('dragstart', function(e) {
// 可选值
e.dataTransfer.effectAllowed = 'none'; // 不允许
e.dataTransfer.effectAllowed = 'copy'; // 复制
e.dataTransfer.effectAllowed = 'move'; // 移动
e.dataTransfer.effectAllowed = 'link'; // 链接
e.dataTransfer.effectAllowed = 'copyMove'; // 复制或移动
e.dataTransfer.effectAllowed = 'copyLink'; // 复制或链接
e.dataTransfer.effectAllowed = 'linkMove'; // 链接或移动
e.dataTransfer.effectAllowed = 'all'; // 所有操作
});5.2 dropEffect(拖放目标设置)
dragover 事件中设置,定义实际执行的操作:dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
// 可选值
e.dataTransfer.dropEffect = 'none'; // 禁止放置 🚫
e.dataTransfer.dropEffect = 'copy'; // 复制 ➕
e.dataTransfer.dropEffect = 'move'; // 移动
e.dataTransfer.dropEffect = 'link'; // 链接 🔗
});六、数据传递
6.1 基本数据类型传递
// 拖拽源:设置数据
dragItem.addEventListener('dragstart', function(e) {
// 文本数据
e.dataTransfer.setData('text/plain', 'Hello World');
// HTML 数据
e.dataTransfer.setData('text/html', '<strong>Bold Text</strong>');
// URL 数据
e.dataTransfer.setData('text/uri-list', 'https://example.com');
});
// 拖放目标:获取数据
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 获取文本数据
const text = e.dataTransfer.getData('text/plain');
console.log('接收到文本:', text);
// 获取 HTML 数据
const html = e.dataTransfer.getData('text/html');
console.log('接收到 HTML:', html);
});6.2 传递对象数据(JSON 序列化)
dataTransfer 只能传递字符串,传递对象需要序列化:<!DOCTYPE html>
<html>
<head>
<style>
.item {
padding: 15px;
margin: 10px;
background: #2196F3;
color: white;
cursor: move;
display: inline-block;
}
.drop-zone {
min-height: 150px;
border: 2px dashed #999;
padding: 20px;
margin: 10px;
}
</style>
</head>
<body>
<div class="item" draggable="true" data-id="1" data-name="产品A" data-price="99.99">
产品A - ¥99.99
</div>
<div class="item" draggable="true" data-id="2" data-name="产品B" data-price="199.99">
产品B - ¥199.99
</div>
<div class="drop-zone" id="cart">购物车(拖放产品到这里)</div>
<script>
// 拖拽源:序列化对象
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('dragstart', function(e) {
// 创建对象
const product = {
id: this.dataset.id,
name: this.dataset.name,
price: parseFloat(this.dataset.price)
};
// 序列化为 JSON 字符串
const jsonData = JSON.stringify(product);
e.dataTransfer.setData('application/json', jsonData);
console.log('拖拽产品:', product);
});
});
// 拖放目标:反序列化对象
const cart = document.getElementById('cart');
cart.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
cart.addEventListener('drop', function(e) {
e.preventDefault();
// 获取 JSON 字符串
const jsonData = e.dataTransfer.getData('application/json');
// 反序列化为对象
const product = JSON.parse(jsonData);
console.log('接收到产品:', product);
// 显示产品信息
const productDiv = document.createElement('div');
productDiv.style.cssText = 'padding: 10px; margin: 5px; background: #4CAF50; color: white;';
productDiv.innerHTML = `
<strong>${product.name}</strong><br>
ID: ${product.id}<br>
价格: ¥${product.price}
`;
this.appendChild(productDiv);
});
</script>
</body>
</html>6.3 传递复杂对象的注意事项
// ❌ 错误:直接传递对象(会被转换为 "[object Object]")
e.dataTransfer.setData('text/plain', { name: 'John' });
// ✅ 正确:序列化后传递
const data = { name: 'John', age: 30, tags: ['developer', 'designer'] };
e.dataTransfer.setData('application/json', JSON.stringify(data));
// 接收时反序列化
const receivedData = JSON.parse(e.dataTransfer.getData('application/json'));
console.log(receivedData.name); // 'John'
console.log(receivedData.tags); // ['developer', 'designer']6.4 传递多种格式的数据
dragItem.addEventListener('dragstart', function(e) {
const data = {
id: 123,
title: '示例标题',
content: '示例内容'
};
// 同时设置多种格式
e.dataTransfer.setData('text/plain', data.title);
e.dataTransfer.setData('text/html', `<h1>${data.title}</h1><p>${data.content}</p>`);
e.dataTransfer.setData('application/json', JSON.stringify(data));
});
// 接收时根据需要选择格式
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 检查可用的数据类型
console.log('可用类型:', e.dataTransfer.types);
// 优先使用 JSON 格式
if (e.dataTransfer.types.includes('application/json')) {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('使用 JSON 数据:', data);
} else if (e.dataTransfer.types.includes('text/html')) {
const html = e.dataTransfer.getData('text/html');
console.log('使用 HTML 数据:', html);
} else {
const text = e.dataTransfer.getData('text/plain');
console.log('使用纯文本数据:', text);
}
});七、跨窗口拖拽
7.1 同源窗口间拖拽
<!-- 窗口 A: source.html -->
<!DOCTYPE html>
<html>
<head>
<title>拖拽源窗口</title>
<style>
.drag-item {
padding: 20px;
background: #4CAF50;
color: white;
cursor: move;
margin: 20px;
display: inline-block;
}
</style>
</head>
<body>
<h2>拖拽源窗口</h2>
<p>将下面的元素拖到另一个窗口</p>
<div class="drag-item" draggable="true" id="item">
拖动我到另一个窗口
</div>
<script>
document.getElementById('item').addEventListener('dragstart', function(e) {
const data = {
message: '来自窗口 A 的数据',
timestamp: new Date().toISOString(),
windowName: 'Window A'
};
e.dataTransfer.setData('application/json', JSON.stringify(data));
e.dataTransfer.effectAllowed = 'copy';
console.log('开始跨窗口拖拽:', data);
});
</script>
</body>
</html><!-- 窗口 B: target.html -->
<!DOCTYPE html>
<html>
<head>
<title>拖放目标窗口</title>
<style>
.drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 20px;
margin: 20px;
text-align: center;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
.received-item {
padding: 15px;
margin: 10px;
background: #4CAF50;
color: white;
border-radius: 4px;
}
</style>
</head>
<body>
<h2>拖放目标窗口</h2>
<p>从另一个窗口拖拽元素到这里</p>
<div class="drop-zone" id="dropZone">
<p>拖放区域</p>
<p style="color: #999;">从另一个窗口拖拽元素到这里</p>
</div>
<script>
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
dropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 接收跨窗口数据
const jsonData = e.dataTransfer.getData('application/json');
if (jsonData) {
const data = JSON.parse(jsonData);
console.log('接收到跨窗口数据:', data);
// 显示接收到的数据
const itemDiv = document.createElement('div');
itemDiv.className = 'received-item';
itemDiv.innerHTML = `
<strong>接收到数据:</strong><br>
消息: ${data.message}<br>
来源: ${data.windowName}<br>
时间: ${data.timestamp}
`;
this.appendChild(itemDiv);
}
});
</script>
</body>
</html>7.2 跨窗口拖拽的限制
7.3 跨窗口拖拽文件
<!DOCTYPE html>
<html>
<head>
<title>文件拖放</title>
<style>
.file-drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 40px;
text-align: center;
margin: 20px;
}
.file-drop-zone.drag-over {
background: #e8f5e9;
border-color: #4CAF50;
}
.file-list {
margin-top: 20px;
text-align: left;
}
.file-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 4px solid #4CAF50;
}
</style>
</head>
<body>
<h2>文件拖放示例</h2>
<p>从文件管理器拖拽文件到下方区域</p>
<div class="file-drop-zone" id="fileDropZone">
<p style="font-size: 48px;">📁</p>
<p>拖拽文件到这里</p>
<p style="color: #999; font-size: 14px;">支持从文件管理器或其他窗口拖拽</p>
</div>
<div class="file-list" id="fileList"></div>
<script>
const fileDropZone = document.getElementById('fileDropZone');
const fileList = document.getElementById('fileList');
fileDropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
fileDropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
fileDropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
fileDropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 获取拖拽的文件
const files = e.dataTransfer.files;
console.log('接收到文件数量:', files.length);
// 显示文件信息
Array.from(files).forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<strong>📄 ${file.name}</strong><br>
类型: ${file.type || '未知'}<br>
大小: ${(file.size / 1024).toFixed(2)} KB<br>
最后修改: ${new Date(file.lastModified).toLocaleString()}
`;
fileList.appendChild(fileItem);
});
});
</script>
</body>
</html>7.4 检测拖拽来源
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 检查是否是文件
if (e.dataTransfer.files.length > 0) {
console.log('拖拽来源: 文件系统');
console.log('文件数量:', e.dataTransfer.files.length);
}
// 检查是否有 URL
else if (e.dataTransfer.types.includes('text/uri-list')) {
const url = e.dataTransfer.getData('text/uri-list');
console.log('拖拽来源: URL -', url);
}
// 检查是否有自定义数据
else if (e.dataTransfer.types.includes('application/json')) {
const data = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('拖拽来源: 自定义数据 -', data);
}
// 纯文本
else if (e.dataTransfer.types.includes('text/plain')) {
const text = e.dataTransfer.getData('text/plain');
console.log('拖拽来源: 纯文本 -', text);
}
});7.5 常用数据类型说明
数据类型 说明 使用场景 浏览器支持 text/plain纯文本 拖拽文本内容 ✅ 所有浏览器 text/htmlHTML 内容 拖拽富文本 ✅ 所有浏览器 text/uri-listURI 列表 拖拽链接、书签 ✅ 所有浏览器 application/jsonJSON 数据 自定义数据传递 ✅ 所有浏览器 Files文件对象 拖拽文件 ✅ 所有浏览器 text/uri-list 详解
text/uri-list 是一个标准的 MIME 类型,用于传递一个或多个 URI(统一资源标识符)。# 开头的行是注释\n 分隔<!DOCTYPE html>
<html>
<head>
<style>
.link-item {
padding: 15px;
margin: 10px;
background: #2196F3;
color: white;
cursor: move;
display: inline-block;
text-decoration: none;
border-radius: 4px;
}
.drop-zone {
min-height: 200px;
border: 2px dashed #999;
padding: 20px;
margin: 20px;
}
.received-link {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-left: 4px solid #4CAF50;
}
</style>
</head>
<body>
<h2>拖拽链接示例</h2>
<!-- 可拖拽的链接 -->
<a href="https://www.example.com" class="link-item" draggable="true" id="link1">
拖动这个链接
</a>
<a href="https://www.github.com" class="link-item" draggable="true" id="link2">
GitHub 链接
</a>
<!-- 拖放区域 -->
<div class="drop-zone" id="dropZone">
<p>拖放链接到这里</p>
</div>
<script>
// 拖拽链接时,浏览器会自动设置 text/uri-list
document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('dragstart', function(e) {
// 浏览器会自动设置 text/uri-list 为链接的 href
// 也可以手动设置
e.dataTransfer.setData('text/uri-list', this.href);
e.dataTransfer.setData('text/plain', this.href);
console.log('拖拽链接:', this.href);
});
});
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'link';
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
// 获取 URI 列表
const uriList = e.dataTransfer.getData('text/uri-list');
if (uriList) {
// 处理 URI 列表(可能包含多个 URI)
const uris = uriList.split('\n').filter(uri => {
// 过滤掉注释和空行
return uri.trim() && !uri.startsWith('#');
});
console.log('接收到的 URI:', uris);
// 显示接收到的链接
uris.forEach(uri => {
const linkDiv = document.createElement('div');
linkDiv.className = 'received-link';
linkDiv.innerHTML = `
<strong>接收到链接:</strong><br>
<a href="${uri}" target="_blank">${uri}</a>
`;
this.appendChild(linkDiv);
});
}
});
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<style>
.drop-zone {
min-height: 200px;
border: 3px dashed #999;
padding: 40px;
text-align: center;
margin: 20px;
background: #f9f9f9;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
}
</style>
</head>
<body>
<h2>从浏览器地址栏拖拽 URL</h2>
<p>尝试从浏览器地址栏或书签栏拖拽 URL 到下方区域</p>
<div class="drop-zone" id="urlDropZone">
<p style="font-size: 48px;">🔗</p>
<p>拖拽 URL 到这里</p>
</div>
<div id="result"></div>
<script>
const urlDropZone = document.getElementById('urlDropZone');
const result = document.getElementById('result');
urlDropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
urlDropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'link';
});
urlDropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
urlDropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
// 获取 URI 列表
const uriList = e.dataTransfer.getData('text/uri-list');
const plainText = e.dataTransfer.getData('text/plain');
console.log('URI List:', uriList);
console.log('Plain Text:', plainText);
if (uriList) {
// 解析 URI 列表
const uris = uriList.split('\n').filter(uri => {
return uri.trim() && !uri.startsWith('#');
});
result.innerHTML = `
<h3>接收到的 URL:</h3>
<ul>
${uris.map(uri => `
<li>
<a href="${uri}" target="_blank">${uri}</a>
</li>
`).join('')}
</ul>
`;
} else if (plainText) {
result.innerHTML = `
<h3>接收到的文本:</h3>
<p>${plainText}</p>
`;
}
});
</script>
</body>
</html>// 设置多个 URI
const uriList = `https://www.example.com
https://www.github.com
# 这是注释
https://www.google.com`;
e.dataTransfer.setData('text/uri-list', uriList);
// 接收时解析
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
const uriList = e.dataTransfer.getData('text/uri-list');
// 解析 URI 列表
const uris = uriList.split('\n').filter(line => {
const trimmed = line.trim();
// 过滤空行和注释
return trimmed && !trimmed.startsWith('#');
});
console.log('解析出的 URI:', uris);
// ['https://www.example.com', 'https://www.github.com', 'https://www.google.com']
});浏览器 支持版本 说明 Chrome ✅ 所有版本 完全支持 Firefox ✅ 所有版本 完全支持 Safari ✅ 所有版本 完全支持 Edge ✅ 所有版本 完全支持 IE ✅ IE 10+ 部分支持 text/uri-list 只能包含 URI,不能包含其他数据text/plain 作为备用八、高级功能
8.1 自定义拖拽图像
<!DOCTYPE html>
<html>
<head>
<style>
.drag-item {
padding: 20px;
background: #2196F3;
color: white;
cursor: move;
margin: 20px;
display: inline-block;
}
.custom-drag-image {
padding: 15px;
background: #FF5722;
color: white;
border-radius: 8px;
position: absolute;
left: -9999px;
}
</style>
</head>
<body>
<div class="drag-item" draggable="true" id="item">
拖动我(自定义拖拽图像)
</div>
<!-- 自定义拖拽图像(隐藏) -->
<div class="custom-drag-image" id="customImage">
🎯 正在拖拽...
</div>
<div style="min-height: 200px; border: 2px dashed #999; margin: 20px; padding: 20px;" id="dropZone">
拖放区域
</div>
<script>
const item = document.getElementById('item');
const customImage = document.getElementById('customImage');
const dropZone = document.getElementById('dropZone');
item.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', 'Custom Image Demo');
// 设置自定义拖拽图像
// 参数: (元素, x偏移, y偏移)
e.dataTransfer.setDragImage(customImage, 50, 25);
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
this.innerHTML = `<p>接收到: ${data}</p>`;
});
</script>
</body>
</html>8.2 拖拽时的视觉反馈
<!DOCTYPE html>
<html>
<head>
<style>
.item {
padding: 15px;
margin: 10px;
background: #4CAF50;
color: white;
cursor: move;
transition: opacity 0.3s;
}
.item.dragging {
opacity: 0.5;
border: 2px dashed #fff;
}
.drop-zone {
min-height: 150px;
border: 2px dashed #999;
margin: 10px;
padding: 20px;
transition: all 0.3s;
}
.drop-zone.drag-over {
background: #e3f2fd;
border-color: #2196F3;
border-width: 3px;
transform: scale(1.02);
}
</style>
</head>
<body>
<div class="item" draggable="true">项目 1</div>
<div class="item" draggable="true">项目 2</div>
<div class="item" draggable="true">项目 3</div>
<div class="drop-zone">拖放区域</div>
<script>
const items = document.querySelectorAll('.item');
const dropZone = document.querySelector('.drop-zone');
items.forEach(item => {
// 拖拽开始 - 添加视觉效果
item.addEventListener('dragstart', function(e) {
this.classList.add('dragging');
e.dataTransfer.setData('text/plain', this.textContent);
});
// 拖拽结束 - 移除视觉效果
item.addEventListener('dragend', function(e) {
this.classList.remove('dragging');
});
});
dropZone.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
});
dropZone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const data = e.dataTransfer.getData('text/plain');
const newItem = document.createElement('div');
newItem.style.cssText = 'padding: 10px; margin: 5px; background: #4CAF50; color: white;';
newItem.textContent = data;
this.appendChild(newItem);
});
</script>
</body>
</html>九、在 Vue 框架中使用注意事项
9.1 Vue 2 中的使用
基本用法
<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in items"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
],
dragData: null // 使用组件状态存储拖拽数据
};
},
methods: {
handleDragStart(item) {
// 直接保存到组件状态
this.dragData = item;
},
handleDrop(event) {
event.preventDefault();
if (this.dragData) {
console.log('接收到:', this.dragData);
// 处理数据...
}
this.dragData = null;
}
}
};
</script>Vue 2 注意事项
<!-- ✅ 正确 -->
<div @dragover.prevent="handleDragOver"></div>
<!-- ❌ 错误:drop 事件不会触发 -->
<div @dragover="handleDragOver"></div>// ❌ 错误:直接修改数组可能不触发更新
this.items.push(newItem);
// ✅ 正确:使用 Vue.set 或替换整个数组
this.items = [...this.items, newItem];
// 或
this.$set(this.items, this.items.length, newItem);// ✅ 推荐:使用组件状态
data() {
return {
dragData: null
};
},
methods: {
handleDragStart(item) {
this.dragData = item; // 直接保存
},
handleDrop(event) {
event.preventDefault();
console.log(this.dragData); // 直接使用
}
}9.2 Vue 3 中的使用
组合式 API (Composition API)
<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in items"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const items = ref([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
]);
const dragData = ref(null);
const handleDragStart = (item) => {
dragData.value = item;
};
const handleDrop = (event) => {
event.preventDefault();
if (dragData.value) {
console.log('接收到:', dragData.value);
// 处理数据
items.value.push({ ...dragData.value });
}
dragData.value = null;
};
</script>Vue 3 注意事项
// ✅ 正确:使用 .value 访问 ref
const dragData = ref(null);
dragData.value = item;
// ❌ 错误:忘记 .value
dragData = item;// ✅ Vue 3 中直接修改数组是响应式的
items.value.push(newItem);
items.value.splice(index, 1);
// 也可以替换整个数组
items.value = [...items.value, newItem];import { reactive, toRefs } from 'vue';
const state = reactive({
items: [],
draggedItem: null
});
// ✅ 使用 toRefs 保持响应式
const { items, draggedItem } = toRefs(state);<template>
<div>
<!-- 拖拽源 -->
<div
v-for="item in sourceItems"
:key="item.id"
draggable="true"
@dragstart="handleDragStart(item)"
>
{{ item.name }}
</div>
<!-- 拖放目标 -->
<div
class="drop-zone"
@dragover.prevent
@drop="handleDrop"
>
拖放区域
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const sourceItems = ref([
{ id: 1, name: '项目 1', metadata: { /* 复杂对象 */ } },
{ id: 2, name: '项目 2', metadata: { /* 复杂对象 */ } }
]);
// 使用 ref 存储拖拽数据
const dragData = ref(null);
// ✅ 推荐:直接使用组件状态
const handleDragStart = (item) => {
// 直接保存,无需序列化
dragData.value = item;
// 不需要使用 dataTransfer!
};
const handleDrop = (event) => {
event.preventDefault();
// 直接使用,无需反序列化
if (dragData.value) {
console.log('接收到数据:', dragData.value);
console.log('可以直接访问复杂对象:', dragData.value.metadata);
// 处理数据...
}
// 清空
dragData.value = null;
};
</script>9.3 Vue 通用最佳实践
// ❌ 场景 1: 跨窗口拖拽(必须使用 dataTransfer)
methods: {
handleDragStart(event, item) {
// 跨窗口无法访问组件状态,必须序列化
event.dataTransfer.setData('application/json', JSON.stringify(item));
},
handleDrop(event) {
const data = JSON.parse(event.dataTransfer.getData('application/json'));
}
}
// ❌ 场景 2: 拖拽文件(必须使用 dataTransfer.files)
methods: {
handleDrop(event) {
event.preventDefault();
const files = event.dataTransfer.files;
console.log('接收到文件:', files);
}
}
// ❌ 场景 3: 从外部拖拽内容(如浏览器地址栏、其他应用)
methods: {
handleDrop(event) {
event.preventDefault();
// 获取拖拽的 URL
const url = event.dataTransfer.getData('text/uri-list');
// 获取拖拽的文本
const text = event.dataTransfer.getData('text/plain');
}
}<!-- ✅ 推荐:同一父组件内使用组件状态 -->
<script>
export default {
data() {
return {
dragData: null // 简单直接
};
},
methods: {
onDragStart(item) {
this.dragData = item; // 无需序列化
},
onDrop() {
console.log(this.dragData); // 直接使用
}
}
};
</script>
<!-- ❌ 不推荐:同一父组件内使用 dataTransfer -->
<script>
export default {
methods: {
onDragStart(event, item) {
// 需要序列化,代码冗余
event.dataTransfer.setData('application/json', JSON.stringify(item));
},
onDrop(event) {
// 需要反序列化,容易出错
const data = JSON.parse(event.dataTransfer.getData('application/json'));
}
}
};
</script>十、在 React 框架中使用注意事项
10.1 React 基本用法
import React, { useState } from 'react';
function DragDropExample() {
const [items] = useState([
{ id: 1, name: '项目 1' },
{ id: 2, name: '项目 2' }
]);
const [draggedItem, setDraggedItem] = useState(null);
const [droppedItems, setDroppedItems] = useState([]);
const handleDragStart = (event, item) => {
setDraggedItem(item);
event.dataTransfer.effectAllowed = 'copy';
// 也可以使用 dataTransfer
event.dataTransfer.setData('application/json', JSON.stringify(item));
};
const handleDragEnd = () => {
setDraggedItem(null);
};
const handleDragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
const handleDrop = (event) => {
event.preventDefault();
if (draggedItem) {
setDroppedItems([...droppedItems, draggedItem]);
setDraggedItem(null);
}
};
return (
<div>
<div>
<h3>拖拽源</h3>
{items.map(item => (
<div
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, item)}
onDragEnd={handleDragEnd}
style={{
padding: '10px',
margin: '5px',
background: '#4CAF50',
color: 'white',
cursor: 'move'
}}
>
{item.name}
</div>
))}
</div>
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
minHeight: '200px',
border: '2px dashed #999',
padding: '20px',
marginTop: '20px'
}}
>
<h3>拖放区域</h3>
{droppedItems.map((item, index) => (
<div key={index} style={{ padding: '10px', background: '#f0f0f0', margin: '5px' }}>
{item.name}
</div>
))}
</div>
</div>
);
}
export default DragDropExample;十一、常见问题汇总
问题 1: drop 事件不触发
// ✅ 正确
element.addEventListener('dragover', (e) => {
e.preventDefault();
});问题 2: 无法传递对象数据
// ✅ 正确:序列化对象
e.dataTransfer.setData('application/json', JSON.stringify(obj));
// 接收时反序列化
const obj = JSON.parse(e.dataTransfer.getData('application/json'));问题 3: 拖拽图像显示不正确
e.dataTransfer.setDragImage(customElement, offsetX, offsetY);问题 4: 移动端不支持
// 监听 touch 事件模拟拖拽
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);问题 5: 跨域拖拽限制
十二、参考资料
大家好,作为一名 macOS 深度乐迷,我一直觉得:音乐不仅是流动的空气,更是我们生命中不曾停歇的数字资产。
然而,目前的流媒体平台( Apple Music, Tidal, Qobuz 等)往往将我们的听歌记录封锁在各自的围墙内。如果你使用 Audirvana 或 Roon 追求极致音质,这些宝贵的聆听痕迹更是难以被统一记录和深度挖掘。
于是,我用 Go 编写了 SonicLens (音眸)。
SonicLens 是一架专为 macOS 用户打造的“声之透镜”。它静默地守候在播放器之后,通过高频采样与无感监控,将你的每一次聆听凝结为属于你个人的、跨平台的音眸轨迹。
🔗 GitHub: https://github.com/vincentchyu/sonic-lens
作为一个程序员,我在实现过程中加入了一些好玩的细节:
(附上 README 中的几张截图)




这个项目是我对“科技与人文”结合的一次尝试。如果你也是对音质有追求、对数据有执念的 macOS 用户,欢迎试用并提出你的建议!
如果觉得还不错,也欢迎点个 Star 鼓励一下。
再次感谢大家的时间。
GitHub 地址:https://github.com/vincentchyu/sonic-lens
小红书可以搜索 #Soniclens #音眸轨迹 词条
这款神器能帮 AI 原生团队自动搞定测试,让开发流程无缝衔接。
热度:🔺401

它用 AI 重构贷款流程,把几小时的审批压缩到秒级。
热度:🔺276

你的代码安全管家,自动扫描漏洞并给出修复方案。
热度:🔺274

给 GitHub 加个涡轮,用镜像界面实现低延迟浏览。
热度:🔺224

最快的方式把 AI 代理塞进你的应用,省去基础设施烦恼。
热度:🔺187

告别解释,直接滑动获取无尽设计灵感,像有个创意总监随时待命。
热度:🔺134

一款本地运行的开源视频编辑器,GPU 加速 AI 生成,免费又强大。
热度:🔺125

用 AI 把涂鸦变成可编辑的矢量图和视频,让创意动起来。
热度:🔺112

拖拽几下,就能生成干净漂亮的响应式表单,连登录都免了。
热度:🔺103

把 MacBook Pro 的刘海变成随时可用的安全记事本,点击即写。
热度:🔺98

我买了火山云的 coding plan lite ,cluade code 接入 auto 模型,我随便问几个问题,token 消耗蹭蹭往上涨,大家都是这样的吗,有没有降低消耗的方案。
邮箱?手机号?信用卡号?还是全封?
同一个手机号/信用卡给不同账号付款 有没有风险?
点赞 + 关注 + 收藏 = 学会了 AutoPiano(自由钢琴)是一款网页钢琴模拟器,支持电脑键盘和鼠标演奏,内置多种乐器音色与教学曲谱,零基础也能轻松上手。 在飞牛 NAS,找到“文件管理”App,在“docker”文件夹里创建一个“autopiano”文件夹。 然后打开“Docker”App,在“Compose”页面创建一个项目。 相关配置如下图所示。 代码如下: 这里配置的端口是 项目构建成功后,在浏览器访问 AutoPiano 内置了一些琴谱,你跟着琴谱打字就可以弹出一首曲子了。 你就弹吧~ 以上就是本文的全部内容啦,有疑问可以在评论区讨论~ 想了解更多NAS玩法可以关注《NAS邪修》👏 往期推荐: 点赞 + 关注 + 收藏 = 学会了整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》



services:
autopiano:
image: wbsu2003/autopiano:latest
container_name: autopiano
ports:
- 2338:80
restart: unless-stopped2338,如果这个端口和你其他项目冲突了,可以设置成别的端口。NAS的IP:2338 就可以弹琴了。
Python 的内置函数 不可变性: 支持集合操作: 字典键:当需要使用集合作为字典的键时, 集合元素:当集合的元素需要是另一个集合时,必须使用 frozenset 用于创建一个不可变的集合对象。与普通集合 set 不同,frozenset 一旦创建就不能修改(如添加或删除元素),这使得它可以作为字典的键或其他集合的元素。基本用法
fs = frozenset([1, 2, 3, 4])
print(fs) # 输出: frozenset({1, 2, 3, 4})特性
frozenset 的内容在创建后无法更改,因此它是哈希的,可以作为字典的键。d = {frozenset([1, 2]): "value"}
print(d) # 输出: {frozenset({1, 2}): 'value'}frozenset 支持常见的集合操作,如并集(union)、交集(intersection)、差集(difference)等。fs1 = frozenset([1, 2, 3])
fs2 = frozenset([3, 4, 5])
print(fs1.union(fs2)) # 输出: frozenset({1, 2, 3, 4, 5})frozenset 在某些操作上比可变集合更高效,尤其是在哈希和比较时。应用场景
frozenset 是唯一选择。groups = {
frozenset(["admin", "editor"]): "high_privilege",
frozenset(["viewer"]): "low_privilege"
}frozenset。s = {frozenset([1, 2]), frozenset([3, 4])}注意事项
示例
# 去重示例
fs = frozenset([1, 2, 2, 3])
print(fs) # 输出: frozenset({1, 2, 3})
# 错误示例(尝试修改会报错)
fs.add(4) # 抛出 AttributeError: 'frozenset' object has no attribute 'add'frozenset 提供了一种安全、高效的方式来处理不可变集合需求,特别适用于需要哈希或不可变性的场景。
突然想到一件事我这有个小县城,一辆电车着火了
旁边银行的人拿着干粉灭火器使劲喷,然后车就烧的一干二净
旁边有人问这车咋着火了,有人说因为他充的是好电,这是个烂车
以为旁边人不懂也就当个笑话,然后过了好久好久
突然我就又听到这句话了,一个年轻人说的,跟我差不多大
这个充电桩充的快,是因为充的好电
非常棒...
在中国银行 app 首页即可看到该活动。
如果有实际消费可以实际消费,如果没有的话,可以给余额宝或是微信零钱通充 10000,然后再提现。
大部分是实际消费的。
活动内容具体看介绍吧。


11 月份的又报名了。
有人中一等奖、二等奖,我就不上传图片了,等自己中了再上传实际的图片。
Chatbox AI / Cherry Studio / Monica 这类第三方工具需要通过 LLM 的 API 去调用模型的。本身开 llm plus 或者 pro 也没办法在第三方平台使用?如果是 api 的费用应该比 plus ,pro 要高吧?
或者 在第三方平台开其会员。但是这样就和中转站一样了吧?我之前试过几个平台,感觉明显的降智,根本无法判断是其声明的模型。通用的提示词和问题,gpt5.2 ,gemini pro 的结果和官方 ui 差别巨大。
所以不是太理解这类平台的用户群体是什么样的?或者应该什么场景使用?
obisdn 总感觉重了
开源了一个 terminal-first 的 Twitter/X CLI 。
不需要 API key ,直接用浏览器 Cookie 读取 for-you / following timeline 、bookmarks 、user posts 。
支持 JSON output 。
可配置的打分算法进行筛选想看的帖子。
目前是直接用 API 来做的,比较稳定,后面还会用 playwright/agent-browser 支持更多的功能。
即上次 openai 出现 bug 重置额度后,今天又重置了额度。官方人员说是因为有用户反馈额度消耗过快,所以重置了。
切换 gpt-5.4 后,额度消耗真的很快,我以为是因为 5.4 输出快导致的,没想到很可能是一个 bug 。
这世界果然都是草台班子。

我知道有的人图片显示正常,也肯定有人建议换节点。
但是:
即便已经能访问 V2EX 的用户,仍然相当一部份用户无法访问 imgur ,因为被主动屏蔽。
发贴发图是为了传递信息,如果你不在意大量的人看到图裂的话,可以无视本贴。
Google 和 V2 对于用户们来说基本是不可以替代的,但是 imgur 有很多代替品,这种玻璃心产品,放弃吧。
https://claude-update-code.squarespace.com/ 在这个站点;
是我怀疑错了还是一个后门程序? 这个站点是从谷歌 sponsor 推荐里点进去的;