赵阿卷 - 终生学习
type: channel
link: https://blog.zwying.com/category/zsxx/
items: 在线手写数据集 CASIA-OLHWDB：联机手写识别的宝藏资源
items: CSS中的嵌套写法（CSS Nesting）
items: 蓝桥杯十五届省赛Web应用开发题目答案解析
items: 手撕BP神经网络之参数更新数学推导
items: 前端Canvas绘图比例被拉伸的“坑点”
items: CNN入门——Tensorflow2实现MNIST手写数据识别
items: Web前端实现自定义页面尺寸打印
items: Typecho通过插件实现构建自定义设置页
items: 修复Typecho Checkbox复选框无法保存的问题
items: 聊一聊无头组件库（Headless UI Framework）

在线手写数据集 CASIA-OLHWDB：联机手写识别的宝藏资源
type: item
date: 2024-07-22T14:59:00+08:00
description: CASIA简介CASIA是一个非常有价值的手写单字数据集。它涵盖了众多的汉字以及字母、数字和符号。由中科院自动化研究所在 2007-2010 年间收集；包含1020人书写的脱机（联机）手写中文单字样本和手写文本；联机数据是采用 Anoto 笔在点阵纸上书写后扫描、分割得到。主要由离线（HWDB）和在线（OLHWDB）两部分。具体可以前往官网了解：https://nlpr.ia.ac.cn/databases/handwriting/Home.html这里只介绍在线手写数据集部分，离线的网上介绍比较多。在线手写数据集单字主要包括OLHWDB1.0 与 OLHWDB1.2。一共收录汉字 7185 个，涵盖了 GB2312 中全部 6763 个汉字。这个数据集以.pot文件存储，其数据组织形式独特。这些版本的数据集可以用于训练和测试在线手写汉字识别模型。CASIA - OLHWDB 为手写汉字识别、字形分析等相关任务提供了丰富的数据资源。pot文件的解析pot文件的解析相关教程并不多，官网也只给出了文件头信息，和一个预览的软件。使用预览软件打开某个pot文件的结果：我这里按照文件头规则直接写了一版python的，会将在线的点集转换为json文件存储，方便后期使用，文件夹的命名使用的是这个汉字字符对应的md5编码，当然你也可以修改为其他的，解析时间很长，因为一个字符大概有100多个样本。import hashlib import json import os import struct def parse_pot_file(filename): with open(filename, 'rb') as f: while True: # 读取样本大小 sample_size_data = f.read(2) if not sample_size_data: break # 文件读取结束 sample_size = struct.unpack('<H', sample_size_data)[0] # 读取字符的 GBK 编码 tag_code_data = f.read(4) tag_code = struct.unpack('<I', tag_code_data)[0] gbk_code = tag_code & 0xFFFF # 提取低16位 character = gbk_code.to_bytes(2, 'big').decode('gbk', errors='ignore').strip('\x00') # print(f"Character: {character}") # c.append(character) # 读取笔画数量 stroke_count_data = f.read(2) stroke_count = struct.unpack('<H', stroke_count_data)[0] # print(f"Number of strokes: {stroke_count}") # 读取每个笔画 strokes = [] for _ in range(stroke_count): stroke = [] while True: # 读取一个坐标对 x, y = struct.unpack('<hh', f.read(4)) if (x, y) == (-1, 0): # 笔画结束标志 break stroke.append((x, y)) strokes.append(stroke) # 将字符编码为md5 md5_hash = hashlib.md5(character.encode('utf-8')).hexdigest() # 创建以MD5为名的文件夹 folder_path = os.path.join(output_dir, md5_hash) os.makedirs(folder_path, exist_ok=True) # 计算已有文件数量，确定当前文件名 existing_files = os.listdir(folder_path) json_filename = f"{len(existing_files) + 1}.json" # 将strokes保存为JSON文件 json_file_path = os.path.join(folder_path, json_filename) with open(json_file_path, 'w', encoding='utf-8') as json_file: json.dump(strokes, json_file, ensure_ascii=False, separators=(',', ':')) # print(f"Strokes: {st…

CSS中的嵌套写法（CSS Nesting）
type: item
date: 2024-05-28T14:52:00+08:00
description: 近日，在刷蓝桥杯中的模拟实战赛时，有这么一道题：叫“汽车私人定制”，题目链接如下。https://www.lanqiao.cn/problems/18456/learning/?contest_id=187 打开css的文件，可以看到代码都是类似scss这种： 最开始以为是使用了什么预编译的技术方案，后来才知道，原来原生浏览器已经开始支持css嵌套语法了，关键词是CSS Nesting。 2024年，可以看到基本上市面上大部分主流浏览器都支持CSS Nesting，也就是说这确确实实成为了一种既定的规范。语法/* Without nesting selector */ parent { /* parent styles */ child { /* child of parent styles */ } } /* With nesting selector */ parent { /* parent styles */ & child { /* child of parent styles */ } } /* the browser will parse both of these as */ parent { /* parent styles */ } parent child { /* child of parent styles */ } 可以看到语法规则和less、scss非常类似。了解更多可以参考MDN：https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting注意事项虽然语法很类似，但需要注意：Concatenation is not possible（无法像 less 或 sass 利用父选择器和其他字符串组合）比如下面这一段： tag p 预期结果：.css-nesting-concatenation .css-nesting-concatenation-p {color: blue;} 实际结果：找不到元素，未生效。 根据 MDN 解释，& 选择只会被当做 选择器 来使用Invalid nested style rules - （嵌套样式内 遇到无效样式的处理） 如何处理在嵌套模式内无效样式？看一下MDN 中，可以看出示例，在遇到无效样式时，会忽略无效样式，继续解析下一个有效样式。.parent { /* .parent styles these work fine */ & %invalid { /* %invalid styles all of which are ignored */ } & .valid { /* .parent .valid styles these work fine */ } } 但这里要注意，不同浏览器处理的结果可能会有不一致的情况，比如下面这个例子： tag i tag p tag a 在我本机的Chrome和Firefox上表现不一致，Firefox和MDN描述的是一致的。最后想说的 在web发展的过程中，有很多API都是浏览器吸收了开源社区的热门库后出现的，比如 document.querySelector 吸收了 jQuery 的精华，或者Promise，JSON的相关API都是先出现在社区，然后被浏览器原生支持。今天的css原生嵌套也是借鉴了less，sass等社区方案而出现的。这才是共同进步。

蓝桥杯十五届省赛Web应用开发题目答案解析
type: item
date: 2024-04-14T22:06:00+08:00
description: 去年报了个蓝桥杯Web，昨天刚打完省赛，10道题基本无压力 ::(呵呵) （[DarkBText]果然是圈钱杯[/DarkBText]），最后一道可能会扣点分，整理一下赛后直播的答案，也包括职业院校组的，给需要的小伙伴们：1. 智能停车系统 ⭐️考点： flex 布局解题答案：.cars { position: absolute; z-index: 2; display: flex; width: 600px; height: 600px; flex-direction: column; /* TODO: 请在下方编写题目代码，不要更改其他选择器里的代码 */ flex-wrap: wrap; align-content:space-between; justify-content:space-between; }2. 分享点滴 ⭐️考察：对象的遍历，字符串基本操作答案1：function appendParamsToURL(url, params) { // 创建一个空字符串来存储参数字符串 let str = ""; // 遍历参数对象中的每个键 for (const key in params) { // 将键值对追加到字符串中 str += `${key}=${params[key]}&`; } // 移除字符串末尾的 "&" 符号 str = str.slice(0, -1); // 判断是否原始 URL 已包含查询字符串（通过是否包含 "?" 符号来判断） // 如果已包含查询字符串，则设置 sym 变量为 "&"，用于以 "&" 作为分隔符追加参数 // 否则，设置 sym 变量为 "?"，作为查询字符串的起始符号 let sym = url.includes("?") ? "&" : "?"; // 连接原始 URL、sym 符号和参数字符串，并返回最终的 URL return `${url}${sym}${str}`; }答案2：function appendParamsToURL(url, params) { // 使用 Object.entries() 获取参数对象的所有键值对并按照规则拼接 const paramArray = Object.entries(params).map( ([key, value]) => `${key}=${value}` ); // 使用join方法拼接参数字符串 const paramString = paramArray.join("&"); // 判断URL中是否已包含参数 let sym = url.includes("?") ? "&" : "?"; // 返回拼接后的URL return `${url}${sym}${paramString}`; }3. 布局切换 ⭐️考察 DOM 操作、class 类名操作 option.addEventListener('click', function () { for (const otherOption of layoutOptions) { // 移除其他选项的 active 类 otherOption.classList.remove('active'); } // TODO：待补充代码 this.classList.add('active'); // 为当前选项添加active类 }4. 产品 360 度展示 ⭐️⭐️考察：Promise、循环答案1：const pipeline = async (initialValue, sequence) => { let res = initialValue; for (const fn of sequence) { res = await fn(res); } return res; };答案2：const pipeline = (initialValue, sequence) => { let res = Promise.resolve(initialValue); for (const fn of sequence) { res = res.then((result) => fn(result)); } return res; };5. 多表单验证 ⭐️考察element-plus组件的使用const validateName = (rule, value, callback) => { if (value === "") { callback(new Error("请输入姓名")); } else { const reg = /[^\u4e00-\u9fa5]/g; if (reg.test(value)) { callback(new Error("只能输入汉…

手撕BP神经网络之参数更新数学推导
type: item
date: 2024-03-26T18:17:00+08:00
description: [RainBowText]揭开这个神秘的面纱，探索其中的数学奥秘[/RainBowText]BP神经网络概念 首先从名称中可以看出，BP神经网络可以分为两个部分，BP和神经网络。BP是Back Propagation的简写 ，意思是反向传播。BP网络能学习和存贮大量的输入-输出模式映射关系，而无需事前揭示描述这种映射关系的数学方程。它的学习规则是使用最速下降法也称为梯度下降法，通过反向传播来不断调整网络的权值和阈值，使网络的误差平方和最小。 其主要的特点是：信号是正向传播的，而误差是反向传播的。 举一个例子，某厂商生产一种产品，投放到市场之后得到了消费者的反馈，根据消费者的反馈，厂商对产品进一步升级，优化，一直循环往复，直到实现最终目的——生产出让消费者更满意的产品。产品投放就是“信号前向传播”，消费者的反馈就是“误差反向传播”。这就是BP神经网络的核心。[card title="🚨有几点需要注意的" color="warning"]一般在提到有多少层时，不包括输出层，只计算隐藏层和输出层在一起的数量一般来说，越深的网络越能学到更深的特征逻辑回归（Logistic）其实是神经网络的一个特例，即不包含隐藏层的神经网络[/card]训练一个BP神经网络的核心步骤 这张图基本反应了训练一个BP神经网络的核心步骤，很容易懂，不过多解释了。下面，我们以一个最简单的BP神经网络为例，来推导其计算过程。见下图：前向传播隐含层输入节点：$$ \begin{aligned} h_{1}=W_{11}\cdot x_{1}+W_{21}\cdot x_{2}+b_{h1}\\ h_{2}=W_{12}\cdot x_{2}+W_{22}\cdot x_{2}+b_{h2}\\ h_{3}=W_{13}\cdot x_{3}+W_{23}\cdot x_{2}+b_{h3} \end{aligned} $$隐含层输出节点：经过激活函数$f\left( x\right) =\dfrac{1}{1+e^{-x}}$运算得到：$$ \begin{aligned} h_{o1}=f\left( h_{1}\right) \\ h_{o2}=f\left( h_{2}\right) \\ h_{o3}=f\left( h_{3}\right) \\ \end{aligned} $$这里提一点激活函数的导数，后面会用到：$$ f^{'}(x)=\frac{0-(-e^{-x})}{(1+e^{-x})^{2}}=\frac{1}{1+e^{-x}}\times (1-\frac{1}{1+e^{-x}})=f(x)(1-f(x)) $$输出层输入节点：$$ \begin{aligned} y_{1}=W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o} \end{aligned} $$输出层输出节点：$$ y_{o}=f\left( y_{1}\right) $$反向传播需要明白微积分中的链式法则，设$x$是实数，$f$和$g$是从实数映射到实数的函数。假设$y=g(x)$并且$z=f(g(x))=f(y)$。那么链式法则就是：$$ \frac{dz}{dx}=\frac{dz}{dy}\frac{dy}{dx} $$在算法中，会计算实际输出$y_{o}$和期望输出$d_{o}$的误差，如果这个误差大于设定的阈值，那么就会进行误差的反向传播。定义损失函数：$$ f_{error}=\dfrac{1}{2}\sum \left( d_{o}-y_{o}\right) ^{2} $$损失函数中的$1/2$是为了后面求导计算方便而添加的。🎯目标：更新$W$、$b$使$f_{error}$更小输出层→隐藏层的更新先看输出层，假设更新后的$b_{o}$为$b_{o}^{'}$，即$b_{o}^{'} = b_{o}+\Delta b_{o}$，只要求出$\Delta b_{o}$，就能得到$b_{o}^{'}$。根据梯度下降的原则，可以知道：$$ \Delta b_{o}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{o}} $$根据上面的推导，可以知道：$$ f_{error}=\dfrac{1}{2} \left( d_{o}-y_{o}\right) ^{2}=\dfrac{1}{2} (d_{o}-f(W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o}))^2 $$根据链式法则：$$ \dfrac{\partial f_{error}}{\partial…

前端Canvas绘图比例被拉伸的“坑点”
type: item
date: 2024-01-20T11:01:00+08:00
description: 昨天在开发项目组项目时，有一个需求是将后端数据库返回的x、y点集渲染到前端。实际上是一个很简单的需求，使用前端的Canvas绘图就能完成，没想到调了大半天还没做出来，比例一直不对，遂放弃，回寝睡觉 ::(犀利) 。大概的意思就是像下面这种，绘图的比例会被拉伸：左边的图像就是昨天出现的“bug”，右侧的图像是正常的样子。在使用HTML5的Canvas元素时，内容或画布被拉伸的原因通常涉及到Canvas元素的大小与实际渲染尺寸之间的不匹配。今早顺着这条思路排查才发现Canvas绘图确实有个“坑点”。首先，Canvas元素会根据其属性中的宽度和高度进行初始化，Canvas 的默认大小为 300 像素 ×150 像素（宽 × 高，像素的单位是 px）。绘图操作会在这个初始化尺寸上进行，无论实际上屏幕上Canvas元素的显示尺寸是多少。绘制完成后，Canvas元素的内容会被放大或缩小到在CSS样式中设置的宽度和高度。如果CSS样式中的宽高与Canvas属性中的宽高不一致，就会导致内容在这个放大或缩小过程中发生变形。因为绘制是在Canvas属性指定的宽高上进行的，而放大或缩小是在绘制完成后的内容上进行的，所以这个过程可能导致图形的失真和比例不对。简单来说，如果在css中为canvas定义宽高，实际上把宽高为300px * 150px的画布进行了拉伸。应该在canvas属性中直接指定宽高。 或者也可以通过js直接指定宽高。const canvas = document.getElementById("myCanvas"); canvas.width=150; canvas.height=150;关于更多前端Canvas绘图教程可以参见MDN：https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial

CNN入门——Tensorflow2实现MNIST手写数据识别
type: item
date: 2023-11-18T11:27:00+08:00
description: 项目组经过我和大师兄一个月的Coding，目前基本开发进度告一段落。可以沉下心来搞一搞机器学习相关的东西了。实际上，一开始进项目组的时候就想尝试采用机器学习（更具体的说是深度学习）的方式来做点不一样的事情，而不是纯数学的东西。当然，数学有数学的优势，机器学习有机器学习的优势。 扯得有点远，回到主题，这是最近学习CNN入门的时候练手用的，也应该算是我的第一个机器学习程序吧。记得做出来的时候，还是有点小小的激动，毕竟人生中第一次也不多，哈哈哈 ::(笑眼) 。中间也踩了不少坑，总结一下，整理成文，供后来者参考。CNN是什么 卷积神经网络（英语：convolutional neural network，缩写：CNN）是一种前馈神经网络，它的人工神经元可以响应一部分覆盖范围内的周围单元，对于大型图像处理有出色表现。 卷积神经网络由一个或多个卷积层和顶端的全连通层（对应经典的神经网络）组成，同时也包括关联权重和池化层（pooling layer）。这一结构使得卷积神经网络能够利用输入数据的二维结构。与其他深度学习结构相比，卷积神经网络在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网络，卷积神经网络需要考量的参数更少，使之成为一种颇具吸引力的深度学习结构。 卷积神经网络的灵感来自于动物视觉皮层组织的神经连接方式。单个神经元只对有限区域内的刺激作出反应，不同神经元的感知区域相互重叠从而覆盖整个视野。 以上内容来自于维基百科 ，如果第一次接触的话，肯定是看的一脸懵。有机会我开个坑，尝试用大白话讲一讲。这里有个gif图，形象的展现了卷积的过程： MNIST数据集 MNIST数据集是一个手写数字识别数据集，常用于机器学习和深度学习的训练与测试。这个数据集包含了大量的手写数字图像，包括0到9的数字。这些图像都是28x28像素的灰度图像，每个图像都对应着一个标签，表示图像中所包含的数字。 MNIST手写数据集👉：官网 。官网中数据集一共分成了四个部分： 官网上提供了数据集的下载，主要包括四个文件：文件下载文件用途train-images-idx3-ubyte.gz训练集图像train-labels-idx1-ubyte.gz训练集标签t10k-images-idx3-ubyte.gz测试集图像t10k-labels-idx1-ubyte.gz测试集标签 在上述文件中，训练集一共包含了 60,000 张图像和标签，而测试集一共包含了 10,000 张图像和标签。测试集中前5000个来自最初NIST项目的训练集.，后5000个来自最初NIST项目的测试集。前5000个比后5000个要规整，这是因为前5000个数据来自于美国人口普查局的员工，而后5000个来自于大学生。 这里有一版整合了四个部分的数据集，推荐用这个，我已经上传至网盘，可以直接下载，我的代码也是基于这个整合后的数据集，下载解压即可✌️： 蓝奏云加载数据集 这里默认已经准备好所有环境，推荐使用Anaconda和PyCharm，新手友好。 加载数据集有两种方式，第一种是直接无需下载MNIST数据集，因为在Tensorflow中自带了，不过任然需要下载，且下载地址在海外，有可能会出现下载失败的情况，所以不推荐。代码如下，如果没有下载会自动下载：from keras.datasets import mnist (X_train, y_train), (X_test, y_test) = mnist.load_data() 下载的数据集路径位于：C:\Users\{用户名}\.keras。目前我还没有找到指定下载路径的方法，默认都放在了C盘下。 第二种方式是手动加载，把下载好的数据集文件放在项目目录下，编写加载数据集代码：# 加载Mnist数据集 def load_mnist(): with np.load('mnist.npz', allow_pickle=True) as data: return (data['x_train'], data['y_train']), (data['x_test'], data['y_test'])这里说明一下各个变量：x_train：训练集手写数据图像y_train：训练集手写数据图像对应的标签x_test：测试集手写数据图像y_test：测试集手写数据图像对应的标签我们可以通过下面函数的打印出形状：# 主函数 def main(): mnist = load_mnist() print_shape(mnist) # 打印形状 def print_shape(data): x_train, x_label = data[0] y_train, y_label = data[1] print(x_train…

Web前端实现自定义页面尺寸打印
type: item
date: 2023-09-20T10:46:00+08:00
description: 项目组最近有一个实现自定义页面尺寸打印的需求： 浏览了网上大部分的教程，基本总结如下：方案一：CSS使用 CSS 的 @page 规则：通过 @page 规则，可以定义打印页面的尺寸和边距。例如，可以使用 @page 规则来定义 A4 纸张的尺寸和边距，或者根据需要定义其他自定义尺寸的页面。@page { size: 210mm 297mm; /* A4 尺寸 */ /* 或者自定义页面尺寸 */ }方案二：Javascript使用 JavaScript 来控制页面的打印参数，包括页面尺寸、边距等。可以通过 JavaScript 改变打印样式表中的参数，实现自定义页面尺寸的打印。function setPrintStyles() { const iframe ＝ document.createElement('iframe'); //插入需要打印的目标元素 document.querySelector(‘body‘).appendChild(iframe) //插入style ………… }方案三：利用浏览器自带打印使用打印预览对话框进行手动设置：在打印预览对话框中，用户可以手动选择页面尺寸、边距等打印参数，从而实现自定义页面尺寸的打印。 方案比较针对方案一，弊端在于部分低版本浏览器兼容性较差，不能很好的实现自定义打印效果。针对方案二，通过js来指定打印参数，麻烦在于需要将整个页面进行复制后（包括样式的复制），再进行打印，兼容性较好，但是代码多。针对方案三，不推荐，不同浏览器、不同操作系统会有不同的打印方案，难以控制保持一致性。最终解决方案：封装方案二最终决定封装一个javascript文件来实现自定义页面尺寸打印。核心部分包括：创建一个iframe标签，来容纳页面内容。复制页面中的原有style样式。包括head中的link stylesheet、style标签中的样式以及外部样式文件。打印完成后移除iframe标签。完整代码实现：const defaultOptions = { el: '', //打印目标dom节点 debug: true, //打开调试模式，会显示iframe, importCss: true, //引入head 中的link stylesheet importStyle: true, //引入style标签中的样式 loadCss: [], //需要载入的第三方样式表 title: '', //打印标题 delay: 300, //延迟打印时间，确保iframe中的静态资源加载完成 beforePrintHandle: null, //打开打印窗口前的钩子函数,可以针对打印文档进行自定义调整，接受一个document参数 afterPrintHandle: null, //打印完成的钩子函数 } let iframe = null let dom = null const checkOptions = options => { if(!options.el) { throw new Error('el must be a nodeType') } return { ...defaultOptions, ...options } } const printf = options => { const op = checkOptions(options) dom = op.el.cloneNode(true) const handle = createIframe(op) if(op.beforePrintHandle) { op.beforePrintHandle(handle.contentDocument); } if (op.afterPrintHandle) { op.afterPrintHandle(); } setTimeout(() => { handle.print(); if (op.debug === false) { removeIframe(); } }, op.delay) } const createIframe = (op) => { const { debug, importCss, importStyle, loadCss, title} = op removeIframe(); iframe = document.createElement('iframe'); if(debug === false) { iframe.style.display = 'none' } //插入需要打印的目标元素 document.querySelector('body').appendChild(iframe) iframe.contentDocument.title = title;…

Typecho通过插件实现构建自定义设置页
type: item
date: 2023-08-28T18:53:00+08:00
description: 在开发typecho主题的时候，typecho本身已经提供了封装好的设置项类，常见的例如有：Checkbox、Radio、Select、Text、Textarea等等。我们很方便的就能通过调用相关类来添加主题设置项，这也是最常见的一种方式。虽然够用，但这也带来了一些局限性，相比较于Wordpress，灵活度还是不够高。 为了解决上述问题，我们可以通过配合插件来从底层自己创建自定义的设置项页面。大致流程有：通过插件添加相关路由创建自定义的设置项表封装相关数据库语句创建全局变量以便于读取设置项 下面详细叙述各个流程：创建插件并添加路由 创建一个插件目录，例如"YourPlugin"，并在该目录下创建一个"Plugin.php"文件。接着就是添加插件基本方法以及需要继承的类，可以参考官方的HelloWorld插件来写。 在activate()方法中，使用Helper中的addRoute添加路由：const widgetName = 'TypechoPlugin\Furry\Router'; // 激活插件方法,如果激活失败,直接抛出异常 public static function activate() { Helper::addRoute('furry_options','/furry/options', self::widgetName, 'furryOptionsPageFunc'); Helper::addRoute('furry_options_api','/furry/api/options', self::widgetName, 'furryOptionsApiFunc'); } 这里我添加了/furry/options以及/furry/api/options两个路由。前者用来承接设置页的路由，后者用来加载数据库中的设置项，以json形式返回给前端。 记得在Action中实现furryOptionsPageFunc以及furryOptionsApiFunc两个方法，如果对于这一块不是很明白的可以参考Typecho的插件开发教程，网上有很多例子。这里我提供furryOptionsPageFunc的例子：// 渲染设置页 public function furryOptionsPageFunc() { if (User::alloc()->hasLogin()) { include_once 'options/index.php'; } else { Helper::options()->response->redirect('/admin/login.php'); } } 当用户登录时，访问/furry/options会加载options文件夹中的index.php。否则就会重定向到登录页面，这是一个简单的 鉴权。创建相关数据表 通过上面的分析，我们还知道，需要创建一张表，专门用来存放设置的数据，同样的，可以使用Typecho提供的数据库操作类来实现，对Typecho数据库Api不熟悉的可以参考我的另一篇文章： Typecho数据库常用API 。我这里给出我的例子：// 创建设置表 private static function createFurryOptionsTable() { $db = Db::get(); $prefix = $db->getPrefix(); $tableName = $prefix . 'furry_options'; if (!$db->fetchRow("SHOW TABLES LIKE '$tableName'")) { $db->query('CREATE TABLE `' . $tableName . '` ( `name` varchar(100) NOT NULL COMMENT \'设置名\', `type` varchar(20) NOT NULL COMMENT \'所属分类名\', `value` text COMMENT \'设置值\', PRIMARY KEY (`name`) )'); } } 在插件激活时，可以调用这个方法，就会创建一张表： 包括name设置名、type所属分类名、value设置值三个字段。这样，后面我们就可以存储自己的设置项了，即使切换主题也不会丢失，因为保存在了数据库中。初始化设置项 表有了，下一步就是往里面写入数据。这里我们可以用一个数组来作为初始化的数据。接着用循环语句通过insert方法来插入数据，我这里举个例子：// 初始化options数据 private static function initOptions() { $arr = [ // 全局设置 ["type" => "global", "name" => "AuthKey…

修复Typecho Checkbox复选框无法保存的问题
type: item
date: 2023-07-03T13:21:00+08:00
description: Cuteen主题后台设置中的开关项是由Typecho的Checkbox改造的，但是在最新的typecho版本中出现了Checkbox无法勾选保存的现象，也就导致了无法开启主题相关功能，追查代码发现： 在typecho/var/Widget/Themes/Config.php中第74行位置有一句： $form->getInput($key)->value($this->options->{$key}); 上面这行代码中，从options中得到值，但是由于主题的复选框是新加入的，因此options中并没有对应的值，导致给了一个空的值给复选框。 当主题选项保存的时候，is_array中的数据是从$this->getInput('xxxx')拿到value，但前面程序已经设置为空值，所以在收集提交选项数据时候，以下代码判断不通过，返回null，最终导致保存勾选无效。$result[$param] = $request->get($param, is_array($this->getInput($param)->value) ? [] : null); 解决方案解决方案有三种：方案一 升级typecho到最新的开发版，在 #1590 中，此问题已修复方案二 回退typecho版本至 1.2.0，❌不推荐这种做法，小于1.2.1的版本中包含xss漏洞。方案三 打开typecho/var/Widget/Themes/Config.php，在第74行左右将其注释，按照下面代码修改： public function config(): Form { $form = new Form($this->security->getIndex('/action/themes-edit?config'), Form::POST_METHOD); themeConfig($form); $inputs = $form->getInputs(); if (!empty($inputs)) { foreach ($inputs as $key => $val) { //将这行注释： //$form->getInput($key)->value($this->options->{$key}); //改为这三行： if (isset($this->options->{$key})) { $form->getInput($key)->value($this->options->{$key}); } } } $submit = new Submit(null, null, _t('保存设置')); $submit->input->setAttribute('class', 'btn primary'); $form->addItem($submit); return $form; }

聊一聊无头组件库（Headless UI Framework）
type: item
date: 2023-05-25T00:24:00+08:00
description: 在最一开始的UI设计中，每个网站都有自己独特的设计风格，而这些设计风格可以被抽象成一套样式系统。其中最主要的样式系统代表就是Bootstrap。组件系统的出现使得我们不必再直接编写原始的CSS代码，而是可以直接使用预定义的CSS类，从而提高了开发效率。类似这样： Primary 但这明显有很多缺点，举个例子来说，比如你需要深度定制的组件的时候就不得不去覆盖样式，或者按照它本身提供的设计系统来设计。类似的还有Antd、ElementUI等等…… 于是你为了满足要求，开始自己手撸各种组件。接着你开始遇到了一些问题，比如代码变得冗长、复杂，难以理解，而且还出现了一些错误，比如在Safari或者IE上无法选择下拉项的问题、无障碍功能无法满足等等。最令人担忧的是可访问性问题，手工制作的组件无法被键盘用户和使用屏幕阅读器的人访问。于是你决定放弃，告诉自己：“好吧，果然还是Antd香，毕竟人们使用它是有原因的”。 那真的就没有办法解决这种痛点吗？无头CSS库就是个不错的选择。在前端深耕的小伙伴们应该对这个无头CSS库不陌生，广泛的定义是指一种不包含任何样式的UI组件库，它们只提供了结构和交互的逻辑，开发者可以根据自己的需求自由地定制样式。这种库的出现可以让开发者更加专注于业务逻辑和交互设计，同时也可以减少样式冲突和代码冗余。 一句话说就是：只有骨架和功能，而没有一丁点样式，你要做的就是给他们“穿衣服”。现在最流行的无头CSS库就是Headless UI，官网链接：https://headlessui.com/。 当然类似的还有，Angular cdk、Radix UI、React Aria 等。它提供可访问的组件，但没有样式——因此可以根据自己的喜好设置它们的样式。像这样，创建一个没有样式的下拉菜单：让我们简单加点CSS： 样式完全由你来决定！而且其组件在多种浏览器、平台和设备上经过了良好测试，并处理了可能永远无法或不想处理的边缘情况：诸如焦点管理、键盘导航、事件监听器、可访问性属性、有效标记和屏幕阅读器支持等内容。 这里有个开源地址，收集了目前的无头组件库，感兴趣的小伙伴可以留意一下：https://github.com/jxom/awesome-react-headless-components