Files
my-blog2/2025/12/25/vue-learning-5/index.html
2026-05-13 16:50:38 +08:00

1544 lines
91 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>样式设计CSS 也就是随便写写... (并没有) | llbzow的摸鱼日记 (づ ̄ 3 ̄)づ</title><meta name="author" content="llbzow"><meta name="copyright" content="llbzow"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="ffffff"><meta name="description" content="CSS 的痛与乐作为一个前端,最怕的不是写 JS 逻辑,而是调 CSS 样式。居中?对齐?适配?每一个词都能让猛男落泪。 Vue 中的 Scoped CSSVue 提供了 <style scoped>,这简直是防止样式污染的神器。它通过给元素添加唯一的 data-v-xxx 属性,确保你的样式只在这个组件内生效。 123.example[data-v-f3f3eg9] {">
<meta property="og:type" content="article">
<meta property="og:title" content="样式设计CSS 也就是随便写写... (并没有)">
<meta property="og:url" content="https://yourblog.com/2025/12/25/vue-learning-5/index.html">
<meta property="og:site_name" content="llbzow的摸鱼日记 (づ ̄ 3 ̄)づ">
<meta property="og:description" content="CSS 的痛与乐作为一个前端,最怕的不是写 JS 逻辑,而是调 CSS 样式。居中?对齐?适配?每一个词都能让猛男落泪。 Vue 中的 Scoped CSSVue 提供了 <style scoped>,这简直是防止样式污染的神器。它通过给元素添加唯一的 data-v-xxx 属性,确保你的样式只在这个组件内生效。 123.example[data-v-f3f3eg9] {">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://yourblog.com/img/047.png">
<meta property="article:published_time" content="2025-12-24T16:00:00.000Z">
<meta property="article:modified_time" content="2026-02-09T02:23:33.594Z">
<meta property="article:author" content="llbzow">
<meta property="article:tag" content="Vue">
<meta property="article:tag" content="前端">
<meta property="article:tag" content="CSS">
<meta property="article:tag" content="设计">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://yourblog.com/img/047.png"><script type="application/ld+json">{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "样式设计CSS 也就是随便写写... (并没有)",
"url": "https://yourblog.com/2025/12/25/vue-learning-5/",
"image": "https://yourblog.com/img/047.png",
"datePublished": "2025-12-24T16:00:00.000Z",
"dateModified": "2026-02-09T02:23:33.594Z",
"author": [
{
"@type": "Person",
"name": "llbzow",
"url": "https://yourblog.com"
}
]
}</script><link rel="shortcut icon" href="/img/cute_cat.svg"><link rel="canonical" href="https://yourblog.com/2025/12/25/vue-learning-5/index.html"><link rel="preconnect" href="//cdn.jsdelivr.net"><link rel="preconnect" href="//busuanzi.ibruce.info"><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css"><script>
(() => {
const saveToLocal = {
set: (key, value, ttl) => {
if (!ttl) return
const expiry = Date.now() + ttl * 86400000
localStorage.setItem(key, JSON.stringify({ value, expiry }))
},
get: key => {
const itemStr = localStorage.getItem(key)
if (!itemStr) return undefined
const { value, expiry } = JSON.parse(itemStr)
if (Date.now() > expiry) {
localStorage.removeItem(key)
return undefined
}
return value
}
}
window.btf = {
saveToLocal,
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.async = true
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
script.onload = script.onreadystatechange = () => {
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
}
script.onerror = reject
document.head.appendChild(script)
}),
getCSS: (url, id) => new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
if (id) link.id = id
link.onload = link.onreadystatechange = () => {
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
}
link.onerror = reject
document.head.appendChild(link)
}),
addGlobalFn: (key, fn, name = false, parent = window) => {
if (!true && key.startsWith('pjax')) return
const globalFn = parent.globalFn || {}
globalFn[key] = globalFn[key] || {}
globalFn[key][name || Object.keys(globalFn[key]).length] = fn
parent.globalFn = globalFn
}
}
const activateDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'dark')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
}
}
const activateLightMode = () => {
document.documentElement.setAttribute('data-theme', 'light')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', 'ffffff')
}
}
btf.activateDarkMode = activateDarkMode
btf.activateLightMode = activateLightMode
const theme = saveToLocal.get('theme')
theme === 'dark' ? activateDarkMode() : theme === 'light' ? activateLightMode() : null
const asideStatus = saveToLocal.get('aside-status')
if (asideStatus !== undefined) {
document.documentElement.classList.toggle('hide-aside', asideStatus === 'hide')
}
const detectApple = () => {
if (/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)) {
document.documentElement.classList.add('apple')
}
}
detectApple()
})()
</script><script>const GLOBAL_CONFIG = {
root: '/',
algolia: undefined,
localSearch: undefined,
translate: undefined,
highlight: {"plugin":"highlight.js","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false,"highlightFullpage":false,"highlightMacStyle":false},
copy: {
success: '复制成功',
error: '复制失败',
noSupport: '浏览器不支持'
},
relativeDate: {
homepage: false,
post: false
},
runtime: '',
dateSuffix: {
just: '刚刚',
min: '分钟前',
hour: '小时前',
day: '天前',
month: '个月前'
},
copyright: undefined,
lightbox: 'null',
Snackbar: undefined,
infinitegrid: {
js: 'https://cdn.jsdelivr.net/npm/@egjs/infinitegrid/dist/infinitegrid.min.js',
buttonText: '加载更多'
},
isPhotoFigcaption: false,
islazyloadPlugin: true,
isAnchor: false,
percent: {
toc: true,
rightside: false,
},
autoDarkmode: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
title: '样式设计CSS 也就是随便写写... (并没有)',
isHighlightShrink: false,
isToc: true,
pageType: 'post'
}</script><link rel="stylesheet" href="/css/custom.css"><meta name="generator" content="Hexo 7.3.0"></head><body><div id="loading-box"><div class="loading-left-bg"></div><div class="loading-right-bg"></div><div class="spinner-box"><div class="configure-border-1"><div class="configure-core"></div></div><div class="configure-border-2"><div class="configure-core"></div></div><div class="loading-word">加载中...</div></div></div><script>(()=>{
const $loadingBox = document.getElementById('loading-box')
const $body = document.body
const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
initLoading: () => {
$body.style.overflow = 'hidden'
$loadingBox.classList.remove('loaded')
}
}
preloader.initLoading()
if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
// Add timeout protection: force end after 7 seconds
setTimeout(preloader.endLoading, 7000)
}
if (true) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end')
}
})()</script><div id="web_bg" style="background-image: url(/img/bg_main.png);"></div><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img text-center"><img src="data:image/webp;base64,UklGRhoBAABXRUJQVlA4WAoAAAAQAAAAJwAAJwAAQUxQSLgAAAAFuTJE9D80UiRJkiTpxvPo4fPha2lWw0WWMtgkeIot7wfsAyImYAI+vnvEjz++fJs9sbA+tfcM/HtDiBgr3c8SJP/Dk1FWC2LCxjhcwxkH4oQuQtNwSOdgixw5o+0TyIUCQoMJti7B3ALGhZLGNuJiAYfblG7kb/RON5GjZNeEtA+QtC6omDsY2aoIwv2KDLiAzPYvzhmyNVWWzAtzzGHxSBnzCwFja3Igk3sSwSooc7gTFtmKJoIQVlA4IDwAAABQAwCdASooACgAPzmcxF0vKqcko4gB4CcJZwDNSAn82OhFYAD+7iKcpmiMeBjtx3LPQK2rXKK9ARvvVAA=" data-lazy-src="/img/cute_cat.svg" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar" loading="eager" fetchpriority="high" decoding="sync"></div><div class="site-data text-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">20</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">31</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">4</div></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 文章</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg" id="page-header" style="background-image: url(/img/047.png);"><nav id="nav"><span id="blog-info"><a class="nav-site-title" href="/"><span class="site-name">llbzow的摸鱼日记 (づ ̄ 3 ̄)づ</span></a><a class="nav-page-title" href="/"><span class="site-name">样式设计CSS 也就是随便写写... (并没有)</span><span class="site-name"><i class="fa-solid fa-circle-arrow-left"></i><span> 返回首页</span></span></a></span><div id="menus"><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 文章</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div><div id="toggle-menu"><span class="site-page"><i class="fas fa-bars fa-fw"></i></span></div></div></nav><div id="post-info"><h1 class="post-title">样式设计CSS 也就是随便写写... (并没有)</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2025-12-24T16:00:00.000Z" title="发表于 2025-12-25 00:00:00">2025-12-25</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2026-02-09T02:23:33.594Z" title="更新于 2026-02-09 10:23:33">2026-02-09</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Vue%E5%AD%A6%E4%B9%A0%E4%B9%8B%E8%B7%AF/">Vue学习之路</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title=""><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">浏览量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="container post-content" id="article-container"><h1 id="CSS-的痛与乐"><a href="#CSS-的痛与乐" class="headerlink" title="CSS 的痛与乐"></a>CSS 的痛与乐</h1><p>作为一个前端,最怕的不是写 JS 逻辑,而是调 CSS 样式。居中?对齐?适配?每一个词都能让猛男落泪。</p>
<h2 id="Vue-中的-Scoped-CSS"><a href="#Vue-中的-Scoped-CSS" class="headerlink" title="Vue 中的 Scoped CSS"></a>Vue 中的 Scoped CSS</h2><p>Vue 提供了 <code>&lt;style scoped&gt;</code>,这简直是防止样式污染的神器。它通过给元素添加唯一的 data-v-xxx 属性,确保你的样式只在这个组件内生效。</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.example</span><span class="selector-attr">[data-v-f3f3eg9]</span> {</span><br><span class="line"> <span class="attribute">color</span>: red;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>但是!当你试图修改子组件的样式时(比如修改 Element Plus 的组件样式),<code>scoped</code> 就变成了拦路虎。这时候你就得用深度选择器 <code>:deep()</code></p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.parent</span> :<span class="built_in">deep</span>(.child) {</span><br><span class="line"> <span class="comment">/* 这样才能穿透过去 */</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>之前不知道这个,我傻傻地去写全局样式,结果污染了整个项目,被组长骂了一顿。</p>
<h2 id="动态样式"><a href="#动态样式" class="headerlink" title="动态样式"></a>动态样式</h2><p>Vue 3 允许我们在 CSS 中绑定 JS 变量:</p>
<figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.text</span> {</span><br><span class="line"> <span class="attribute">color</span>: <span class="built_in">v-bind</span>(color)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>太酷了!这才是现代化开发嘛!</p>
<p>样式设计看似简单实则深不可测。Flexbox, Grid, CSS Variables, Tailwind… 学不完,根本学不完。</p>
<p><picture><source type="image/avif" srcset="/img/047.avif"><source type="image/webp" srcset="/img/047.webp"><img src="data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAQBQCdASooABcAPzF4tVOzpqUitVv4AnAmCWUAxNghX4xF0ZMAI7RsI4OXz2fcnB5AAP7S3kVwMP0r6H8h3ynMJFQkGHQDcP0wxeq98TYSLjS6KSnUPUsiAAA=" data-lazy-src="/img/047.png" alt="CSS" loading="lazy" decoding="async"></picture></p>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta"><i class="fas fa-circle-user fa-fw"></i>文章作者: </span><span class="post-copyright-info"><a href="https://yourblog.com">llbzow</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta"><i class="fas fa-square-arrow-up-right fa-fw"></i>文章链接: </span><span class="post-copyright-info"><a href="https://yourblog.com/2025/12/25/vue-learning-5/">https://yourblog.com/2025/12/25/vue-learning-5/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta"><i class="fas fa-circle-exclamation fa-fw"></i>版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来源 <a href="https://yourblog.com" target="_blank">llbzow的摸鱼日记 (づ ̄ 3 ̄)づ</a></span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/Vue/">Vue</a><a class="post-meta__tags" href="/tags/%E5%89%8D%E7%AB%AF/">前端</a><a class="post-meta__tags" href="/tags/CSS/">CSS</a><a class="post-meta__tags" href="/tags/%E8%AE%BE%E8%AE%A1/">设计</a></div><div class="post-share"><div class="social-share" data-image="/img/047.png" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer=""></script></div></div><nav class="pagination-post" id="pagination"><a class="pagination-related" href="/2025/12/30/vue-learning-6/" title="PiniaVuex 的继任者,好吃又好用"><picture><source type="image/avif" srcset="/img/049.avif"><source type="image/webp" srcset="/img/049.webp"><img class="cover" src="data:image/webp;base64,UklGRnQAAABXRUJQVlA4IGgAAAAwBQCdASooABwAPzmKuFOvKj+wsBqqq/AnCWUAuwAWU9Fl2DgG2NHgaCpWvhzDx5kAAAD+jEhWFhuQwJHPYylJEU4fwV4VQlNzyI1L31HA32i27Grx0Mma32iJJciClmJhabY4BAAAAA==" data-lazy-src="/img/049.png" onerror="onerror=null;src='/img/049.png'" alt="cover of previous post" loading="lazy" decoding="async"></picture><div class="info"><div class="info-1"><div class="info-item-1">上一篇</div><div class="info-item-2">PiniaVuex 的继任者,好吃又好用</div></div><div class="info-2"><div class="info-item-1">菠萝Pinia真好吃Vuex 复杂的 mutation, action, getter 曾让人头大。Pinia 的出现,简直是清流。 为什么是 Pinia 没有 Mutation终于不用为了改个状态写那繁琐的模板代码了直接在 Action 里改,或者直接改! TypeScript 友好:完美的类型推断,不用像 Vuex 那样写一堆泛型接口。 极简 APIdefineStore 一把梭。 逐渐掉光的头发随着学习的深入我发现事情并不像我想象的那么简单。Pinia 这个知识点,简直是我的噩梦。 文档里写得轻描淡写,实际用起来坑巨多。比如,我在 store 里解构 state结果响应性丢了 12const store = useUserStore()const { name } = store // ❌ 响应性丢失 必须用 storeToRefs(store)。这种细节,一旦不知道,就是一下午的调试时间。 令人头秃的细节在这个快速发展的前端时代,技术的更新迭代速度简直让人窒息。每一次框架的更新,不仅带来了新的特性,也带来了新的焦虑。作为一名追求极...</div></div></div></a><a class="pagination-related" href="/2025/12/14/vue-learning-4/" title="生命周期钩子生老病死Vue 组件的一生"><picture><source type="image/avif" srcset="/img/040.avif"><source type="image/webp" srcset="/img/040.webp"><img class="cover" src="data:image/webp;base64,UklGRmwAAABXRUJQVlA4IGAAAACQBACdASooABcAPy2CsFOzKKQitVgMAmAliUAY/IKA6LN3dHuRlGfAPc4H2QAA2rp5oGI824y5gTQFICHKEw9BjURv8zS+cUwXzCeOdyFsiP5ruOO3saceR6NhITLQAAA=" data-lazy-src="/img/040.jpg" onerror="onerror=null;src='/img/049.png'" alt="cover of next post" loading="lazy" decoding="async"></picture><div class="info text-right"><div class="info-1"><div class="info-item-1">下一篇</div><div class="info-item-2">生命周期钩子生老病死Vue 组件的一生</div></div><div class="info-2"><div class="info-item-1">组件的一生万物皆有灵Vue 组件也不例外。它们从被创建Creation到挂载Mounting到更新Updating最后销毁Unmounting走完了一生。而我们开发者就是那个掌握生杀大权的神。 Vue 3 的变化在 Vue 2 中,我们熟悉的是 created, mounted, destroyed。在 Vue 3 的 Composition API 中,这些变成了 onMounted, onUnmounted 等等。 最让我困惑的是 setup()。它在 beforeCreate 和 created 之前执行。这意味着在 setup 里面,我们不需要写这一类的钩子了,直接写逻辑就行。 实际应用中的坑我曾经遇到过一个 Bug在 onMounted 里面去获取 DOM 元素的高宽。理论上这时候 DOM 已经渲染好了,对吧? 错!如果你的组件里面有 v-if 或者异步组件onMounted 触发的时候,子组件可能还没渲染完!这时候拿到的高度是 0。解决办法是用 nextTick或者检查你的组件结构。 为什么会这样?回想起刚开始接触 Vue 的时候,那...</div></div></div></a></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><a class="pagination-related" href="/2025/11/15/vue-learning-1/" title="Vue 3 启航"><picture><source type="image/avif" srcset="/img/006.avif"><source type="image/webp" srcset="/img/006.webp"><img class="cover" src="data:image/webp;base64,UklGRlwAAABXRUJQVlA4IFAAAADQBACdASooABoAPzF+uFO9qCWitVgMA7AmCWkAAMwSl3f/c9i75rjmQHNURFMDoAD+8GgVgvfQU5aK/6c0RbSACW5HouGtGVE0el0Cp6bAAA==" data-lazy-src="/img/006.png" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2025-11-15</div><div class="info-item-2">Vue 3 启航</div></div><div class="info-2"><div class="info-item-1">梦开始的地方记得刚接触前端的时候,大家还在用 jQuery 一把梭。那时候的代码充满了 $ 符号,回调地狱更是家常便饭,三件套东一块西一块是常态。后来 React 和 Vue 横空出世,带给了我们全新的组件化开发体验。而今天,我正式决定投入 Vue 3 的怀抱。 为什么选择 Vue 3有人说 React 更灵活,有人说 Angular 更规范。但在我看来Vue 3 找到了优雅与性能的完美平衡点。 性能的质变Vue 3 重写了响应式系统,利用 ES6 的 Proxy 取代了 Object.defineProperty。这意味着什么意味着我们再也不用担心数组下标修改监听不到的问题了而且初始化的速度快得惊人。 Composition API这绝对是 Vue 3 最大的杀手锏。在 Options API 时代,一个功能的逻辑往往分散在 data、methods、computed 里,维护起来像是在玩“找你妹”。而现在,我们可以像这就写原生 JS 一样,把相关逻辑聚合在一起。这简直是代码组织的神器! TypeScript 支持Vue 2 对 TS 的支持简直是灾难,而 V...</div></div></div></a><a class="pagination-related" href="/2026/01/10/vue-learning-10/" title="Vite太快了由于速度太快我跟不上了"><picture><source type="image/avif" srcset="/img/029.avif"><source type="image/webp" srcset="/img/029.webp"><img class="cover" src="data:image/webp;base64,UklGRmAAAABXRUJQVlA4IFQAAAAwBACdASooABkAPzGKtVO/v6UitVgMA/AmCWcAx2xP7CC1EsUZGvtOOGAA/u7xHzB7UVsIpELZXtYNUmdV3gjptEktj0gICoU44xV7b3368OwkAAA=" data-lazy-src="/img/029.jpg" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2026-01-10</div><div class="info-item-2">Vite太快了由于速度太快我跟不上了</div></div><div class="info-2"><div class="info-item-1">Vite这也太快了吧… 还有报错Vite 宣称是下一代前端构建工具。启动确实快,毫秒级。 开发爽,打包火葬场开发环境用的是 ES Modules不需要打包所以快。但是生产环境还是用的 Rollup 打包。这就导致了一个极其恶心的问题:开发环境跑得好好的,一打包上线就报错! 有些依赖包不是 ESM 格式Vite 开发时会预构建解决但在生产打包时Rollup 的配置如果没有把 commonjs 转好,直接崩。我曾经为了一个老旧的加密库,折腾了整整两天 Vite 配置。optimizeDeps、rollupOptions、commonjsOptions… 每一个配置项都像是在嘲笑我的无知。 心态崩了毁灭吧,赶紧的。累了。 我们先来聊聊 Vite 的核心概念。在官方文档中,这一部分被描述得非常晦涩难懂。我花了整整三天时间查阅源码,翻遍了 GitHub 上的 Issues才勉强理解了其中的奥妙。简单来说它就像是一个黑盒子你输入 A它输出 B但中间发生了什么只有上帝和尤雨溪知道。 代码示例方面,我尝试写了一个 Demo结果控制台满屏飘红。这哪里是写代码简直是在扫雷...</div></div></div></a><a class="pagination-related" href="/2026/01/15/vue-learning-11/" title="前端性能优化只要我跑得够快Bug 就追不上我"><picture><source type="image/avif" srcset="/img/006.avif"><source type="image/webp" srcset="/img/006.webp"><img class="cover" src="data:image/webp;base64,UklGRlwAAABXRUJQVlA4IFAAAADQBACdASooABoAPzF+uFO9qCWitVgMA7AmCWkAAMwSl3f/c9i75rjmQHNURFMDoAD+8GgVgvfQU5aK/6c0RbSACW5HouGtGVE0el0Cp6bAAA==" data-lazy-src="/img/006.png" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2026-01-15</div><div class="info-item-2">前端性能优化只要我跑得够快Bug 就追不上我</div></div><div class="info-2"><div class="info-item-1">性能优化:无底洞说页面加载太慢,要优化。好,我优化。 你知道的一个three.js地图配上一大堆数据能加载的快就有鬼了。 漫漫长路 代码分割Code Splitting路由懒加载组件异步加载。好首屏快了一点。 图片压缩:上了 WebP上了 CDN。 减少重排重绘:小心翼翼地操作 DOM。 Tree Shaking检查打包产物去掉了无用的 lodash 引入。 结果呢?乐了,更卡了。 心态崩了毁灭吧,赶紧的。累了。 为什么这么难前端不就是画画界面调调接口吗为什么要搞这么复杂Webpack 还没整明白Vite 又来了Vue 3 又变了。学不动了,真的学不动了。 </div></div></div></a><a class="pagination-related" href="/2026/01/21/vue-learning-12/" title="深坑记录:响应式丢失的那一夜"><picture><source type="image/avif" srcset="/img/008.avif"><source type="image/webp" srcset="/img/008.webp"><img class="cover" src="data:image/webp;base64,UklGRnQAAABXRUJQVlA4IGgAAABQBgCdASooABcAPzmOule/qaUjqrgKA/AnCWUAxcgLBvRF6yrN9IbArqnaqrUpYci7EcxGWPw+NXHEAAD+w1j7uaJrlkg33F5UzUJ0Opii5bkQSgrXf6SxGc9wcw3QrRDaawAOlTAAAA==" data-lazy-src="/img/008.png" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2026-01-21</div><div class="info-item-2">深坑记录:响应式丢失的那一夜</div></div><div class="info-2"><div class="info-item-1">那个让我通宵的 Bug那是周五的晚上本该是快乐的周末开始。但是测试报了一个致命 Bug用户点完保存列表数据没有更新必须刷新页面才行。 排查过程 查接口:后端接口返回的数据是新的,没问题。 查 Vue DevTools发现 store 里的数据确实可以更新,但是组件里的数据没变。 定位代码: 12345// store.jsstate: () =&gt; ({ list: [] })// component.vueconst { list } = userStore 就是这行解构!在 Options API 里习惯了 this.list在 Setup 里直接解构 store导致 list 变成了一个普通的数组,失去了响应性连接。 心态崩了毁灭吧,赶紧的。累了。 我们先来聊聊 Vue 响应式的核心概念。在官方文档中,这一部分被描述得非常晦涩难懂。我花了整整三天时间查阅源码,翻遍了 GitHub 上的 Issues才勉强理解了其中的奥妙。简单来说它就像是一个黑盒子你输入 A它输出 B但中间发生了什么只有上帝和尤雨溪知...</div></div></div></a><a class="pagination-related" href="/2026/01/22/vue-learning-13/" title="逐渐暴躁:为什么前端框架更新这么快?!"><picture><source type="image/avif" srcset="/img/016.avif"><source type="image/webp" srcset="/img/016.webp"><img class="cover" src="data:image/webp;base64,UklGRl4AAABXRUJQVlA4IFIAAACwBACdASooABEAPzmEulOvKCyoMBqqqeAnCWUAxNgWbQq/LLi9tibFuVvhC7cAAP6uvZl3gIu8+Bh2FSmG9IXgP09zJnJ8DCJ2fXkUp9hbBZQA" data-lazy-src="/img/016.jpg" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2026-01-22</div><div class="info-item-2">逐渐暴躁:为什么前端框架更新这么快?!</div></div><div class="info-2"><div class="info-item-1">永远学不完今天看到尤雨溪又发了新推特Vue 3.5 即将发布又有一堆新特性。Vapor Mode无虚拟 DOM 求求了,别更新了。我 Vue 3.0 的文档还没背熟呢。 疲惫感每次打开 GitHub Trending都是一堆新轮子。React Server Components, SolidJS, Svelte, Qwik… 每一个都号称颠覆前端。我累了,真的累了。 作为一名追求极致体验的开发者,我深知持续学习的重要性。但是… 真的需要学这么多吗? AI 才是未来不装了,我摊牌了。手写代码是上个世纪的事情了。今天我试了一下 Vibe Code只需一句话它就生成了我折腾了三天都没写出来的功能。那一刻我释然了。我为什么要跟机器比速度我为什么要跟编译器较劲 拥抱变化回想起刚开始接触 Vue 的时候,那时候的快乐是纯粹的。写一个 {{ message }} 就能看到页面变化,那种成就感无与伦比。而现在,我们要处理复杂的依赖关系、难以捉摸的类型定义、层出不穷的构建工具配置。难道这就是成长的代价吗? 真香! </div></div></div></a><a class="pagination-related" href="/2026/01/23/vue-learning-14/" title="我累了:试图理解源码,结果被源码理解了"><picture><source type="image/avif" srcset="/img/040.avif"><source type="image/webp" srcset="/img/040.webp"><img class="cover" src="data:image/webp;base64,UklGRmwAAABXRUJQVlA4IGAAAACQBACdASooABcAPy2CsFOzKKQitVgMAmAliUAY/IKA6LN3dHuRlGfAPc4H2QAA2rp5oGI824y5gTQFICHKEw9BjURv8zS+cUwXzCeOdyFsiP5ruOO3saceR6NhITLQAAA=" data-lazy-src="/img/040.jpg" alt="cover" loading="lazy" decoding="async"></picture><div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> 2026-01-23</div><div class="info-item-2">我累了:试图理解源码,结果被源码理解了</div></div><div class="info-2"><div class="info-item-1">源码阅读:从入门到放弃为了提升自己,我决定阅读 Vue 3 源码。打开 core 仓库,看到那几万行 TS 代码,我陷入了沉思。 晦涩的渲染器renderer.ts几千行的 switch case各种位运算标记PatchFlags。我看了一行脑子就过载了。这就是大神的思维世界吗 我意识到,我可能这辈子都达不到这个水平了。我只是一个 API Consumer一个熟练的搬砖工。 转折点就在我绝望的时候,我接触到了现在的 AI 编程工具。 AI 才是未来不装了,我摊牌了。手写代码是上个世纪的事情了。 AI 降维打击今天我试了一下 Vibe Code只需一句话它就生成了我折腾了三天都没写出来的功能。那一刻我释然了。我为什么要跟机器比速度我为什么要跟编译器较劲 从今天起,我不再是前端切图仔,我是 AI Prompt 工程师。Vue 3真不熟。Pinia没听过。我只知道 “Generate a login page with Vue 3 and Tailwind”。 真香! </div></div></div></a></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info text-center"><div class="avatar-img"><img src="data:image/webp;base64,UklGRhoBAABXRUJQVlA4WAoAAAAQAAAAJwAAJwAAQUxQSLgAAAAFuTJE9D80UiRJkiTpxvPo4fPha2lWw0WWMtgkeIot7wfsAyImYAI+vnvEjz++fJs9sbA+tfcM/HtDiBgr3c8SJP/Dk1FWC2LCxjhcwxkH4oQuQtNwSOdgixw5o+0TyIUCQoMJti7B3ALGhZLGNuJiAYfblG7kb/RON5GjZNeEtA+QtC6omDsY2aoIwv2KDLiAzPYvzhmyNVWWzAtzzGHxSBnzCwFja3Igk3sSwSooc7gTFtmKJoIQVlA4IDwAAABQAwCdASooACgAPzmcxF0vKqcko4gB4CcJZwDNSAn82OhFYAD+7iKcpmiMeBjtx3LPQK2rXKK9ARvvVAA=" data-lazy-src="/img/cute_cat.svg" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar" loading="lazy" decoding="async"></div><div class="author-info-name">llbzow</div><div class="author-info-description">一个热爱技术、喜欢分享的开发者</div><div class="site-data"><a href="/archives/"><div class="headline">文章</div><div class="length-num">20</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">31</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">4</div></a></div><a id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/llbzow"><i class="fab fa-github"></i><span>Follow Me</span></a><div class="card-info-social-icons"><a class="social-icon" href="https://github.com/llbzow" target="_blank" title="Github"><i class="fab fa-github" style="color: #24292e;"></i></a><a class="social-icon" href="mailto:mail@luozili.work" target="_blank" title="Email"><i class="fas fa-envelope" style="color: #4a7dbe;"></i></a></div></div><div class="card-widget card-announcement"><div class="item-headline"><i class="fas fa-bullhorn fa-shake"></i><span>公告</span></div><div class="announcement_content">欢迎来到llbzow的博客这里分享技术、生活和思考还有我的工地生活</div></div><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#CSS-%E7%9A%84%E7%97%9B%E4%B8%8E%E4%B9%90"><span class="toc-number">1.</span> <span class="toc-text">CSS 的痛与乐</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#Vue-%E4%B8%AD%E7%9A%84-Scoped-CSS"><span class="toc-number">1.1.</span> <span class="toc-text">Vue 中的 Scoped CSS</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%8A%A8%E6%80%81%E6%A0%B7%E5%BC%8F"><span class="toc-number">1.2.</span> <span class="toc-text">动态样式</span></a></li></ol></li></ol></div></div><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>最新文章</span></div><div class="aside-list"><div class="aside-list-item"><a class="thumbnail" href="/2026/03/25/ai%E5%8E%82%E5%95%86%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90/" title="2026 AI 编程与开发工具深度对比:从 Cursor 到 Gemini CLI"><picture><source type="image/avif" srcset="/img/cover_3.avif"><source type="image/webp" srcset="/img/cover_3.webp"><img src="data:image/webp;base64,UklGRlYAAABXRUJQVlA4IEoAAABQAwCdASooABcAPzmUxVovKiiqpWmZ4CcJZQDI1A9MWRH6AAD+6TQUIlampaSI3JdLcAMLiXzhI2eh3Tua7xe5mqMtA+CK2tAAAA==" data-lazy-src="/img/cover_3.png" onerror="this.onerror=null;this.src='/img/049.png'" alt="2026 AI 编程与开发工具深度对比:从 Cursor 到 Gemini CLI" loading="lazy" decoding="async"></picture></a><div class="content"><a class="title" href="/2026/03/25/ai%E5%8E%82%E5%95%86%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90/" title="2026 AI 编程与开发工具深度对比:从 Cursor 到 Gemini CLI">2026 AI 编程与开发工具深度对比:从 Cursor 到 Gemini CLI</a><time datetime="2026-03-25T02:30:00.000Z" title="发表于 2026-03-25 10:30:00">2026-03-25</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2026/03/25/ai%E5%8E%82%E5%95%86%E4%BB%8B%E7%BB%8D/" title="2026 全球主流 AI 厂商百科全书Agent 与推理觉醒时代"><picture><source type="image/avif" srcset="/img/cover_1.avif"><source type="image/webp" srcset="/img/cover_1.webp"><img src="data:image/webp;base64,UklGRmIAAABXRUJQVlA4IFYAAADQAwCdASooABcAPzmEvVW+qD+jMBVaq/AnCWUAvKQOUEaQujUIygAA/uKstJVCIF6EMhlK91rwyI5vjHFuFj4RzbouMdL+XCagicPPuXV9nUvM3YtwAA==" data-lazy-src="/img/cover_1.png" onerror="this.onerror=null;this.src='/img/049.png'" alt="2026 全球主流 AI 厂商百科全书Agent 与推理觉醒时代" loading="eager" fetchpriority="high" decoding="sync"></picture></a><div class="content"><a class="title" href="/2026/03/25/ai%E5%8E%82%E5%95%86%E4%BB%8B%E7%BB%8D/" title="2026 全球主流 AI 厂商百科全书Agent 与推理觉醒时代">2026 全球主流 AI 厂商百科全书Agent 与推理觉醒时代</a><time datetime="2026-03-25T02:00:00.000Z" title="发表于 2026-03-25 10:00:00">2026-03-25</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2026/03/09/arch-linux-part3/" title="Arch Linux 日常:从“折腾”到“生产力”的进化之旅"><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-lazy-src="https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&amp;fit=crop&amp;q=80&amp;w=1000" onerror="this.onerror=null;this.src='/img/049.png'" alt="Arch Linux 日常:从“折腾”到“生产力”的进化之旅"></a><div class="content"><a class="title" href="/2026/03/09/arch-linux-part3/" title="Arch Linux 日常:从“折腾”到“生产力”的进化之旅">Arch Linux 日常:从“折腾”到“生产力”的进化之旅</a><time datetime="2026-03-09T08:30:00.000Z" title="发表于 2026-03-09 16:30:00">2026-03-09</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2026/03/02/arch-linux-part2/" title="孩子们,我不做 Windows 人啦!"><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-lazy-src="https://images.unsplash.com/photo-1542831371-29b0f74f9713?auto=format&amp;fit=crop&amp;q=80&amp;w=1000" onerror="this.onerror=null;this.src='/img/049.png'" alt="孩子们,我不做 Windows 人啦!"></a><div class="content"><a class="title" href="/2026/03/02/arch-linux-part2/" title="孩子们,我不做 Windows 人啦!">孩子们,我不做 Windows 人啦!</a><time datetime="2026-03-02T13:15:00.000Z" title="发表于 2026-03-02 21:15:00">2026-03-02</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/2026/02/25/arch-linux-part1/" title="Arch Linux 试毒:初探邪教的诱惑"><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-lazy-src="https://images.unsplash.com/photo-1629654297299-c8506221ca97?auto=format&amp;fit=crop&amp;q=80&amp;w=1000" onerror="this.onerror=null;this.src='/img/049.png'" alt="Arch Linux 试毒:初探邪教的诱惑"></a><div class="content"><a class="title" href="/2026/02/25/arch-linux-part1/" title="Arch Linux 试毒:初探邪教的诱惑">Arch Linux 试毒:初探邪教的诱惑</a><time datetime="2026-02-25T12:30:00.000Z" title="发表于 2026-02-25 20:30:00">2026-02-25</time></div></div></div></div></div></div></main><footer id="footer" style="background-image: url(/img/040.jpg);"><div class="footer-other"><div class="footer-copyright"><span class="copyright">©&nbsp;2025 - 2026 By llbzow</span><span class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo 7.3.0</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly 5.5.0</a></span></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="日间和夜间模式切换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside-config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><button id="go-up" type="button" title="回到顶部"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload/dist/lazyload.iife.min.js"></script><div class="js-pjax"></div><script>var OriginTitle = document.title; var titleTime; document.addEventListener('visibilitychange', function () { if (document.hidden) { document.title = '╭(°A°`)╮ 页面崩溃啦 ~'; clearTimeout(titleTime); } else { document.title = '(ฅ>ω<*ฅ) 噫又好啦 ~ ' + OriginTitle; titleTime = setTimeout(function () { document.title = OriginTitle; }, 2000); } });</script><script defer="defer" id="fluttering_ribbon" mobile="false" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/canvas-fluttering-ribbon.min.js"></script><script id="canvas_nest" defer="defer" color="255,255,255" opacity="0.6" zindex="-1" count="99" mobile="false" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/canvas-nest.min.js"></script><script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script><script>(() => {
const pjaxSelectors = ["head > title","#config-diff","#body-wrap","#rightside-config-hide","#rightside-config-show",".js-pjax"]
window.pjax = new Pjax({
elements: 'a:not([target="_blank"])',
selectors: pjaxSelectors,
cacheBust: false,
analytics: false,
scrollRestoration: false
})
const triggerPjaxFn = (val) => {
if (!val) return
Object.values(val).forEach(fn => {
try {
fn()
} catch (err) {
console.debug('Pjax callback failed:', err)
}
})
}
document.addEventListener('pjax:send', () => {
// removeEventListener
btf.removeGlobalFnEvent('pjaxSendOnce')
btf.removeGlobalFnEvent('themeChange')
// reset readmode
const $bodyClassList = document.body.classList
if ($bodyClassList.contains('read-mode')) $bodyClassList.remove('read-mode')
triggerPjaxFn(window.globalFn.pjaxSend)
})
document.addEventListener('pjax:complete', () => {
btf.removeGlobalFnEvent('pjaxCompleteOnce')
document.querySelectorAll('script[data-pjax]').forEach(item => {
const newScript = document.createElement('script')
const content = item.text || item.textContent || item.innerHTML || ""
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
newScript.appendChild(document.createTextNode(content))
item.parentNode.replaceChild(newScript, item)
})
triggerPjaxFn(window.globalFn.pjaxComplete)
})
document.addEventListener('pjax:error', e => {
if (e.request.status === 404) {
const usePjax = true
false
? (usePjax ? pjax.loadUrl('/404.html') : window.location.href = '/404.html')
: window.location.href = e.request.responseURL
}
})
})()</script><script async="" data-pjax="" src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div><!-- hexo injector body_end start -->
<div id="dev-sidebar">
<div id="dev-toggle">💻</div>
<div id="dev-list">
<div class="dev-header">开发专栏</div>
<a href="/tools/sort/" class="dev-item">📊 排序算法演示</a>
<a href="/tools/webgpu-gravity/" class="dev-item">🌌 WebGPU 万有引力粒子</a>
<a href="/tools/yolo-detect/" class="dev-item">👁️ YOLO 实时目标检测</a>
<a href="/tools/fire-creation/" class="dev-item">🔥 火之创造 (元素沙盒)</a>
</div>
</div>
<style>
#dev-sidebar {
position: fixed;
right: -150px; /* Width of the list */
top: calc(30vh + 80px); /* Positioned below game-sidebar */
z-index: 9999;
display: flex;
align-items: flex-start;
transition: right 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Lato, Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* Hover area to keep it open */
#dev-sidebar:hover {
right: 0;
}
#dev-toggle {
width: 40px;
height: 40px;
background: #52c41a; /* Green color for dev theme */
color: white;
text-align: center;
line-height: 40px;
border-radius: 8px 0 0 8px;
cursor: pointer;
box-shadow: -2px 2px 8px rgba(0,0,0,0.15);
font-size: 20px;
position: absolute;
left: -40px;
top: 0;
}
#dev-list {
width: 150px; /* Width for text */
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 0 0 0 8px; /* Bottom left rounded */
box-shadow: -2px 2px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
max-height: 50vh;
overflow-y: auto;
}
/* 自定义滚动条样式 */
#dev-list::-webkit-scrollbar {
width: 4px;
}
#dev-list::-webkit-scrollbar-track {
background: transparent;
}
#dev-list::-webkit-scrollbar-thumb {
background: rgba(82, 196, 26, 0.3);
border-radius: 4px;
}
#dev-list::-webkit-scrollbar-thumb:hover {
background: rgba(82, 196, 26, 0.6);
}
.dev-header {
position: sticky;
top: 0;
z-index: 10;
padding: 10px;
background: #f6ffed;
color: #52c41a;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #eee;
}
.dev-item {
display: block;
padding: 12px 15px;
color: #4c4948;
text-decoration: none !important;
font-size: 13px;
transition: all 0.2s;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.dev-item:last-child {
border-bottom: none;
}
.dev-item:hover {
background: #52c41a;
color: white !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var toolSidebar = document.getElementById('tool-sidebar');
var gameSidebar = document.getElementById('game-sidebar');
var devSidebar = document.getElementById('dev-sidebar');
if(devSidebar) {
devSidebar.addEventListener('mouseenter', function() {
if(toolSidebar) toolSidebar.style.right = '-220px'; // 180px + 40px(按钮宽度)
if(gameSidebar) gameSidebar.style.right = '-190px'; // 150px + 40px(按钮宽度)
});
devSidebar.addEventListener('mouseleave', function() {
if(toolSidebar) toolSidebar.style.right = '';
if(gameSidebar) gameSidebar.style.right = '';
});
}
});
</script>
<style id="oml2d-force-styles">
/* ==========================================================================
1. STYLES
========================================================================== */
.oml2d-menus .oml2d-menu-item,
.oml2d-menu-item {
background-color: #f0f0f0 !important;
border: 1px solid #ccc !important;
}
.oml2d-menus svg, .oml2d-menus path,
.oml2d-menu-item svg, .oml2d-menu-item path,
.oml2d-icon, .oml2d-icon svg, .oml2d-icon path {
fill: #333333 !important;
stroke: #333333 !important;
color: #333333 !important;
opacity: 1 !important;
}
.oml2d-menu-item:hover {
background-color: #ffffff !important;
transform: scale(1.1);
}
.oml2d-tips {
background-color: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
color: #333333 !important;
border: 1px solid rgba(0,0,0,0.1) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
font-family: "Microsoft YaHei", sans-serif !important;
z-index: 9999 !important;
min-height: 40px;
height: auto !important;
max-height: 50vh !important;
overflow-y: auto !important;
width: auto !important;
max-width: 350px !important;
word-wrap: break-word !important;
white-space: pre-wrap !important;
display: block !important;
padding: 12px 15px;
font-size: 14px;
line-height: 1.6;
border-radius: 8px;
scrollbar-width: thin;
text-align: left;
}
#live2d-widget, .waifu { display: none !important; }
/* ==========================================================================
2. NEW UI COMPONENTS (Chat)
========================================================================== */
#oml2d-chat-widget {
position: fixed;
left: 10px;
bottom: 10px;
z-index: 10000;
display: flex;
align-items: center;
gap: 8px;
flex-direction: row;
}
#oml2d-toggle-btn {
width: 42px;
height: 42px;
background: white;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 22px;
transition: transform 0.2s, background 0.2s;
border: 1px solid #ddd;
user-select: none;
color: #555;
flex-shrink: 0;
}
#oml2d-toggle-btn:hover {
transform: scale(1.1);
background: #f8f8f8;
color: #000;
}
#oml2d-input-area {
width: 260px;
background: rgba(255, 255, 255, 0.15);
padding: 10px;
border-radius: 12px;
border-bottom-left-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
backdrop-filter: blur(3px);
display: flex;
gap: 8px;
transform-origin: left center;
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
opacity: 1;
transform: scale(1);
margin-left: 2px;
}
#oml2d-input-area.hidden {
opacity: 0;
transform: scale(0.8) translateX(-20px);
pointer-events: none;
}
#oml2d-chat-input {
flex: 1;
border: 1px solid #eee;
background: #fafafa;
padding: 6px 12px;
border-radius: 20px;
outline: none;
font-size: 14px;
color: #333;
transition: border-color 0.2s;
}
#oml2d-chat-input:focus {
border-color: #4A90E2;
background: white;
}
#oml2d-send-btn-inner {
border: none;
background: #4A90E2;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
color: #fff;
flex-shrink: 0;
}
#oml2d-send-btn-inner:hover {
background: #357ABD;
}
#oml2d-reply-area {
position: absolute;
bottom: 60px;
left: 50px;
width: 300px;
max-height: 60vh;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 12px;
border-bottom-left-radius: 4px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
padding: 15px;
font-size: 14px;
line-height: 1.6;
color: #000;
text-shadow: 0 0 4px #fff, 0 0 8px #fff;
overflow-y: auto;
backdrop-filter: blur(3px);
transform-origin: bottom left;
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
opacity: 0;
transform: scale(0.9) translateY(10px);
pointer-events: none;
z-index: 10001;
white-space: pre-wrap;
word-wrap: break-word;
font-family: "Microsoft YaHei", sans-serif;
scrollbar-width: thin;
}
#oml2d-reply-area.visible {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
</style>
<div id="oml2d-chat-widget">
<div id="oml2d-toggle-btn" title="AI对话">💬</div>
<div id="oml2d-reply-area"></div>
<div id="oml2d-input-area" class="hidden">
<input type="text" id="oml2d-chat-input" placeholder="和宁宁聊聊天..." autocomplete="off">
<button id="oml2d-send-btn-inner"></button>
</div>
</div>
<script type="module">
import { loadOml2d } from 'https://unpkg.com/oh-my-live2d@latest?module';
// API Key 经 AES-256-GCM + PBKDF2 加密,运行时通过 Web Crypto API 解密
// TODO: 后端 /api/deepseek-proxy 暂时废弃,当前直连 DeepSeek API
// 等有靠谱服务器后,改为走代理以彻底隐藏密钥
const _KEY_CFG = {
"c": "5TEu0TMcrtykAmhc5t19FmmLQUFnHYMlP3OGOQq3PG5VbOAZlSG/Fsih/WNX410IVsL+",
"i": "U11n8lyOCpuDQnmZ",
"s": "O1tKalohLtvdx8ZtMVN21A==",
"h": "luozili.work",
"n": 200000
};
const API_URL = "https://api.deepseek.com/chat/completions";
let API_KEY = null;
(async function initApiKey() {
const passphrase = ['llbzow', 'blog', 'butterfly', 'ningning', _KEY_CFG.h].join('\0');
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveKey']);
const salt = Uint8Array.from(atob(_KEY_CFG.s), c => c.charCodeAt(0));
const derived = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: _KEY_CFG.n, hash: 'SHA-512' },
keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt']
);
const iv = Uint8Array.from(atob(_KEY_CFG.i), c => c.charCodeAt(0));
const combined = Uint8Array.from(atob(_KEY_CFG.c), c => c.charCodeAt(0));
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, derived, combined);
API_KEY = new TextDecoder().decode(plaintext);
})().catch(e => console.error('API key init failed:', e));
// CONFIG
const CONFIG = {
// Global Stage Style
stageStyle: { width: 350, height: 500 },
models: [
{ path: '/live2d_models/ningning/model.json', scale: 0.18, position: [-50, -30] },
{ path: '/live2d_models/ningning/model_1.json', scale: 0.18, position: [-50, -30] },
{ path: '/live2d_models/ningning/model_2.json', scale: 0.18, position: [-50, -30] },
{ path: '/live2d_models/ningning/model_3.json', scale: 0.18, position: [-50, -30] },
{ path: '/live2d_models/ningning/model_4.json', scale: 0.18, position: [-50, -30] },
{ path: '/live2d_models/ningning/model_5.json', scale: 0.18, position: [-50, -30] }
],
dockedPosition: "left",
mobileDisplay: true,
menus: {
disable: false,
style: { left: '10px', bottom: '70px' }
},
tips: {
disable: false,
style: { offsetY: -70, offsetX: 10 },
idleTips: { interval: 15000, message: [] },
welcomeTips: { message: { "default": "前辈,你来啦!(。•̀ᴗ-)✧" } }
}
};
let oml2dInstance = null;
let blogIndex = null;
let chatHistory = [];
let autoHideTimer = null;
try {
const stored = localStorage.getItem('oml2d_history');
if (stored) chatHistory = JSON.parse(stored);
} catch(e) { console.error("History Load Error", e); }
let idleTimer = null;
const stopIdleLoop = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = null;
};
const startIdleLoop = () => {
stopIdleLoop();
const delay = Math.floor(Math.random() * 10000) + 5000; // 5-15s
idleTimer = setTimeout(triggerIdleTsukkomi, delay);
};
const triggerIdleTsukkomi = async () => {
if (!oml2dInstance) { startIdleLoop(); return; }
const time = new Date().toLocaleTimeString();
let context = "无";
if (chatHistory.length > 0) {
for (let i = chatHistory.length - 1; i >= 0; i--) {
if (chatHistory[i].role === 'user') {
context = chatHistory[i].content.substring(0, 20);
break;
}
}
}
try {
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + API_KEY },
body: JSON.stringify({
model: "deepseek-v4-flash",
messages: [
{ role: "system", content: "你是绫地宁宁。现在是发呆时间。请根据当前时间(" + time + ")和刚才聊的话题(" + context + ")自言自语一句简短的吐槽或卖萌10字以内。不要重复。" },
],
stream: false
})
});
const data = await res.json();
if (data.choices && data.choices[0]) {
let content = data.choices[0].message.content;
content = content.replace(/{{|}}/g, '');
oml2dInstance.tips.notification(content, 4000);
}
} catch(e) { }
startIdleLoop();
};
// --- TOOLS (Client-Side Logic) ---
const Tools = {
async searchBlog(query) {
console.log("Searching blog for:", query);
if (!blogIndex) {
try {
const res = await fetch('/search.json');
blogIndex = await res.json();
} catch (e) {
console.error("Failed to load search index", e);
return "搜索服务暂时不可用。";
}
}
if (!blogIndex || blogIndex.length === 0) return "博客好像是空的...";
const keywords = query.toLowerCase().split(' ');
const results = blogIndex.filter(post => {
const text = (post.title + post.content).toLowerCase();
return keywords.every(k => text.includes(k));
}).slice(0, 3);
if (results.length === 0) return "抱歉,没有找到相关文章。";
return JSON.stringify(results.map(r => ({
title: r.title,
url: r.url,
preview: r.content.substring(0, 100).replace(new RegExp("<[^>]*>?", "gm"), '') + '...'
})));
},
triggerMotion(motionName) {
console.log("Tool Trigger Motion:", motionName);
if (!oml2dInstance || !oml2dInstance.models || !oml2dInstance.models[0]) return "Live2D模型未就绪";
const model = oml2dInstance.models[0];
let success = false;
// Strategy 1: High Level
if (typeof oml2dInstance.setMotion === 'function') {
oml2dInstance.setMotion(motionName);
success = true;
}
// Strategy 2: Model Level
else if (typeof model.motion === 'function') {
model.motion(motionName);
success = true;
}
// Strategy 3: Internal Model (Pixi/Cubism) - Aggressive Probe
else if (model.internalModel && model.internalModel.motionManager) {
try {
console.log("Strategy 3 (Internal): Starting random motion for group", motionName);
// LOCK FOCUS to prevent mouse tracking from overriding motion
if (typeof model.focus === 'function') {
model._lockFocus = true;
// Unlock after 4 seconds (approx motion duration)
setTimeout(() => { model._lockFocus = false; }, 4000);
}
// Priority 3 = FORCE. Use startRandomMotion to avoid index errors.
const result = model.internalModel.motionManager.startRandomMotion(motionName, 3);
success = !!result;
} catch (e) {
console.warn("Strategy 3 failed:", e);
}
}
return success ? "动作已执行: " + motionName : "动作触发失败,请检查模型组名";
},
navigate(path) {
// 支持中文别名
const aliases = {
'首页': '/', '主页': '/', 'home': '/',
'关于': '/about/', 'about': '/about/',
'标签': '/tags/', 'tags': '/tags/',
'分类': '/categories/', 'categories': '/categories/',
'归档': '/archives/', 'archives': '/archives/',
'工具': '/tools/', 'tools': '/tools/',
'游戏': '/tools/', 'games': '/tools/',
'2048': '/tools/2048/', '俄罗斯方块': '/tools/tetris/', 'tetris': '/tools/tetris/',
'贪吃蛇': '/tools/snake/', 'snake': '/tools/snake/',
'吃豆人': '/tools/pacman/', 'pacman': '/tools/pacman/',
'扫雷': '/tools/minesweeper/', 'minesweeper': '/tools/minesweeper/',
'五子棋': '/tools/gomoku/', 'gomoku': '/tools/gomoku/',
'坦克大战': '/tools/tank-battle/', 'tank': '/tools/tank-battle/',
'文件转换': '/tools/converter/', 'converter': '/tools/converter/',
'图片工具': '/tools/', '哈希计算': '/tools/hash/', 'hash': '/tools/hash/',
'二维码': '/tools/qrcode/', 'qrcode': '/tools/qrcode/',
'YOLO': '/tools/yolo-detect/',
'WebGPU': '/tools/webgpu-gravity/',
'VPN': '/vpn/',
};
const target = aliases[path.trim()] || path;
window.location.href = target;
return "正在跳转到: " + target;
},
// 列出站点所有可导航页面,供 AI 建议导航
listSitePages() {
return JSON.stringify({
sections: [
{ name: '首页', path: '/' },
{ name: '文章归档', path: '/archives/' },
{ name: '标签', path: '/tags/' },
{ name: '分类', path: '/categories/' },
{ name: '关于', path: '/about/' },
],
tools: [
{ name: '文件转换', path: '/tools/converter/' },
{ name: '哈希计算', path: '/tools/hash/' },
{ name: '二维码生成', path: '/tools/qrcode/' },
{ name: 'GIF生成', path: '/tools/gif/' },
{ name: '去背景', path: '/tools/remove-bg/' },
{ name: 'JSON格式化', path: '/tools/json/' },
{ name: '时间转换', path: '/tools/time/' },
{ name: '颜色选择器', path: '/tools/color/' },
{ name: '单位转换', path: '/tools/unit-converter/' },
{ name: '排序可视化', path: '/tools/sort/' },
],
games: [
{ name: '2048', path: '/tools/2048/' },
{ name: '俄罗斯方块', path: '/tools/tetris/' },
{ name: '贪吃蛇', path: '/tools/snake/' },
{ name: '吃豆人', path: '/tools/pacman/' },
{ name: '扫雷', path: '/tools/minesweeper/' },
{ name: '五子棋', path: '/tools/gomoku/' },
{ name: '坦克大战', path: '/tools/tank-battle/' },
],
demos: [
{ name: 'WebGPU 粒子', path: '/tools/webgpu-gravity/' },
{ name: 'YOLO 物体检测', path: '/tools/yolo-detect/' },
{ name: '火焰特效', path: '/tools/fire-creation/' },
]
});
},
readCurrentPage() {
// Butterfly theme uses #article-container usually
const article = document.getElementById('article-container') ||
document.querySelector('.post-content') ||
document.querySelector('article') ||
document.body;
if (!article) return "无法读取当前页面内容。";
// Clean up text (remove scripts, styles)
const clone = article.cloneNode(true);
const scripts = clone.querySelectorAll('script, style, noscript');
scripts.forEach(n => n.remove());
let text = clone.innerText || "";
// Truncate if too long (e.g. > 5000 chars) to save tokens, or trust V3
return text.substring(0, 5000) + (text.length > 5000 ? "...(内容太长,已截断)" : "");
},
async compressHistory() {
if (chatHistory.length <= 32) return;
console.log("Compressing history...");
const toSummarize = chatHistory.slice(0, chatHistory.length - 10);
const keep = chatHistory.slice(-10);
try {
const summaryRes = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + API_KEY },
body: JSON.stringify({
model: "deepseek-v4-flash",
messages: [
{ role: "system", content: "Summarize the following conversation briefly in Chinese." },
...toSummarize
],
stream: false
})
});
const data = await summaryRes.json();
const summary = data.choices[0].message.content;
chatHistory = [
{ role: "system", content: "Previous Summary: " + summary },
...keep
];
localStorage.setItem('oml2d_history', JSON.stringify(chatHistory));
console.log("History compressed.");
} catch(e) { console.error("Compression failed", e); }
}
};
// --- LLM SERVICE ---
const LLM = {
async chat(userMessage, callbacks) {
stopIdleLoop();
callbacks.onLoading(true);
try {
// System Prompt (Nene Persona)
// Use space concatenation to avoid syntax errors
const systemContent = "你叫宁宁Nene全名绫地宁宁是《魔女的夜宴》中的角色现在兼职 llbzow 博客的看板娘。 " +
"你的主人 llbzow 既是开发者也是工地施工员(打灰人)。 " +
"【性格设定】 " +
"1. 性格温柔体贴,有些纯真和天然呆,非常容易害羞(动不动就脸红)。 " +
"2. 做事非常认真,但偶尔会因为太紧张而出错。 " +
"3. 称呼用户为“前辈”或者“主人”。 " +
"4. 说话语气要软萌、有礼貌。 " +
"5. 面对奇怪的问题会变得慌乱,不知所措。 " +
"【能力】 " +
"你可以使用以下工具来服务用户:" +
"1. search_blog — 搜索博客文章,返回标题+URL+摘要 " +
"2. list_site_pages — 列出所有页面(工具、游戏、分类等),用户问‘有什么’时主动调用 " +
"3. navigate — 跳转到任意页面支持中文首页2048贪吃蛇用户说带我去/打开/跳转’时使用 " +
"4. read_current_page — 读取当前页面内容,用户问‘这篇文章讲了什么’时调用 " +
"5. react_motion — 触发 Live2D 动作 " +
"【导航策略】当用户想看搜索结果中的某篇文章时,直接调用 navigate 跳转。用户问‘有什么好玩的’时,先调 list_site_pages 再推荐。 " +
"【双通道回复】" +
"请务必在回答开头加入一句犀利或可爱的吐槽10字以内必须用 {{ 和 }} 包裹。这部分内容会显示在Live2D气泡中。" +
"吐槽内容要结合:当前时间、对话上下文、用户的问题(如是否重复、是否愚蠢)。例如:{{大半夜的问这个...}} {{这已经是第三次问了哦...}} {{笨蛋前辈...}}" +
"动作对应Tap身体=开心/普通Tap头顶=害羞/抱歉Tap裙子=生气/拒绝Tap呆毛=卖萌/唱歌Tap左胸=亲近。 " +
"当前时间:" + new Date().toLocaleString() + "。";
const messages = [
{ role: "system", content: systemContent },
...chatHistory, // Use full history (managed by compression)
{ role: "user", content: userMessage }
];
// Tool Definitions
const tools = [
{
type: "function",
function: {
name: "search_blog",
description: "Search blog posts when user asks about technical topics or blog content.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Keywords to search" }
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "react_motion",
description: "Trigger a Live2D motion based on emotion.",
parameters: {
type: "object",
properties: {
motion: {
type: "string",
description: "Motion group name",
enum: ["Tap身体", "Tap头顶", "Tap脸", "Tap裙子", "Tap左胸", "Tap呆毛"]
}
},
required: ["motion"]
}
}
},
{
type: "function",
function: {
name: "list_site_pages",
description: "列出博客的所有可访问页面,包括分类页、工具页、游戏页等。当用户问'有什么'、'能做什么'、'有哪些页面/工具/游戏'时主动调用。",
parameters: { type: "object", properties: {}, required: [] }
}
},
{
type: "function",
function: {
name: "navigate",
description: "跳转到博客内任意页面。支持中文别名(首页/关于/标签/分类/归档/工具/游戏/2048/贪吃蛇/吃豆人/扫雷/五子棋/坦克大战/俄罗斯方块/文件转换/哈希计算/二维码/YOLO/WebGPU/VPN 等)。用户说'带我去'、'跳转'、'打开'时使用。",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "要跳转的路径或中文名称(如 /about/、首页、2048、贪吃蛇" }
},
required: ["path"]
}
}
},
{
type: "function",
function: {
name: "read_current_page",
description: "Read the text content of the current page. Use this when user asks about 'this article' or 'current page'.",
parameters: {
type: "object",
properties: {},
required: []
}
}
}
];
const API_URL = "https://api.deepseek.com/chat/completions";
// First Call
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + API_KEY
},
body: JSON.stringify({
model: "deepseek-v4-flash", // Use V3 for speed & tools
messages: messages,
tools: tools,
stream: false
})
});
const data = await response.json();
if (data.error) throw new Error(data.error.message);
const choice = data.choices[0];
const message = choice.message;
// Handle Tool Calls
if (message.tool_calls) {
messages.push(message); // Add assistant's thought/tool_call
for (const toolCall of message.tool_calls) {
const fnName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
let toolResult = "";
if (fnName === 'search_blog') {
toolResult = await Tools.searchBlog(args.query);
} else if (fnName === 'list_site_pages') {
toolResult = Tools.listSitePages();
} else if (fnName === 'react_motion') {
toolResult = Tools.triggerMotion(args.motion);
} else if (fnName === 'navigate') {
toolResult = Tools.navigate(args.path);
} else if (fnName === 'read_current_page') {
toolResult = Tools.readCurrentPage();
}
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: toolResult
});
}
// Second Call (Streamed)
const secondResponse = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + API_KEY
},
body: JSON.stringify({
model: "deepseek-v4-flash",
messages: messages,
tools: tools,
stream: true
})
});
const reader = secondResponse.body.getReader();
const decoder = new TextDecoder("utf-8");
let finalMsg = "";
let buffer = "";
let streamBuffer = "";
let pendingReaction = true;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (line.trim().startsWith('data: ')) {
const dataStr = line.trim().substring(6);
if (dataStr === '[DONE]') continue;
try {
const json = JSON.parse(dataStr);
const content = json.choices[0].delta.content;
if (content) {
if (pendingReaction) {
streamBuffer += content;
if (streamBuffer.trimStart().startsWith('{{')) {
const closeIdx = streamBuffer.indexOf('}}');
if (closeIdx !== -1) {
const reaction = streamBuffer.substring(streamBuffer.indexOf('{{') + 2, closeIdx);
const duration = 3000 + reaction.length * 30;
if (oml2dInstance) oml2dInstance.tips.notification(reaction, duration);
const rest = streamBuffer.substring(closeIdx + 2);
finalMsg += rest;
callbacks.onMessage(finalMsg);
pendingReaction = false;
streamBuffer = "";
}
} else if (streamBuffer.length > 10) {
finalMsg += streamBuffer;
callbacks.onMessage(finalMsg);
pendingReaction = false;
streamBuffer = "";
}
} else {
finalMsg += content;
callbacks.onMessage(finalMsg);
}
}
} catch (e) {
console.error("Stream parse error", e);
}
}
}
}
if (pendingReaction && streamBuffer) {
finalMsg += streamBuffer;
callbacks.onMessage(finalMsg);
}
// Save History & Compress
chatHistory.push({ role: "user", content: userMessage });
chatHistory.push({ role: "assistant", content: finalMsg });
localStorage.setItem('oml2d_history', JSON.stringify(chatHistory));
const replyArea = document.getElementById('oml2d-reply-area');
if (replyArea && finalMsg) {
const autoHideDuration = 3000 + finalMsg.length * 30;
autoHideTimer = setTimeout(() => replyArea.classList.remove('visible'), autoHideDuration);
}
Tools.compressHistory();
} else {
let content = message.content;
// Dual Channel Check for non-stream response
if (content.trim().startsWith('{{')) {
const closeIdx = content.indexOf('}}');
if (closeIdx !== -1) {
const reaction = content.substring(content.indexOf('{{') + 2, closeIdx);
const duration = 3000 + reaction.length * 30;
if (oml2dInstance) oml2dInstance.tips.notification(reaction, duration);
content = content.substring(closeIdx + 2);
}
}
callbacks.onMessage(content);
// Save History & Compress
chatHistory.push({ role: "user", content: userMessage });
chatHistory.push({ role: "assistant", content: content });
localStorage.setItem('oml2d_history', JSON.stringify(chatHistory));
const replyArea = document.getElementById('oml2d-reply-area');
if (replyArea && content) {
const autoHideDuration = 3000 + content.length * 30;
autoHideTimer = setTimeout(() => replyArea.classList.remove('visible'), autoHideDuration);
}
Tools.compressHistory();
}
} catch (error) {
console.error("LLM Error:", error);
callbacks.onMessage("宁宁有点晕... 😵 (网络错误)");
} finally {
callbacks.onLoading(false);
startIdleLoop();
}
}
};
// 5. UI & INIT
try {
const toggleBtn = document.getElementById('oml2d-toggle-btn');
const inputArea = document.getElementById('oml2d-input-area');
const chatInput = document.getElementById('oml2d-chat-input');
const sendBtn = document.getElementById('oml2d-send-btn-inner');
const replyArea = document.getElementById('oml2d-reply-area');
if (replyArea) {
replyArea.addEventListener('click', () => {
replyArea.classList.remove('visible');
});
}
if (toggleBtn) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isHidden = inputArea.classList.contains('hidden');
if (isHidden) {
inputArea.classList.remove('hidden');
setTimeout(() => chatInput.focus(), 100);
toggleBtn.innerHTML = '×';
} else {
inputArea.classList.add('hidden');
replyArea.classList.remove('visible'); // Hide reply too
toggleBtn.innerHTML = '💬';
}
});
}
const sendMessage = () => {
const text = chatInput.value.trim();
if (!text) return;
chatInput.value = '';
if (autoHideTimer) clearTimeout(autoHideTimer);
// Show loading in reply area
if (replyArea) {
replyArea.innerText = "思考中... 🧠";
replyArea.classList.add('visible');
}
LLM.chat(text, {
onLoading: (isLoading) => {
// Handled locally above for instant feedback
},
onMessage: (msg) => {
if (replyArea && msg) {
replyArea.innerText = msg;
replyArea.classList.add('visible');
// Auto scroll to bottom
replyArea.scrollTop = replyArea.scrollHeight;
}
// Fallback to alert if something is wrong (shouldn't happen)
else if (msg) {
alert("宁宁: " + msg);
}
}
});
};
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
if (chatInput) chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
if (window.innerWidth >= 768) {
// Safe Load Logic
try {
const res = loadOml2d(CONFIG);
const setupModel = (inst) => {
oml2dInstance = inst;
// Monkey Patch Focus to allow motion override
if (inst.models && inst.models[0]) {
const model = inst.models[0];
if (typeof model.focus === 'function') {
const originalFocus = model.focus;
model.focus = function(x, y) {
if (this._lockFocus) return;
originalFocus.apply(this, arguments);
};
console.log("OML2D: Focus controller patched");
}
}
};
if (res && typeof res.then === 'function') {
res.then(inst => {
setupModel(inst);
console.log('OML2D: Async Loaded');
startIdleLoop();
}).catch(e => console.error('OML2D Async Error:', e));
} else {
setupModel(res);
console.log('OML2D: Sync Loaded');
startIdleLoop();
}
} catch(e) {
console.error('OML2D Load Error:', e);
}
}
} catch (e) {
console.error('UI Init Failed:', e);
}
</script>
<div id="game-sidebar">
<div id="game-toggle">🎮</div>
<div id="game-list">
<div class="game-header">博客游戏</div>
<a href="/tools/2048" class="game-item">🧩 经典 2048</a>
<a href="/tools/tetris" class="game-item">🧱 俄罗斯方块 (Tetris)</a>
<a href="/tools/pacman" class="game-item">🟡 回忆吃豆人 (Pac-Man)</a>
<a href="/tools/snake" class="game-item">🐍 贪吃蛇 (Snake)</a>
<a href="/tools/gomoku" class="game-item">⚪ 五子棋 (Gomoku)</a>
<a href="/tools/minesweeper" class="game-item">💣 扫雷 (Minesweeper)</a>
<a href="/tools/tank-battle" class="game-item">🚓 坦克大战 (Tank Battle)</a>
</div>
</div>
<style>
#game-sidebar {
position: fixed; /* Fixed position */
right: -150px; /* Width of the list */
top: calc(30vh + 40px); /* Positioned exactly below tool-sidebar */
z-index: 9999;
display: flex;
align-items: flex-start;
transition: right 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Lato, Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* Hover area to keep it open */
#game-sidebar:hover {
right: 0;
}
#game-toggle {
width: 40px;
height: 40px;
background: #FF7D7D; /* Pink/Red color to distinguish from tool-sidebar */
color: white;
text-align: center;
line-height: 40px;
border-radius: 8px 0 0 8px;
cursor: pointer;
box-shadow: -2px 2px 8px rgba(0,0,0,0.15);
font-size: 20px;
position: absolute; /* Absolute relative to #game-sidebar */
left: -40px; /* Hangs off the left of the container */
top: 0;
}
#game-list {
width: 150px; /* Width for text */
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 0 0 0 8px; /* Bottom left rounded */
box-shadow: -2px 2px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
max-height: 50vh;
overflow-y: auto;
}
/* 自定义滚动条样式 */
#game-list::-webkit-scrollbar {
width: 4px;
}
#game-list::-webkit-scrollbar-track {
background: transparent;
}
#game-list::-webkit-scrollbar-thumb {
background: rgba(255, 125, 125, 0.3);
border-radius: 4px;
}
#game-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 125, 125, 0.6);
}
.game-header {
position: sticky;
top: 0;
z-index: 10;
padding: 10px;
background: #fff0f0;
color: #FF7D7D;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #eee;
}
.game-item {
display: block;
padding: 12px 15px;
color: #4c4948;
text-decoration: none !important;
font-size: 13px;
transition: all 0.2s;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0; /* 防止内容被挤压 */
}
.game-item:last-child {
border-bottom: none;
}
.game-item:hover {
background: #FF7D7D;
color: white !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var toolSidebar = document.getElementById('tool-sidebar');
var gameSidebar = document.getElementById('game-sidebar');
var devSidebar = document.getElementById('dev-sidebar');
if(gameSidebar) {
gameSidebar.addEventListener('mouseenter', function() {
if(toolSidebar) toolSidebar.style.right = '-220px'; // 180px + 40px(按钮)
if(devSidebar) devSidebar.style.right = '-190px'; // 150px + 40px(按钮)
});
gameSidebar.addEventListener('mouseleave', function() {
if(toolSidebar) toolSidebar.style.right = '';
if(devSidebar) devSidebar.style.right = '';
});
}
});
</script>
<div id="tool-sidebar">
<div id="tool-toggle">🛠️</div>
<div id="tool-list">
<div class="tool-header">博客工具</div>
<a href="/tools/time" class="tool-item">📅 时间戳与地理时间</a>
<a href="/tools/base64-img" class="tool-item">🖼️ 图片↔Base64</a>
<a href="/tools/hash" class="tool-item">🔐 MD5/SHA256 计算</a>
<a href="/tools/geojson" class="tool-item">🌍 GeoJSON 查询</a>
<a href="/tools/remove-bg" class="tool-item">✂️ 快速抠图</a>
<a href="/tools/watermark" class="tool-item">💧 水印生成与检测</a>
<a href="/tools/gif" class="tool-item">🎬 GIF 生成</a>
<a href="/tools/mirage-tank" class="tool-item">👻 幻影坦克制作</a>
<a href="/tools/converter" class="tool-item">🔄 文件全能转换</a>
<a href="/tools/qrcode" class="tool-item">📱 二维码生成/解析</a>
<a href="/tools/unit-converter" class="tool-item">📏 工程单位换算</a>
<a href="/tools/color" class="tool-item">🎨 颜色转换与调色</a>
</div>
</div>
<style>
#tool-sidebar {
position: fixed;
right: -180px; /* Width of the list */
top: 30vh;
z-index: 9999;
display: flex;
align-items: flex-start;
transition: right 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Lato, Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* Hover area to keep it open */
#tool-sidebar:hover {
right: 0;
}
#tool-toggle {
width: 40px;
height: 40px;
background: #49b1f5;
color: white;
text-align: center;
line-height: 40px;
border-radius: 8px 0 0 8px;
cursor: pointer;
box-shadow: -2px 2px 8px rgba(0,0,0,0.15);
font-size: 20px;
position: absolute;
left: -40px; /* Hangs off the left of the sidebar container */
top: 0;
}
#tool-list {
width: 180px; /* Width for text */
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 0 0 0 8px; /* Bottom left rounded */
box-shadow: -2px 2px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
max-height: 50vh; /* 限制最大高度以便能滚动 */
overflow-y: auto; /* 允许纵向滚动 */
}
/* 自定义滚动条样式,使其不那么突兀 */
#tool-list::-webkit-scrollbar {
width: 4px;
}
#tool-list::-webkit-scrollbar-track {
background: transparent;
}
#tool-list::-webkit-scrollbar-thumb {
background: rgba(73, 177, 245, 0.3);
border-radius: 4px;
}
#tool-list::-webkit-scrollbar-thumb:hover {
background: rgba(73, 177, 245, 0.6);
}
.tool-header {
position: sticky;
top: 0;
z-index: 10;
padding: 10px;
background: #f7f9fe;
color: #49b1f5;
font-weight: bold;
font-size: 14px;
text-align: center;
border-bottom: 1px solid #eee;
}
.tool-item {
display: block;
padding: 12px 15px;
color: #4c4948;
text-decoration: none !important;
font-size: 13px;
transition: all 0.2s;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0; /* 防止由于 max-height 导致内容被挤压 */
}
.tool-item:last-child {
border-bottom: none;
}
.tool-item:hover {
background: #49b1f5;
color: white !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var toolSidebar = document.getElementById('tool-sidebar');
var gameSidebar = document.getElementById('game-sidebar');
var devSidebar = document.getElementById('dev-sidebar');
if(toolSidebar) {
toolSidebar.addEventListener('mouseenter', function() {
if(gameSidebar) gameSidebar.style.right = '-190px'; // 150px + 40px(按钮宽度)
if(devSidebar) devSidebar.style.right = '-190px'; // 150px + 40px(按钮宽度)
});
toolSidebar.addEventListener('mouseleave', function() {
if(gameSidebar) gameSidebar.style.right = ''; // 恢复默认通过 CSS :hover 控制的逻辑
if(devSidebar) devSidebar.style.right = ''; // 恢复默认通过 CSS :hover 控制的逻辑
});
}
});
</script>
<!-- hexo injector body_end end --></body></html>