普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月30日首页

央行:10月债券市场共发行各类债券63574.6亿元

2025年11月30日 18:47
央行发布2025年10月份金融市场运行情况。 10月份,债券市场共发行各类债券63574.6亿元。国债发行11695.5亿元,地方政府债券发行5604.7亿元,金融债券发行8010.8亿元,公司信用类债券1发行11836.2亿元,信贷资产支持证券发行343.4亿元,同业存单发行25649.0亿元。(证券时报)

央行:10月银行间债券市场现券成交26.6万亿元

2025年11月30日 18:46
央行发布2025年10月份金融市场运行情况。10月份,银行间债券市场现券成交26.6万亿元,日均成交1.5万亿元,同比增加10.2%,环比增加3.9%。单笔成交量在500-5000万元的交易占总成交金额的48.06%,单笔成交量在9000万元以上的交易占总成交金额的45.68%,单笔平均成交量4177.69万元。交易所债券市场现券成交3.3万亿元,日均成交1937.9亿元。商业银行柜台债券成交6.6万笔,成交金额587.3亿元。(e公司)

小米汽车:11月新增17家门店

2025年11月30日 18:38
小米汽车11月30日公布开店新进展:11月新增17家门店,全国131城已有441家门店;12月计划新增36家门店,预计覆盖衡阳、绵阳等7座城市;截止11月30日,全国已有249家服务网点,覆盖全国144城。(界面)

JavaScript新手必看系列之预编译

作者 甜味弥漫
2025年11月30日 18:14
前言

预编译是JavaScript的核心概念,也是新手向中级进阶的必经之路。理解它,意味着你能:

  • 彻底搞懂变量提升
  • 理解函数声明的“特权”
  • 避免常见的作用域陷阱

本文用最简单的语言和示例,带你快速掌握全局预编译与函数预编译的完整过程。

What's that?

简单说,JS在执行代码前会进行“准备工作” —— 预编译。在这个阶段JS的V8引擎会进行变量声明,函数声明的提升

预编译的两种场景

1. 全局作用域下的预编译

分三步:

1.创建 GO(Global Object)

  • 在浏览器中就是 window 对象

  • 在 Node.js 中就是 global 对象

2.找到变量声明,作为作为GO属性名,值为undefined

  1. 找到函数声明,作为GO属性名,值为函数体

举个简单的例子:

console.log(a) // undefined
var a = 1
console.log(a) // 1

发什么了什么?

  1. 创建 GO ={}
  2. 找到变量声明 var a ,GO = { a : undefined }
  3. 找函数声明,没有函数声明就不找

执行过程如下:

//预编译后就相当于:
var a = undefined
console.log(a) //输出 undefined
  a = 1
console.log(a) //输出 1

2.函数作用域下的预编译

函数较为复杂,四步:

  1. 创建AO(Activation Object)

  2. 找形参和变量声明,作为AO属性名,值为 undefined

  3. 将实参赋给形参

  4. 找函数声明,作为AO 属性名,值为函数体

举例:

function fn(a) {
  console.log(a);
  var a = 123
  console.log(a);
  function a() {}
  var b = function() {}
  console.log(b);
  function c() {}
  var c = a
  console.log(c);
}
fn(1)

干了什么?

  1. 创建AO = {

2.找形参和变量声明,作为AO属性名,值为 undefined

AO = { a : undefined
b : undefined
c : undefined }

3.将实参赋给形参

AO = { a : 1 b : undefined
c : undefined }

4.找函数声明,作为AO 属性名,值为函数体

AO = { a : function a() {}

b : undefined
c : function c() {} }

函数体内执行过程:

// 预编译后的状态:
var c
var b
var a
console.log(a); // function a() {}
 a = 123
console.log(a); // 123
b = function() {}
console.log(b);  //function b() {}
c = a
console.log(c); //123

掌握了预编译,你就真正理解了 JavaScript 的执行机制,这对后续学习闭包、作用域链等概念至关重要!

希望这篇文章能帮助你彻底理解 JavaScript 预编译,如果有任何疑问,欢迎在评论区讨论!

江西铜业:拟收购境外上市公司SolGold Plc股份,目前仍处非正式要约阶段

2025年11月30日 17:53
11月30日,江西铜业(600362.SH)公告称,公司已向伦敦证券交易所上市公司SolGold Plc提交两项非约束性现金要约,最新一项非约束性现金要约拟以每股26便士的价格收购其全部股份。目前公司持有目标公司12.19%的股份,但该收购事宜仍处于非正式要约阶段,且已被目标公司董事会拒绝。公司保留提出正式要约的权利,但具体是否提出存在不确定性。根据英国《城市收购及合并守则》,公司需在2025年12月26日前发布正式要约公告或明确放弃收购的公告。(每经网)

前端跨页面通讯终极指南②:BroadcastChannel 用法全解析

2025年11月30日 17:50

前言

上一篇介绍了PostMessage跨页面通讯的方式。有没有一种更简洁的方式,兄弟页面也能像父子页面一样通讯。今天就介绍一个更高效、更简洁的方案——BroadcastChannel API,它能轻松搞定父子、子父、兄弟页面间的通讯。

1. BroadcastChannel是什么?

BroadcastChannel 接口表示给定的任何浏览上下文都可以订阅的命名频道。它允许同源的不同浏览器窗口、标签页、frame 或者 iframe 下的不同文档之间相互通信。消息通过 message 事件进行广播,该事件在侦听该频道的所有 BroadcastChannel 对象上触发,发送消息的对象除外。

简单来说,它就像一个“无线电台”,多个页面只要订阅了同一个“频道”(指定相同的频道名称),就能接收该频道发送的所有消息,实现数据的双向流转。

需要注意,BroadcastChannel仅支持同源页面,兼容性略低,需要搭配其他方案作为降级处理。

2. 如何使用

BroadcastChannel的使用流程如下:

  1. 创建频道
  2. 订阅消息
  3. 发送消息
  4. 关闭订阅

2.1 创建频道(主题)

通过 new BroadcastChannel('channel-name')创建一个“频道”(相当于发布-订阅中的“主题”)。所有加入同一频道的上下文,共享同一个通信通道。

// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');

2.2 订阅消息(监听)

通过监听 message事件订阅该频道的消息(相当于订阅者注册回调):

channel.onmessage = (e) => {
  console.log('收到消息:', e.data); // e.data 就是发送的消息内容
  // 根据消息类型执行对应逻辑
  if (e.data.type === 'refresh') {
    // 执行刷新操作
  }
};

2.3 发布消息(发送)

通过 postMessage()向频道发送消息(相当于发布者触发发布):

channel.postMessage({
  type: 'refresh',
  data: { id: 123 }
});

2.4 退订(关闭)

通过 close()方法离开频道(可选,浏览器通常会在上下文销毁时自动清理):

window.addEventListener('beforeunload', () => {
  channel.close();
});
// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');

// 2. 接收消息
channel.onmessage = (e) => {
  console.log('收到消息:', e.data); // e.data 就是发送的消息内容
  // 根据消息类型执行对应逻辑
  if (e.data.type === 'refresh') {
    // 执行刷新操作
  }
};

// 3. 发送消息(支持字符串、对象等多种数据类型)
channel.postMessage({
  type: 'refresh',
  data: { id: 123 }
});

// 4. 关闭频道(页面卸载时调用,避免内存泄漏)
window.addEventListener('beforeunload', () => {
  channel.close();
});

3、实践场景

下面我们针对前端最常见的父子页面(父窗口打开子窗口)、子父页面(子窗口向父窗口反馈)、兄弟页面(同一父页面打开的多个子窗口)三种场景,分别给出具体的实现代码。

3.1 父子通讯

父页面点击“刷新子页面”按钮,子页面接收到指令后刷新数据。

父页面代码(打开子窗口并发送消息):

// 1. 创建频道
const parentChannel = new BroadcastChannel('parent-child-channel');

// 2. 点击按钮向子窗口发送消息
document.getElementById('refreshChildBtn').addEventListener('click', () => {
  parentChannel.postMessage({
    type: 'refresh-data',
    message: '父页面指令:请刷新数据'
  });
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  parentChannel.close();
});

子页面代码(接收父页面消息并执行操作):

// 1. 订阅同一个频道(频道名必须和父页面一致)
const childChannel = new BroadcastChannel('parent-child-channel');

// 2. 接收父页面消息
childChannel.onmessage = (e) => {
  const { type, message } = e.data;
  if (type === 'refresh-data') {
    // 显示接收的消息
    document.getElementById('message').textContent = message;
    // 执行刷新数据的逻辑
    refreshData();
  }
};

// 刷新数据的核心函数
function refreshData() {
  // 模拟请求接口刷新数据
  console.log('子页面正在刷新数据...');
  // 这里写具体的刷新逻辑
}

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  childChannel.close();
});

3.2 子父通讯(子窗口向父窗口反馈结果)

需求:子页面完成表单提交后,向父页面发送“提交成功”的消息,父页面接收到后关闭子窗口并刷新自身数据。

子页面代码(提交表单后发送消息):

// 1. 订阅频道(和父页面保持一致)
const childChannel = new BroadcastChannel('child-parent-channel');

// 2. 表单提交逻辑
document.getElementById('submitForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  try {
    // 模拟表单提交接口请求
    await submitFormData();
    // 提交成功后向父页面发送消息
    childChannel.postMessage({
      type: 'submit-success',
      data: { formId: 456, status: 'success' }
    });
    // 延迟关闭子窗口,确保消息发送完成
    setTimeout(() => {
      window.close();
    }, 300);
  } catch (error) {
    console.error('提交失败:', error);
  }
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  childChannel.close();
});

父页面代码(接收子页面消息并执行操作):

// 1. 创建频道
const parentChannel = new BroadcastChannel('child-parent-channel');

// 2. 接收子页面消息
parentChannel.onmessage = (e) => {
  const { type, data } = e.data;
  if (type === 'submit-success') {
    console.log('子页面表单提交成功:', data);
    // 执行父页面刷新逻辑
    parentRefreshData();
    // (可选)关闭子窗口(如果子窗口未自行关闭)
    // childWindow.close();
  }
};

// 父页面刷新数据函数
function parentRefreshData() {
  console.log('父页面正在刷新数据...');
  // 具体刷新逻辑
}

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  parentChannel.close();
});

3.3 兄弟通讯(多个子窗口间同步状态)

需求:父页面打开两个子窗口A和B,当子窗口A修改数据后,子窗口B实时同步更新数据。

子窗口A代码(修改数据后发送消息):

// 1. 订阅兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');

// 2. 模拟修改数据操作
document.getElementById('updateDataBtn').addEventListener('click', () => {
  const newData = {
    id: 789,
    content: '子窗口A修改后的新内容',
    updateTime: new Date().toLocaleString()
  };
  // 保存修改后的数据
  saveData(newData);
  // 向频道发送消息,通知其他兄弟窗口
  brotherChannel.postMessage({
    type: 'data-updated',
    newData: newData
  });
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  brotherChannel.close();
});

子窗口B代码(接收消息并同步数据):

// 1. 订阅同一个兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');

// 2. 接收子窗口A发送的消息
brotherChannel.onmessage = (e) => {
  const { type, newData } = e.data;
  if (type === 'data-updated') {
    console.log('收到子窗口A的更新消息:', newData);
    // 同步更新页面数据展示
    document.getElementById('dataContent').textContent = newData.content;
    document.getElementById('updateTime').textContent = newData.updateTime;
  }
};

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  brotherChannel.close();
});

接收输入如下:

image.png

4. 总结

最后总结一下:对比传统的postMessage跨页面通讯方案,BroadcastChannel兄弟页面无需转发,几行代码就能轻松实现通讯。

百利天恒:收到与百时美施贵宝就iza-bren项目里程碑付款2.5亿美元

2025年11月30日 17:47
百利天恒11月30日公告称,全资子公司SystImmune近期收到由BMS支付的2.5亿美元里程碑付款。根据合作协议,公司后续还有资格获得最高可达2.5亿美元的近期或有付款,以及在达到特定的开发、注册和销售里程碑后最高可达71亿美元的额外付款。(界面)

el-button源码解读3——:class="buttonKls"与颜色系统的关系

作者 Joie
2025年11月30日 17:44

说明 :class="buttonKls" 与颜色系统的关系:

1. buttonKls 的作用

buttonKls 是一个计算属性,生成按钮需要的所有 CSS 类名:

const buttonKls = computed(() => [
  // el-button
  ns.b(),
  ns.m(_type.value),
  ns.m(_size.value),
  ns.is('disabled', _disabled.value),
  ns.is('loading', props.loading),
  ns.is('plain', _plain.value),
  ns.is('round', _round.value),
  ns.is('circle', props.circle),
  ns.is('text', _text.value),
  ns.is('link', props.link),
  ns.is('has-bg', props.bg),
])

假设 type="primary"buttonKls 可能包含:

['el-button', 'el-button--primary', 'el-button--small', 'is-loading', ...]

2. 颜色系统的触发机制

颜色系统通过 CSS 类选择器触发。当 buttonKls 包含 'el-button--primary' 时,会匹配到对应的 CSS 规则。

3. 完整的关系链

用户使用: <el-button type="primary">
    ↓
props.type = 'primary'_type.value = 'primary'buttonKls = ['el-button', 'el-button--primary', ...]
    ↓
:class="buttonKls"  →  class="el-button el-button--primary"
    ↓
CSS 匹配: .el-button--primary { ... }
    ↓
button-variant('primary') 生成 CSS 变量
    ↓
应用颜色样式

4. 在 CSS 中的对应关系

button.scss 中的定义:

  @each $type in (primary, success, warning, danger, info) {
    @include m($type) {
      @include button-variant($type);
    }
  }

$type = 'primary' 时:

  • @include m('primary') → 生成 .el-button--primary 选择器
  • @include button-variant('primary') → 生成颜色相关的 CSS 变量

5. 实际渲染过程

步骤 1:Vue 组件生成类名

<el-button type="primary">按钮</el-button>
// buttonKls 计算后
['el-button', 'el-button--primary']
<!-- 最终渲染 -->
<button class="el-button el-button--primary">
  按钮
</button>

步骤 2:CSS 匹配类名

浏览器看到 class="el-button--primary",匹配到:

// button.scss 中生成的
.el-button--primary {
  // button-variant('primary') 生成的 CSS 变量
  --el-button-bg-color: var(--el-color-primary);
  --el-button-border-color: var(--el-color-primary);
  --el-button-text-color: var(--el-color-white);
  --el-button-hover-bg-color: var(--el-color-primary-light-3);
  --el-button-active-bg-color: var(--el-color-primary-dark-2);
}

步骤 3:应用颜色

基础样式使用这些 CSS 变量:

.el-button {
  background-color: var(--el-button-bg-color);
  border-color: var(--el-button-border-color);
  color: var(--el-button-text-color);
}

因为 .el-button--primary 定义了这些变量,所以按钮会显示 primary 的颜色。

6. 关键理解

:class="buttonKls" 是连接 Vue 组件和 CSS 颜色系统的桥梁:

Vue 组件层面          CSS 样式层面
─────────────────────────────────
buttonKls            .el-button--primary
  ↓                       ↓
'el-button--primary'  →  匹配 CSS 选择器
  ↓                       ↓
应用类名              应用颜色样式

7. 不同类型的关系

用户传入 buttonKls 包含 CSS 匹配 颜色应用
type="primary" 'el-button--primary' .el-button--primary 蓝色 (#409eff)
type="success" 'el-button--success' .el-button--success 绿色 (#67c23a)
type="warning" 'el-button--warning' .el-button--warning 橙色 (#e6a23c)
type="danger" 'el-button--danger' .el-button--danger 红色 (#f56c6c)

8. 完整示例

<el-button type="primary" size="small" :loading="true">
  提交
</el-button>

生成的类名:

buttonKls = [
  'el-button',           // 基础类
  'el-button--primary',  // 类型类(触发颜色系统)
  'el-button--small',    // 尺寸类
  'is-loading'           // 状态类
]

最终 HTML:

<button class="el-button el-button--primary el-button--small is-loading">
  提交
</button>

CSS 匹配:

.el-button--primary {  // ← 这个类触发了颜色系统
  --el-button-bg-color: var(--el-color-primary);
  // ...
}

9. 总结

  • :class="buttonKls" 生成类名(如 el-button--primary
  • CSS 通过类选择器匹配这些类名
  • 匹配到的规则通过 button-variant 生成颜色变量
  • 基础样式使用这些变量,最终显示对应颜色

核心关系buttonKls 中的类型类名(如 el-button--primary)是触发颜色系统的开关,CSS 通过这个类名应用对应的颜色样式。

沪电股份:向香港联交所递交H股发行上市申请并刊发申请资料

2025年11月30日 17:43
沪电股份(002463.SZ)公告称,已于2025年11月28日向香港联交所递交了发行H股股票并在香港联交所主板挂牌上市的申请,并于同日在香港联交所网站刊登了本次发行上市的申请资料。该申请资料为草拟版本,其所载资料可能会适时作出更新和变动。公司本次发行上市尚需取得相关政府机关、监管机构、证券交易所的批准、核准或备案。(财联社)

搞懂作用域链与闭包:JS底层逻辑变简单

作者 闲云ing
2025年11月30日 17:39

JS底层小揭秘:作用域链与闭包,代码+图解一看就懂

在 JavaScript 的学习过程中,理解其底层运行机制是进阶的关键,而作用域链和闭包更是其中的核心概念。,很多人只停留在“会用”,没搞懂底层逻辑。本文结合代码+调用栈图解,从V8引擎的运行机制出发,拆解这两个概念的本质,帮你从底层视角搞懂 JS 的执行规则。

一、先搭好JS底层的基础框架

JS代码能运行,依赖V8引擎的三个核心模块:

  1. 调用栈:分编译阶段(处理变量/函数提升)和执行阶段(创建执行上下文并压入栈,执行完弹出);
  2. 执行上下文:全局执行上下文(始终在栈底)+ 函数执行上下文(函数调用时创建);
  3. 作用域:定义变量的查找范围和生命周期,包含let/const块级作用域(依托栈结构的词法环境),以及var变量提升特性。

二、作用域链:静态的变量查找路径

作用域链(词法作用域链)的核心是:它由函数声明的位置决定,编译阶段就固定了,和调用顺序无关

案例1:为什么bar里的myName取全局值?

对应代码与图示:

function bar(){
  console.log(myName); // 输出“极客时间”
}
function foo() {
  var myName = '极客邦'
  bar() // 在foo内部调用bar
}
var myName = '极客时间'
foo();

1.jpg

2.jpg

运行逻辑拆解:
  1. 编译阶段:barfoo被声明在全局作用域,因此它们的作用域链默认“自身→全局”;

  2. 执行阶段:

    1. foo调用时,创建foo执行上下文(变量环境包含myName="极客邦")并压入栈;
    2. foo内部调用bar,创建bar执行上下文并压入栈;
    3. bar中查找myName:自身变量环境无→通过outer指向的全局执行上下文查找→取全局的"极客时间"

案例2:块级作用域下的变量查找

对应代码与图示:

function bar () {
  var myName = '极客世界';
  let test1 = 100;
  if (1) {
    let myName = "Chrome 浏览器";
    console.log(test); 
  }
}
function foo() {
  var myName = '极客邦';
  let test = 2;
  {
    let test = 3;
    bar();
  }
}
var myName = '极客时间';
let myAge = 10;
let test = 1;
foo();

3.jpg

查找过程:
  1. bar内部if块的console.log(test),先查自身块级词法环境(只有myName="Chrome 浏览器")→ 没找到;
  2. bar函数的词法环境(有test1=100)→ 没找到;
  3. bar的变量环境(有myName="极客世界")→ 没找到;
  4. 查全局执行上下文的词法环境(有test=1)→ 但由于bar的作用域链是“自身块级→bar函数→全局”,运行中会输出1

三、闭包:函数“背着”变量的专属背包

闭包是词法作用域的延伸——外部函数执行后,其内部变量被嵌套函数引用,因此不会被垃圾回收,形成一个“变量背包”供嵌套函数使用

案例:闭包的形成与运行

对应代码与图示:

function foo() {
  var myName = '极客时间'
  let test1 = 1
  const test2 = 2
  var innerBar = {
    getName: function() {
      console.log(test1) // 输出1
      return myName
    },
    setName: function(newName) {
      myName = newName // 修改foo内部的myName
    }
  }
  return innerBar
}

var bar = foo() // foo执行上下文出栈
bar.setName("极客邦")
console.log(bar.getName()); // 输出1 + “极客邦”

5.jpg

6.jpg

闭包运行流程:
  1. foo执行时:创建执行上下文,变量环境存储myName="极客时间",词法环境存储test1=1test2=2,并压入栈;

  2. foo返回innerBar后:foo执行上下文出栈,但getName/setName引用了myNametest1,这两个变量被保留在内存中(形成闭包,即“专属背包”);

  3. 调用bar.setName/getName时

    1. setName执行时,通过闭包找到myName并修改为"极客邦"
    2. getName执行时,通过闭包找到test1并输出1,同时返回修改后的myName

四、核心总结

  1. 作用域链是静态的:由函数声明位置决定,编译阶段固定,和调用顺序无关;
  2. 闭包是词法作用域的延伸:嵌套函数引用外部函数变量,导致外部函数变量不被回收,形成“变量背包”;
  3. 底层逻辑的关键:理解调用栈、执行上下文、作用域的关系,是搞懂JS变量查找和内存管理的基础。

JavaScript 的底层运行机制中,词法作用域是基础,它决定了作用域链的静态查找规则,而闭包则是词法作用域的延伸,通过保留自由变量实现了函数对外部作用域的持久访问。理解这些概念,不仅能帮助我们写出更符合 JS 运行逻辑的代码,还能解决实际开发中变量作用域、内存泄漏等常见问题。掌握作用域链与闭包,是深入理解 JavaScript 语言特性的重要一步。

element-plus源码解读3——【scss】颜色系统完整流程

作者 Joie
2025年11月30日 17:30

一、基础颜色定义(源头)

位置:packages/theme-chalk/src/common/var.scss

scss知识点:$ 表示变量

  • 定义所有颜色的基础值
// types
$types: primary, success, warning, danger, error, info;

// Color
$colors: () !default;
$colors: map.deep-merge(
  (
    'white': #ffffff,
    'black': #000000,
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  ),
  $colors
);

二、自动生成颜色变体(light/dark)

位置:packages/theme-chalk/src/common/var.scss

定义一个mixin: set-color-mix-level:

将基础颜色与白色或黑色混合

生成指定级别的浅色或深色变体

将生成的颜色添加到$colors map中

// mix colors with white/black to generate light/dark level
@mixin set-color-mix-level(
  $type,
  $number,
  $mode: 'light',
  $mix-color: $color-white
) {
  $colors: map.deep-merge(
    (
      $type: (
        /**
        * roundColor:将颜色的 RGB 通道值四舍五入为整数,并返回 rgba() 格式的颜色。
        * color.mix:混合颜色 第一个参数和第二个参数按照第三个参数的百分比混合颜色
        * map.get($colors, $type, 'base'):获取颜色
        * math.percentage(math.div($number, 10)):将数字转换为百分比
        */
          '#{$mode}-#{$number}': roundColor(
            color.mix(
              $mix-color,
              map.get($colors, $type, 'base'),
              math.percentage(math.div($number, 10))
            )
          ),
      ),
    ),
    $colors
  ) !global;
}

使用

// $colors.primary.light-i
// --el-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff

// 外层循环:遍历颜色类型
@each $type in $types {
  // 内层循环:生成 1-9 的变体
  @for $i from 1 through 9 {
    // 调用 mixin 生成不同亮度的颜色
    @include set-color-mix-level($type, $i, 'light', $color-white);
  }
}


// 第 1 轮:$type = primary
@include set-color-mix-level(primary, 1, 'light', $color-white);  // 生成 primary-light-1
@include set-color-mix-level(primary, 2, 'light', $color-white);  // 生成 primary-light-2
@include set-color-mix-level(primary, 3, 'light', $color-white);  // 生成 primary-light-3
// ... 继续到 9

// 第 2 轮:$type = success
@include set-color-mix-level(success, 1, 'light', $color-white);   // 生成 success-light-1
@include set-color-mix-level(success, 2, 'light', $color-white);   // 生成 success-light-2
// ... 继续到 9

// 依此类推,直到遍历完所有 6 种类型

三、生成 CSS 变量(全局)

位置:packages/theme-chalk/src/var.scss

// join var name
// joinVarName(('button', 'text-color')) => '--el-button-text-color'
// 将$list遍历中间用-拼接每一个$item
@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

===============================================================

// set css var value, because we need translate value to string
// for example:
// @include set-css-var-value(('color', 'primary'), red);
// --el-color-primary: red;
// 返回 变量名:颜色值 的形式
@mixin set-css-var-value($name, $value) {
  #{joinVarName($name)}: #{$value};
}

================================================================

@mixin set-css-color-type($colors, $type) {
  // 生成基础颜色变量
  @include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
  // 结果:--el-color-primary: #409eff;

  // 生成浅色变量(3, 5, 7, 8, 9)
  @each $i in (3, 5, 7, 8, 9) {
    @include set-css-var-value(
      ('color', $type, 'light', $i),
      map.get($colors, $type, 'light-#{$i}')
    );
  }
  // 结果:
  // --el-color-primary-light-3: #79bbff;
  // --el-color-primary-light-5: #a0cfff;
  // --el-color-primary-light-7: #c6e2ff;
  // --el-color-primary-light-8: #d9ecff;
  // --el-color-primary-light-9: #ecf5ff;

  // 生成深色变量
  @include set-css-var-value(
    ('color', $type, 'dark-2'),
    map.get($colors, $type, 'dark-2')
  );
  // 结果:--el-color-primary-dark-2: ...;
}

=================================================================

:root {
  color-scheme: light;

  // --el-color-#{$type}
  // --el-color-#{$type}-light-{$i}
  @each $type in (primary, success, warning, danger, error, info) {
    @include set-css-color-type($colors, $type);
  }
  
生成的css变量,以primary为例子
:root {
  --el-color-primary: #409eff;
  --el-color-primary-light-3: #79bbff;
  --el-color-primary-light-5: #a0cfff;
  --el-color-primary-light-7: #c6e2ff;
  --el-color-primary-light-8: #d9ecff;
  --el-color-primary-light-9: #ecf5ff;
  --el-color-primary-dark-2: #337ecc;
}

四、组件级使用——以el-button为例子

位置:packages/theme-chalk/src/button.scss

// generate css var from existing css var
// for example:
// @include css-var-from-global(('button', 'text-color'), ('color', $type))
// --el-button-text-color: var(--el-color-#{$type});
@mixin css-var-from-global($var, $gVar) {
  $varName: joinVarName($var);
  $gVarName: joinVarName($gVar);
  #{$varName}: var(#{$gVarName});
}

==========================================================

$button-color-types 是一个 SCSS 变量(map),定义在 button-variant mixin 内部。它是一个嵌套的 map,用于定义按钮在不同状态下的颜色映射关系。

$button-color-types: (
    '': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
      ),
      'border-color': (
        'color',
        $type,
      ),
      'outline-color': (
        'color',
        $type,
        'light-5',
      ),
      'active-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'hover': (
      'text-color': (
        'color',
        'white',
      ),
      'link-text-color': (
        'color',
        $type,
        'light-5',
      ),
      'bg-color': (
        'color',
        $type,
        'light-3',
      ),
      'border-color': (
        'color',
        $type,
        'light-3',
      ),
    ),
    'active': (
      'bg-color': (
        'color',
        $type,
        'dark-2',
      ),
      'border-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'disabled': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
        'light-5',
      ),
      'border-color': (
        'color',
        $type,
        'light-5',
      ),
    ),
  );
  
  // 结构层次
  $button-color-types (第一层:状态)
  ├─ '' (默认状态)
  │   ├─ 'text-color' → ('color', 'white')
  │   ├─ 'bg-color' → ('color', $type)
  │   ├─ 'border-color' → ('color', $type)
  │   ├─ 'outline-color' → ('color', $type, 'light-5')
  │   └─ 'active-color' → ('color', $type, 'dark-2')
  │
  ├─ 'hover' (悬停状态)
  │   ├─ 'text-color' → ('color', 'white')
  │   ├─ 'link-text-color' → ('color', $type, 'light-5')
  │   ├─ 'bg-color' → ('color', $type, 'light-3')
  │   └─ 'border-color' → ('color', $type, 'light-3')
  │
  ├─ 'active' (激活状态)
  │   ├─ 'bg-color' → ('color', $type, 'dark-2')
  │   └─ 'border-color' → ('color', $type, 'dark-2')
  │
  └─ 'disabled' (禁用状态)
      ├─ 'text-color' → ('color', 'white')
      ├─ 'bg-color' → ('color', $type, 'light-5')
      └─ 'border-color' → ('color', $type, 'light-5')

================================================================

  @each $type, $typeMap in $button-color-types {
    // 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
    // $typeColor:属性名,例如:'text-color'
    // $list:属性值,例如:('color', 'white')
    @each $typeColor, $list in $typeMap {
      // 调用 css-var-from-global 生成 CSS 变量
      // 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
      @include css-var-from-global(('button', $type, $typeColor), $list);
    }
  }
  
 // 当 $type = 'primary' 时,会生成:
 /* 默认状态 */
--el-button-text-color: var(--el-color-white);
--el-button-bg-color: var(--el-color-primary);
--el-button-border-color: var(--el-color-primary);
--el-button-outline-color: var(--el-color-primary-light-5);
--el-button-active-color: var(--el-color-primary-dark-2);

/* 悬停状态 */
--el-button-hover-text-color: var(--el-color-white);
--el-button-hover-link-text-color: var(--el-color-primary-light-5);
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-hover-border-color: var(--el-color-primary-light-3);

/* 激活状态 */
--el-button-active-bg-color: var(--el-color-primary-dark-2);
--el-button-active-border-color: var(--el-color-primary-dark-2);

/* 禁用状态 */
--el-button-disabled-text-color: var(--el-color-white);
--el-button-disabled-bg-color: var(--el-color-primary-light-5);
--el-button-disabled-border-color: var(--el-color-primary-light-5);

=====================================================================

完整源码:
@mixin button-variant($type) {
  $button-color-types: (
    '': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
      ),
      'border-color': (
        'color',
        $type,
      ),
      'outline-color': (
        'color',
        $type,
        'light-5',
      ),
      'active-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'hover': (
      'text-color': (
        'color',
        'white',
      ),
      'link-text-color': (
        'color',
        $type,
        'light-5',
      ),
      'bg-color': (
        'color',
        $type,
        'light-3',
      ),
      'border-color': (
        'color',
        $type,
        'light-3',
      ),
    ),
    'active': (
      'bg-color': (
        'color',
        $type,
        'dark-2',
      ),
      'border-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'disabled': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
        'light-5',
      ),
      'border-color': (
        'color',
        $type,
        'light-5',
      ),
    ),
  );

  // 外层循环,遍历第一层(状态):'', 'hover', 'active', 'disabled'
  // $type状态名例如'hover';
  // $typeMap:状态对应的属性值,例如:('text-color': (
  //   'color',
  //   'white',
  // ),)
  @each $type, $typeMap in $button-color-types {
    // 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
    // $typeColor:属性名,例如:'text-color'
    // $list:属性值,例如:('color', 'white')
    @each $typeColor, $list in $typeMap {
      // 调用 css-var-from-global 生成 CSS 变量
      // 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
      @include css-var-from-global(('button', $type, $typeColor), $list);
    }
  }

  &.is-plain,
  &.is-text,
  &.is-link {
    @include button-plain($type);
  }
}

五、应用样式(最终渲染)

位置:packages/theme-chalk/src/button.scss

@include b(button) {
  display: inline-flex;
  justify-content: center;
  align-items: center;

  line-height: 1;
  // min-height will expand when in flex
  height: map.get($input-height, 'default');
  white-space: nowrap;
  cursor: pointer;
  color: getCssVar('button', 'text-color');
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: getCssVar('button', 'font-weight');
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: getCssVar('button', 'bg-color');
  border: getCssVar('border');
  border-color: getCssVar('button', 'border-color');

  &:hover {
    color: getCssVar('button', 'hover', 'text-color');
    border-color: getCssVar('button', 'hover', 'border-color');
    background-color: getCssVar('button', 'hover', 'bg-color');
    outline: none;
  }
 
 // 这些生成el-button的基础变量
  ======================================================
 
   @each $type in (primary, success, warning, danger, info) {
    @include m($type) {
      @include button-variant($type);
    }
  }

  
  // $type = 'primary' 时,最终编译后的 CSS(.el-button--primary):
  .el-button--primary {
  /* 基础样式 */
  display: inline-flex;
  justify-content: center;
  align-items: center;
  
  /* 颜色(使用 CSS 变量) */
  color: var(--el-button-text-color);
  /* ↓ 展开为 */
  color: var(--el-color-white);  /* #ffffff */
  
  background-color: var(--el-button-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary);  /* #409eff */
  
  border-color: var(--el-button-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary);  /* #409eff */
}

.el-button--primary:hover {
  background-color: var(--el-button-hover-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary-light-3);  /* #79bbff */
  
  border-color: var(--el-button-hover-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary-light-3);  /* #79bbff */
}

.el-button--primary:active {
  background-color: var(--el-button-active-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary-dark-2);  /* #337ecc */
  
  border-color: var(--el-button-active-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary-dark-2);  /* #337ecc */
}

六、完整流程图

┌─────────────────────────────────────────────────────────┐
│ 阶段 1: 基础颜色定义                                     │
│ var.scss: primary.base = #409eff                       │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 2: 自动生成颜色变体                                 │
│ set-color-mix-level()                                   │
│ - primary.light-3 = #79bbff (30% 白色混合)             │
│ - primary.dark-2 = #337ecc (20% 黑色混合)              │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 3: 生成全局 CSS 变量                                │
│ var.scss: :root {                                       │
│   --el-color-primary: #409eff                          │
│   --el-color-primary-light-3: #79bbff                  │
│   --el-color-primary-dark-2: #337ecc                    │
│ }                                                       │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 4: 按钮组件使用颜色                                  │
│ button-variant('primary')                                │
│ 生成组件级 CSS 变量:                                     │
│ --el-button-bg-color: var(--el-color-primary)           │
│ --el-button-hover-bg-color: var(--el-color-primary-light-3)│
│ --el-button-active-bg-color: var(--el-color-primary-dark-2)│
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 5: 最终渲染                                         │
│ .el-button--primary {                                   │
│   background-color: var(--el-button-bg-color);          │
│   /* 浏览器解析为 #409eff */                            │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

七、优势

  • 统一管理:所有颜色在一个地方定义。
  • 自动计算:light/dark 变体自动生成。
  • 易于定制:修改基础色即可影响所有变体和组件。
  • 性能:使用 CSS 变量,支持运行时动态切换主题。

赣锋锂业:赣锋国际拟向中非基金发行1亿美元可交换票据

2025年11月30日 17:28
赣锋锂业(002460.SZ)公告称,公司全资子公司赣锋国际拟向中非发展基金有限公司发行总额为1亿美元的可交换票据,中非基金可根据票据金额及应付利息转换成赣锋国际全资子公司MaliLithiumB.V.的A类优先股。公司为本次交易提供担保,并授权经营层办理相关事宜。(财联社)

CSS 视口单位进化论:从 100vh 的「骗局」到 dvh 的救赎

作者 兔子零1024
2025年11月30日 17:22

关键词:CSS / 视口单位 / 移动端适配 / 响应式设计 / dvh / svh / lvh / 100vh bug / 前端工程化 / 用户体验


引言:一个持续了十年的移动端「谎言」

如果你做过移动端 H5 开发,或者写过全屏落地的营销页,你一定遇到过那个著名的「100vh 骗局」。

设计师给了你一张完美的设计稿:首屏全屏,底部按钮固定,不允许有滚动条
你自信满满地写下:

.hero-section {
  height: 100vh;
}

然后你在 iPhone 的 Safari 里打开,灾难发生了:底部的按钮被浏览器底部的工具栏遮住了一半,或者页面莫名其妙出现了一截滚动条

1.jpg

(图解:左侧是理想设计,按钮在最底部;右侧是 Safari 现状,浏览器工具栏无情地盖住了你的按钮)

为了解决这个问题,前端社区发明了无数「歪门邪道」:

  • window.innerHeight 动态计算高度赋给 CSS 变量 --vh
  • 监听 resize 事件疯狂重绘。
  • 使用 position: fixed; top: 0; bottom: 0; 这种古老的 hack。

为什么一个简单的「全屏」需求,在移动端会如此艰难?
这背后其实是 Web 标准与移动端原生 UI(动态地址栏、底部工具栏)长达十年的博弈。

好消息是,CSS 终于迎来了真正解决问题的新一代视口单位:svhlvhdvh 以及它们在宽度和逻辑方向上的兄弟们。

这不是简单的语法升级,这是 Web 终于承认:移动端的屏幕,从来就不是一个静态的矩形。


一、旧时代的妥协:为什么 100vh 不是真的 100% 高度?

1.1 完美的初衷与残酷的现实

在 PC 时代,vh (Viewport Height) 的定义非常完美:视口高度的 1%。浏览器窗口多高,100vh 就多高。

但在移动端,情况变了。iOS Safari 和 Chrome Android 为了给用户更多阅读空间,设计了动态工具栏

  • 初始状态:地址栏、底部工具栏展开,可视区域变小。
  • 上滑状态:地址栏缩小/隐藏,工具栏消失,可视区域变大。

这时候,100vh 该等于「展开时的高度」还是「隐藏时的高度」?

1.2 浏览器的「摆烂」选择

早期的 Safari 做了一个影响深远的决定:为了避免滚动时页面元素跳动(Reflow),100vh 永远等于「地址栏隐藏时的最大高度」

2.jpg

(图解:无论你的工具栏是否展开,Safari 始终认为 100vh 是那个「最大值」。这就导致当工具栏展开(实际只有 80% 高度)时,你的 100vh 内容溢出了 20%。)

这就解释了为什么你的按钮被遮住了:

  • 浏览器告诉你 100vh 是 800px(假设全屏高度)。
  • 但实际上底部的工具栏占了 80px,可视区域只有 720px。
  • 你的按钮画在了第 750px 的位置,正好被工具栏盖得严严实实。

这个「特性」被无数开发者吐槽为 Bug,但浏览器厂商坚持这是「Feature」。

「他们为了滚动的丝滑,牺牲了布局的精确。
在很长一段时间里,我们只能用 JS 来修补 CSS 的谎言。」


二、新秩序的建立:svh、lvh、dvh 的三态哲学

W3C 终于意识到,移动端的视口不是一个固定的值,而是一个在「大」与「小」之间薛定谔的波动状态

于是,CSS Viewport Units Level 3 引入了一套全新的单位体系,把视口高度拆分为三种状态。

3.jpg

2.1 svh (Small Viewport Height):永远不再被遮挡

定义:视口高度的最小值(即地址栏、工具栏全部展开时的可视高度)。

  • 应用场景首屏全屏页、底部固定按钮、Modal 弹窗
  • 价值:如果你写 height: 100svh,你的页面底部绝对不会被工具栏遮挡。它是最安全的「一屏」。
/* 以前的 JS Hack */
.modal {
  height: calc(var(--vh, 1vh) * 100);
}

/* 现在的原生写法 */
.modal {
  height: 100svh;
}

2.2 lvh (Large Viewport Height):沉浸式的极致

定义:视口高度的最大值(即地址栏、工具栏全部收起时的可视高度)。

  • 应用场景背景图、视差滚动容器
  • 价值:当你希望背景铺满整个「潜在」屏幕,不在乎一部分被遮挡时,用 lvh。它等同于旧时代的 100vh

2.3 dvh (Dynamic Viewport Height):动态的完美与代价

定义动态视口高度。它会随着地址栏的伸缩,实时在 svhlvh 之间变化。

  • 应用场景追求极致体验的流式布局
  • 代价:当用户滚动页面时,100dvh 的值会不断变化。这意味着浏览器可能需要不断重绘(Repaint)甚至重排(Reflow)。
  • 现状:现代浏览器对 dvh 做了很多优化,性能损耗在大多数场景下已可忽略不计。

三态哲学总结:

  • svh 是保底的安全(Safe)。
  • lvh 是理想的宏大(Large)。
  • dvh 是真实的动态(Dynamic)。

三、维度的扩张:宽度、逻辑方向与极值

这套逻辑不仅修复了高度问题,还顺带重构了整个视口单位体系。

3.1 宽度的进化:svw / lvw / dvw

虽然移动端宽度的变化(主要是滚动条出现/消失)不如高度那么剧烈,但逻辑是一致的。

  • vw:依然是默认视口宽度(通常包含滚动条)。
  • svw / lvw / dvw:处理滚动条存在与否时的细微差异(在桌面端更明显)。

3.2 逻辑方向:vi / vb —— 国际化的必修课

随着 CSS 逻辑属性(Logical Properties)的普及,我们不再总是谈论 widthheight,而是谈论 inline(文本流方向)和 block(块堆叠方向)。

  • vi (Viewport Inline):视口在行内方向的大小。

    • 横屏/竖屏书写模式下:等于 vw
    • 竖排书写模式(如古诗词页面):等于 vh
    • svi / lvi / dvi:对应的三态变体。
  • vb (Viewport Block):视口在块级方向的大小。

    • 横屏/竖屏书写模式下:等于 vh
    • svb / lvb / dvb:对应的三态变体。

实战案例:一个适配横竖屏书写的古诗卡片

.poem-card {
  /* 无论横排还是竖排,永远占满视口在「块」方向上的 80% */
  block-size: 80vb; 
  
  /* 无论横排还是竖排,永远占满视口在「行」方向上的 90% */
  inline-size: 90vi;
  
  /* 自动适配书写模式 */
  writing-mode: vertical-rl; /* 切换这个属性,布局依然完美 */
}

3.3 极值单位:vmin / vmax 的三态

老朋友 vmin(vw/vh 中较小者)和 vmax(较大者)也全员升级:

  • svmin / svmax
  • lvmin / lvmax
  • dvmin / dvmax

场景:做横竖屏适配的 H5 游戏或画报时,svmin 是保证内容在任何旋转状态下都完整可见的神器。


四、工程实战:如何在 2025 年写好一个全屏页面?

4.1 放弃 100vh,拥抱 dvh(但要做降级)

在 2025 年,如果你的目标浏览器支持率允许(iOS 15.4+, Chrome 108+),dvh 是全屏容器的最佳选择

.full-screen-hero {
  /* 降级方案:给旧浏览器一个固定的值 */
  height: 100vh;
  
  /* 现代方案:使用动态高度,完美贴合 */
  height: 100dvh;
}

4.2 关键交互区域用 svh

对于底部的操作栏(Action Bar),不要用 dvh,因为你不想让按钮在用户手指滑动时「跳来跳去」。svh 锁定位置

.bottom-bar {
  position: fixed;
  bottom: 0;
  width: 100%;
  /* 确保它永远在可视区底部,哪怕地址栏展开 */
  bottom: calc(100vh - 100svh); /* 高级技巧:计算工具栏高度偏移 */
}

/* 或者更简单的布局思维: */
.app-container {
  min-height: 100svh;
  display: grid;
  grid-template-rows: 1fr auto;
}

4.3 慎用 lvh,除非你在做特效

lvh 在实际 UI 布局中用得很少。它更多用于视觉背景,或者那种「滑一下就全屏」的沉浸式阅读体验。如果你用 lvh 做布局容器,用户大概率会因为点不到底部的按钮而骂娘。

「成熟的工程师懂得:
UI 的稳定性(svh)优先于 UI 的充满感(lvh),
而 dvh 是两者之间的优雅平衡。」


五、总结:从「对抗」到「接纳」

回顾 CSS 视口单位的进化史,其实是一部 Web 与移动端原生特性的磨合史。

  • vh 时代:Web 试图假装自己还在 PC 上,无视了移动端复杂的 UI 变化,结果撞得头破血流(100vh bug)。
  • JS Hack 时代:开发者用脚本强行修正高度,对抗浏览器的默认行为,性能差且代码丑陋。
  • svh/dvh 时代:标准终于接纳了移动端的复杂性。我们不再强求一个「唯一的 100%」,而是承认「屏幕是会变的」

这给了我们两个重要的启示:

  1. 不存在完美的静态适配。移动端设备极其碎片化,折叠屏、刘海屏、动态栏……拥抱动态(Dynamic)才是终极解法
  2. 工具的粒度决定了体验的细腻度。从粗糙的 vh 到精细的 svh/lvh/dvh,前端工程化的本质,就是不断提升对像素级体验的掌控力。

下一次,当设计师问你「能不能把这个页面铺满全屏,不要滚动条,也不要被遮挡」时,
你可以自信地合上电脑(或者打开 VS Code),告诉他:
「当然可以,因为现在的 CSS,终于懂手机了。」

三百多万人围观的 AI 油画视频,是技术的神作,还是没有灵魂的电子垃圾

作者 张子豪
2025年11月30日 17:21

「比蒙娜丽莎更美的,就是正在燃烧的蒙娜丽莎」,这是多年前一档辩论节目里,大家对于艺术价值的不同理解方式,那时听到可能觉得挺激进,笑一笑便过了。

最近一段把几幅经典油画「复活」的 AI 视频,在 X 上引起了巨大的争议,视频刷到 300 多万播放,被不少人称之为栩栩如生的艺术;比经典油画作品更好看的,是会流动的油画?AI 做的东西是不是没有任何艺术价值?

网友分享的油画视频里,经典的油画元素不再静止,颜料开始流动,天空的云朵、火山的喷发、还有海浪的汹涌都变得生动自然,仿佛那些存在几百年前的画布,突然拥有了生命一样。

乍看之下,这就是一场视觉盛宴的享受;如果不是其中几个视频,忘了去掉右下角 Google Veo 视频生成的水印,甚至会觉得完全是用 CG 特效制作实现,毕竟对油画风格来说,没有很明显的「AI 味」。

但是点开评论区之后,发现网友们撕成了两派,有人说,这就是新时代的艺术,是全新的审美体验;有人就不买单,用 AI 时代最刻薄的词汇——Slop(垃圾/泔水)一言蔽之,说等到 AI 有意识了,再来谈配不配成为艺术。

同样的一条 AI 视频,让人看到了艺术、技术、恐惧、愤怒、敬畏、厌烦,还有时代变化。

如果不说这是 AI,你的第一反应是

X 网友发布的这则视频,其实并不是他本人的原创,在评论区有人指出来,说他没有标注视频来源,也没说明使用了 AI,只是单纯地为了赚取流量。现在这波流量,也确实是被他赚到了。

视频最早是出现 YouTube 上,一位有着 2000 多粉丝的博主@bandyquantguy,他是宾夕法尼亚州立大学艺术与建筑学院的一名助理教学教授。频道内发布的内容,基本上都是不同油画的动态视频作品,长度在一分半到 3 分钟不等。

而那条被转发到 X 上的视频,并收获了三百多万的观看,是他将近一个月之前的作品。

当我看着满屏的动态油画时,说实话,根本没想到这是否通过 AI 生成。一方面是对油画艺术的不了解,是否有相关的技术,或者爱好者在专门做类似的工作。另一方面,大多数时候,我们所说的 AI 味,主要是在制作一些写实的画面,像现实世界、真人版等。而这种风格化本身就足够突出的内容,AI 的处理反而不会太突兀。

有网友评价,这是他见过最原汁原味的 AI 艺术作品之一,因为视频没有生硬地模仿现实,而是创造了一种介于梦境,与现实之间的流体美感。

第一眼都是觉得「震撼」,而这种迎面而来的视觉冲击,在知道它是 AI 生成的之后,也变成了争议的起点。大多数的人会觉得,这样的作品很棒;但对剩下一部分人来说,如果光靠 AI 就能得到原本属于「艺术」的内容,那该有多可怕。

Slop,AI 是原罪

所以,打压和看衰,成了评论区的另一种态度,Slop 就是代表性的关键词。

在 AI 语境下,Slop 指的是通过 AI 大量生成的、看似有内容实则空洞的劣质品。像是之前奥特曼推出 Sora,就有人犀利的丢下断言,Sora 生成的视频,全部都是 AI Slop。

这次,也有网友说,这样的油画视频,不应该放在社交媒体上,Sora 才是他最好的归宿,那里都是同样的 AI 垃圾。为什么画面如此精美的视频,会被称为 Slop?反而一些 AI 味明显的视频,激不起大家这么热烈的反馈。

因为它是机器盲目的困境。

反对者认为,AI 生成的内容,是缺乏意义,就像是一台机器盲目的梦境,它甚至不知道自己正在做梦。

他们的观点是,艺术不仅仅是停留在好看,那只是媚俗。艺术需要人类的意图、深度和复杂性。在这个视频里,原本油画的笔触,变成了毫无逻辑的像素流动,就像是单纯地为了展示「我能动」而动,没有任何节奏、理由或概念支撑这个视频的内容。

对他们来说,AI 最大的问题从来不是不够好看,而是「不够人」

AI 的每一次选择,只不过是概率。而人类创作一个作品,画一幅油画,背后包含的是对人生、对世界的思考和回应;有情感、时间、技巧、犹豫和失败等经历。

支持者觉得,现在的 AI,就像相机刚被发明时一样,不是在替代传统,而是在扩张想象力。甚至有网友说,「我想艺术家们在作画前,脑海中可能就有这样的画面,现在我们也能走进他们的灵感了。

是结果,还是过程重要

如果这个视频是一个人类艺术家,花费 1000 个小时,一帧一帧手绘出来的,评论区又会发生什么。除了震撼,大概还是一样,会有人说,这视频顶多用来作为我的手机屏保,除了好看也就仅此而已了。

艺术是主观觉得还是客观认定呢,其实都没有明确的界定。网友的期待,大概是希望,艺术应该是需要「努力」才能抵达的地方,而 AI 正在稀释「努力」在艺术中的价值。

前段时间,一幅名为《太空歌剧院》的画作,拿到了艺术比赛的头奖,还有 AI 画作甚至在拍卖市场,以十万、百万的价格被拍走。

我们在一个输入提示词就能生成图像的世界里,任何一个人不需要复杂的技巧,也不需要付出多少汗水,都有机会创作自己的作品,作品的意义也不再靠时间来定义。

▲提示词:将油画纹理动画化为粘稠流体模拟,厚重的颜料笔触融化并流动。旋转的天空、粘稠的黄色光芒、翻腾的蓝色云朵。

当 AI 把时间成本压缩到几秒钟,这种神圣感瞬间崩塌,剩下的就只有廉价。「这不难做吧」、「我用 Veo 3 也能生成」,这样的东西,自然就成不了艺术了。

更有趣的是,有网友提到,这是一种很明显的社会心理学现象,巴浦洛夫的狗。现在我们只要看到 AI 的标签,就有了条件反射,瞬间进入狂怒模式,无论作品本身好坏,一律打成 Slop。

具体来说,就是眼睛告诉我,这个视频还不错,但大脑告诉我,这是 AI,AI 做的都是不好的,为了调和这种矛盾,我就必须强行说服自己,它看起来很糟糕。

心理效应是存在,更多的我想其实还是,AI 内容的泛滥,正在把我们的审美阈值无限拔高。

没有 AI 的时候,让一幅油画像这样动起来,可以说是「魔法」一样的存在。现在如果这些作品没有极强的故事性,或情感内核,仅仅是视觉特效,已经很难打动被各种 AI 效果喂饱的我们。

▲ YouTube 上该博主的其他油画视频

无论评论区吵得多么不可开交,一个事实是无法改变:AI 不会消失,艺术也不会因为 AI 的出现而消失。

就像一些网友说的,「电力曾让蜡烛工厂破产,但人类具有适应性」。现在的混乱,也许只是新旧审美体系,交替时发生的阵痛。

如果在 100 年前我们按一下播放键,就能听到录好的歌,大概也会有人觉得,只有黑胶唱片出来的声音才是真音乐;现在我们只是习惯了,现场、黑胶、手机、音响都有好音乐。

艺术,从来看的是最终的愿景,是我想让你看到什么,而不是用了什么工具,我花了多久才做出来。一个活过来的 AI 艺术,就算是简单的几行提示词,一样倾注了真正属于创作者的叙事、情感与意图。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


看完《疯狂动物城2》这样发合照火爆朋友圈,Nano Banana Pro 新玩法来了 | 附教程

作者 张子豪
2025年11月30日 17:20

疯狂动物城2 前几天刚正式上映,超过哪吒,成为中国影史动画电影预售票房冠军,双休日买好票准备去看了吗。

社交媒体上,早在预售的时候,就已经都是警官兔朱迪与狐尼克,出现在不同网友的电影院合影里

照片里的网友,左拥右抱着兔朱迪和狐尼克,作出自拍的姿势,而背后的影院屏幕上还显示着疯狂动物城2 的海报。同时人脸的一致性保持,让人觉得这就是电影放映后的粉丝福利,动画片主角跑出来免费合影。

评论区都在求攻略,有网友贴心地给出了制作指南。输入下面的提示词,然后图 1 上传自己的照片,图 2 和 图 3 分别上传朱迪和尼克的照片,图 4 是一张电影海报,通过豆包或者剪映 App 的生图功能,就能得到一张同款合照。

不要换脸,人物脸部不能有任何改变!将图1和图2 和图 3 的三人融合成一张三人俯拍自拍照,画面构图紧凑,三位主体靠得很近,图 1 人物居中,头部略微上仰,眼神直视镜头,营造出强烈的视觉冲击力。左侧和右侧人物站得略靠后,整体都需要保持人脸相似度,略微内扣身体,拍摄角度为高角度俯拍,使头部比例被夸张放大,典型的日韩视觉自拍风格。简洁干净,进一步凸显人物主体。整体画面清晰度高,用iphone前置自拍,最终呈现出精致、时尚、略带的合影效果。要求人物实现无缝融进画面,视觉过渡自然,整体画面光线明亮且均匀,背景为超大电影院坐满了观众,这三人背对银幕,银幕显示参考图 4《疯狂动物城2》电影精美海报填满屏幕。

▲图片和提示词来源:小红书@424180556

我们也使用 Nano Banana Pro 生成了同样的合照,不过得益于它的强大,我们完全不需要输入太多的提示词,只是上传四张图片,然后简单的告诉它要做什么就可以。

▲提示词:把这个男生和另外两个疯狂动物城角色放到一张合照里,他们就像是拿着手机在自拍,背景是电影院,电影院的屏幕上显示着我发给你疯狂动物城海报;16:9 的大小,4K 画质。

甚至还可以脑洞大开,直接丢给它这四张图片,然后输入提示词「合照」,完全不用担心人脸会改变,我们得到了这些 Nano Banana Pro 发挥自己的脑洞,生成的图片;横屏和竖屏的电影海报都有了。

▲提示词:合照

我们还可以试试最近玩 Nano Banana Pro 特别热门的提示词玩法,输入「xx,但不是xx」,上面在海报前拍照的那张图片,我们输入的提示词就是「合照,但不是合照」。

除了合照的玩法,配合新升级的 Nano Banana Pro,我们还有很多疯狂动物城2 联动玩法。

玩法一:全世界都是疯狂动物城

还记得 GPT-4o 刚出来那会儿,铺天盖地都是吉卜力风,现在疯狂动物城风也来了,而且还带来了更丰富的控制。

就像这张现实世界的街景图,只是把路上的行人,换成了可爱的动物角色,而马路和建筑还保留着原来的风格。

提示词:疯狂动物城风格的真实场景融合。迪士尼级别角色设计,富有表现力的形状,柔软毛发质感,流畅环境光照。保持原始照片结构,保留现实世界的光线、透视和材质。将拟人化元素自然融入环境中。高分辨率,电影级色彩调节。

拿着这套提示词,几乎可以把所有的照片,都转成这种现实+疯狂动物城的风格。还记得我们之前 AI DONE 活动预告里面,4 位 AI 大佬走在斑马线上吗,现在你还能不能,分辨出从左到右都是谁。

还有这张在网上疯狂传播的 AI 合照,把硅谷最有权有势的 CEO 都集结在一起。

▲ 由于提示词里面提到了「迪士尼」,所以这个狮市长看起来更像其他迪士尼系列电影的角色

除了保留一定的现实世界风格,把整个图片都换成疯狂动物城风,也是一样的好玩,Nano Banana Pro 表示,这都是小 case。

把开头那张只是转换了路上行人的照片,全部换成疯狂动物城的元素,色彩更加鲜艳,元素也更协调。

提示词:将这个现实世界的照片转换为《疯狂动物城》风格的动物城市,保留原始图片原始的全部元素和布局结构,但转换为风格化的拟人化基础设施,4K 画质。

同样的提示词,我们还把爱范儿在大兴安岭拍摄的样片,也进行一次风格转换。Nano Banana Pro 把大兴安岭的冷,描绘地更加极致,雪花飘在了衣服上。

还有随手拍摄的合照,也能一键换装,全员变身可爱的小动物。

▲ 原图来源:Unsplash@绵绵

如果希望留住能识别的面部特征,而不是直接把整个头部都换成动物头,实现起来也很简单,只用加一句提示词。

完全保留人物的人类面部特征和身份。

玩法二:合照,三个人可不够

开头只有朱迪和尼克的合影,只是疯狂动物城里面的两个主要角色,在电影里还有狸宝、宝伯特、马飞扬,闪电、夏奇羊、牛局长等数十个动物角色。

在 Nano Banana Pro 里,这些角色可以全部一次性发给它处理,所有角色都在舞台上。

提示词:图一是我,图2到图8,每张图片里面有4个疯狂动物城里面的角色,现在你需要把我和他们放在一张合照里面,就像是在线下的电影院,这些电影角色主创都在台上,我和他们一起合照,我在中间,拿着手机自拍,然后他们都在我旁边或者身后。

有了这么多的角色图片,我们还可以做一张疯狂动物城警局的照片墙,Judy、Nick、Bogo、Clawhauser 都在那块墙上,然后我们上传的照片也被贴到同一块墙上。

提示词:
创建一个16:9、4K分辨率的最终场景,设置在《疯狂动物城》警察局(Zootopia Police Department ,ZPD)内部。
一个真实感的警察局办公室走廊,主体背后是一个大型“ZPD员工照片墙”。

场景前方:
用户(来自图像1)站在中心位置,举着手机自拍。
保留用户真实的人类面孔和身份信息。
不要将脸转换为动物,仅应用非常轻微的《疯狂动物城》风格灯光和色彩调节。

背景:
用一整面墙填满用户身后的区域,并排列成网格状的拍立得风格员工ID照片。
使用图像2-8中的所有角色(每张包含四个角色),并将它们安排为单独的拍立得ID肖像展示在墙上。

每个角色肖像应满足以下要求:
– 保持上传图片中原始角色身份不变。
– 遵循一致的《疯狂动物城》宣传艺术外观:柔和迪士尼灯光、干净背景、清晰人物轮廓线条。
– 匹配官方ZPD ID照片框架比例(头像或胸部以上)。

同时,将用户自己的“ZPD风格ID照片”添加到墙上,放置于网格中央附近的位置,用与其他相同样式的拍立得边框呈现。

视觉规则:
– 保持真实办公环境中的墙壁材质、灯光效果的一致性。
– 所有ID照片必须对齐边框,统一色调,相同尺寸。
– 不要扭曲用户自拍姿势。
– 角色保持卡通化,但融入现实环境中。
– 可适度添加ZPD标志、警徽图标以及蓝色点缀灯光以增强真实性。

最终目标:
结果应展现出仿佛该用户已加入了ZPD,在官方员工照片墙前,与28位《疯狂动物城》的角色及其个人ID肖像一起合影留念之效果;画面需高清晰度,4K质量,有电影感但具备现实主义氛围感。

玩法三:这是我的疯狂动物城 ID

和风格转换有点类似,但又不完全相同。我们可以让 AI 来猜测,上传的人像图片,会是疯狂动物城里面的谁。

提示词:根据你对这张人像图片的分析,将这幅肖像转化为迪士尼《疯狂动物城》风格,最适合这张肖像图片的拟人化动物角色。保留原始面部结构和性格特征。柔软的毛发质感、富有表现力的眼睛、流畅的迪士尼光影效果、友好的配色方案以及电影海报级别的质量。

虽然 Nano Banana 大多数时候都只记得朱迪,但还是会根据我们上传的图片,调节图片的色彩、眼神的信息,同时保持我们的服装和姿势不变。

不喜欢这种身份证,还可以生成疯狂动物城的同款角色海报。

把图一的角色替换到图二的角色海报里,成为新的官宣角色。

由于 Nano Banana Pro 现在的指令理解和遵循能力,都得到了提升,所以我们的提示词甚至不需要太复杂的描述,一般口语化的表达,它也能听懂。

然后直接让他把狐尼克和兔朱迪换成马斯克和奥特曼。

▲提示词:把这个海报里面的两个角色替换成 Elon Musk 和 Sam Altman,然后名字也改成对应的马斯克和奥特曼

如果觉得这种 ID 照片还不够正式的话,直接让 Nano Banana Pro 生成一张带着边框,风格明显的证件照。

提示词:
将这张肖像转化为《疯狂动物城》风格的官方身份证照片,同时完全保留人物的人类面部特征和身份。
保持人物原本的眼睛、鼻子、嘴巴以及整体脸型,不进行动物替换。
仅应用迪士尼《疯狂动物城》的视觉风格:柔和边缘光照效果、平滑阴影处理、温和色彩搭配、灯光边缘处微妙的毛发质感、人类眼睛稍微放大但仍然保留人类特点,以及电影级卡通写实效果。
使用干净的工作室风格ZPD身份证背景(蓝色渐变或中性背景)。
保持人物可识别性及自然肤色,仅添加与《疯狂动物城》宣传画一致的轻度艺术化处理。
不改变面部结构;增强表情清晰度,并在眼睛上增加些许迪士尼式光泽效果。
高质量,符合官方文件要求的框架设计。

还可以在提示词里面指定好名字、身份编号、工作部门等信息;Nano Banana Pro 也完全能准确渲染所有的文字。

尽管 Nano Banana Pro 比起之前的版本更能理解我们的意图,但有时候多尝试几次,还是会给我们一些额外的惊喜。

最近社交媒体上很多人在尝试,用尽可能少的提示词,看 Nano Banana Pro 会生成什么。还有一种更高级的玩法是,Gemini 3.0 Pro 多模态推理和理解能力的提升,用来做逆向工程表现也很不错。

例如,我们拿疯狂动物城这张海报来测试,在 Gemini 3.0 Pro 模型里面,输入提示词「提取这张图片的提示词,越详细越好。」然后再把提示词给 Nano Banana Pro,得到的 AI 图片,还原度其实算非常高。

▲ 提取到的提示词:
A high-quality 3D animated movie poster for Disney’s “Zootopia 2,” characterized by incredible detail, highly textured fur, vibrant saturated colors, and bright daylight.
Composition & Angle: A dynamic, chaotic street scene captured with a low-angle shot looking upwards through a wide-angle fisheye lens, creating strong perspective distortion and a sense of overwhelming crowds.
Central Characters: In the center, Nick Wilde (fox) wearing a pink floral Hawaiian shirt, grey pants, and a tie, is running forward with a panicked, wide-mouthed expression. Next to him, Judy Hopps (rabbit) in her blue police uniform is running with a shocked but determined look. Behind them, a massive blue python with yellow underbelly scales (new character Gary) is hilariously coiled through the crowd with a surprised expression and tongue sticking out.
Environment & Key Props: A large grey metal street light pole cuts diagonally from the top left to bottom right. Mounted on it is a green street sign that clearly reads “Disney” logo above “疯狂动物城” with a large background number “2”. Below the sign, a yellow traffic light housing displays a glowing cyan LED number “2” instead of standard lights.
Background & Crowd: To the left, a pink double-decker bus filled with slow-moving sloths hanging out of windows (including Flash). A dense, diverse crowd fills the street, including a giraffe stretching its neck, an elephant, a lynx holding up a smartphone taking a photo, a beaver, and a rhino in casual clothes. Overhead, a horse is swinging on a rope across the street. In the foreground, a green lizard is tripped up, and a seal wears headphones.
Setting: In the far distance under a bright blue sky with clouds, the towering, futuristic skyscrapers of the Zootopia skyline are visible. The overall atmosphere is energetic, humorous, and crowded.
Text Elements (Visual Placement): Floating 3D text in the middle left reads “老搭档 新案件” and middle right reads “嗨翻全城”. At the very bottom center, text reads “11月26日 全国影院献映”.
Technical Specs: 8k resolution, cinematic lighting, highly detailed render. –ar 2:3

在我们的测试中发现 AI Studio 生成的图片,有时候并不是按照指令的 4K 要求给出原图,大小一般在 1MB 以下,而在 Gemini App 或者网页里,生成的图片则是完整的 4K 分辨率,图片大小一般在 7MB 左右。

Gemini 使用 Nano Banana Pro 的次数是有限制,在部分时候会像 ChatGPT 一样,弹出使用旧模型的提示。

但大多数时候,每天的使用次数是足够普通用户的使用。

我们喜欢疯狂动物城,大概是因为它打造的乌托邦,是一个勇敢善良的美丽新世界。现在 AI 迷人的地方,正是让我们也有了进入这个新世界的能力,同样地体验和传播,这份纯真的美好。

原文链接:https://mp.weixin.qq.com/s/8EJNx4Qjww09htrEfenJDA

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


东鹏饮料:发行境外上市股份(H股)获得中国证监会备案

2025年11月30日 17:11
11月30日,东鹏饮料(605499.SH)公告称,公司已收到中国证监会出具的《关于东鹏饮料(集团)股份有限公司境外发行上市备案通知书》,公司拟发行不超过66,446,000股境外上市普通股并在香港联合交易所主板上市。备案通知书主要内容包括公司拟发行股份数量、重大事项报告要求、发行上市后的报告义务、备案有效期等。公司本次境外发行上市尚需取得相关监管机构、证券交易所的批准、核准,存在不确定性。(每经网)
❌
❌