普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月3日首页

任天堂 Switch 2 最全汇总:不锁区版 3200 元支持 4K 游戏,还有这些升级你要知道

作者 苏伟鸿
2025年4月3日 01:44

iPhone 发布会一年一度,但 Switch 发布会,要等个 8 年才有一场。

用一支 3 分钟不到的预告片吊起全世界胃口后,任天堂足足让我们再躁候了 2 个半月,让传言和猜测飞一会儿,才终于在今天召开 Switch 2 的首次直面会,为我们真正介绍这款新机器。

任天堂首先公布了 Switch 2 的发售日期:6 月 5 日,有两个售价如下:

  • 日本国内专用版 49980 日元,折合人民币约 2431 元
  • 多语言版本 69980 日元,港版标价 3450 港币,人民币 3221 元左右

值得一提的是,「日本国内专用版」Switch 2 只提供日文语言,其他国家和地区的玩家需要购买「多语言版本」,这个版本也支持日语。

大家对「锁区」这件事不用太恐慌,其实更像是 Switch 推出了个「日本国行版」,发行策略和 Switch 腾讯版有点类似,都是独立的销售体系,我们只要买「多国语言版」就行。至于游戏卡,任天堂表示只要插入任何主机都能游玩。

任天堂着重介绍了外界猜测最多的全新 Joy-Con 2 手柄。从上一代的滑轨卡扣式设计,改成了更方便安装拆卸的磁吸式设计,可以更方便、牢固安装到机身或者握把上,而一按背面的按钮就能轻松卸下。

上一条预告片中惊鸿一瞥的 Joy-Con 鼠标模式则是本次直面会的重头戏。Joy-Con 2 拆下后可以和鼠标一样立在平面上移动交互,不仅代替一些老游戏中的摇杆交互,还会有一些专门为「鼠标模式」设计的新游戏。

家里的老的 Switch 一代手柄别急着丢,Switch 2 也能继续用。

之前已经物料中不小心曝光的全新按键「C 键」也正式揭晓了用途:GameChat,可以说是 Switch 官方的语音房间功能,玩游戏时可以和朋友们连麦,并共享实时游戏画面,就是传输的流畅度和画质不是特别好。

任天堂的硬件工程师在采访中透露,GameChat 的部分灵感源自疫情期间在家办公时的线上会议,我就说这个界面怎么有种似曾相识的感觉……

为了配合开麦功能,Switch 2 还内置了一个麦克风,通话时还能降噪。

除了连麦,任天堂还推出了全新的外接摄像头配件,能够实时共享自己的脸部 Reaction,更具临场感。

Switch 2 顶部多出来的 USB-C 口用途也正式揭晓:可以用来外接摄像头,也能用来充电,也许未来任天堂还会带来更多丰富的外接配件。

除了能开语音房连麦打游戏,Switch 2 也将支持同一个游戏分享给附近的 Switch 联机游玩,老款 Switch 能够加入串流游玩。

为了让新玩家更加熟悉 Switch 2 的结构和按键功能,任天堂还贴心准备了一个《Nintendo Switch 2 Welcome Tour》的小游戏,操控一个小人探索 Switch 2——随着主机发售上线,售价 990 日元。

任天堂也带来了 Switch 2 Pro 手柄,同样配备全新 C 键,还增加了背部的 GR/GL 自定义按键,以及 3.5 毫米耳机接口,还改善了耐用性和握持感,摇杆在使用时也非常安静,任天堂内部称之为「Aerial 摇杆」。

Switch 2 全新升级的处理器,还是带来了更强的画面表现力。掌机模式下,Switch 2 支持最高 120fps 1080P 的画质,还支持 HDR、VRR 等现代游戏机功能。搭配全新的底座,在主机模式时最高能输出 4K 画面,相比于 Switch 确实清晰不少。

得益于 Switch 2 更强的性能,任天堂也将为一些兼容的 Switch 游戏推出「NS2 增强版」,包括《塞尔达传说:旷野之息/王国之泪》《宝可梦传说 ZA》《星之卡比 探索发现》《马力欧派对 空前盛会》等,部分游戏可以免费更新,还有一些则需要额外购买售价 1000 日元(折合人民币约 48元)的「升级包」,或者加入任天堂的网络会员服务。

除了画质,Switch 2 的鼠标模式、麦克风、摄像头玩法也将适配部分升级后的游戏,增加更多新玩法,比如这个新的马力欧派对小游戏,看起来有点日本综艺那味了。

总的来说,Switch 2 一共支持三种游戏:

  • Switch 2 独占游戏
  • Switch 兼容游戏,但 Switch 2 增强版,可能有画质或玩法的升级
  • Switch 兼容游戏,直接运行即可,可能会有帧数或画面的提升

需要注意的是,在超过一万个 Switch 游戏中,有一百多个 Switch 游戏并不兼容 Switch 2,其中就包括《健身环大冒险》《Labo》等话题游戏。

Switch 2 还将配备 256GB 的内置存储空间(是的,容量比 iPhone 标配版还大),外置存储卡带将支持 microSD Express 卡,但不再支持之前的 microSD 卡。

以上就是 Switch 2 的基本情况, 任天堂也为我们带来了更让人期待的游戏阵容——
首先是备受期待的《马力欧卡丁车 世界》,采用「开放世界」设计,支持最多 24 位玩家,和 Switch 2 同步发售。
另一款马力欧世界观的大作,是咚奇刚 3D 新作《咚奇刚 BANAZA》,采用全新的 3D 动作玩法,将于今年 7 月 17 日发售。

值得一提的是,Switch 2 游戏也有涨价趋势——《马力欧赛车 世界》的价格大幅上涨:数字版 8980日元(折合人民币 436.94 元),实体版售价为9980日元(折合人民币 485.60 元);而《咚奇刚 BANAZA》的价格维持现状:数字版售价 7980 日元(折合人民币 388.28 元),实体版售价 8980 日元。

此外,「星之卡比」之父樱井政博也带来了《星之卡比 AirRiders》,从宣传片看来,似乎是沉寂已久的星之卡比赛车系列新作。

虽然没有「红帽子」,但「绿帽子」的新作没有缺席——无双玩法系列新作《塞尔达无双:封印战记》将于今年冬季发售,设定为《王国之泪》的前传故事,讲述塞尔达公主和古代贤者们对抗魔王的故事。

除此之外,还有一款名为《Drag x Drive》的游戏,操控轮椅小人打篮球,将主要利用 Joy-Con 2 的鼠标模式进行操作——这个异质玩法非常任天堂。
第三方游戏也有重磅产品——由 From Software 开发、宫崎英高担任监督的 Switch 2 独占游戏《The DuskBloods》官宣,将于 2026 发售,是独特的 PVPVE 玩法。
宫崎英高的另一款大作《艾尔登法环》,以及《赛博朋克 2077》《双影奇境》《最终幻想 7 重制版》《霍格沃茨之遗》等更多话题度拉满的第三方游戏,都官宣了推出 Switch 2 版本。
还有经典老游戏支持——Switch 2 将可以模拟游玩 Nintendo Game Cube(NGC)游戏,任天堂也顺势推出新的 NGC 手柄。

虽然是一款全新的机器,但 Switch 2 在发售当日,首先就能玩上老 Switch 游戏库中绝大部分的游戏,还能享受更好的增强画质,第一方新作《马力欧赛车 世界》,以及部分像《赛博朋克 2077》这样的第三方新作都将随 Switch 2 同步发售,《咚奇刚 BANANZA》也在下个月推出,因此不愁冲 Switch 2 首发没游戏玩。

更大问题可能是,买不买得到?

直面会结束后,任天堂也公布了 Switch 2 预约的抽选方式,条件可谓非常苛刻,以日本的规则为例(港版、美版的抽选规则类似):
  • 截至 2025 年 2 月 28 日,Nintendo Switch游戏软件累计游玩时间达 50 小时以上(体验版软件及免费软件不计入)
  • 申请时需具有累计1年以上的 Nintendo Switch Online 会员资格,且申请时仍处于有效订阅状态
  • 家庭计划成员中,仅主账号的持有者符合上述条件
  • 仅限 Nintendo Account 国家或地区设置为日本的用户参与申请及购买
自 Switch 2 第一条预告发布以后,任天堂股价节节攀升,甚至创下历史新高,市值首次突破千亿美元关口,说明市场对 Switch 2 抱有非常高的期待值。
所以 Switch 2 怎么样?更强,更大,更贵,但也好像,没什么不同。
全新的「鼠标模式」可能是一种新的变数:有人觉得是更全能的交互方式,可以用 Switch 玩更具操作难度的游戏;也有人觉得是对 Switch 多样可玩性的背离,是玩法设计上的偷懒。
至于答案,只有真正拿到手上开始玩的那一刻,才能知道了。
践行前社长岩田聪「游戏人口扩大化」战略十余年后,任天堂已经从一家专注电子游戏的公司,转型为一家超大型的 IP 管理公司:走进电影院有任天堂的电影,想去游乐园有超级任天堂世界,想听歌还有任天堂音乐服务,任天堂甚至在最近发布了一个「Nintendo Today!」的应用,专门用来更新发布任天堂动态,提供各种任天堂内容。

但对于任天堂而言, 游戏依旧是他们展示创意的最佳形式,而 Switch 2 就是最好的平台。

8 年过去,科技行业的变化几乎可以说翻天覆地,但却并没有尽数反映在 Switch 2 身上。

对于任天堂而言,一台游戏机最重要的不是参数,而是如何让更多人能享受到游戏的乐趣,也就是所谓的「枯萎技术的水平思考」。

让家庭主妇挥舞起游戏手柄的 Wii 、小学生和老人都爱玩的 NDS 如此,能实现各种意想不到玩法的 Switch 更是如此。

所以,对于 Switch 2,任天堂是这么说的:

Nintendo Switch2 虽然是性能提升的 Switch,但我们不希望顾客觉得 Switch 2 真厉害,而是想,这是任天堂制造的新 Switch——我们希望这能成为 Nintendo Switch 的新标准,因此才命名为 Nintendo Switch 2。

或许以后,我们会像更新 iPhone 一样更新 Switch,更新的是硬件的标准,但快乐的本质不会变。

附上我们对任天堂 Switch 2 的选购建议:

  • 除非你长期生活在日本,否则不需要考虑买日本限定版,直接选购多语言版本即可
  • 如果你是国行 Switch 用户,Switch 2 目前仍没有公布国行计划,港版 Switch 2 售价是 3200RMB,这是离我们最近的渠道
  • 多语言版本的 Switch 2,本质上跟现在的 Switch 没什么不同,你的游戏、账号都能得到保留
  • 你并不一定需要首发购入,因为 6 月随着 Switch 2 发售的独占游戏并不多,你可以考虑等到 7 月甚至更晚,货源更充足时再选择入手,买游戏机,最重要的还是看能玩到什么游戏
  • 不用过于担心 Switch 2 买回来后没有游戏可玩,Switch 拥有超过一万个游戏的游戏库,如果你只有一台游戏机,那么 Switch 2 是一个不错的升级选项
  • 如果你之前没有 Switch,那么择机买一台 Switch 2 也是很好的选择

最后,附上 Switch 2 的规格表:

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

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


每日一题-有序三元组中的最大值 II🟡

2025年4月3日 00:00

给你一个下标从 0 开始的整数数组 nums

请你从所有满足 i < j < k 的下标三元组 (i, j, k) 中,找出并返回下标三元组的最大值。如果所有满足条件的三元组的值都是负数,则返回 0

下标三元组 (i, j, k) 的值等于 (nums[i] - nums[j]) * nums[k]

 

示例 1:

输入:nums = [12,6,1,2,7]
输出:77
解释:下标三元组 (0, 2, 4) 的值是 (nums[0] - nums[2]) * nums[4] = 77 。
可以证明不存在值大于 77 的有序下标三元组。

示例 2:

输入:nums = [1,10,3,4,19]
输出:133
解释:下标三元组 (1, 2, 4) 的值是 (nums[1] - nums[2]) * nums[4] = 133 。
可以证明不存在值大于 133 的有序下标三元组。 

示例 3:

输入:nums = [1,2,3]
输出:0
解释:唯一的下标三元组 (0, 1, 2) 的值是一个负数,(nums[0] - nums[1]) * nums[2] = -3 。因此,答案是 0 。

 

提示:

  • 3 <= nums.length <= 105
  • 1 <= nums[i] <= 106

【小羊肖恩】从暴力到贪心:想清楚贪心的每一步,就可以顺序遍历啦~!

作者 Yawn_Sean
2023年10月1日 12:23

对于这个问题,数据范围是 $100$ 的情况,我们可以直接暴力枚举所有合法的 $(i, j, k)$ 三元组,看其值取最大即可。 注意要求如果是负值时,返回 $0$.

时间复杂度为 $\mathcal{O}(n^3)$.

具体代码如下——

###Python

class Solution:
    def maximumTripletValue(self, nums: List[int]) -> int:
        n = len(nums)
        ans = 0
        for k in range(n):
            for j in range(k):
                for i in range(j):
                    ans = max(ans, (nums[i] - nums[j]) * nums[k])
        return ans

接下来我们思考如何处理 $n \leq 10^5$ 的情况。

首先,输出的数值最小为 $0$,同时数组中每个元素均为正数,因此,我们要让 (nums[i] - nums[j]) * nums[k] 最大,只需要对于固定的 $k$ 找到其前面 nums[i] - nums[j] 的最大值。

为了使得 nums[i] - nums[j] 尽可能大,我们需要对于固定的 j 找到其前面最大的 nums[i] 再将两者相减。

以上逻辑可见代码注释。

因此我们只需要维护三个东西:到当前位置的最大值,到当前位置为止最大的 nums[i] - nums[j],到当前位置为止最大的 (nums[i] - nums[j]) * nums[k]. 而从上面的分析可以看出,这些都可以遍历过程中实现。

因此时间复杂度是 $\mathcal{O}(n)$ 的。

具体代码如下——

###Python

class Solution:
    def maximumTripletValue(self, nums: List[int]) -> int:
        # 当前最大值
        curr_max = 0
        # 当前最大的 nums[i] - nums[j]
        curr_v = 0
        # 当前最大的 (nums[i] - nums[j]) * nums[k]
        ans = 0
        n = len(nums)
        
        for i in range(n):
            # 答案的最大值根据最大的 nums[i] - nums[j] 和当前数值的乘积更新
            ans = max(ans, nums[i] * curr_v)
            # nums[i] - nums[j] 的最大值根据此前最大值减去当前数值更新
            curr_v = max(curr_v, curr_max - nums[i])
            # 更新前缀最大值
            curr_max = max(curr_max, nums[i])
        return ans

两种方法:枚举 j / 枚举 k(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2023年10月1日 12:10

方法一:枚举 j

枚举 $j$,为了让 $(\textit{nums}[i] - \textit{nums}[j]) * \textit{nums}[k]$ 尽量大,我们需要知道 $j$ 左侧元素的最大值(让 $\textit{nums}[i] - \textit{nums}[j]$ 尽量大),以及 $j$ 右侧元素的最大值(让乘积尽量大)。

也就是计算 $\textit{nums}$ 的前缀最大值 $\textit{preMax}$ 和后缀最大值 $\textit{sufMax}$,这可以用递推预处理:

  • $\textit{preMax}[i] = \max(\textit{preMax}[i-1], \textit{nums}[i])$
  • $\textit{sufMax}[i] = \max(\textit{sufMax}[i+1], \textit{nums}[i])$

代码实现时,可以只预处理 $\textit{sufMax}$ 数组,$\textit{preMax}$ 可以在计算答案的同时算出来。

视频讲解 第二题。

class Solution:
    def maximumTripletValue(self, nums: List[int]) -> int:
        n = len(nums)
        suf_max = [0] * (n + 1)
        for i in range(n - 1, 1, -1):
            suf_max[i] = max(suf_max[i + 1], nums[i])

        ans = pre_max = 0
        for j, x in enumerate(nums):
            ans = max(ans, (pre_max - x) * suf_max[j + 1])
            pre_max = max(pre_max, x)
        return ans
class Solution {
    public long maximumTripletValue(int[] nums) {
        int n = nums.length;
        int[] sufMax = new int[n + 1];
        for (int i = n - 1; i > 1; i--) {
            sufMax[i] = Math.max(sufMax[i + 1], nums[i]);
        }

        long ans = 0;
        int preMax = nums[0];
        for (int j = 1; j < n - 1; j++) {
            ans = Math.max(ans, (long) (preMax - nums[j]) * sufMax[j + 1]);
            preMax = Math.max(preMax, nums[j]);
        }
        return ans;
    }
}
class Solution {
public:
    long long maximumTripletValue(vector<int>& nums) {
        int n = nums.size();
        vector<int> suf_max(n + 1);
        for (int i = n - 1; i > 1; i--) {
            suf_max[i] = max(suf_max[i + 1], nums[i]);
        }

        long long ans = 0;
        int pre_max = nums[0];
        for (int j = 1; j < n - 1; j++) {
            ans = max(ans, 1LL * (pre_max - nums[j]) * suf_max[j + 1]);
            pre_max = max(pre_max, nums[j]);
        }
        return ans;
    }
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))

long long maximumTripletValue(int* nums, int n) {
    int* suf_max = malloc(n * sizeof(int));
    suf_max[n - 1] = nums[n - 1];
    for (int i = n - 2; i > 1; i--) {
        suf_max[i] = MAX(suf_max[i + 1], nums[i]);
    }

    long long ans = 0;
    int pre_max = nums[0];
    for (int j = 1; j < n - 1; j++) {
        ans = MAX(ans, 1LL * (pre_max - nums[j]) * suf_max[j + 1]);
        pre_max = MAX(pre_max, nums[j]);
    }

    free(suf_max);
    return ans;
}
func maximumTripletValue(nums []int) int64 {
    ans := 0
    n := len(nums)
    sufMax := make([]int, n+1)
    for i := n - 1; i > 1; i-- {
        sufMax[i] = max(sufMax[i+1], nums[i])
    }

    preMax := 0
    for j, x := range nums {
        ans = max(ans, (preMax-x)*sufMax[j+1])
        preMax = max(preMax, x)
    }
    return int64(ans)
}
var maximumTripletValue = function(nums) {
    let n = nums.length;
    let sufMax = Array(n);
    sufMax[n - 1] = nums[n - 1];
    for (let i = n - 2; i > 1; i--) {
        sufMax[i] = Math.max(sufMax[i + 1], nums[i]);
    }

    let ans = 0;
    let preMax = nums[0];
    for (let j = 1; j < n - 1; j++) {
        ans = Math.max(ans, (preMax - nums[j]) * sufMax[j + 1]);
        preMax = Math.max(preMax, nums[j]);
    }
    return ans;
};
impl Solution {
    pub fn maximum_triplet_value(nums: Vec<i32>) -> i64 {
        let n = nums.len();
        let mut suf_max = vec![0; n + 1];
        for i in (2..n).rev() {
            suf_max[i] = suf_max[i + 1].max(nums[i]);
        }

        let mut ans = 0;
        let mut pre_max = nums[0];
        for j in 1..n - 1 {
            ans = ans.max((pre_max - nums[j]) as i64 * suf_max[j + 1] as i64);
            pre_max = pre_max.max(nums[j]);
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:枚举 k

枚举 $k$,我们需要知道 $k$ 左边 $\textit{nums}[i] - \textit{nums}[j]$ 的最大值。

类似 121. 买卖股票的最佳时机,为了计算 $\textit{nums}[i] - \textit{nums}[j]$ 的最大值,我们需要知道 $j$ 左边的 $\textit{nums}[i]$ 的最大值。

因此,在遍历的过程中:

  • 维护 $\textit{nums}[i]$ 的最大值 $\textit{preMax}$。
  • 维护 $\textit{preMax} - \textit{nums}[j]$ 的最大值 $\textit{maxDiff}$。
  • 计算 $\textit{maxDiff} \cdot \textit{nums}[k]$,更新答案的最大值。

代码实现时,要先更新 $\textit{ans}$,再更新 $\textit{maxDiff}$,最后更新 $\textit{preMax}$。为什么?

这个顺序是精心设置的:

  • 首先更新 $\textit{ans}$,此时 $\textit{maxDiff}$ 还没有更新,表示在当前元素左边的两个数的最大差值。
  • 然后更新 $\textit{maxDiff}$,此时 $\textit{preMax}$ 还没有更新,表示在当前元素左边的最大值。
  • 最后更新 $\textit{preMax}$。

能否修改更新顺序?

$\textit{ans}$ 依赖 $\textit{maxDiff}$,$\textit{maxDiff}$ 依赖 $\textit{preMax}$。如果修改更新顺序,那么 $\textit{maxDiff}$ 或者 $\textit{preMax}$ 会包含当前元素,就不是左边元素的计算结果了,这违反了题目 $i<j<k$ 的规定。

class Solution:
    def maximumTripletValue(self, nums: List[int]) -> int:
        ans = max_diff = pre_max = 0
        for x in nums:
            ans = max(ans, max_diff * x)
            max_diff = max(max_diff, pre_max - x)
            pre_max = max(pre_max, x)
        return ans
class Solution {
    public long maximumTripletValue(int[] nums) {
        long ans = 0;
        int maxDiff = 0;
        int preMax = 0;
        for (int x : nums) {
            ans = Math.max(ans, (long) maxDiff * x);
            maxDiff = Math.max(maxDiff, preMax - x);
            preMax = Math.max(preMax, x);
        }
        return ans;
    }
}
class Solution {
public:
    long long maximumTripletValue(vector<int>& nums) {
        long long ans = 0;
        int max_diff = 0, pre_max = 0;
        for (int x : nums) {
            ans = max(ans, 1LL * max_diff * x);
            max_diff = max(max_diff, pre_max - x);
            pre_max = max(pre_max, x);
        }
        return ans;
    }
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))

long long maximumTripletValue(int* nums, int n) {
    long long ans = 0;
    int max_diff = 0, pre_max = 0;
    for (int i = 0; i < n; i++) {
        ans = MAX(ans, 1LL * max_diff * nums[i]);
        max_diff = MAX(max_diff, pre_max - nums[i]);
        pre_max = MAX(pre_max, nums[i]);
    }
    return ans;
}
func maximumTripletValue(nums []int) int64 {
    var ans, maxDiff, preMax int
    for _, x := range nums {
        ans = max(ans, maxDiff*x)
        maxDiff = max(maxDiff, preMax-x)
        preMax = max(preMax, x)
    }
    return int64(ans)
}
var maximumTripletValue = function(nums) {
    let ans = 0, maxDiff = 0, preMax = 0;
    for (const x of nums) {
        ans = Math.max(ans, maxDiff * x);
        maxDiff = Math.max(maxDiff, preMax - x);
        preMax = Math.max(preMax, x);
    }
    return ans;
};
impl Solution {
    pub fn maximum_triplet_value(nums: Vec<i32>) -> i64 {
        let mut ans = 0;
        let mut max_diff = 0;
        let mut pre_max = 0;
        for x in nums {
            ans = ans.max(max_diff as i64 * x as i64);
            max_diff = max_diff.max(pre_max - x);
            pre_max = pre_max.max(x);
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

思考题

如果 $\textit{nums}$ 中有负数,要怎么做?

欢迎在评论区分享你的思路/代码。

更多相似题目,见下面数据结构题单中的「§0.1 枚举右,维护左」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 【本题相关】常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天 — 2025年4月2日首页

ant design pro 模版简化工具

作者 windyrain
2025年4月2日 23:24

2023年8月,我满怀期待的开创了自己的小公司,北京微谏科技有限公司,当时的业务仅有一份前领导介绍的 AI 前端外包。转眼间 2 年就过去了,虽然服务了一些客户,也做了一些有趣的项目,但毕竟不是自己的产品,无法决定产品迭代,也无法持续创造营收。所以我下定决心,一定要做完全属于自己的东西。

之前做外包项目,在开发后台管理系统的时候,采用 ant-design-pro 搭建的项目,生成好后,需要手动调整很多代码,才能让界面回归到一个简单可用的状态。所以我开发的第一款产品就是 ant design pro 模版简化工具,他能让你的后台管理项目快速启动。原本需要半天的时间,用上这款工具后,只需要1分钟。让你专注于业务的开发。

可能对于中,大型公司,这款工具并没有什么作用,但是对于小型的非科技型企业、计算机专业的学生、偶尔接接私活儿的程序员们,不太擅长前端的后端工程师,我觉得还是挺有帮助的。

以下是使用全局安装的 pro 命令初始化的 simple 脚手架,不得不说还是挺漂亮的,但是外包开发主打的就是一个快,漂亮只是加分项。

这里面有一些用不上的部分,包括但不限于 mock 数据,单元测试,Ant 相关Logo/提示信息,多语言,欢迎页,管理页。所以我们需要先把他删除掉。然后增添的一个基础的,可以增删改查的模块就ok了,大体思路参考下方代码。

import { execSync } from 'child_process';
import fs from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';

// 获取脚本路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// 执行脚本路径
const mockPath = path.join(__dirname, '../mock');
const testsPath = path.join(__dirname, '../tests');
const jestPath = path.join(__dirname, '../jest.config.ts');
const srcUserLoginTest = path.join(__dirname, '../src/pages/User/Login/login.test.tsx');
const srcUserLoginSnapshots = path.join(__dirname, '../src/pages/User/Login/__snapshots__');
const typesPath = path.join(__dirname, '../types');
const srcComponentsFooter = path.join(__dirname, '../src/components/Footer');
const srcComponentsRightContent = path.join(__dirname, '../src/components/RightContent/index.tsx');
const srcLocales = path.join(__dirname, '../src/locales');
const srcPagesAdmin = path.join(__dirname, '../src/pages/Admin.tsx');
const srcPagesWelCome = path.join(__dirname, '../src/pages/Welcome.tsx');
const srcPagesTableList = path.join(__dirname, '../src/pages/TableList');
const srcPages404 = path.join(__dirname, '../src/pages/404.tsx');
const srcServices = path.join(__dirname, '../src/services');
const srcConfigOneApi = path.join(__dirname, '../config/oneapi.json');
const srcAccess = path.join(__dirname, '../src/access.ts');
const publicIcons = path.join(__dirname, '../public/icons');
const publicCNAME = path.join(__dirname, '../public/CNAME');
const publicIco = path.join(__dirname, '../public/favicon.ico');
const publicSvg = path.join(__dirname, '../public/logo.svg');
const publicProIcon = path.join(__dirname, '../public/pro_icon.svg');

// 删除不需要的代码
[
  mockPath,
  testsPath,
  jestPath,
  srcUserLoginTest,
  srcUserLoginSnapshots,
  typesPath,
  srcComponentsFooter,
  srcComponentsRightContent,
  srcLocales,
  srcPagesAdmin,
  srcPagesWelCome,
  srcPagesTableList,
  srcPages404,
  srcServices,
  srcConfigOneApi,
  srcAccess,
  publicIcons,
  publicCNAME,
  publicIco,
  publicSvg,
  publicProIcon,
].forEach((itemPath) => {
  execSync(`rm -rf ${itemPath}`);
});

// 将 replace-source-code 的内容覆盖到 ../ 中
execSync(`cp -r ${path.join(__dirname, './replace-source-code/*')} ${path.join(__dirname, '../')}`);

const args = process.argv.slice(2);

const title = args.length > 0 ? args[0] : '后台管理系统';

// 替换系统标题
execSync(
  `sed -i '' 's/{{title}}/${title}/g' ${path.join(__dirname, '../config/defaultSettings.ts')}`,
);

执行后我们就可以得到一个清爽的后台界面了。

接下来,就可以的专心的进行业务上的开发了。

如果你想体验这款工具,可以访问我的公司官网,目前在线生成开放了100次的免登录生成代码。如果您看到了“今日免登录体验次数已耗尽,请登录后使用”的提示,也请给予我一点信任,免费注册一下我的网站,这样你就可以享受无限次的后台模版生成服务了。

希望可以通过这篇文章与有需求的小伙伴们交个朋友,创业路途遥远,唯有日拱一卒。如果大家对我的工具有什么好的建议,也可以在评论区里提出,感谢大家,祝前程似锦,大展宏图。

我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs

作者 浪遏
2025年4月2日 23:00

前言

前些时候 ,远程实习要求实现鉴权登录 , 采用 Auth.js 可用于在 Nextjs 项目中实现登录鉴权 ,今天我写一个 demo , 为大家展示一下 auth 鉴权的数据流转 ~

我们首先介绍一下: NextAuth.js 是一个专为 Next.js 应用设计的灵活且易于使用认证库,它极大地简化了添加登录、注册、登出等认证功能的过程。该库以其灵活性和强大的功能而闻名,支持多种认证方式,包括电子邮件与密码、OAuth 2.0 提供商(如 Google、GitHub、Facebook 等),以及自定义提供商。

主要特点

  • 丰富的内置提供者:支持众多 OAuth 和 OpenID Connect 提供商,方便快捷地与第三方服务集成。
  • 会话管理:提供简明的 API 来处理用户会话,轻松获取当前用户的会话信息。
  • 数据库兼容性:支持与多种数据库集成,适用于存储用户数据,并能无缝对接无头 CMS 和自定义后端。
  • 多语言支持:根据用户的语言偏好显示错误消息及其他文本,增强用户体验。
  • 可定制页面:允许开发者创建符合应用设计风格的自定义登录、注册或错误页面。
  • 安全优先:采用一系列安全默认设置,帮助保护应用免受常见的安全威胁。
  • API 路由整合:利用 Next.js 的 API 路由机制来实现身份验证逻辑,让开发者可以自由创建用于登录、登出等操作的自定义端点。
  • 会话管理选项:提供选择,既可以通过 JSON Web Tokens (JWT) 实现无状态会话管理,也可以选择基于数据库的会话管理。
  • 适配器支持:为了满足将用户数据持久化到数据库的需求,NextAuth.js 提供了与不同数据库系统(如 Prisma、TypeORM 等)集成的适配器。

demo 效果展示

Gitee:gitee.com/luli1314520…

开源不易 , 点个小小赞支持下 ~

认证演示项目登录流程分析

项目结构

项目使用Next.js框架构建,采用App Router架构,主要目录结构:

  • /app - 前端页面和API路由
    • /app/auth/signin - 登录页面
    • /app/api/auth/[...nextauth] - NextAuth API路由
  • /auth - NextAuth配置
  • /services - 业务逻辑层
  • /models - 数据访问层
  • /types - 类型定义
  • /lib - 工具函数

认证方案

项目使用NextAuth.js实现第三方OAuth认证,支持以下登录方式:

  • Google OAuth登录
  • GitHub OAuth登录

认证数据流转图

┌─────────────┐      ┌─────────────────┐      ┌────────────────────┐
│             │      │                 │      │                    │
│  用户界面   │─────▶│  NextAuth API   │─────▶│  OAuth提供商       │
│  (客户端)   │◀─────│  (服务端路由)   │◀─────│  (Google/GitHub)   │
│             │      │                 │      │                    │
└─────────────┘      └─────────────────┘      └────────────────────┘
       │                     │                         │
       │                     │                         │
       ▼                     ▼                         │
┌─────────────┐      ┌─────────────────┐               │
│             │      │                 │               │
│  会话状态   │◀────▶│  数据存储       │◀──────────────┘
│  (JWT)      │      │  (用户信息)     │
│             │      │                 │
└─────────────┘      └─────────────────┘

数据流转详解

  1. 用户界面 → NextAuth API → OAuth提供商
    • 用户在前端界面点击登录按钮,触发认证流程
    • NextAuth API构建OAuth授权URL并重定向用户
    • 用户被引导至第三方OAuth提供商(Google/GitHub)进行身份验证
  2. OAuth提供商 → NextAuth API → 数据存储
    • 用户在OAuth提供商完成身份验证
    • 提供商重定向回NextAuth回调URL,附带授权码
    • NextAuth API使用授权码换取用户信息
    • 用户信息被保存到数据存储中
  3. NextAuth API → 会话状态
    • 认证成功后,NextAuth创建JWT令牌
    • JWT包含必要的用户信息和认证元数据
    • 令牌加密并存储在HTTP-only cookie中
  4. 会话状态 用户界面
    • 前端通过 useSession钩子或 getSession函数获取会话状态
    • 会话状态包含用户身份和权限信息
    • 用户界面根据会话状态调整显示内容(如显示用户资料)
  5. 会话状态 数据存储
    • JWT令牌中包含用户标识符(如UUID)
    • 可通过标识符从数据存储中获取完整用户信息
    • 会话更新时可能涉及数据存储的变更(如更新登录时间)

登录流程(前端到后端)

1. 前端登录入口

文件: app/auth/signin/page.tsx

登录页面是服务端组件,显示登录按钮并检查用户是否已登录:

export default async function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl: string | undefined };
}) {
  const session = await auth();
  if (session) {
    return redirect(searchParams.callbackUrl || "/");
  }
  
  // 登录页面UI
}

此组件对应数据流转图中的用户界面(客户端),负责展示登录界面并检查会话状态。

2. 客户端登录按钮

文件: app/auth/signin/client.tsx

客户端组件,处理登录按钮点击事件:

export function SignInButton({ 
  provider, 
  callbackUrl,
  children
}: { 
  provider: string; 
  callbackUrl?: string;
  children: React.ReactNode;
}) {
  return (
    <button
      onClick={() => signIn(provider, { callbackUrl: callbackUrl || '/' })}
      className="..."
    >
      {provider === 'google' && <FaGoogle className="..." />}
      {provider === 'github' && <FaGithub className="..." />}
      <span>{children}</span>
    </button>
  );
}

此组件触发用户界面→NextAuth API的数据流,调用 signIn函数发起认证请求。

3. NextAuth API路由

文件: app/api/auth/[...nextauth]/route.ts

处理NextAuth的API请求:

import NextAuth from "next-auth";
import { authOptions } from "@/auth/config";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

此文件对应数据流转图中的NextAuth API(服务端路由),处理所有认证相关的HTTP请求,包括:

  • 认证请求(重定向到OAuth提供商)
  • 回调处理(接收OAuth提供商的回调)
  • 会话查询(前端获取会话状态)

4. NextAuth配置

文件: auth/config.ts

配置NextAuth认证选项,包括:

  • 认证提供商(Google、GitHub)
  • 登录回调
  • JWT处理
  • Session处理

关键代码:

export const authOptions: NextAuthConfig = {
  providers,
  pages: {
    signIn: "/auth/signin",
  },
  callbacks: {
    async signIn({ user, account, profile }) { ... },
    async redirect({ url, baseUrl }) { ... },
    async session({ session, token }) { ... },
    async jwt({ token, user, account }) { ... },
  },
};

这个配置文件定义了NextAuth API如何与OAuth提供商数据存储交互,以及如何处理会话状态

5. NextAuth实例

文件: auth/index.ts

创建NextAuth实例并导出相关函数:

import NextAuth from "next-auth";
import { authOptions } from "./config";

export const { handlers, signIn, signOut, auth } = NextAuth(authOptions);

这些导出的函数促成了数据流转图中的多个流程:

  • signIn:用户界面→NextAuth API
  • auth:NextAuth API→会话状态
  • signOut:用户界面→NextAuth API→会话状态(清除)

6. 用户信息处理

文件: services/user.ts

处理用户信息保存逻辑:

export async function saveUser(user: User) {
  try {
    const existUser = await findUserByEmail(user.email);
    if (!existUser) {
      await insertUser(user);
    } else {
      user.id = existUser.id;
      user.uuid = existUser.uuid;
      user.created_at = existUser.created_at;
    }
    return user;
  } catch (e) {
    throw e;
  }
}

此服务对应数据流转图中的NextAuth API→数据存储流程,负责将OAuth提供商返回的用户信息持久化到数据存储中。

7. 数据存储

文件: models/user.ts

示例项目使用内存数组存储用户信息,实际项目应使用数据库:

// 演示用简化版本,实际项目中应使用数据库
const users: User[] = [];

export async function findUserByEmail(email: string): Promise<User | null> { ... }
export async function findUserByUuid(uuid: string): Promise<User | null> { ... }
export async function insertUser(user: User): Promise<User> { ... }

此模块实现了数据流转图中的**数据存储(用户信息)**组件,提供用户数据的CRUD操作。

登录流程详解

  1. 用户点击登录按钮
    • 前端调用 signIn(provider)函数
    • NextAuth.js将用户重定向到第三方OAuth提供商(Google/GitHub)
    • 数据流:用户界面→NextAuth API→OAuth提供商
  2. 第三方认证
    • 用户在第三方平台完成认证
    • 第三方平台将用户重定向回应用的回调URL
    • 数据流:OAuth提供商→NextAuth API
  3. 处理回调
    • NextAuth.js API接收回调请求
    • 调用 jwt回调处理令牌
    • 保存用户信息到后端存储
    • 数据流:NextAuth API→数据存储NextAuth API→会话状态
  4. 创建会话
    • 调用 session回调构建会话信息
    • 返回包含用户信息的会话对象
    • 数据流:会话状态用户界面
  5. 完成登录
    • 用户被重定向到指定的回调URL或首页
    • 前端可通过 useSession钩子或 auth()函数访问会话信息
    • 数据流:会话状态用户界面会话状态数据存储

JWT与会话状态管理

JWT(JSON Web Token)在认证流程中扮演核心角色,对应数据流转图中的**会话状态(JWT)**节点:

  1. JWT创建
    • 用户成功认证后,NextAuth创建包含用户信息的JWT令牌
    • JWT中存储必要的用户信息(如UUID、头像URL等)
    • 数据流:NextAuth API→会话状态
  2. JWT存储
    • JWT令牌加密后存储在HTTP-only cookie中
    • 浏览器每次请求自动发送cookie,实现无状态认证
    • 数据流:会话状态用户界面
  3. 会话构建
    • 服务端通过 auth()函数解析JWT令牌获取会话信息
    • 客户端通过 useSession()钩子访问会话状态
    • 数据流:会话状态用户界面
  4. 会话与数据存储交互
    • 会话中的用户标识可用于从数据存储获取完整用户信息
    • 可通过会话中的用户ID更新数据存储中的用户信息
    • 数据流:会话状态数据存储

类型扩展

NextAuth类型扩展,支持JWT和Session中的自定义字段:

declare module "next-auth" {
  interface JWT {
    user?: {
      uuid?: string;
      nickname?: string;
      avatar_url?: string;
      created_at?: string;
    };
  }

  interface Session {
    user: {
      uuid?: string;
      email?: string | null;
      name?: string | null;
      nickname?: string | null;
      avatar_url?: string | null;
      created_at?: string | null;
    }
  }
}

用户类型定义,对应数据存储中的用户数据结构:

export interface User {
  id?: number;
  uuid?: string;
  email: string;
  created_at?: string;
  nickname: string;
  avatar_url: string;
  signin_type?: string;
  signin_ip?: string;
  signin_provider?: string;
  signin_openid?: string;
}

安全考虑

  1. 环境变量:OAuth客户端ID和密钥存储在环境变量中
  2. 重定向检查:验证重定向URL的合法性
  3. JWT令牌:使用JWT保存会话状态,加密存储防篡改
  4. 无密码存储:使用OAuth方式不需要存储用户密码
  5. HTTP-only Cookie:JWT存储在HTTP-only cookie中,防止JavaScript访问
  6. CSRF保护:NextAuth内置CSRF令牌验证机制

数据流转优化建议

  1. 数据库集成
    • 将内存存储替换为持久化数据库(MongoDB、PostgreSQL等)
    • 优化数据存储与NextAuth API的交互性能
  2. 令牌刷新机制
    • 实现OAuth访问令牌自动刷新功能
    • 延长用户会话有效期,减少重复登录
  3. 缓存层引入
    • 在数据存储与会话状态之间添加缓存层(如Redis)
    • 减轻数据库负担,提高频繁会话查询性能
  4. 前端状态管理
    • 优化前端会话状态管理,减少不必要的API调用
    • 实现优雅的会话过期处理和自动重新认证

参考

参半再推溶菌酶牙膏系列新品,进一步加码情绪价值 | 最前线

2025年4月2日 22:28

近日,口腔护理品牌参半再推溶菌酶牙膏系列新品——零度清新牙膏。该产品主打10倍长效凉感、24小时清新口气,通过突破性凉感技术、香氛技术及配方、感官体验设计等,解决了市面大部分清新品类牙膏单一香型、时效短暂、辣口等凉感痛点。

参半成立于2018年,产品矩阵覆盖美白、清新、抗敏、护龈等各细分品类,并拥有益生菌(SP)、溶菌酶、羟基磷灰石、沸石等多个明星产品系列,此次参半推出的零度清新牙膏正是其溶菌酶产品家族的又一新品。2024年,参半线上线下销售额整体突破20亿,并成为线上牙膏份额里的第一名。

去年,参半面向市场推出溶菌酶系列,以溶菌酶成分为主要配方体系,分别搭载热感、清新、双氟双锶抗敏技术等,推出39°热感、酶可护、酶植清多款产品,协同解决牙黄、异味等口腔护理需求。

Mordor Intelligence数据显示,中国口腔护理市场规模正以4%年复合增长率稳定增长,预计到2029年将达人民币689.77亿元规模。

在市场扩容背后,消费者对牙膏产品需求不断演化。国内消费者对牙膏的需求演变经历了基础清洁、功能补足到精准强化三个阶段,如今正迈入第四阶段——消费者期望牙膏不仅满足实用性,更能传递情绪价值。

此前,参半创始人尹阔在接受36氪专访时,也提到年轻人在牙膏上需要情绪价值,“一个日用品如果使用时间久了,你会觉得腻。倒也不是说这个产品不好,你就是想换一换,或者和长辈用一样的东西,你想用不一样的,觉得应该有自己的选择。”

因此,在产品研发中,参半除了注重功效提,还注重产品的刷感、口感以及香型、留香时长等方面,以丰富消费者的情绪价值、感官愉悦体验等。

据悉,研发方面,参半与全球香精香料生产商奇华顿进行了深度合作;在凉感方面,参半采用奇华顿EverCool和coolshot凉感技术,结合成熟的释放包裹技术,其口腔凉感持续时长超普通牙膏10倍。在口气清新方面,新品运用奇华顿BreathZap技术来中和口腔异味,还将ScentTrek香氛技术应用其中。此外,该款新品还在溶菌酶基础上,创新添加葡聚糖酶、木瓜蛋白酶,通过三重复合配方达到更好的口腔护理效果。

“我们通过消费者调研发现,在晨间牙膏的需求选择上,有超过65%的消费者会将‘使用时的清凉感’列为重要决策因素,这一比例在18-35岁年轻群体更是高达80%。同时,消费者对凉感的期待已经转向更持久、更具层次感、更具冲击力的清凉体验,这正是零度清新牙膏的研发初衷。”参半口腔产品研发中心负责人说道。

氪星晚报 |永辉上海学习胖东来调改二店开业;鸿蒙版微信迎来重要更新:支持通过分享面板分享文字、链接和文件到微信;美国关税阴影笼罩,意大利工业家联合会下调GDP预期

2025年4月2日 22:15

大公司:

达美乐将接受DoorDash订单,结束Uber独家经营权

达美乐将结束Uber Eats的独家经营权,开始通过美国外卖服务公司DoorDash接受订单。根据达美乐和DoorDash两家公司的声明,从5月开始,美国顾客将可以通过DoorDash订购达美乐披萨。(界面)

鸿蒙版微信迎来重要更新:支持通过分享面板分享文字、链接和文件到微信

4月2日,鸿蒙版微信迎来重要更新 ,新版本支持通过分享面板分享文字、链接和文件到微信,同时支持微信运动、扫码添加企微联系人、长按消息收藏、消息撤回后重新编辑发送、字体大小调节、聊天记录分类搜索、搜索群成员、长按识别网页二维码、转发多个聊天会话、联系人推荐给好友等功能。

新希望:预计一季度盈利4.3亿元至5亿元,同比扭亏为盈

36氪获悉,新希望公告,预计2025年一季度归属于上市公司股东的净利润盈利4.3亿元至5亿元,上年同期亏损19.34亿元。扣除非经常性损益后的净利润盈利4.31亿元至5.01亿元。基本每股收益盈利0.09元/股至0.11元/股。业绩变动原因主要是生猪养殖业务减亏以及饲料业务量利同增。

北京机器人产业发展基金等入股云鲸智能

36氪获悉,天眼查App显示,近日,云鲸智能创新(深圳)有限公司发生工商变更,新增北京机器人产业发展投资基金(有限合伙)、北京首石科幻产业股权投资基金(有限合伙)、Futurobot Holdings, L.P.为股东,同时注册资本由约338万人民币增至约351万人民币。该公司成立于2016年10月,现由东莞云朋企业管理合伙企业(有限合伙)、抖音集团旗下北京量子跃动科技有限公司、广西腾讯创业投资有限公司及上述新增股东等共同持股。官网显示,该公司是一家立足家用机器人领域的公司。

将在中国裁员20%?香奈儿中国:不回应

近日有媒体报道,法国奢侈品牌香奈儿将启动中国区战略调整,计划2025年年内将中国员工总数从462人缩减至373人,裁员幅度接近20%,涉及多个部门。对此,香奈儿中国表示不做回应。(界面)

雷军发布《致所有关心小米SU7事故用户和公众的一封信》?小米:假的

小米SU7高速碰撞燃爆事件继续发酵。4月2日,一篇以雷军之名的《致所有关心小米SU7事故的用户和公众的一封信》在网上传播,信中提到了事故的善后工作包括承诺探寻真相、反思与行动、透明化沟通,以及用十年坚守兑现安全承诺等。很快,有业内人士称这个截图为假,发布者为一个资深车评人,他删除了该图并表示“那个图是假的,抱歉”。记者联系到小米,小米方面回应称此为假消息。目前,小米官微和雷军个人微博并未发布上述信息。4月1日晚间,雷军曾发声表示“将站出来”解决问题。 (证券时报)

爱玛科技:一季度净利润同比预增25.12%

36氪获悉,爱玛科技4月2日晚发布2025年第一季度业绩预告,预计2025年第一季度实现归属于上市公司股东的净利润约为6.05亿元,同比增长25.12%。公司持续专注于电动两轮车、三轮车的研发与制造,始终以用户需求为导向,通过技术创新与精准产品开发推动产品结构优化升级。同时,深化渠道网络覆盖及供应链协同效能,实现高附加值车型占比进一步提升,推动经营业绩稳步增长。

云天励飞:剩余算力服务验收合格

36氪获悉,云天励飞公告,全资子公司励飞科技已完成向德元方惠交付AI训练及推理算力服务及对应交付物,并已完成剩余算力服务(对应合同中约40%的算力规模)的验收相关工作。验收结论为:训练及推理基础算力服务(合计对应合同约40%的算力规模),以及智能算力调度及AI大模型开发服务平台,满足合同约定验收要求。根据合同约定及验收结果,该项目已实现服务费正常结算并已回款。此次剩余算力服务验收合格对公司经营业绩产生积极影响,有利于提升市场竞争力和促进公司健康可持续发展。

张小泉回应控股股东被执行31.3亿:上市公司独立经营,目前一切正常

今日,“张小泉集团及法人等被执行31.3亿”话题登上热搜。记者致电张小泉股份投关部门,相关负责人表示,张小泉集团是张小泉股份的控股股东,双方在财务、业务、人员等方面均是独立经营,上市公司目前的经营一切正常。针对控股股东的债务问题或其涉及的冻结事项,投资者可以关注公司公告,公司一直都有对外披露相关事项。(新浪科技)

永辉上海学习胖东来调改二店开业

36氪获悉,4月2日,永辉超市上海第二家“学习胖东来”自主调改店——浦江万达广场店焕新开业。作为闵行区首家调改门店,其以“环境焕新+商品升级+服务进阶+员工关怀”的崭新面貌登场,不仅是永辉全国调改版图的又一落子,更为上海建设国际消费中心城市注入新动能。据了解,该店开业后,永辉全国调改门店数量将攀升至48家,预计在今年年中调改门店将达100家,全年达200家。

投融资:

“鑫辰佰盛”完成数千万元A轮融资

36氪获悉,近日,厦门鑫辰佰盛互娱传媒有限公司宣布完成数千万元A轮融资,由知化数标(深圳)科技集团股份有限公司投资。本轮资金将用于AI内容生产引擎研发、海外区域运营中心建设及跨产业生态合作开发,AI驱动的创作民主化、短剧IP的跨媒介增值、以及虚实融合的文化消费场景重构。

新产品:

竹芒科技发布彩宝3.0 Pro

4月1日,拥有街电搜电两大品牌的竹芒科技发布了彩宝3.0系列新品和智能运营平台。据介绍,彩宝3.0Pro柜机在硬件防护方面,采用了军工级防护体系。具备IPX4防水设计,行业首创的“控水机架”实现前后干湿分离,即使在潮湿的环境中也能正常运行。此次发布的充电宝3.0 Pro产品拥有10000mAh容量,配合22.5W超级快充,实测30分钟可充至80%,以此大幅提升用户使用体验。智能运营方面,AI调度功能将提升资产流转效率35%,全面优化代理商运维能力。

今日观点:

美国关税阴影笼罩,意大利工业家联合会下调GDP预期

意大利工业家联合会(Confindustria)周三下调了对意大利的经济增长预期,并警告称,美国即将出台的贸易关税可能会进一步恶化该国的经济前景。意大利工业家联合会表示,预计今年意大利国内生产总值将增长0.6%,仅为政府官方预测值1.2%的一半,也低于该协会去年10月预计的0.9%。在去年第三季度经济停滞之后,第四季度环比增长0.1%。大多数分析师预计,短期内意大利经济不会出现显著回升。(新浪财经)

其他值得关注的新闻:

工信部:前2个月云计算大数据服务同比增长8.8%,集成电路设计同比增长13.5%

36氪获悉,工信部发布数据,前2个月,软件产品收入4253亿元,同比增长8.3%。其中工业软件产品收入441亿元,同比增长6.4%;基础软件产品收入276亿元,同比增长6.7%。信息技术服务收入保持两位数增长。前2个月,信息技术服务收入12585亿元,同比增长10.3%。其中,云计算大数据服务同比增长8.8%;集成电路设计同比增长13.5%。前2个月,信息安全产品和服务收入393亿元,同比增长6.8%。前2个月,嵌入式系统软件收入1735亿元,同比增长11.9%。

vue与react的简单问答

2025年4月2日 21:39

1. Vue 4.0 的编译时宏(definePropsdefineEmits)如何通过 AST 转换实现类型安全?对比 TypeScript 泛型的优劣。

实现原理:

在 Vue 的编译时阶段,defineProps 和 defineEmits 会通过 AST(抽象语法树)转换生成类型安全的代码。具体流程如下:

  1. 解析宏‌:编译器识别 defineProps 和 defineEmits,提取它们的参数(如类型定义)。
  2. 类型推断‌:基于参数生成运行时类型校验逻辑,或与 TypeScript 类型系统集成。
  3. 代码生成‌:将类型信息转换为运行时验证代码(如 Props 的校验函数)或静态类型声明。

示例代码:

// 编译前(用户代码)
defineProps<{ count: number }>();
defineEmits<{ (e: 'update', value: string): void }>();

// 编译后(生成代码)
{
  props: { count: { type: Number, required: true } },
  emits: ['update'],
  // 可能生成运行时校验逻辑
}

对比 TypeScript 泛型:

  • 优势‌:

    • 框架集成‌:编译时宏能直接生成框架所需的运行时逻辑(如 Props 校验),而 TypeScript 泛型仅提供静态类型检查。
    • 简洁性‌:无需手动编写类型与运行时代码的映射。
  • 劣势‌:

    • 灵活性‌:TypeScript 泛型支持更复杂的类型操作(如联合类型、条件类型),而编译时宏可能受限于框架设计。
    • 工具链依赖‌:编译时宏需要特定编译器支持,而 TypeScript 泛型是语言原生特性。

2. 在 Vue 4.0 中,如何通过 Composition API 实现跨组件的状态共享?对比 Vuex 的适用场景。

实现方式:

使用 Composition API 的 provide/inject 或工厂函数实现状态共享:

// sharedState.ts
import { ref, provide, inject } from 'vue';

const key = Symbol('sharedState');

export function createSharedState() {
  const state = ref({ count: 0 });
  return { state };
}

export function useSharedState() {
  return inject(key) || createSharedState();
}

// 根组件
provide(key, createSharedState());

// 子组件
const { state } = useSharedState();

对比 Vuex:

  • Composition API 适用场景‌:

    • 中小型应用或局部状态共享。
    • 需要更灵活的状态逻辑组合(如复用逻辑片段)。
  • Vuex 适用场景‌:

    • 大型应用,需集中式状态管理。
    • 需要严格的全局状态变更追踪(如 DevTools 集成、时间旅行调试)。

3. 如何用 Vue 4.0 的 Suspense 实现异步组件的加载状态管理?关键代码及与 React Suspense 的差异。

关键代码:

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
);
</script>

与 React Suspense 的差异:

  1. 错误处理‌:Vue 使用 <ErrorBoundary> 配合 onErrorCaptured,而 React 直接在 Suspense 边界捕获。
  2. 并发模式‌:React Suspense 支持并发渲染特性(如优先级中断),Vue 目前未实现类似机制。
  3. 组合方式‌:Vue 的 Suspense 需要显式包裹异步组件,React 的 Suspense 可以更灵活地嵌套使用。

4. 设计一个 Vue 4.0 的自定义指令实现图片懒加载,支持 Intersection Observer 的回调。

实现代码:

// lazyLoadDirective.ts
import type { Directive } from 'vue';

const lazyLoad: Directive<HTMLImageElement, string> = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value;
          observer.unobserve(el);
          // 触发自定义回调
          if (binding.arg === 'callback') {
            (binding.instance as any)[binding.modifiers?.callback]?.();
          }
        }
      });
    });
    observer.observe(el);
    el._observer = observer; // 保存 observer 实例以便卸载时使用
  },
  beforeUnmount(el) {
    el._observer?.unobserve(el);
  }
};

export default lazyLoad;

使用方式:

<template>
  <img v-lazy="imageUrl" v-lazy:callback.onVisible />
</template>

<script setup>
import { ref } from 'vue';

const imageUrl = ref('path/to/image.jpg');

function onVisible() {
  console.log('Image is visible!');
}
</script>

5. 在 Vue 4.0 中,如何通过 Teleport 实现模态框的全局挂载?关键代码及与 React Portal 的异同。

关键代码:

<template>
  <button @click="showModal = true">Open Modal</button>
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <p>Modal Content</p>
      <button @click="showModal = false">Close</button>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from 'vue';

const showModal = ref(false);
</script>

与 React Portal 的异同:

  • 相同点‌:

    • 目标:将子组件渲染到 DOM 树的其他位置(如 body 末尾)。
    • 应用场景:模态框、弹出菜单等需要脱离父容器样式限制的场景。
  • 不同点‌:

    • 语法‌:Vue 使用 <Teleport to="selector">,React 使用 createPortal(children, domNode)
    • 动态目标‌:Vue 允许动态绑定 to(如 :to="dynamicTarget"),React 需手动管理容器节点。
    • 组件化‌:Vue 的 Teleport 是内置组件,React Portal 是函数调用。

SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照

作者 冴羽
2025年4月2日 20:02

前言

Svelte,一个语法简洁、入门容易,面向未来的前端框架。

从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

image.png

Svelte 以其独特的编译时优化机制著称,具有轻量级高性能易上手等特性,非常适合构建轻量级 Web 项目

为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。

如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

仅服务端模块

SvelteKit 会像一个好朋友一样,保守您的秘密。在同一个代码仓库中编写后端和前端代码时,很容易不小心将敏感数据(例如包含 API 密钥的环境变量)导入到前端代码中。SvelteKit 提供了一种完全防止这种情况发生的方法:仅服务端模块(server-only modules)。

私有环境变量

$env/static/private$env/dynamic/private 模块只能导入到仅在服务端上运行的模块中,例如 hooks.server.js+page.server.js

仅服务端工具

$app/server 模块包含一个 read 函数,用于从文件系统读取资源,同样只能被服务端运行的代码导入。

您的模块

您可以通过两种方式使您的模块成为仅服务端模块:

  • 在文件名中添加 .server,例如 secrets.server.js
  • 将它们放在 $lib/server 中,例如 $lib/server/secrets.js

工作原理

任何时候,当您有公开的代码,导入仅服务端代码时(无论是直接还是间接)...

// @errors: 7005
/// file: $lib/server/secrets.js
export const atlantisCoordinates = [
/* 已编辑 */
];
// @errors: 2307 7006 7005
/// file: src/routes/utils.js
export { atlantisCoordinates } from '$lib/server/secrets.js';

export const add = (a, b) => a + b;
/// file: src/routes/+page.svelte
<script>
import { add } from './utils.js';
</script>

...SvelteKit 将报错:

Cannot import $lib/server/secrets.js into public-facing code:
- src/routes/+page.svelte
  - src/routes/utils.js
    - $lib/server/secrets.js

尽管公开代码 — src/routes/+page.svelte — 只使用了 add 导出而没有使用秘密的 atlantisCoordinates 导出,秘密代码也可能最终出现在浏览器下载的 JavaScript 中,因此这个导入链被认为是不安全的。

此功能也适用于动态导入,甚至是像 await import(`./${foo}.js`) 这样的插值导入,只有一个小注意点:在开发过程中,如果公开代码和仅服务端模块之间存在两个或更多的动态导入,则在第一次加载代码时不会检测到非法导入。

[!NOTE] 像 Vitest 这样的单元测试框架不区分仅服务端代码和公开代码。因此,当运行测试时,非法导入检测会被禁用,这由 process.env.TEST === 'true' 决定。

进一步阅读

快照

临时的 DOM 状态 — 比如侧边栏的滚动位置、<input> 元素的内容等 — 在从一个页面导航到另一个页面时会被丢弃。

例如,如果用户填写了一个表单但在提交之前离开并返回,或者用户刷新页面,他们填写的值将会丢失。在需要保留这些输入的情况下,您可以创建一个 DOM 状态的快照,当用户返回时可以恢复这个状态。

要实现这一点,从 +page.svelte+layout.svelte 中导出一个带有 capturerestore 方法的 snapshot 对象:

<!--- file: +page.svelte --->
<script>
  let comment = $state('');

  /** @type {import('./$types').Snapshot<string>} */
  export const snapshot = {
    capture: () => comment,
    restore: (value) => comment = value
  };
</script>

<form method="POST">
  <label for="comment">评论</label>
  <textarea id="comment" bind:value={comment} />
  <button>发表评论</button>
</form>

当您离开这个页面时,capture 函数会在页面更新之前立即被调用,返回的值会与浏览器历史栈中的当前记录关联。如果您返回此页面,restore 函数会在页面更新后立即被调用,并传入存储的值。

数据必须是可以序列化为 JSON 的,这样它才能被保存到 sessionStorage 中。这样就可以在页面重新加载时,或者用户从其他网站返回时恢复状态。

[!NOTE] 避免从 capture 返回非常大的对象 — 一旦被捕获,对象将在会话期间保留在内存中,在极端情况下可能会因太大而无法保存到 sessionStorage 中。

Svelte 中文文档

点击查看中文文档:

  1. SvelteKit 仅服务端模块
  2. SvelteKit 快照

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng…

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

AI编程-cursor无限使用, 还有谁不会🎁🎁🎁??

2025年4月2日 19:57

大家好,我是前端小张同学,最近AI是炒的是非常火热,各路神仙都来参与,cursor,Trea,Windsurf,MarsCode 啊等等,今天就给大家带来 cursor 的免费一直使用的技巧吧,希望大家会喜欢,谢谢。

image.png

1:安装cursor and 导入vscode配置和拓展

安装 cursor 想必大家都知道(Cursor - The AI Code Editor),进行下载安装就行了,重要的是可能还有小伙伴不知道怎么导入vscode配置,今天我就给大家一篇文章讲明白。在这里给大家附上 官方教程

1.1: 如何导入vscode配置,三种方法

1:安装完成后,选择导入

image.png

2:如果你没有以上页面,请使用手动导入。 打开 cursor 编辑器 选择 File > preference > cursor Settings

image.png

点击 VScode import 导入你的配置

3:手动执行你的命令去导入 ctrl + shift + p 打开命令行,输入vscode import,选择import VSCode extensions 回车即可,等待自动安装。

image.png

2:重置cursor

2.1:重头戏 如何重置 首先在这里给大家推荐一个 github仓库 [go-cursor-help] (github.com/yuaotian/go…)

首先在这里建议大家使用这条规则,轻松快捷

Solution 2: Account Switch
1. File -> Cursor Settings -> Sign Out (文件 - > 光标设置 - > 注销)
2. Close Cursor (关闭 cursor)
3. Run the machine code reset script(运行机器代码重置脚本)
4. Login with a new account (使用新帐户登录)

1:退出自己的cursor 账号

image.png

2:关闭 cursor软件

3:删除账户,重新登录

image.png

4:运行机器码重置脚本 根据自己的电脑系统决定执行什么脚本。

macOS

curl -fsSL aizaozao.com/accelerate.… -o ./cursor_mac_id_modifier.sh && sudo bash ./cursor_mac_id_modifier.sh && rm ./cursor_mac_id_modifier.sh

Linux

curl -fsSL aizaozao.com/accelerate.… | sudo bash

Windows

irm aizaozao.com/accelerate.… | iex

windows用户 打开 powershell,请注意用 管理员身份打开

到这里,你再继续用原账号登录,恭喜你已刷新使用时长。

3:cursor 的使用技巧

3.1:编辑器的侧边栏怎么设置?

1:打开vscode setting.json文件,加入以下代码。

   "workbench.activityBar.orientation": "vertical",

image.png

3.2 如何与AI对话,指定某些文件进行分析?

在对话输入框中,我们可以通过 @符号去唤起 一些操作 比如说 选择 文件 , 选择目录,选择指定的 code

image.png

这样他就能对我们选中的文件进行分析。

image.png

3.2.1:@git的作用

这也是我比较喜欢的一个功能,它可以帮助你去进行 commit 信息进行对比or 分析。

image.png

就像这样

image.png

3.3:如何选择模型

在对话窗口下选择你想要的模型,默认是 自动选择,目前比较好用的是 claude-3.5-sonnet 和 gpt-4o,建议大家使用。

image.png

好了,以上就是今天给大家分享的内容,我们下期见,我是前端小张同学,期待你的关注。

Caddy Web服务器初体验:简洁高效的现代选择

2025年4月2日 19:16

Caddy简介

Caddy是一款使用Go语言编写的开源Web服务器和反向代理服务器,旨在提供易于使用且高效的性能。它支持HTTP/HTTPS协议,并可作为反向代理服务器、负载均衡器和WebSocket支持等。Caddy的灵活性和模块化架构使其适合容器化环境和微服务架构。

Caddy的主要特点

  1. 默认启用HTTPS:Caddy集成了Let’s Encrypt,可以自动申请、更新和管理SSL证书,无需额外操作。
  2. 配置简洁:Caddy的配置文件(Caddyfile)简洁易读,降低了新手的学习成本。
  3. 动态配置管理:通过REST API,可以在运行时更改Caddy的配置,无需重新启动服务器。
  4. 现代化特性:支持Prometheus metrics,使用结构化的JSON作为访问日志。

Caddy与Nginx的对比

特性 Caddy Nginx
配置方式 Caddyfile, JSON, REST API Nginx配置文件(nginx.conf)
自动HTTPS支持 是,默认启用自动TLS证书管理 否,需手动配置SSL证书
适用范围 7层(应用层),反向代理和Web服务,内置负载均衡 支持4层(传输层)和7层(应用层)反向代理、负载均衡等
扩展性 插件化架构,支持扩展 模块化架构,支持静态编译的模块
性能 较高(适合轻量应用) 非常高(适合高并发应用)
配置简洁性 Caddyfile格式简洁,易于上手 配置相对复杂,灵活但不够直观
系统资源占用 较低 较低,适合高并发处理
编写语言 Go语言 C语言
Access日志格式 结构化,默认JSON格式,支持自定义 非结构化,默认标准日志格式,支持自定义

Caddy的基本用法

安装方式

  1. 二进制安装:下载Caddy的二进制文件并移动到PATH下即可使用。
  2. Docker Compose安装:使用Docker容器快速部署Caddy。

Docker Compose配置示例

version: "3.8"
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - ACME_AGREE=true

volumes:
  caddy_data:
  caddy_config:

配置方式

  1. Caddyfile配置:简洁易读的配置文件。
  2. JSON配置:适合高级使用场景和动态配置。
  3. REST API配置:动态管理和变更配置。

Caddyfile示例

example.com {
    reverse_proxy 127.0.0.1:3000
    log {
        output file /var/log/caddy/access.log {
            mode 644
        }
        format json
    }
}

JSON配置示例

{
  "apps": {
    "http": {
      "servers": {
        "example": {
          "listen": [":80"],
          "routes": [
            {
              "match": [
                {
                  "host": ["example.com"]
                }
              ],
              "handle": [
                {
                  "handler": "static_response",
                  "body": "Hello, world!"
                }
              ]
            }
          ]
        }
      }
    }
  }
}

常见配置示例

  1. 直接回复

    localhost:2017 {
        respond "Hello, world!"
    }
    
  2. 配置静态文件

    localhost:2016 {
        root * /var/www/mysite
        file_server {
            browse
            hide .git
            precompressed zstd br gzip
        }
    }
    
  3. 配置反向代理

    example.com {
        reverse_proxy localhost:8000
    }
    
  4. 配置负载均衡

    example.com {
        reverse_proxy / backend1.example.com backend2.example.com
    }
    

Caddy的持久化存储

  1. 配置文件:自定义配置文件需要放置在合理的位置。
  2. 数据目录:用于存储TLS证书和其他关键数据。
  3. 配置目录:保存最后一次有效的配置。

在使用Docker容器时,需要挂载这些目录以确保数据持久化。

volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile
  - caddy_data:/data
  - caddy_config:/config

【JS】instanceof 和 typeof 的使用

2025年4月2日 18:46

instanceoftypeof

instanceof

instanceof 用于检查一个对象是否是某个构造函数的实例。换句话说,它会检查对象的原型链上是否存在该构造函数的 prototype 属性。 示例代码

let a = new Number(1)
console.log(a instanceof Number);  // true
console.log(a.__proto__.constructor === Number) // true
console.log(a.__proto__ === Number.prototype) // true
console.log('------')
let b = 1
console.log(b instanceof Number); // false
console.log(b.__proto__.constructor === Number) // true
console.log(b.__proto__ === Number.prototype) // true (临时包装对象)

按照上面的说法
x instanceof Y 检查 x 的原型链上是否有 Y.prototype
可以等效为 x.__proto__ === Y.prototype (但是又不完全等效,因为instanceof会在整个原型链上递归查找)
如果我们仅看这个简单的等效,对比上面的4、9行代码。
a是对象,b是原始类型。严格来说,原始类型是没有__proto__的,但是JS引擎会在访问他们的属性的时候,临时包装成对象,使其看起来有__proto__,所以在第9行,还是会输出 true
所以这里为什么第7行,输出是false呢,不是按照上面的规则来,就检查x.__proto__ === Y.prototype 吗,既然第9行为true,但是第7行为false呢?
这里就涉及到另外一条规则了,如果x是原始类型,那么会直接返回false,因为原始类型没有原型链,上面的第9行是包装之后才有了原型链。

工作原理

x instanceof Y 的完整行为:

  1. 如果 x 是原始类型(如 1"a"true),直接返回 false(因为原始类型没有原型链)。
  2. 如果 x 是对象,则沿着 x 的原型链向上查找,检查是否有 Y.prototype
    • 先检查 x.__proto__ === Y.prototype,如果是,返回 true
    • 如果不是,继续检查 x.__proto__.__proto__ === Y.prototype,依此类推,直到原型链尽头(null)。
class Animal {}
class Dog extends Animal {}

const dog = new Dog();
console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true(因为 Dog 继承 Animal)
console.log(dog instanceof Object);  // true(所有对象最终继承 Object)

instanceof 的实现

function myInstanceof(left, right) {
    if (left === null || typeof left !== 'object') return false // 不是对象或null
    if (typeof right !== 'function' || !right.prototype) {
        // 对于JS中的函数,typeof 返回 'function'
        // 但是对于其他对象,typeof 返回 'object'
        // 这里我们需要判断 right 是否是一个函数
        throw new TypeError('Right-hand side of instanceof is not callable');
    }
    let proto = Object.getPrototypeOf(left) // 获取对象的原型
    let prototype = right.prototype // 获取构造函数的原型
    while (proto) {
        if (proto === prototype) return true // 找到原型链上的prototype
        proto = Object.getPrototypeOf(proto) // 继续向上查找原型链
    }
    return false
}

typeof

用来返回变量的基本类型,以字符串的形式返回,且不会检查原型链

console.log(typeof 42);           // "number"
console.log(typeof "hello");      // "string"
console.log(typeof true);         // "boolean"
console.log(typeof undefined);    // "undefined"
console.log(typeof null);         // "object"(历史遗留 bug)
console.log(typeof {});           // "object"
console.log(typeof []);           // "object"(数组也是对象)
console.log(typeof function() {}); // "function"
console.log(typeof Symbol());     // "symbol"
console.log(typeof 123n);         // "bigint"

其中数组、对象、null都会被判断为object。函数也是对象,但是typeof对其进行了特殊处理,返回了function。

  • typeof null === "object"
    这是 JavaScript 早期的一个 Bug,但由于历史原因无法修复。
  • typeof [] === "object"
    数组本质是对象,无法直接区分数组和普通对象(可以用 Array.isArray() 判断)。
  • typeof function() {} === "function"
    函数虽然是对象,但 typeof 对其特殊处理,返回 "function"

总结

操作符 适用场景 不适用场景
typeof 检查原始类型、undefinedfunction 无法区分对象的具体类型(如数组 vs 普通对象)
instanceof 检查对象是否是某个类的实例(包括继承) 不适用于原始类型

推荐组合使用:

  • 先 typeof 判断是否是原始类型。
  • 如果是对象,再用 instanceof 或 Array.isArray() 进一步判断。

项目中遇到浏览器跨域前端和后端解决方案以及大概过程

作者 干就完了1
2025年4月2日 18:29

前言: ‌浏览器出于安全考虑,要求请求的 ‌协议(HTTP/HTTPS)、域名、端口(如 90/455 ‌ 三者完全一致,否则视为跨域。跨域问题的本质是 ‌浏览器通过同源策略保护用户数据安全‌,强制要求前后端资源同源。开发中因为环境分离、多域名部署等场景就会触发该策略的拦截机制‌。

一. 纯前端解决方案

1. ‌本地开发环境推荐使用代理解决‌‌

1.在Vue/React项目中配置 vue.config.js 或 webpack.config.js(vue3中是vite.config.ts文件)

//vue3的Vite示例
export default defineConfig({
  // 服务端渲染
    server: {
      // 端口号
      port: "8980",
      host: "0.0.0.0",
      // 本地跨域代理 http://192.145.1.95:1216
      proxy: {
        "/admin-api": {
          // 这里填写后端地址
          // target: "https://test.com",
          target: VITE_API_PATH, //或者封装起来
          changeOrigin: true,
          rewrite: path => path.replace(/^\/admin-api/, ""),
          secure: false // 验证 SSL 证书
        }
      },
    },
});
// vue2的webpack示例示例
module.exports = {
    devServer: {
        proxy: {
            '/admin-api': {
                target: 'https://test.com',
                changeOrigin: true
            }
        }
    }
};

// 不同的前端语言大致实现思路都一样

2.重启开发服务器,前端请求本地路径包含 /api 自动代理到后端接口地址。

2. ‌利用WebSocket协议解决‌‌

WebSocket 解决跨域的核心原理是在 HTTP 握手阶段通过服务器主动验证 Origin 字段完成跨域授权,而非依赖浏览器同源策略的默认拦截机制‌

1.前端使用WebSocket建立连接:

const socket = new WebSocket('ws://test.com:9090');
socket.onmessage = (event) => { console.log(event.data); };

2.后端需实现WebSocket服务端(如Socket.io)。

3.postMessage(跨窗口通信)

1.父窗口向iframe子窗口发送消息

const iframe = document.getElementById('child-frame');
iframe.contentWindow.postMessage('data', 'http://child.com');

2.子窗口监听消息:

window.addEventListener('message', (event) => {
    if (event.origin === 'http://parent.com') console.log(event.data);
});

二. 后端解决方案

1. ‌CORS(跨域资源共享)--->生产环境推荐 ‌‌

1.后端在响应头中设置 Access-Control-Allow-Origin,允许指定源访问:

// Spring Boot示例
@CrossOrigin(origins = "http://localhost:9090")
@GetMapping("/api/data")
public String getData() { /*...*/ }

2.若需携带Cookie,需设置 Access-Control-Allow-Credentials: true 并指定具体源(不能为*)。

3.预检请求(如PUT、DELETE)需处理OPTIONS方法,返回允许的HTTP方法和头信息。

2. ‌Nginx反向代理--->生产环境推荐‌‌

1.在Nginx配置文件中添加代理规则:

server {
    listen 90;
    server_name frontend.com;
    location /api {
        proxy_pass http://test.com:9090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

2.重启Nginx服务,使前端通过统一域名访问后端接口

3. ‌JSONP(仅GET请求) ‌‌

1.前端动态创建<script>标签,指定回调函数名:

function handleData(data) { /*...*/ }
const script = document.createElement('script');
script.src = 'http://test.com/data?callback=handleData';
document.body.appendChild(script);

2.后端返回包裹回调函数的数据:

handleData({ "result": "success" });

三、其他场景方案

同主域不同子域‌:设置 document.domain = 'test.com'(需主域相同)‌

IE兼容性‌:使用XDomainRequest对象替代XMLHttpRequest‌

结尾

推荐方案‌:生产环境优先使用CORS或Nginx反向代理‌,开发环境用本地代理‌更方便快捷(配置简单且无侵入性‌)

慎用方案‌:JSONP仅适用于简单GET请求且安全性较低‌(需注意XSS风险‌),postMessage适用于特定跨窗口场景‌。

如果兄弟们还有其他更方便的解决方案欢迎评论区讨论分享,具体实现还需根据项目来定最适合的解决方案

不同命名风格在 Vue 中后台项目中的使用分析

2025年4月2日 18:21

Vue 中后台项目命名风格实践与分析

在中后台项目开发中,命名风格往往被视为“非核心”细节,但它却直接影响着团队协作效率、代码可读性、工程规范一致性与项目的可维护性。

尤其是在 Vue 驱动的中后台系统中,随着页面模块、字段配置、路由管理日益庞杂,统一命名风格已成为结构化开发的基础。

本文从实战角度出发,分析中后台项目中常见的几种命名风格使用场景,结合项目经验给出推荐规范,适用于绝大多数 Vue(含 Vue 2 / Vue 3)系统。

🧩 1. 路由路径(path):推荐使用 kebab-case

✅ 推荐格式:

path: '/user-center/detail/:id'
path: '/order-manage/list'

🚫 不推荐:

path: '/UserCenter/Detail/:id'

✅ 推荐理由:

  • URL 标准推荐小写字母,避免兼容性问题;
  • Linux 环境下大小写敏感,命名不规范容易出错;
  • kebab-case 可读性强,更便于前端路由维护;
  • 对接 SEO、浏览器插件等工具也更友好。

📛 2. 路由名称(name):推荐使用 camelCase

✅ 推荐:

name: 'userCenterDetail'

🚫 不推荐:

name: 'UserCenterDetail'
name: 'user-center-detail'

✅ 推荐理由:

  • camelCase 是 JS 的原生变量命名方式;
  • 路由跳转中常用 router.push({ name: xxx })
  • 有利于 IDE 自动补全与团队协作。

📦 3. 页面组件文件名:推荐使用 PascalCase

✅ 推荐:

List.vue
Detail.vue
UserInfo.vue

🚫 不推荐:

list.vue
userinfo.vue

✅ 推荐理由:

  • 页面组件和普通组件都是 Vue 单文件组件,统一 PascalCase 更规范;
  • 易于区分组件 vs 工具函数;
  • 配合模块化结构(如 UserCenter/List.vue)视觉更清晰。

🗂 4. 文件夹命名风格对比

文件夹类型 推荐命名 示例 说明
页面模块文件夹 PascalCase UserCenter/ 用于组织具体业务模块页面
功能类文件夹 小写复数 constants/, api/ 存放字段配置、接口封装等
通用组件文件夹 PascalCase components/Common/ 推荐组件内再细分 PascalCase 子模块

🧱 5. 表格字段配置命名(columns)

export const userCode = {
  title: '用户编号',
  dataIndex: 'userCode',
  width: '200px',
  align: 'center',
  scopedSlots: { customRender: 'userCode' },
};

✅ 命名建议:

  • dataIndex: 使用 camelCase
  • 字段对象名与 dataIndex 保持一致;
  • 配置文件统一放入 constants/columns.js,便于复用与查找。

🧭 6. 命名风格对照表(总结)

项目 推荐命名风格 示例
路由路径 path kebab-case /project-config/edit/:id
路由名称 name camelCase projectConfigEdit
页面组件文件名 PascalCase Edit.vue, Detail.vue
页面模块目录 PascalCase ProjectConfig/
功能文件夹 小写复数 constants/, hooks/
字段配置对象名 camelCase userStatus, projectCode

🎁 私藏 Tips:团队项目如何悄悄推进命名规范

  1. 路由路径统一为 kebab-case,命名统一为 camelCase
  2. 页面模块用 PascalCase 文件夹,组件文件用 PascalCase 文件名;
  3. 字段配置集中放在 constants/columns.js,使用统一导出格式;
  4. 字段名与 dataIndex 保持一致,可对接字段推荐系统或自动生成器;
  5. 路由 namepath 命名也可纳入“路由资产库”统一管理;
  6. 项目初期定规范,后期不背锅。

✅ 命名风格不是细节,是架构的一部分

命名风格看似琐碎,但它决定了项目结构是否“可预测”、协作是否“无摩擦”。
统一的命名风格不仅让代码更美观,更是一种工程思维的体现。

你不是在写代码,你是在建立秩序。
命名风格,就是最不引人注意的力量。

💡 路由也能资产化?是的,SBERT 了解一下

未来,我们可以像管理字段资产一样,管理路由资产。

结合语义向量技术(如 SBERT),可以为每条路由路径与路由名称生成语义向量,实现:

  • 🔍 通过自然语言搜索页面(如“编辑用户资料” → /user/edit/:id
  • 🔐 权限分配时智能推荐页面(根据路由语义匹配用户角色)
  • 🤖 自动生成路由配置片段(低代码辅助工具)
  • 🧭 检测语义重复路由、结构异常等质量问题

当你的路由也是“结构化数据 + 语义向量”,整个系统将拥有前所未有的自我感知和可治理性。

这不是幻想,而是工程智能化时代的必经之路。


📌 如果你也在思考如何统一命名、构建前端资产体系,欢迎点赞、收藏或私信交流,我们一起把命名变成项目最强大的隐形护盾。

echarts 实现环形渐变

2025年4月2日 18:09

前言

最近产品在ecahrts官网上找到一个 饼图 想要实现这种从头到尾的渐变交互效果,一开始以为非常简单,echarts应该是提供了类似的配置项,知道做起来才发现,这其中没那么简单。

官网案例分析

官网例子中的渐变并不是echarts提供的配置项实现的,而是通过一张 图片 作为背景实现的渐变,所以一开始想着是先来实现一个渐变的饼图,然后通过多个饼图进行拼接来实现类似指针一样的效果,这样就能够实现自定义这个渐变的颜色,并且也很快就写出来一个demo

认识 Echarts 渐变色

在 echarts 的渐变色中,提供了三种类型,包括线性渐变(linear gradient)、径向渐变(radial gradient)和纹理填充(pattern)。

主要了解了一下 线性渐变 以及 径向渐变 的实现效果,在这之后,也意识到了一个严重的问题:通过echarts提供的颜色填充,貌似没办法实现案例里面这种从头到尾的渐变效果,通过线性渐变能够实现下面这种效果

image-20250402170925813.png

这种固定方向的渐变,但是并不符合我们的要求,

并且我也上网找了一些饼图渐变的案例,发现都是通过这种线性渐变来实现的,只不过会去计算这个渐变的角度,来实现类似从头到尾的渐变,但是一旦进度的幅度较大,就马上露馅了。

  • 例子

image-20250402171205454.png

image-20250402171307616.png

可以看到一旦我调大某一个区域的比例,就会发现最后的实现原理还是线性渐变,只不过动态的计算了角度,这种适合多个比例差不多的饼图,但是一旦有某个块比例过大,就还是会出现样式不够美观

奇思妙想

突然意识到,我们最终的目的是自定义这个圆环的起点和终点的颜色,这并不是非得用echarts提供的渐变功能,图片本身并没有问题,图片最大的限制就在于颜色是定好的,但是我们是不是可以让图片的颜色变成动态生成的?

当然可以!

与似乎,就有了下面的方案,通过 canvas 动态生成渐变背景,在讲这张背景图作为圆环的背景图,这样我们就能够实现自定义圆环的起点颜色和终点颜色了

canvas 生成渐变背景

canvas生成背景这个并不是什么难事,百度一下就能够找到类似的案例,然后丢给ai进行美化一下,修改参数变成自己想要的一个函数,我定义的是能够通过传入起点角度,起点颜色,终点颜色 图片大小 四个参数生成一张 base64 的图片

/**
 * 创建圆形渐变图片
 * @param startAngle 起始角度
 * @param startColor 起始颜色
 * @param endColor 结束颜色
 * @param size 大小
 * @returns
 */
export function createCircularGradientImage(startAngle = 0, startColor = '#fff', endColor = 'blue', size = 200) {
  // 创建一个canvas元素
  const canvas = document.createElement('canvas')
  // 设置canvas的宽度
  canvas.width = size
  // 设置canvas的高度
  canvas.height = size
  // 获取2D绘图上下文
  const ctx = canvas.getContext('2d')
  // 检查是否成功获取上下文
  if (!ctx) {
    throw new Error('ctx is null')
  }
  // 创建圆锥渐变
  // 参数:起始角度,圆心x坐标,圆心y坐标
  const gradient = ctx.createConicGradient(startAngle, size / 2, size / 2)
  // 添加渐变的起始颜色
  gradient.addColorStop(0, startColor)
  // 添加渐变的结束颜色
  gradient.addColorStop(1, endColor)
  // 设置填充样式并绘制矩形
  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, size, size)
  // 将canvas转换为base64格式的图片数据
  const res = canvas.toDataURL('image/png')
  // 从DOM中移除canvas元素
  canvas.remove()
  // 返回生成的图片数据
  return res
}

最终我们能够得到一张类似这样的图片

image-20250402151912179.png

结果

接下来的步骤就简单了,参考官网的案例,我们只不过是替换了图片的来源,这样就能够通过传参获得一个自定义颜色的结果。

const _panelImageURL = createCircularGradientImage(0, '#E5E5FF', 'red')

最后的效果:

image-20250402155934106.png

至于文字颜色和阴影颜色,这些都有着很明显的配置项,这里就不做过多的赘述了,本文主要是分享一下通过canvas构造图片来实现渐变的这种思路

如果有大佬有更好的实现渐变的思路欢迎评论区留言!

❌
❌