Z

zodream梦想开源/个人编程日记

简单的个人编程日记

基于 SkiaSharp 的轮廓获取

基于 SkiaSharp 的轮廓获取 源代码 示例 事前须知 本文基于透明图片的透明度获取轮廓;如不透明图片获取轮廓需要先把图片转成灰度图片,根据灰度值获取轮廓。 原理 需要获取一个物体的起始点,通常是从左至右逐行遍历像素点,获取第一个不透明点 获取相邻的下一个不透明点,通常是以当前点为中心,顺时针遍历八个方向的点,找到的一个个点就是下一个点。 初始点第一个方向为正上个方,下一个点的第一个方向就应该是相对与上一个点的方向顺时针转2下。 o c o o o o b o o a o o o o o // 从 a 找到 b,方向为 1 (0 是正上方) // 那个 b 就在 a 的 1 方向,a 就在 b 的 5方向 // 但 b 的 6 方向已经被 a 找过了, 所以 b 的起始方向就是 7 // 总结 b 在 a 的 n 方向,则 b 的起始方向为 n + 6 代码 /// <summary> /// 边界算法 /// </summary> /// <returns></returns> private static SKPath? TraceContour(SKPixmap pixMap, int beginX, int beginY) { var path = new SKPath(); path.MoveTo(beginX, beginY); var directItems = new int[][] { [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1] }; var beginDirect = 0; var isBegin = false; var curX = beginX; var curY = beginY; while (!isBegin) { var i = 0; var direct = beginDirect; var hasPoint = false; while (i ++ <= directItems.Length) { var x = curX + directItems[direct][0]; var y = curY + directItems[direct][1]; // 判断点是否是透明像素点 if (IsTransparent(pixMap, x, y)) { direct = (direct + 1) % directItems.Length; continue; } hasPoint = true; curX = x; curY = y; if (curX == beginX && curY == beginY) { isBegin = true; path.Close(); } else { path.LineTo(curX, curY); } beginDirect = (direct + 6) % directItems.Length; break; } if (!hasPoint) { // 所有方向都没有找到下一个不透明点,表明这就是一个孤点 return null; } } return path; } 升级版:获取外轮廓,允许点的 /// <summary> /// 物体轮廓获取 /// </summary> public class ImageContourTrace { public ImageContourTrace() { } public ImageContourTrace(bool isOutline) { IsOutline = isOutline; } /// <summary> /// 外边框,即靠近物体的透明区域 /// </summary> public bool IsOutline { get; set; } /// <summary> /// 是否需要获取一个点 /// </summary> public bool IsAllowDot { get; set; } /// <summary> /// 获取图片上所有物体轮廓 /// </summary> /// <param name="image"></param> /// <returns></returns> public async Task<SKPath[]> GetContourAsync(SKBitmap image, CancellationToken token = default) { using var imagePixMap = image.PeekPixels(); return await GetContourAsync(imagePixMap, token); } /// <summary> /// 获取图片上所有物体轮廓 /// </summary> /// <param name="image"></param> /// <returns></returns> public async Task<SKPath[]> GetContourAsync(SKImage image, CancellationToken token = default) { using var imagePixMap = image.PeekPixels(); return await GetContourAsync(imagePixMap, token); } /// <summary> /// 获取图片上所有物体轮廓 /// </summary> /// <param name="image"></param> /// <returns></returns> public Task<SKPath[]> GetContourAsync(SKPixmap pixMap, CancellationToken token = default) { return Task.Factory.StartNew(() => { return GetContour(pixMap, token); }, token); } /// <summary> /// 获取所有物体的轮廓 /// </summary> /// <param name="pixMap"></param> /// <returns></returns> public SKPath[] GetContour(SKPixmap pixMap, CancellationToken token = default) { var items = new List<SKPath>(); for (var i = 0; i < pixMap.Height; i++) { for (var j = 0; j < pixMap.Width; j++) { if (token.IsCancellationRequested) { return [..items]; } if (IsTransparent(pixMap, j, i) || Contains(items, j, i)) { continue; } var path = GetContour(pixMap, j, i); if (path is null) { continue; } items.Add(path); } } return [.. items]; } /// <summary> /// 根据坐标获取轮廓边界算法 /// </summary> /// <param name="pixMap"></param> /// <param name="beginX"></param> /// <param name="beginY"></param> /// <returns></returns> public SKPath? GetContour(SKPixmap pixMap, int beginX, int beginY) { var path = new SKPath(); path.MoveTo(beginX, beginY - (IsOutline ? 1 : 0)); var directItems = new int[][] { [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1] }; var beginDirect = 0; var isBegin = false; var curX = beginX; var curY = beginY; while (!isBegin) { var i = 0; var direct = beginDirect; var hasPoint = false; while (i++ <= directItems.Length) { var x = curX + directItems[direct][0]; var y = curY + directItems[direct][1]; if (IsTransparent(pixMap, x, y)) { direct = (direct + 1) % directItems.Length; if (IsOutline) { path.LineTo(x, y); } continue; } hasPoint = true; curX = x; curY = y; if (curX == beginX && curY == beginY) { isBegin = true; path.Close(); } else if (!IsOutline) { path.LineTo(curX, curY); } beginDirect = (direct + 6) % directItems.Length; break; } if (!hasPoint) { if (IsOutline) { path.Close(); return path; } // 所有方向都没有不透明点,就是一个孤点 return IsAllowDot ? path : null; } } return path; } private static bool Contains(IEnumerable<SKPath> items, int x, int y) { foreach (var item in items) { if (item.Contains(x, y)) { return true; } } return false; } private static bool IsTransparent(SKPixmap pixMap, int x, int y) { if (x < 0 || y < 0 || x >= pixMap.Width || y >= pixMap.Height) { return true; } return pixMap.GetPixelColor(x, y).Alpha == 0; } }

2024/11/22
articleCard.readMore

SkiaSharp 把 pixel byte[] 转成 SKBitmap

SkiaSharp 把 pixel byte[] 转成 SKBitmap 关键方法 /// <summary> /// 把像素字节数组转图片 /// </summary> /// <param name="buffer">像素数组,例如: [r, g, b, a, r, g, b, a ...]</param> /// <param name="width">图片的宽度,例如: 512</param> /// <param name="height">图片的高度,例如: 1024</param> /// <param name="format">指定像素数组的组成方式,例如:SKColorType.Rgba8888</param> /// <returns></returns> public static SKBitmap Decode(byte[] buffer, int width, int height, SKColorType format) { var data = SKData.CreateCopy(buffer); var newInfo = new SKImageInfo(width, height, format); var bitmap = new SKBitmap(); bitmap.InstallPixels(newInfo, data.Data); return bitmap; } public static SKImage Decode(byte[] buffer, int width, int height, SKColorType format) { var newInfo = new SKImageInfo(width, height, format); var data = SKData.CreateCopy(buffer); return SKImage.FromPixels(newInfo, data); } 解密不知的图片格式 SkiaSharp 默认支持 png jpg 等文件格式,但是一些不支持的文件格式怎么显示呢? 例如:pvr 格式就不能直接解码。 第一步,先解析 pvr 文件,获取 pvr 文件头得到:图片的宽高,编码方式,数据开始的开始的位置 读取内容数据 判断 编码方式 是否被 SkiaSharp 支持,所有支持的颜色编码方式在 SKColorType 中 不支持的编码方式,需要先转成支持的,例如转成 SKColorType.Rgba8888, 即:每个像素占四个字节,分别为 [Red,Green,Blue,Alpha],总字节为 宽*高*4, 以逐行横行存储, (行*宽+列)*4 获取像素点的位置 调用上述方法 Decode(转换后的数据, 宽, 高, SKColorType.Rgba8888) 即可 可能出现的问题 Q: Attempted to read or write protected memory A: SkiaSharp 是线程安全的,两个线程不应访问同一个对象。SKBitmap 只能在UI主线程中操作,SKImage 则可以在所有子线程中操作

2024/8/19
articleCard.readMore

nas 使用 Docker 安装 gogs

nas 使用 Docker 安装 gogs 环境 NAS设备:绿联DXP4800 Plus NAS系统:UGOS PRO 第一步:安装 Docker 第二步:安装 Docker 镜像 无法直接 DockerHub 可以使用 阿里云的镜像加速 搜索 gogs 下载 第三步:安装 gogs 配置自动重启 配置文件保存位置 配置端口映射 访问网页进行GOGS配置 图片来源:https://www.cnblogs.com/yuexiaoyun/articles/11946103.html 图片来源:https://www.cnblogs.com/yuexiaoyun/articles/11946103.html 一些问题 Q: nas重启后网址无法打开 A:可能是gogs安装时,端口配置错误,可以到gogs的文件保存位置下找到 gogs/conf/app.ini 文件修改 [server]HTTP_PORT为 3000 即可 参考 【基于docker搭建gogs】

2024/8/4
articleCard.readMore

复制 android 手机中的文件到电脑

复制 android 手机中的文件到电脑 方法一:直接复制 使用USB连接电脑 在手机上选择“USB用于传输文件” 优点 不需要其他操作,简单方便 缺点 文件一多就会复制很慢很慢 方法二:使用第三方app直接传 缺点 需要安装app 传输慢 android/data 下的文件可能无法获取到并传输,如果再手机上把 android/data复制到 Download,可能无法复制目录中的文件 方法三:先再手机上使用第三方app压缩再传输 例如使用 ES文件浏览器 优点 压缩后,传输会快很多 缺点 有些文件名中有特殊字符会导致压缩失败 方法三:使用adb pull 下载 需要在电脑中安装 adb 使用USB连接电脑 在手机上选择“USB用于传输文件” 再手机 设置 中打开 开发者模式,开启 USB调试功能 电脑上打开终端命令行 # adb pull <手机中的文件路径> <保存在电脑的位置> adb pull /storage/emulated/0/Android/data/com.xxx files 优点 就算文件多,传输也很快 缺点 一些文件名中有特殊字符会导致传输中断,无法跳过,可能是 linux 和 windows 系统的原因导致的,电脑是 linux 系统的话不会出现这个问题 方法四【推荐】:使用adb压缩下载 注意千万不要使用以下命令 这两种命令的原理就是压缩/storage/emulated/0/Android/data/com.xx不保存直接输出终端,再保存终端输出的内容到 aaa.tar 文件,但是会导致文件出现0xFF 0xFE 和 很多 0x0字节,导致解压缩软件无法读取 adb exec-out tar chf - -C /storage/emulated/0/Android/data/com.xx files > aaa.tar adb shell 'tar -cf - /storage/emulated/0/Android/data/xx 2>/dev/null' > backup.tar.gz 可以使用先压缩到手机,再下载出来 # 压缩 /storage/emulated/0/Android/data/com.xx 到 /storage/emulated/0/Download/files.tar 文件中 adb exec-out tar chf /storage/emulated/0/Download/files.tar /storage/emulated/0/Android/data/com.xx # 下载 adb pull /storage/emulated/0/Download/files.tar files.tar tar 命令 -c: 建立压缩档案 -x:解压 -t:查看内容 -r:向压缩归档文件末尾追加文件 -u:更新原压缩包中的文件 -f: 文件 -h: 解除软链接 所以 tar chf <保存的文件> <要压缩的目录> 就是 tar -c <保存的文件> -h -f <要压缩的目录> 的意思 优点 所有的文件都能传输 在手机上压缩更快 缺点 需要手机空间大

2024/8/4
articleCard.readMore

AI学习笔记

分词原理 将每个字归类为: 词首(B)、词中(M)、词尾(E)、单子词(S) 文字生成原理 输入前一个字,输出后一个字;训练时就是将n个字符的内容 [0, n-1] 作为输入,将 [1, n] 做为预测值。

2024/6/14
articleCard.readMore

升级 SiteServer CMS 并迁移到 Linux 服务器

前言 以前使用 SiteServer CMS 6.8 在 Windows server 12 的服务器上部署了两个企业站,现在需要换到 Linux 服务器上。 SiteServer CMS 6.8 使用的是 Asp.net 不支持Linux 系统,所以需要升级到 SiteServer CMS 7 SiteServer CMS 7 使用的是 .NET Core 支持跨平台。 这里干脆升级到最新的 SiteServer CMS 7.30 (.NET 8) 先决条件 Windows 服务器未过期或拥有Windows电脑并安装了 IIS + MySQL + Asp net core 运行时 在Windows中运行SSCMS SSCMS老版本升级 概念讲解 SSCMS 的模板语法不需要变动; 主要是数据表的变动; 分步指导 假设旧版本的数据已经在MySQL数据库中 在Windows上部署 SSCMS 7.30 从 官网 直接下最新的 windows 版本即可 解压放到一个文件里,例如 D:/sscms 根据 “SSCMS老版本升级” 升级文档进行操作即可 更新数据表 新建一个文件 D:/sscms/old.json { "Database": { "Type": "MySql", "ConnectionString": "Server=127.0.0.1;Uid=root;Pwd=root;Database=sqldb_old;SslMode=none;CharSet=utf8" } } 在 D:/sscms 文件夹中打开终端 PowerShell,运行命令更新数据 .\sscms data backup -d backup -c old.json .\sscms data update -d backup 新建一个文件 D:/sscms/sscms.json, 注意:sqldb_new 数据库必须为空 { "Database": { "Type": "MySql", "ConnectionString": "Server=127.0.0.1;Uid=root;Pwd=root;Database=sqldb_new;SslMode=none;CharSet=utf8" } } 运行命令恢复数据 sscms data restore -d update 完成数据恢复后,需要在浏览器中进入 http://<域名>/ss-admin/syncDatabase/ 数据库升级界面,点击升级按钮,完成数据库升级。 注意: D:/sscms/sscms.json 需要增加 SecurityKey 字段,不然后台登录验证码无法显示 { "IsProtectData": false, "IsSafeMode": false, "SecurityKey": "2cf8fcc7150b6f839784231b8b959217cc0840f20623813b", "Database": { "Type": "MySql", "ConnectionString": "Server=127.0.0.1;Uid=root;Pwd=root;Database=sqldb_new;SslMode=none;CharSet=utf8" }, "Redis": { "ConnectionString": "" }, "IsDisablePlugins": false, "AdminRestriction": { "Host": "", "AllowList": [], "BlockList": [] }, "Cors": { "IsOrigins": false, "Origins": [] } } 升级中的一些问题 后台登录验证码无法显示 D:/sscms/sscms.json 需要增加 SecurityKey 字段,具体值需要新安装一个SSCMS系统(删除D:/sscms/sscms.json文件即可),复制新的 sscms.json 文件更改 Database.ConnectionString 值即可 老版本的密码不能用了 新安装一个SSCMS系统(删除D:/sscms/sscms.json文件即可),设置管理员密码,然后到数据库中 找到表 siteserver_administrator 复制 Password、 PasswordSalt 两个字段值即可。 后台登录提示字段不存在 需要手动复制新系统的数据表结构。因为SSCMS版本变动表结构也发生了变动。 登录进入显示需要创建站点 需要修改数据表 siteserver_site 的字段 SiteType 值为 web 前台页面导航栏显示不全 需要到后台的栏目管理->编辑栏目手动勾选 栏目组 导航即可 前台页面的包含文件不加载 需要到后台的显示管理-> 包含文件管理 添加即可 在 linux 系统安装 netcore 环境 这里使用的是阿里云ECS服务器 使用命令 lsb_release -a 查询的系统信息为 LSB Version: :core-4.1-amd64:core-4.1-noarch Distributor ID: AlibabaCloud Description: Alibaba Cloud Linux release 3 (Soaring Falcon) Release: 3 Codename: SoaringFalcon 所以安装 netcore 的方法参考:在RHEL和CentOS Stream中运行SSCMS sudo rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm sudo dnf install aspnetcore-runtime-8.0 # 验证dotnet core runtime是否安装成功 dotnet --info 配置站点 假设已安装过 Nginx a站点 nginx 配置 server { listen 80; server_name a.com; root /data/a; # 设置站点根目录 location / { proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; send_timeout 600; } } a 站点的 /data/a/sscms.json { "IsProtectData": false, "IsSafeMode": false, "SecurityKey": "2cf8fcc7150b6f839784231b8b959217cc0840f20623813b", "Database": { "Type": "MySql", "ConnectionString": "Server=127.0.0.1;Uid=root;Pwd=root;Database=sqldb_a;SslMode=none;CharSet=utf8" }, "Redis": { "ConnectionString": "" }, "IsDisablePlugins": false, "AdminRestriction": { "Host": "", "AllowList": [], "BlockList": [] }, "Cors": { "IsOrigins": false, "Origins": [] } } 创建 a 站点的服务文件 /etc/systemd/system/a.service [Unit] Description=a [Service] WorkingDirectory=/data/a ExecStart=/usr/bin/dotnet /data/a/SSCMS.Web.dll Restart=always # Restart service after 10 seconds if the sscms service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=sscms User=root Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target b 站点 a站点 nginx 配置 server { listen 80; server_name b.com; root /data/b; # 设置站点根目录 location / { proxy_pass http://localhost:5001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; send_timeout 600; } } b 站点的 /data/b/sscms.json { "Urls": "http://localhost:5001", "IsProtectData": false, "IsSafeMode": false, "SecurityKey": "2cf8fcc7150b6f839784231b8b959217cc0840f20623813b", "Database": { "Type": "MySql", "ConnectionString": "Server=127.0.0.1;Uid=root;Pwd=root;Database=sqldb_b;SslMode=none;CharSet=utf8" }, "Redis": { "ConnectionString": "" }, "IsDisablePlugins": false, "AdminRestriction": { "Host": "", "AllowList": [], "BlockList": [] }, "Cors": { "IsOrigins": false, "Origins": [] } } 创建 b 站点的服务文件 /etc/systemd/system/b.service [Unit] Description=b [Service] WorkingDirectory=/data/b ExecStart=/usr/bin/dotnet /data/b/SSCMS.Web.dll Restart=always # Restart service after 10 seconds if the sscms service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=sscms User=root Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target 启动服务 sudo systemctl enable a.service sudo systemctl start a.service sudo systemctl status a.service # 验证服务是否启动 没有报错即配置正确, b站点服务同理 导入数据 这里需要注意 在 Windows 中数据表表名都是小写,但是在Linux 环境下是区分大小写的,所以需要手动更改 sql 文件中的数据库表名,再导入 例如 siteserver_administratorsinroles 需要改为 siteserver_AdministratorsInRoles 具体对应表需要重新再 linux 下安装即可 导入模板文件、资源文件 总结 升级过程还是比较繁琐的,需要有耐心。

2024/5/7
articleCard.readMore

周报:没有干货

本周没有什么干货分享。 折腾UI 不是专业美术,所以必须得积累点UI样式,不然用到时没有好的实现方案。 会员页 参考至X的页面,主要是给用户自己提供交流通道。 账户注销页面 参考至 winui 设置页面,基本上PC 基于winui 的应用都用这个样式。 忘了参考哪里了,反正我的其他项目都用这个样式。 消息页面 参考至 哔哩哔哩 我的消息,风格类似。 看剧集 刷完美剧《群星 Constellation》 平行宇宙 刷完美剧《洛基 Loki》1、2季 恶作剧之神从良登基。 刷完日漫《间谍过家家 SPY×FAMILY》1、2季 从小孩的视角看低幼的事情就是笑点。 看源码 看大神千行 C 代码实现 GPT 的 训练 过程。 https://github.com/karpathy/llm.c 没办法,AI是未来,所以必须得了解AI、使用AI、然后创作AI。

2024/4/11
articleCard.readMore

RestAPI 架构设计

占位

2024/4/6
articleCard.readMore

会员系统架构设计

前言 本文主要从简单到详情讲解会员系统的设计逻辑,不涉及任何编程,主要通过图文方式通俗的讲述会员系统的前后端逻辑。 先决条件 本文适合以下人: 想要了解会员登录注册过程的 想要自己实现会员系统的技术人员 一些集成产品二次开发的技术人员 不适合的人: 使用 Wordpress 等现成系统的 概念讲解 会员系统是一切网站的基础,不论是后台管理。 分步指导 简单的会员系统 主要就两个字段: 账号、密码 使用于小型网站:就只有一个管理员管理发布内容的系统。 权限会员系统 权限根据详细程度又可以细分不同的权限模型 DAC(自主访问控制 Discretionary Access Control) MAC(强制访问控制模型 Mandatory Access Control) RBAC(基于角色的访问控制模型 Role-based Access Control) ABAC(基于属性的访问控制模型 Attribute-Based Access Control) 前后台分离式会员系统 常见问题 会员系统怎么选型? 扩展知识 会员层级 设计会员等级体系:普通会员、VIP会员、钻石会员等 会员资金积分 会员推荐系统 总结 参考资源 权限管理--浅析权限管理模型(DAC, MAC, RBAC, ABAC)

2024/4/6
articleCard.readMore

周报:寻找优质的周刊

周刊相对普通博客,提供了更多有趣新知识。 怎么找周刊 Github 搜索 weekly 基本都是关于软件技术的周刊 相关周刊月刊 RSS 整合站点 https://www.fre123.com/weekly 周刊推荐 潮流周刊 https://weekly.tw93.fun/ 每周分享潮流技术和潮流工具,标题什么的不重要。 HelloGitHub月刊 https://hellogithub.com/periodical 每月分享 GitHub 上有趣、入门级的开源项目。 总结与感想 周刊月刊这种文章主要还是用来找乐子的,随便发现点有用的东西,当作放松方式还是不错的,至少能够拣点有用的东西。 不过,也不要寄希望于挖到什么大宝贝,因为写的人也不一定经常用里面推荐的工具,当时可能只是抱着“哟,看起不错哦”的心态记录一下。 总之,写写周报,就是闲时炫耀一下自己发现的“大宝贝”。

2024/4/2
articleCard.readMore

开发日志:对Markdown的代码块新增引用来源支持

功能参考来源 SourceCodeTrace 使用方法 ```php {1-4} {1} (http://) 解释说明: 使用 () 添加网址 使用 {} 添加起始行号或高亮行号,使用 - 表示范围,, 只能用于高亮,表示多个 示例 只有引用网址 ```php (https://github.com/zodream/html/blob/master/src/MarkDown.php) 有引用网址和起始行号 ```php {501} (https://github.com/zodream/html/blob/master/src/MarkDown.php) protected function parseQuoteLine(string $block): array { $res = array_map('intval', explode('-', $block)); if ($res[0] < 1) { $res[0] = 1; } if (count($res) === 1 || $res[1] < $res[0]) { $res[1] = $res[0]; } return $res; } 有引用网址和起始行号,还有高亮 ```php {501} {503,506} (https://github.com/zodream/html/blob/master/src/MarkDown.php) protected function parseQuoteLine(string $block): array { $res = array_map('intval', explode('-', $block)); if ($res[0] < 1) { $res[0] = 1; } if (count($res) === 1 || $res[1] < $res[0]) { $res[1] = $res[0]; } return $res; } 只有高亮 ```php {2-4} protected function parseQuoteLine(string $block): array { $res = array_map('intval', explode('-', $block)); if ($res[0] < 1) { $res[0] = 1; } if (count($res) === 1 || $res[1] < $res[0]) { $res[1] = $res[0]; } return $res; }

2024/3/30
articleCard.readMore

周报:怎么写技术类的教程文章

本周还是在思考网站内容的发展方向。 使用 ClaudeAI 学习了下怎么写技术类的教程文章。 准备朝着这个标准写。同时也想改改已有的文章,让文章都超这个标准靠拢。 怎么写技术类的教程文章 一篇优秀的技术教程文章应当具备以下几个基本结构和要素: 标题 标题应该简洁明了,能够准确表达教程主题,吸引读者阅读。 前言 前言部分通常会简单介绍所要讲解的技术/工具是什么,解决什么问题,在哪些场景下会使用。让读者对整体内容有一个基本认知。 先决条件 列出学习和实践本教程所需要具备的基础知识技能、环境配置等前提条件。 概念讲解 对教程涉及的关键概念、理论原理进行深入浅出的讲解,融入适量的图示和案例,帮助读者理解和消化。 分步指导 教程实战部分最为关键。通过分步骤的形式,引导读者一步一步操作实践。文字通俗易懂,操作简明流畅。可以附带运行输出截图。 常见问题 总结实践过程中可能遇到的常见问题,并给出解决方法和建议。 扩展知识 对教程内容进行适度延伸阐述,如进阶技巧、高级玩法、场景应用等,满足不同层次读者的需求。 总结 全文总结并重申教程主题和重点,同时可以指出一些注意事项和最佳实践。 参考资源 列出撰写教程时参考的权威资料出处,供读者进一步了解和学习。 此外,代码示例尽量保持简洁,重点突出,避免过多冗余。插图和动图可以辅助说明。层次分明的标题结构也有助于读者查阅和理解。交错适度的互动性语句会增加教程的亲和力。良好的教程文章不仅传递知识,还需要关注知识传递的体验。

2024/3/29
articleCard.readMore

css display:flex 布局尺寸超出问题

使用 flex 布局中,经常会出现父容器的尺寸被子元素撑开,而不是子元素自适应父容器的尺寸。 例如: 解决方法 在子元素的样式上设置 width 为 0 即可 width: 0;

2024/3/25
articleCard.readMore

周报:SEO优化的思考

本周主要是对网站进行SEO优化,想通过SEO优化提高网站的流量。 从关键词优化入手: 先是从本网站的访客来源入手, 方式一:从搜索引擎站点工具查看 当前的搜索引擎都对用户搜索关键字进行了加密,所以基本只能从各大搜索引擎提供的站长工具查看。 [error] tsserver exited. code: null. signal: sigterm vscode 正在初始化 js/ts 语言功能 typescript language server exited with error. error message is: channel closed vscode tsserver exited. code: null. signal: sigterm 瀑布流元素被切割 从这些来源关键词,不难发现基本是:用户编程过程中遇到了某个问题,编程软件出BUG、代码实现有问题等。 所以编程遇到问题才是本站的流量来源关键。 方式二:流量统计工具 这种方式就是查看着陆页,那些文章点击多、受欢迎。 还有就是用户来源,来源搜索引擎、友链等占比情况,确定是做SEO优化还是发站外链接为主。 再到内容优化: 寻找热门的关键词,选择内容创作方向。 本站最初的选择方向是编程方向,所以选择的内容也是这个方向。 所以需要提高相关关键词的含量。 最后才发现,问题还是出现在内容上! 本网站的内容基本上是编程技术相关的内容。 第一内容没有用户粘性,基本上访客访问的都是编程出现问题,寻找解决方法的。 第二内容并没有比同类站点更专业,受限于本人对技术深度探究有限,并不能写出令人一亮的技术观点。 第三受众理解不清晰,本站的内容都是平时工作中遇到的东西,基本上很难有同类,而且有教程用B站,编程用ChatGPT,其他制作用Canva类集成大平台。 因此本站发展方向要么是生活类思考观点做用户粘性;要么做小问题专业解答接收搜索引擎的流量。 思考:在AI的趋势下怎么生存? AI是大趋势,AI能快速提供相对准确的答案(但需要一定的相关知识判断力)。 AI能提供文字、图像、视频的生成,但是还是需要人的主导。

2024/3/20
articleCard.readMore

Edge 浏览器不适用 Edge Image Viewer 打开图片

背景 在使用 Edge 浏览器 浏览网页时查看图片,默认会调用 Edge Image Viewer 打开图片,增加了图片编辑等功能,但是实际是跳转了一个网址,导致加载显示图片太慢。 所以怎么关闭 Edge Image Viewer 呢? 解决方法 在浏览器右上角打开进入 设置 在设置页面点击 下载 关闭 “允许边缘图像查看器打开图像” 即可

2024/3/10
articleCard.readMore

SEO 学习笔记(一) 内容来源

SEO 学习笔记(一) 内容来源 学习目标 了解如何获取内容 idea 如何快速更新文章 原创 完全基于个人兴趣写作,记录关于个人的生活学习工作内容。 这类文章完全取决个人的兴趣爱好,当兴趣爱好跟浏览者的喜好一致时,获得的流量就多,反之,就没什么点击量。 优点: 搜索引擎都喜欢原创的文章内容。 受众集中,转换为订阅者、粉丝的几率高,访客的粘性高,访客定向性高符合广告主青睐。 个人的表达欲得到了满足。 缺点: 更新频率不可能太高。 受众固定。 不适合赚快钱的新人。 搬运 从其他平台转载内容。例如:从国内平台搬运至外网。 优点: 更新频率高,几乎没有创作成本。 因为是已经验证过的高流量内容,几乎可以复制高流量现象。 缺点: 侵权,版权问题。 同行竞争,导致相同的内容多,反而可能被平台限制。 难以获得广告收益。 有封号、被定义垃圾站的风险。 案例: 国内视频网与国外视频网的视频搬运转载。 伪原创 俗称蹭热度,查看搜索引擎流量热点,进行相关创作。这是最好的方法。 原文转换 翻译转换。 AI 视频图片内容生成。 这种相对比搬运高级一点,但不多,因为核心内容不变。 案例: 从微信公众号选取热门文章,进行AI配音放到 Youtube 上。 选取热门关键词进行创作 这种方法跟原创几乎没有区别,一样的需要花费大量时间进行创作,但是这种方法比原创更能把握流量热点。 用计算机术语来讲就是:面向搜索引擎创作。 步骤: 使用SEO工具获取热门关键词 使用搜索引擎搜索这些关键词,选择合适的参考文章。 进行内容创作,同时加入一些相关的关键词。 使用 Google Search Console 和 Google Analytics 挖掘网站关键词 选取关键词进行搜索

2024/3/6
articleCard.readMore

PHP 实现双因素身份认证(2FA)

PHP 实现双因素身份认证(2FA) 双因素身份认证,简单理解就是使用账户密码登录后需要使用一个动态码确认,账户密码 加 动态码 两种方式登录,多一步就多一点安全性, 但是,这种方式也牺牲了方便。因此,有多种形式的动态码确认,常见的就有:基于TOTP验证APP,例如Google Authenticator、微软的 Authenticator;网上银行的U盾,这类第三方专属物理设备验证。 TOTP 今天,需要实现的是基于 TOTP (基于时间的一次性密码)实现的两步验证。 需要实现的步骤如下: 用户登录后,需要手动启用两步验证, 生成专有的恢复码和包含密钥的二维码, 用户使用Authenticator扫码后,需要提供Authenticator生成的动态码进行启用, 用户重新登录后需要提供动态码才能完成登录操作 代码实现 依赖 TwoFactorAuth composer require robthree/twofactorauth 生成密钥 use RobThree\Auth\TwoFactorAuth; $provider = new TwoFactorAuth('你的域名'); $secret_key = $provider->createSecret(); $qr = $provider->getQRCodeImageAsDataUri('用户的名称获取ID', $secret_key); 显示二维码即可,当然要保存 $secret_key 跟用户关联上; 验证动态码 基本原理: 每30秒生成一个动态码 TC = floor(unixtime(now) / 30) TOTP = HASH(SecretKey, TC) use RobThree\Auth\TwoFactorAuth; $provider = new TwoFactorAuth('你的域名'); $provider->verifyCode($secret_key, $_POST['code']); // bool 参考 【TwoFactorAuth】

2024/2/27
articleCard.readMore

winui3 自定义标题栏

winui3 自定义标题栏 在 MainWindow.xaml 添加 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition MinHeight="48"/> <RowDefinition/> </Grid.RowDefinitions> <!-- 标题栏 --> <Border x:Name="AppTitleBar" VerticalAlignment="Top"> <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" /> </Border> </Grid> 在 MainWindow.xaml.cs 中添加 public MainWindow() { this.InitializeComponent(); // 自定义标题栏 ExtendsContentIntoTitleBar = true; SetTitleBar(AppTitleBar); } 左侧添加按钮 如果只是在左侧添加按钮,可以直接使用 xaml 定义 在 MainWindow.xaml 添加 <Grid> <Grid.RowDefinitions> <RowDefinition MinHeight="48"/> <RowDefinition/> </Grid.RowDefinitions> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition MinHeight="auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Button Content="返回" Click="BackBtn_Click"/> <!-- 标题栏 --> <Border x:Name="AppTitleBar" Grid.Column="1" VerticalAlignment="Top"> <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" /> </Border> </Grid> </Grid> 添加自定义可点击内容 如果需要在中间添加可点击可输入内容,那么就需要这样做 在 MainWindow.xaml 添加 <Grid> <Grid.RowDefinitions> <RowDefinition MinHeight="48"/> <RowDefinition/> </Grid.RowDefinitions> <Border x:Name="AppTitleBar" VerticalAlignment="Top"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock x:Name="AppTitle" Text="{StaticResource AppTitleName}" VerticalAlignment="Top" Margin="0,8,0,0" /> <TextBox x:Name="SearchTb" Grid.Column="1" /> </Grid> </Border> </Grid> 在 MainWindow.xaml.cs 中添加 public MainWindow() { this.InitializeComponent(); // 自定义标题栏 ExtendsContentIntoTitleBar = true; SetTitleBar(AppTitleBar); var _baseWindowHandle = WindowNative.GetWindowHandle(this); var windowId = Win32Interop.GetWindowIdFromWindow(_baseWindowHandle); var _appWindow = AppWindow.GetFromWindowId(windowId); var dpiScale = Content.XamlRoot.RasterizationScale; var nonClientSource = InputNonClientPointerSource.GetForWindowId(_appWindow.Id); var inputPoint = SearchTb.TransformToVisual(Content).TransformPoint(new Point(0, 0)); nonClientSource.ClearRegionRects(NonClientRegionKind.Passthrough); nonClientSource.SetRegionRects(NonClientRegionKind.Passthrough, // 可以添加多个区域 [new( (int)Math.Round(inputPoint.X * dpiScale), (int)Math.Round(inputPoint.Y * dpiScale), (int)Math.Round(SearchTb.ActualWidth * dpiScale), // 请注意,要获取到可点击元素的显示尺寸才行 (int)Math.Round(SearchTb.ActualHeight * dpiScale) )]); } Command 绑定 使用 Command={TemplateBinding BackCommand} 可能无效 可以使用 Command="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=BackCommand}"

2023/11/21
articleCard.readMore

WPF MVVM 获取List 多选数据

WPF MVVM 获取List 多选数据 单选绑定 使用 SelectedIndex 或 SelectedItem 都可以 <ListBox x:Name="dataGrid1" SelectedIndex="{Binding SelectedIndex}" SelectedItem="{Binding SelectedItem}"> </ListBox> 多选绑定 要先添加依赖项 System.Windows.Interactivity.dll xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" <ListBox x:Name="dataGrid1"> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding SelectionChangeCommand}" CommandParameter="{Binding SelectedItems,ElementName=dataGrid1}"/> </i:EventTrigger> </i:Interaction.Triggers> </ListBox>

2023/11/21
articleCard.readMore

php 接入 WebAuthn 登录

php 接入 WebAuthn 登录 字节数据传输(关键问题) 解决方法:使用 base64 编解码; 接下来 需要解决 base64 算法不一致问题, 例如:浏览器自带的 js window.btoa(val: string): string // base64 编码 window.atob(val: string): string // base64 解码 例如:php 自带的 base64_encode(val: string): string // base64 编码 base64_decode(val: string): string // base64 解码 实际,webAuthn 一些数据是 ArrayBuffer 所以默认的就不行了 js base64 处理 class Base64 { private static chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; private static lookup = new Uint8Array(256); private static booted = false; private static ready() { if (this.booted) { return; } this.booted = true; for (let i = 0; i < this.chars.length; i++) { this.lookup[this.chars.charCodeAt(i)] = i; } } public static encode(arraybuffer: ArrayBuffer): string { const bytes = new Uint8Array(arraybuffer); const len = bytes.length; let base64 = ''; for (let i = 0; i < len; i += 3) { base64 += this.chars[bytes[i] >> 2]; base64 += this.chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; base64 += this.chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; base64 += this.chars[bytes[i + 2] & 63]; } if (len % 3 === 2) { base64 = base64.substring(0, base64.length - 1); } else if (len % 3 === 1) { base64 = base64.substring(0, base64.length - 2); } return base64; } public static decode(base64: string): ArrayBuffer { const len = base64.length; const bufferLength = len * 0.75; const arraybuffer = new ArrayBuffer(bufferLength); const bytes = new Uint8Array(arraybuffer); let p = 0; for (let i = 0; i < len; i += 4) { const encoded1 = this.lookup[base64.charCodeAt(i)]; const encoded2 = this.lookup[base64.charCodeAt(i + 1)]; const encoded3 = this.lookup[base64.charCodeAt(i + 2)]; const encoded4 = this.lookup[base64.charCodeAt(i + 3)]; bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } return arraybuffer; } public static toBuffer(val: string): ArrayBuffer { const items: number[] = []; for (let i = 0; i < val.length; i++) { items.push(val.charCodeAt(i)); } return new Uint8Array(items); } } php 实际只要实现一个 base64 解码就行了,其他的通过字符串传给前端,前端在转成 ArrayBuffer public static function decodeBase64(string $base64): string { static $lookup = []; if (empty($lookup)) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; for ($i = 0; $i < strlen($chars); $i ++) { $lookup[ord(substr($chars, $i, 1))] = $i; } } $len = strlen($base64); $maxLen = (int)floor($len * .75); $buffer = []; for ($i = 0; $i < $len; $i += 4) { $encoded1 = $lookup[ord(substr($base64, $i, 1))]; $encoded2 = $lookup[ord(substr($base64, $i + 1, 1))]; $encoded3 = $lookup[ord(substr($base64, $i + 2, 1))]; $encoded4 = $lookup[ord(substr($base64, $i + 3, 1))]; $buffer[] = chr(($encoded1 << 2) | ($encoded2 >> 4)); $buffer[] = chr((($encoded2 & 15) << 4) | ($encoded3 >> 2)); $buffer[] = chr((($encoded3 & 3) << 6) | ($encoded4 & 63)); } if (count($buffer) > $maxLen) { array_splice($buffer, $maxLen); } return implode('', $buffer); } 准备工作 一个 php 的 CBOR 解码库 composer install spomky-labs/cbor-php 一个 php 的 pem 转换库, 因为从浏览器获取的公钥是没法直接通过 openssl_get_publickey 加载的 这些都可以通过一个库解决,不过依赖库比较多 composer install web-auth/webauthn-framework 步骤 第一步,注册 Passkey, 在已登录的情况下,点击一个按钮进行注册 <button class="register-webauth">注册Passkey</button> $('.register-webauth').on('click',function() { if (!navigator.credentials) { return; } // 从后台获取注册需要数据,包含当前登录账户的id $.getJSON(baseUri + '/passkey/register_option', res => { const data = res.data; data.challenge = Base64.toBuffer(data.challenge); data.user.id = Base64.toBuffer(data.user.id); navigator.credentials.create({ publicKey: data }) .then((credential: any) => { const response = credential.response as AuthenticatorAttestationResponse; // 保存注册成功的 credentialId 和 公钥 $.post(baseUri + '/passkey/register', {credential: { id: credential.id, clientDataJSON: Base64.encode(response.clientDataJSON), attestationObject: Base64.encode(response.attestationObject), publicKeyAlgorithm: response.getPublicKeyAlgorithm(), }}, res => {}, 'json'); }) .catch(console.error); }); }).toggle(!!navigator.credentials); 注册需要的数据 passkey/register_option return [ 'challenge' => $challenge, // 随机的字符串,防止重复操作,需要保存,获取到 公钥后需要验证 'rp' => [ 'name' => Option::value('site_title'), 'id' => request()->host() ], 'user' => [ 'id' => (string)$user->getIdentity(), // 用户id 'name' => $user->email, // 用户邮箱 'displayName' => $user->name // 显示的名 ], 'pubKeyCredParams' => [[ 'alg' => -7, // ES256 公钥类型 'type' => 'public-key' ], [ 'type' => 'public-key', 'alg' => -257 // RS256 公钥类型,这个选项好像是必须的,不然可能不成功 ]], 'timeout' => $timeout * 1000, 'excludeCredentials' => [], 'attestation' => 'none', 'authenticatorSelection' => [ 'authenticatorAttachment' => "platform", "residentKey" => "preferred", 'requireResidentKey' => false, 'userVerification' => 'preferred' ], 'extensions' => [ 'credProps' => true ] ]; 保存注册成功的公钥 passkey/register public static function register(array $credential): void { $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON'])); if ($clientDataJSON['type'] !== 'webauthn.create') { throw new \Exception('type is error'); } $challenge = base64_decode($clientDataJSON['challenge']); // TODO 验证临时 $obj = static::parseAuthenticatorData($credential['attestationObject']); // 解码 attestationObject,获取 公钥 if (empty($obj) || empty($obj['publicKey'])) { throw new Exception('attestation is error'); } self::saveCredential($credential['id'], $obj['publicKey'], intval($credential['publicKeyAlgorithm'])); // TODO 保存公钥 } 登录页添加,使用 Passkey 登录的按钮 <button class="login-webauth">Passkey 登录</button> $('.login-webauth').on('click',function() { if (!navigator.credentials) { return; } // 从后台获取登录需要数据 $.getJSON(baseUri + '/passkey/login_option', res => { const data = res.data; data.challenge = Base64.toBuffer(data.challenge); navigator.credentials.get({ publicKey: data }) .then((credential: any) => { const response = credential.response as AuthenticatorAssertionResponse; // 获取登录结果,验证数据有效,根据id登录 $.post(baseUri + '/passkey/login', { credential: { id: credential.id, clientDataJSON: Base64.encode(response.clientDataJSON), authenticatorData: Base64.encode(response.authenticatorData), userHandle: Base64.encode(response.userHandle), signature: Base64.encode(response.signature) }, redirect_uri: $('[name=redirect_uri]').val() }, res => {}, 'json'); }) .catch(console.error); }); }).toggle(!!navigator.credentials); 登录需要的数据 passkey/login_option return [ 'challenge' => $challenge, // 防止重复操作的随机的字符串 'timeout' => $timeout * 1000, 'rpId' => request()->host(), 'allowCredentials' => [], 'userVerification' => 'preferred' ]; 验证登录数据,登录id passkey/login public static function login(array $credential): void { $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON'])); $challenge = base64_decode($clientDataJSON['challenge']); $key = sprintf('%s-%s', self::REGISTER_KEY, $challenge); if (!cache()->has($key)) { throw new \Exception('challenge is expired'); } $userId = base64_decode($credential['userHandle']); // 可以验证 credentialId 的 hash 值是否一致 $signature = $credential['signature']; self::loadCredential(intval($userId), $credential['id'], $signature, $credential); } /** * 验证的登录数据 * @param int $userId * @param string $credentialId * @param string $signature * @param array $credential * @return void * @throws \Exception */ protected static function loadCredential(int $userId, string $credentialId, string $signature, array $credential) { // 获取保存的 公钥 $key = ''; if (empty($key)) { throw new \Exception('验证失败'); } $data = CBOR::decodeBase64($credential['authenticatorData']); $pkey = openssl_get_publickey($key); if (empty($pkey)) { throw new Exception('public key is error'); } if (!openssl_verify($data.self::hash(CBOR::decodeBase64($credential['clientDataJSON'])), CBOR::decodeBase64($signature), $pkey, \OPENSSL_ALGO_SHA256)) { throw new \Exception('signature is error'); } // TODO 登录 } private static function hash(string $val): string { return \hash('sha256', $val, true); } 源码 PassKey主文件 CBOR PEM

2023/10/15
articleCard.readMore

Burp Suite 抓包

Burp Suite 抓包 http 打开 Proxy -> Proxy settings 确认代理地址 127.0.0.1:8080 打开 系统设置 -> 网络与Internet -> 代理 -> 手动设置代理 开启 使用代理服务器 地址和端口 填 Burp Suite Proxy settings 的代理地址 点击保存即可 使用浏览器打开一个网址 在 Burp Suite Proxy -> HTTP history 查看所有的请求响应内容 https 第1、2步 同上 使用浏览器打开 http:.//127.0.0.1:8080 页面,点击页面右上角的 CA Certificate 下载证书 打开浏览器(EDGE)的设置 搜索 证书, 点击管理证书,受信任的根证书颁发机构,导入 下载的证书 在证书列表中找到导入的证书 PortSwigger CA, 导出新的证书,再导入新的证书 使用浏览器打开一个网址 在 Burp Suite Proxy -> HTTP history 查看所有HTTPS的请求响应内容

2023/10/15
articleCard.readMore

lnmp php集成环境安装包使用

lnmp php集成环境安装包使用 安装 官网 screen -S lnmp # 如果命令不存在 先运行 yum install screen cd /usr/local # 进入php安装的目录 wget http://soft.vpser.net/lnmp/lnmp2.0.tar.gz -O lnmp2.0.tar.gz && tar zxf lnmp2.0.tar.gz && cd lnmp2.0 && ./install.sh lnmp 输入 mysql root 账户的密码 选择 mysql 和 php 的版本 等待安装完成 添加 站点 cd /usr/local/nginx/conf/vhost vi site.conf server { server_name zodream.cn; root /data/httpd/www; index index.html index.htm index.php; location / { autoindex on; } include enable-php.conf access_log /data/httpd/www/access_log/site.log; error_log /data/httpd/www/access_log/error.logcrit; listen 443 ssl; ssl_certificate /data/httpd/ssl/zodream.cn.pem; ssl_certificate_key /data/httpd/ssl/zodream.cn.key; } 使用命令 lnmp restart 重启整个环境 /etc/init.d/mysql restart /etc/init.d/nginx restart /etc/init.d/php-fpm restart 运维脚本 备份数据库和站点文件 编辑 /usr/local/lnmp2.0/tools/backup.sh ######~备份到哪里~###### Backup_Home="/home/backup/" ######~需要备份哪些文件夹~###### Backup_Dir=("/home/wwwroot/vpser.net" "/home/wwwroot/lnmp.org") ######~需要备份哪些数据库~###### Backup_Database=("lnmp" "vpser") ######~数据库root 的账户密码~###### MYSQL_UserName='root' MYSQL_PassWord='yourrootpassword' ######~是否需要备份到其他ftp地址,0 需要配置 ftp信息, 1 则是本地服务器,不需要ftp信息~###### Enable_FTP=1 验证站点是否崩溃挂掉了 编辑 /usr/local/lnmp2.0/tools/check502.sh ######~填入您的站点网址即可~###### CheckURL="http://www.xxx.com" 分割 nginx 日志文件 编辑 /usr/local/lnmp2.0/tools/cut_nginx_logs.sh ######~保存日志的文件夹~###### log_files_path="/home/wwwlogs/" ######~多个日志文件名~###### log_files_name=(access vpser licess) 定时执行脚本 确定安装了 crontab yum install vixie-cron crontabs chkconfig crond on service crond start 添加任务 crontab -e # 五分钟检测一次 */5 * * * * check502.sh >/dev/null 2>&1 # 凌晨自动切割nginx日志 0 0 * * * cut_nginx_logs.sh >/dev/null 2>&1 # 凌晨2点进行一次数据与文件备份 0 2 * * * backup.sh >/dev/null 2>&1 遇到的问题 主要查看 nginx 的 error_log 错误日志文件 Q: PHP message: PHP Warning: Unknown: open_basedir restriction in effect. File(/data/httpd/www/ecshop/index.php) is not within the allowed path(s): (/home/wwwroot/default/:/tmp/:/proc/) in Unknown on line 0 A:打开 nginx/conf/fastcgi.conf 配置文件,在 fastcgi_param PHP_ADMIN_VALUE "open_basedir=" 中添加站点执行文件的根目录,多个路径以:分隔,例如:fastcgi_param PHP_ADMIN_VALUE "open_basedir=$document_root/:/tmp/:/proc/:/data/httpd/www/phpMyAdmin";

2023/6/28
articleCard.readMore

js 进行在线编辑器开发

js 进行在线编辑器开发 基于 textarea 开发的 markdown 编辑器 textarea 最简单,但是可以编辑的内容也少。 需要的了解的知识 const element: HTMLTextAreaElement; element.selectionStart // 获取或设置选中的起始位置 element.selectionEnd // 获取或设置选中的结束位置 // 获取选中的文字 const v = element.value; const selectValue = v.substring(element.selectionStart, element.selectionEnd); // 替换选中的内容 const replace = ''; element.value = v.substring(0, element.selectionStart) + replace + v.substring(element.selectionEnd); // 移动光标到指定位置,移动光标到开始位置 element.selectionStart = 0 element.selectionEnd = 0 // 移动光标到结尾 element.selectionStart = element.value.length element.selectionEnd = element.value.length // 需要把焦点设置到元素,才会显示光标 element.focus(); textarea 只接受字符串,所以图片,超链接等需要使用 markdown 格式转成对应的字符串 基于 div 开发的 富文本编辑器 把div 设置为可编辑模式 <div contentEditable="true"></div> 需要了解的知识 默认换行是自动添加 '<div><br></div>' 获取选中 const element: HTMLDivElement; const sel = window.getSelection(); const range = sel.getRangeAt(0); // range 就是当前选中的内容了 range.startContainer // 选中的起始节点 range.startOffset // 在起始节点的具体位置 range.endContainer // 选中的结束节点 range.startOffset // 在结束节点的具体位置 // 当未选中任何内容时 range.startContainer === range.endContainer && range.startOffset === range.endOffset range.startContainer // 节点的类型,有 Text 、HtmlElement range.startOffset // 当 节点类型为 Text 时,则为字符串的位置, 当为 HtmlElement 时,则为在节点中子元素的位置, 例如0 则是元素的最前面 // 选中 区域 const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(selectRange); // 选中指定元素 const selectRange = document.createRange(); // 选中整个元素 selectRange.selectNodeContents(element); // 选中元素的一部分 selectRange.setStart(element, 0); selectRange.setEnd(element, 0); sel.removeAllRanges(); sel.addRange(selectRange); // 截断字符串节点 const node: Text node.splitText(offset) // 返回新创建的后一部分字符串的节点, 自动会添加到页面上的 基于 div 开发的 代码编辑器 相对来说,代码编辑器相对简单,就只要 显示对应行号、对部分字符串加样式即可,当然更高级的需要加代码提示候选就更复杂点了。 const element: HtmlDivElement; element.contentEditable = 'true'; 基本逻辑 对回车进行换行处理,实际就是在选择的地方切断,把后面一部分放到另一行 新增行,需要同时增加行号,及在行号列表中增加新的一行,并同时个更新后面的行号 对行的高度要跟行号的高度进行同步 缩进或者文字输入需要提取整行内容进行处理 let isComposition = false; // 判断是否是输入法输入 element.addEventListener('keydown', e => { }); element.addEventListener('keyup', () => { if (isComposition) { return; } }); element.addEventListener('compositionstart', () => { isComposition = true; }); element.addEventListener('compositionend', () => { isComposition = false; }); div 的尺寸变化要进行行号高度更新 let lastHeight = 0; const resizeObserver = new ResizeObserver(entries => { for (const item of entries) { if (item.contentRect.height === lastHeight) { continue; } if (lastHeight === 0) { // 这里的意思是, 显示隐藏切换时进行高度更新 this.updateLineNoStyle(); } lastHeight = item.contentRect.height; } }); resizeObserver.observe(element);

2023/5/16
articleCard.readMore

使用 indexnow 注意事项

无法删除提交网址 只有第一次需要验证key文件,所以请验证完成后立即删除这个文件。 请注意不要泄漏这个key,因为这个key 是没有是使用限制的,在其他地方也能使用这个key,可以通过搜索引擎的indexnow查看提交记录,如果确定不是自己提交的,需要马上生成一个新的,旧的就会失效。 生成key的网址就是提交的网址,例如:https//www.bing.com/indexnow,只要不使用就不会生效

2023/4/18
articleCard.readMore

Godot 使用字体图标 例如: Iconfont、FontAwesome

Godot 使用字体图标 例如: Iconfont、FontAwesome 新增一个控件 例如 IconLabl 新建一个场景,继承至用户界面,命名为 IconLabel 增加一个子控件 Label 设置Font .ttf 增加脚本 C# 版 using Godot; using System; using System.Text.RegularExpressions; [Tool] public partial class IconLabel : Control { private string text; [Export] public string Text { get { return text; } set { text = value; ApplyText(); } } private int fontSize = 16; [Export] public int FontSize { get { return fontSize; } set { fontSize = value; ApplyFontSize(); } } private Label IconTb; public override Vector2 _GetMinimumSize() { return new Vector2(FontSize, FontSize); } // Called when the node enters the scene tree for the first time. public override void _Ready() { IconTb = GetNode<Label>("Label"); ApplyFontSize(); ApplyText(); } private void ApplyFontSize() { if (IconTb is null) { return; } if (fontSize > 0) { IconTb.AddThemeFontSizeOverride("font_size", FontSize); } } private void ApplyText() { if (IconTb is null) { return; } if (string.IsNullOrWhiteSpace(Text)) { IconTb.Text = string.Empty; return; } var text = Regex.Replace(Text, @"(&#x|\\u)([0-9a-f]+);?", match => { return Convert.ToChar(Convert.ToInt32(match.Groups[2].Value, 16)).ToString(); }, RegexOptions.IgnoreCase); IconTb.Text = text; CustomMinimumSize = new Vector2(FontSize * text.Length, FontSize); } } 支持 Xaml 和 十六进制写法  \ue001

2023/4/11
articleCard.readMore

angular 15 对指定页面进行访问限制

angular 15 对指定页面进行访问限制 原本的访问控制是通过实现 CanActivate 接口完成的,当时并不支持 Observable 异步返回判断结果,现在终于实现了! 例如:需要登录才能访问 /** * 需要登录才能访问的页面控制 * @param _ * @param state * @returns */ export const CanActivateViaAuthGuard: CanActivateFn = (_, state) => { return inject(AuthService).canActivate(state.url); }; // 使用注册到路由上 const routes: Routes = [ { path: 'finance', canActivate: [CanActivateViaAuthGuard], component: HomeComponent }, ] @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class BackendModule {} 例如:需要登录且需要相关权限才能访问 /** * 需要有某种权限才能访问的页面控制 * @param roles * @returns */ export function CanActivateAuthRole(...roles: string[]): CanActivateFn { return (_, state) => { return inject(AuthService).canActivate(state.url, ...roles); } } // 使用注册到路由上 const routes: Routes = [ { path: 'finance', canActivate: [CanActivateAuthRole('admin')], component: HomeComponent }, ] @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class BackendModule {} AuthService 实现 @Injectable() export class AuthService { constructor( // 使用 ngrx-store 保存全局数据的 private store: Store<AppState>, // 生成登录网址 private router: Router, // 消息提示组件 private toastrService: DialogService) {} public canActivate(uri: string, ...roles: string[]) { return this.store .select(selectAuth) .pipe(map(res => { /* * res: { guest: boolean, // 是否是游客状态 roles: string[], // 保存当前用户的所有权限 } */ if (res.guest) { // 跳转到登录页面,并指定登录后的返回网址 return this.router.createUrlTree(['/auth'], {queryParams: {redirect_uri: uri}}); } if (roles.length === 0) { return true; } if (res.roles) { for (const item of roles) { if (res.roles.indexOf(item)) { return true; } } } this.toastrService.error('无权限访问'); return false; })); } } 完整代码 CanActivate AuthService Store

2023/4/2
articleCard.readMore

CSS 使用 column-count 实现瀑布流出现内容分割的解决办法

起因 使用 column-count 实现瀑布流时,出现内容被分割,大部分都在同一列,但是在一列的最后一个会出现部分内容被分割到了另一列。 .message-box { column-count: 2; column-gap: .8rem; } .message-list-item { display: block; } 第一种方法(推荐) .message-list-item { page-break-inside: avoid; } 原文链接 column-count瀑布流导致元素被截断-解决方法 第二种方法 .message-list-item { -webkit-column-break-inside: avoid; } 原文链接 关于column-count多列布局内容被切割在下列的解决方法以及瀑布流实现方式 第三种方法 .message-list-item { height: 100%; overflow: auto; } 原文链接 CSS3多列样式column布局内容被截断

2023/3/31
articleCard.readMore

angular 15 实现按下确认键,焦点移动到下一个表单或提交表单

angular 15 实现按下确认键,焦点移动到下一个表单或提交表单 需求:登录界面,输入账户后,按下回车键,焦点移动到密码输入框,再次按下回车键,直接提交表单 实现方法 import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core'; @Directive({ selector: '[appFocusNext]' }) export class FocusNextDirective implements OnInit, OnDestroy { static InputItems: FocusNextDirective[] = []; /** * 分组 */ @Input() public appFocusNext: any = 0; /** * 排序,越大越往后 */ @Input() public order = 0; constructor( private elementRef: ElementRef, ) { } @HostListener('keydown', ['$event']) onKeydown(e: KeyboardEvent) { if (e.key !== 'Enter') { return; } e.preventDefault(); e.stopPropagation(); FocusNextDirective.FocusNext(this); } ngOnInit(): void { FocusNextDirective.Add(this); } ngOnDestroy(): void { FocusNextDirective.Remove(this); } /** * 移动焦点到当前项 * @returns */ public focus() { if (!this.elementRef.nativeElement) { return; } const element = this.elementRef.nativeElement; const tagName = element.tagName.toLocaleLowerCase(); if (tagName === 'textarea' || tagName === 'select') { (element as HTMLTextAreaElement).focus(); return; } if (tagName === 'input') { const type = (element as HTMLInputElement).type.toLocaleLowerCase(); console.log(type); if (type === 'button' || type === 'submit' || type === 'reset') { element.click(); return; } (element as HTMLInputElement).focus(); return; } if (tagName === 'form') { (element as HTMLFormElement).submit(); return; } element.click(); } /** * 注册到可操作项 * @param item * @returns */ private static Add(item: FocusNextDirective) { if (this.InputItems.indexOf(item) >= 0) { return; } this.InputItems.push(item); } /** * 移除当前 * @param item */ private static Remove(item: FocusNextDirective) { const i = this.InputItems.indexOf(item); if (i >= 0) { this.InputItems.splice(i, 1); } } /** * 根据当前触发,移动焦点到下一项 * @param source */ private static FocusNext(source: FocusNextDirective) { let found = false; let next: FocusNextDirective = undefined; for (const item of this.InputItems) { if (item.appFocusNext !== source.appFocusNext) { continue; } if (item === source) { found = true; continue; } if (item.order < source.order || (!found && item.order === source.order)) { continue; } if (item.order === source.order && found) { next = item; break; } if (!next) { next = item; continue; } if (next.order > item.order) { next = item; continue; } } next?.focus(); } } 使用方法 <input appFocusNext> <input appFocusNext> <button appFocusNext>提交</button> <input appFocusNext="1"> <button appFocusNext="1" [order]="1">提交</button> <input appFocusNext="1"> 参数说明 appFocusNext 属性值是作为分组使用 order 作为排序用,数字越大排在后面,最后触发,默认按照初始化顺序 缺陷或不足 自定义的组件,暂时没有找到办法获取对象,只获取到了HTMLElement 不会验证表单的内容 不会自动触发上层表单

2023/3/31
articleCard.readMore

input 确认按键事件在手机端不生效

input 确认按键事件在手机端不生效 起因 需要在页面上做一个搜索框,单独加一个按钮显得多余,所以直接使用确认键跳转 一般做法 <input type="text" name="keywords" onkeydown="onKeydown(event)"> function onKeydown(event: KeyboardEvent) { if (event.key === 'Enter') { // TODO return; } } 这种做法就只有一个输入框,通过代码实现按键完成事件触发 在PC浏览器中是可以触发的,但在手机端没有触发事件, 通过单独调试,在手机端依然执行了方法,也获取到了event.key, 但是在项目就是没有触发的事件 原因 当页面存在多个表单项时,手机端的确认键会自动移动焦点到下一个表单项,只有最后一个才会有效执行,如果不在form 标签中,则时自动查找整个网页, 所以,实际上只有最后一个表单项,才能实际接受到 event.key === 'Enter' 通过 Form 自带事件实现 <form onsubmit="//TODO"> <input type="text" name="keywords"> </form> 这样就可以保证手机也可以支持,这个实际就是:表示 input 就是最后一项,可以确认了 input search <input type="search" name="keywords"> html5 自带一个搜索输入框,可以在手机键盘确认显示为搜索 自带一个清除图标和功能 input::-ms-clear { display: none; } 通过 css 可以移除这个 同样的还有 密码框的 小眼睛 input[type="password"]::-ms-reveal { display: none; }

2023/3/18
articleCard.readMore

C# 使用socket 进行通讯

基于UDP 发送 // 本机ip和端口 var localIp = new IPEndPoint(IPAddress.Parse(ip), port); var udpSocket = new Socket(serverIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp); udpSocket.Bind(localIp); public void Send(string ip, int port, byte[] buffer) { var remote = new IPEndPoint(IPAddress.Parse(ip), port); udpSocket.SendTo(buffer, remote); } 数据必须一次发送,不能分段发送,不然顺序会混乱,数据大小也要注意,因为接收时只能接收一次,多出的数据就会接收不到了 接收 // 本机ip和端口 var localIp = new IPEndPoint(IPAddress.Parse(ip), port); var udpSocket = new Socket(serverIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp); udpSocket.Bind(localIp); var buffer = new byte[65536]; EndPoint sendIp = new IPEndPoint(IPAddress.Any, port); var length = udpSocket.ReceiveFrom(buffer, Constants.UDP_BUFFER_SIZE, SocketFlags.None, ref sendIp); TCP 发送 var remoteIp = new IPEndPoint(IPAddress.Parse(ip), port); var socket = new Socket(clientIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp); socket.Connect(remoteIp); var buffer = new byte[8]; socket.Send(buffer); private void Send(byte[] buffer, int length) { var index = 0; while (index < length) { var size = socket.Send(buffer, index, length - index, SocketFlags.None); index += size; } } 请注意,发送数据可以分多次,因为是保持顺序的,不会乱的,接收端也可以分多次接收,但是,不能保证一次发送全部数据,最好封装一个方法,保证数据全部发送了 接收 第一步接收每一个连接 var localIp = new IPEndPoint(IPAddress.Parse(ip), port); var tcpSocket = new Socket(serverIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp); tcpSocket.Bind(localIp); tcpSocket.Listen(10); while (true) { var client = tcpSocket.Accept(); } 第二步,从每一个连接中接收数据 var buffer = new byte[8]; client.Receive(buffer); private byte[] Receive(int length) { var buffer = new byte[length]; var index = 0; while (index < length) { var size = client.Receive(buffer, index, length - index, SocketFlags.None); index += size; } return buffer; } 接收数据也会出现一次性接收不完,要分几次才能接收一段数据,最好是保持一问一答的方式发送和接收数据,保证数据的完整,不会出现丢包的问题

2023/3/5
articleCard.readMore

Maui开发中Windows应用开启管理员权限

Maui开发中Windows应用开启管理员权限 首先打开文件夹 Platforms/Windows 在文件 app.manifest 中添加 <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'> <security> <requestedPrivileges xmlns='urn:schemas-microsoft-com:asm.v3'> <requestedExecutionLevel level='requireAdministrator' uiAccess='false' /> </requestedPrivileges> </security> </trustInfo> 然后再文件 Package.appxmanifest(右键->查看代码) 中添加 <Capabilities> <rescap:Capability Name="runFullTrust" /> <rescap:Capability Name="allowElevation" /> </Capabilities>

2023/2/28
articleCard.readMore

Maui 中自定义控件

Maui 中自定义控件 在 Maui 中自定义控件分为两种方式。 一种是纯代码构建的,在一个cs文件中 另一种是模板分离,分为xaml和cs代码文件 第一种直接通过代码创建控件,并赋予属性,没法在外部修改模板 第二种分为xaml和cs文件,在xaml 中定义模板,cs 写属性,类似于WPF中的 UserControl 但又更像是 CustomControl 具体说明 <?xml version="1.0" encoding="utf-8" ?> <ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ZoDream.FileTransfer.Controls.MessageTipListItem"> <ContentView.ControlTemplate> <ControlTemplate> <Label Text="{TemplateBinding Text}" VerticalOptions="Center" HorizontalOptions="Center" Padding="0,10"/> </ControlTemplate> </ContentView.ControlTemplate> <Button Text="121"> </ContentView> 这里分为两部分,一部分是放在 ContentView.ControlTemplate 中,通过 TemplateBinding 获取自定义属性,类似于 WPF 中 CustomControl 定义在 Themes/Generic.xaml 中 Style.Template, 用法也类似 特别注意,直接作为 ContentView.Content 的 Button,这里写的控件是无法获取当前控件的属性的,如果在 ContentView.ControlTemplate中没有使用 ContentPresenter 接收,就不会显示,默认 ContentView.ControlTemplate 就有一个 ContentPresenter 在 cs 中通过 BindableProperty 创建定义属性 public string Title { get { return (string)GetValue(TitleProperty); } set { SetValue(TitleProperty, value); } } // Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc... public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(DialogPanel), string.Empty); public ICommand TapCommand { get { return (ICommand)GetValue(TapCommandProperty); } set { SetValue(TapCommandProperty, value); } } // Using a DependencyProperty as the backing store for YesCommand. This enables animation, styling, binding, etc... public static readonly BindableProperty TapCommandProperty = BindableProperty.Create(nameof(TapCommand), typeof(ICommand), typeof(DialogPanel), null);

2023/2/28
articleCard.readMore

TencentOS Server 3.1 安装 Nginx 1.23、PHP 8.2、MariaDB 10.11

和 CentOS 的命令一样 一些其他辅助命令 yum list installed #查询已安装程序 ps auxf |grep nginx # 查询正在运行进程 Nginx 安装基本编译环境 sudo yum -y install gcc gcc-c++ # nginx编译时依赖gcc环境 sudo yum -y install pcre pcre-devel # 让nginx支持重写功能 sudo yum -y install zlib zlib-devel # zlib库提供了很多压缩和解压缩的方式,nginx使用zlib对http包内容进行gzip压缩 sudo yum -y install openssl openssl-devel # 安全套接字层密码库,用于通信加密 下载源码包 下载 pcre2 下载 zlib 源码包放到 /usr/local/src 下解压 cd /usr/local/src #进入src文件夹 wget https://nginx.org/download/nginx-1.23.3.tar.gz #下载nginx源码包 解压三个源码压缩包 tar -zxvf pcre2-10.42.tar.gz # 解压缩 tar -zxvf zlib-1.2.13.tar.gz tar -zxvf nginx-1.23.3.tar.gz 编译Nginx cd nginx-1.23.3 ./configure --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf --pid-path=/usr/local/nginx/logs/nginx.pid --with-http_ssl_module --with-pcre=../pcre2-10.42 --with-zlib=../zlib-1.2.13 make #编译 make install #安装 如果后面遇到某个功能没有,只要修改 configure 中的参数就行了,重复执行 make make install 即可 配置启动Nginx /usr/local/nginx/sbin/nginx #启动服务 /usr/local/nginx/sbin/nginx -s reload #重新加载服务 /usr/local/nginx/sbin/nginx -s stop #停止服务 ps -ef | grep nginx #查看服务进程 更改配置 vi /usr/local/nginx/conf/nginx.conf # 这是站点配置文件 开始设置开启启动项 vi /usr/lib/systemd/system/nginx.service [Unit] Description=nginx After=network.target [Service] Type=forking ExecStart=/usr/local/nginx/sbin/nginx ExecStop=/usr/local/nginx/sbin/nginx -s stop ExecReload=/usr/local/nginx/sbin/nginx -s reload PrivateTmp=true [Install] WantedBy=multi-user.target 启用开机启动服务 systemctl enable nginx.service 最终的启动运行命令 service nginx start service nginx stop service nginx restart service nginx reload PHP 8 安装基本编译环境 yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel sqlite-devel gmp-devel oniguruma-devel readline-devel libxslt-devel 安装CMake wget https://github.com/Kitware/CMake/releases/download/v3.25.2/cmake-3.25.2.tar.gz tar -zxvf cmake-3.25.2.tar.gz cd cmake-3.25.2 ./bootstrap gmake ln -s /usr/local/src/cmake-3.25.2/bin/cmake /usr/bin/cmake cmake --version 安装libzip 必须先安装CMake 下载 libzip yum remove libzip wget https://libzip.org/download/libzip-1.9.2.tar.gz tar -zxvf libzip-1.9.2.tar.gz cd libzip-1.9.2 mkdir build cd build cmake .. make make install 安装完成后,查看是否存在/usr/local/lib64/pkgconfig目录,如果存在,执行如下命令来设置PKG_CONFIG_PATH: export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig/" 下载源码包 cd /usr/local/src wget https://www.php.net/distributions/php-8.2.3.tar.gz # 下载 tar -zxvf php-8.2.3.tar.gz # 解压缩 编译安装 cd php-8.2.3 ./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --enable-mbstring --enable-ftp --enable-gd --enable-gd-jis-conv --enable-mysqlnd --enable-pdo --enable-sockets --enable-fpm --enable-xml --enable-soap --enable-pcntl --enable-cli --enable-bcmath --with-openssl --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --with-zlib --with-iconv --with-curl --with-zip --with-gettext make #编译 make install #安装 修改配置文件 cd /usr/local/php/etc cp php-fpm.conf.default php-fpm.conf cd /usr/local/php/etc/php-fpm.d cp www.conf.default www.conf find /usr/local/src/php-8.2.3 -name php.ini* cp /usr/local/src/php-8.2.3/php.ini-production /usr/local/php/etc/php.ini vim /usr/local/php/etc/php.ini 将 ;cgi.fix_pathinfo=1 改为 cgi.fix_pathinfo=0 将 expose_php = On 改为 expose_php = Off 隐藏版本号 vim /usr/local/php/etc/php-fpm.conf 将 [global] 下的 pid = run/php-fpm.pid 启用, 后面 php-fpm.service 中的 PIDFile 必须设成一样的路径,否则会出现找不到php-fpm.pid的情况 vim /usr/local/php/etc/php-fpm.d/www.conf 将 user = nobody group = nobody 改为 user = www group = www 需要先添加 www 用户和 www 组 初始命令 /usr/local/php/sbin/php-fpm -R # 这里后面带个 -R 表示用root 用户启动 /usr/bin/pkill -9 php-fpm pstree -p | grep php 设置开机启动项 vi /usr/lib/systemd/system/php-fpm.service [Unit] Description=php-fpm After=network.target [Service] Type=forking PIDFile=/usr/local/php/var/run/php-fpm.pid ExecStart=/usr/local/php/sbin/php-fpm ExecStop=/usr/bin/pkill -9 php-fpm PrivateTmp=true [Install] WantedBy=multi-user.target systemctl enable php-fpm.service 可以使用systemctl命令管理php-fpm: systemctl start php-fpm.service #启动 systemctl stop php-fpm.service #停止 service php-fpm start service php-fpm stop service php-fpm restart service php-fpm reload 搭配Nginx运行PHP站点 配置 Nginx vi /usr/local/nginx/conf/nginx.conf 在 http 下添加 client_max_body_size 200M; #配置客户端请求体最大值 client_body_buffer_size 50m; #配置请求体缓存区大小 server_tokens off; # 隐藏响应头中的版本号 gzip on; gzip_min_length 1k; #不压缩临界值,大于1k的才压缩 gzip_buffers 4 16k; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript text/css application/font-woff; # 需要压缩什么文件就加上文件类型,对于图片还是不要使用gzip压缩,直接在本地修改成其他图片格式效果更好 server { server_name zodream.cn; root /data/www/html; # 设置站点根目录 location / { # root html; index index.php index.html index.htm; try_files $uri $uri/ /index.php?$query_string; # 美化和伪静态需要设置路由重写 } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } ## 配置 xxsqladmin 路径指向phpmyadmin location ~ ^/xxsqladmin/.*\.php$ { root /data/shop/phpmyadmin; set $real_script_name $fastcgi_script_name; if ($fastcgi_script_name ~ "^/xxsqladmin(.+?\.php)(.*)$") { set $real_script_name $1; } fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$real_script_name; include fastcgi_params; } ## 配置 xxsqladmin 资源文件路径指向phpmyadmin location ~ ^/xxsqladmin.* { root /data/shop/phpmyadmin; rewrite ^/xxsqladmin(.*)$ /$1 break; } # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # location ~ /\.ht { deny all; } # 添加https支持 listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/zodream.cn/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/zodream.cn/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } 修改完保存文件并重启Nginx服务器 MariaDB 安装 yum install mariadb-server 如果本地已安装 mayql 则会出现冲突 增加 参数即可 --allowerasing 启动和设置开机启动 systemctl start mariadb # 开启服务 systemctl enable mariadb # 设置为开机自启动服务 设置修改密码 mysql_secure_installation 使用 Certbot 获取 letsencrypt 证书 yum install certbot python3-certbot-nginx certbot --nginx 如果出现 找不到 nginx ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx ln -s /usr/local/nginx/conf/ /etc/nginx 新增用户 cat /etc/passwd # useradd -s /sbin/nologin -M www groupadd www useradd -g www www -s /sbin/nologin 赋予权限 chown -R www:www /run/php-fpm PHP拓展安装 imagick sudo yum -y install ImageMagick-devel cd /usr/local/src tar xvf imagick-3.7.0.tgz cd imagick-3.7.0 /usr/local/php/bin/phpize ./configure --with-php-config=/usr/local/php/bin/php-config make make install php.ini extension=imagick.so redis 安装 redis 主程序 cd /usr/local/src wget https://download.redis.io/redis-stable.tar.gz tar -xzvf redis-stable.tar.gz cd redis-stable make make install redis-server 现在,我们新建目录 /usr/local/redis ,把./redis.conf,src/redis-server,src/redis-cli 三个文件复制到该目录下 mkdir /usr/local/redis cp redis.conf src/redis-server src/redis-cli /usr/local/redis/ cd /usr/local/redis vi redis.conf daemonize yes # 开启守护线程 masterauth <redis_password> # 设置密码 启动 ./redis-server redis.conf 安装 phpredis cd /usr/local/src tar xvf redis-5.3.7.tgz cd redis-5.3.7 /usr/local/php/bin/phpize ./configure --with-php-config=/usr/local/php/bin/php-config make make install php.ini extension=redis.so 安装多版本php 7 下载源码包 cd /usr/local/src wget https://www.php.net/distributions/php-7.4.33.tar.gz # 下载 tar -zxvf php-7.4.33.tar.gz # 解压缩 编译安装 -–prefix 是安装目录,--with-config-file-path 是配置文件存放目录 cd php-7.4.33 ./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --enable-mbstring --enable-ftp --enable-gd --enable-gd-jis-conv --enable-mysqlnd --enable-pdo --enable-sockets --enable-fpm --enable-xml --enable-soap --enable-pcntl --enable-cli --enable-bcmath --with-openssl --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --with-zlib --with-iconv --with-curl --with-gettext make #编译 make install #安装 修改配置文件 同原本的配置方法 [PHP8配置]() 注意将 /usr/local/php7/etc/php-fpm.d/www.conf 中的端口改成其他的,不要和原本的冲突 listen = 127.0.0.1:9001 设置开机启动项 vi /usr/lib/systemd/system/php7-fpm.service [Unit] Description=php7-fpm After=network.target [Service] Type=forking PIDFile=/usr/local/php7/var/run/php-fpm.pid ExecStart=/usr/local/php7/sbin/php-fpm ExecStop=/usr/bin/pkill -9 php7-fpm PrivateTmp=true [Install] WantedBy=multi-user.target systemctl enable php7-fpm.service 可以使用systemctl命令管理php7-fpm: service php7-fpm start service php7-fpm stop service php7-fpm restart service php7-fpm reload 配置站点 /usr/local/nginx/conf/nginx.conf 将 fastcgi_pass 配置成PHP7监听的端口 server { location ~ \.php$ { fastcgi_pass 127.0.0.1:9001; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 启动PHP7,重启nginx 即可

2023/2/20
articleCard.readMore

angular 14 使用 ng-template 实现tree 结构显示

angular 14 使用 ng-template 实现tree 结构显示 美化版 当然是使用两个自定义控件 <tree> <tree-node></tree-node> </tree> 利用 ng-template 标签 <ul class="tree-box"> <ng-container *ngFor="let item of items"> <ng-container *ngTemplateOutlet="fileItemTpl;context: {$implicit: item}"></ng-container> </ng-container> </ul> <ng-template #fileItemTpl let-file> <ng-container *ngIf="file"> <ng-container *ngIf="file.type < 1"> <li class="tree-parent" [ngClass]="{open: file.open}"><div class="name" (click)="toggleOpen(file)">{{ file.name }}</div> <ul *ngIf="file.children"> <ng-container *ngFor="let it of file.children"> <ng-container *ngTemplateOutlet="fileItemTpl;context: {$implicit: it}"></ng-container> </ng-container> </ul> </li> </ng-container> <ng-container *ngIf="file.type > 0"> <li><div class="tree-name">{{ file.name }}</div></li> </ng-container> </ng-container> </ng-template> 了解 ng-template 使用,需要搭配 *ngTemplateOutlet 使用 具体: ng-template 的数据传递,通过 *ngTemplateOutlet 的 context: 传递对象, 其中 $implicit 没默认名称 ng-template 的数据获取,通过 let- 获取,例如: let-file 的意思就是:在 template 中 const file = $implicit, 相当于完整写法 let-file="$implicit"

2022/8/16
articleCard.readMore

angular 14 替换 ComponentFactoryResolver 实现动态创建组件

angular 14 替换 ComponentFactoryResolver 实现动态创建组件 需求 移植弹出框到 angular 项目中。 方案 直接使用其他项目依赖 比较常用的有以下两个 ngx-toastr ng-bootstrap 但是通过查看源码,发现都使用使用 ComponentFactoryResolver 来实现组件的新建,然后通过document.body.appendChild 添加到页面上的, 但是,angular 文档中提示,ComponentFactoryResolver 不推荐使用。 Deprecated: Angular no longer requires Component factories. Please use other APIs where Component class can be used directly. Note: since v13, dynamic component creation via ViewContainerRef.createComponent does not require resolving component factory: component class can be used directly. 自己实现 没有了 ComponentFactoryResolver, 那么就使用 ViewContainerRef.createComponent 主要思路: 新建一个容器组件,获取到 ViewContainerRef @Component({ selector: 'app-dialog-container', template: '', styles: [''], }) export class DialogContainerComponent { constructor( private service: DialogService, private viewContainerRef: ViewContainerRef, ) { this.service.containerRef = this.viewContainerRef; } } 新建一个全局的服务提供者, interface IDialogRef { id: any; element: ComponentRef<any>; } @Injectable({ providedIn: 'root' }) export class DialogService { private dialogItems: IDialogRef[] = []; public containerRef: ViewContainerRef; constructor( private injector: Injector, ) { } /** * 加载loading * @param option * @returns loading 的id, 使用 remove(id: any) 进行关闭 */ public loading(option?: DialogLoadingOption): any { option = Object.assign({}, option, { time: 2000, closeable: true, }); return this.createDailog(DialogLoadingComponent, option); } /** * 创建组件 * @param component * @param option * @returns */ private createDailog<T>(component: Type<T>, option: any): any { const dialogId = ++ DialogService.guid; if (!this.containerRef) { return; } const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector); const dialogRef = this.containerRef.createComponent(component, { injector: dialogInjector }); this.dialogItems.push({ id: dialogId, element: dialogRef }); return dialogId; } /** * 删除组件 * @param i */ private removeAt(i: number) { const item = this.dialogItems[i]; this.dialogItems.splice(i, 1); const dialogRef = item.element; dialogRef.destroy(); } } 作为对比,ViewContainerRef.createComponent 比 ComponentFactoryResolver 使用更简单,而且更符合 angular 特色 constructor( private resolver: ComponentFactoryResolver, private applicationRef: ApplicationRef, private injector: Injector, @Inject(DOCUMENT) private document: Document, ) { } // 添加组件 const dialogFactory = this.resolver.resolveComponentFactory(component); const dialogRef = dialogFactory.create(dialogInjector); this.applicationRef.attachView(dialogRef.hostView); this.document.body.appendChild(dialogRef.location.nativeElement); // 删除组件 this.applicationRef.detachView(dialogRef.hostView); this.document.body.removeChild(dialogRef.location.nativeElement); this.dialogItems.splice(i); 在组件内部实现删除功能 export class DialogMessageComponent implements OnDestroy { constructor( private data: DialogPackage<DialogMessageOption>, private service: DialogService, ) { } @HostListener('click') public close() { this.service.remove(this.data.dialogId); } } 以模块的方式提供 @NgModule({ imports: [ CommonModule, ], declarations: [ ...COMPONENTS ], exports: [ ...COMPONENTS ], }) export class DialogModule { static forRoot(): ModuleWithProviders<DialogModule> { return { ngModule: DialogModule, providers: [ DialogService ] }; } } 使用 第一步,在 app.module.ts 中导入 DialogModule.forRoot(), 第二步,在 app.component.ts 页面中添加容器 <app-dialog-container></app-dialog-container> 第三步,使用 constructor( private toastrService: DialogService) { this.toastrService.loading(); } 总结 ViewContainerRef.createComponent 比 ComponentFactoryResolver 使用更简单,而且更符合 angular 特色,始终保持所有创建的内容在框架内。 完整代码请查看 Angular-ZoDream

2022/6/30
articleCard.readMore

c# 动态安装和卸载dll

c# 动态安装和卸载dll 需求 自动加载指定文件夹下的所有dll文件 第一种方法使用其他框架MAF或MEF 自定义代码 public class PluginLoadContext: AssemblyLoadContext { private readonly AssemblyDependencyResolver _resolver; public PluginLoadContext(string pluginPath) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly? Load(AssemblyName assemblyName) { var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) { return LoadUnmanagedDllFromPath(libraryPath); } return IntPtr.Zero; } } public void LoadDll(string path) { var loadContext = new PluginLoadContext(path); var assem = loadContext.LoadFromAssemblyName(AssemblyName.GetAssemblyName(path)); if (assem == null) { return; } var types = assem.GetTypes(); // TODO 获取需要的类 } 可能出现的问题 Q: 在主程序中使用 typeof(IRule).IsAssignableFrom(types[0]) 判断是否继承至公共接口,会出现 false 的情况 A:说明 LoadDll 加载dll 时又引入了公共类库,只需要在 dll 解决方案下的依赖项属性中设置 复制本地设为否(CopyLocal=false) 即可,或者不要把公共类库复制放到dll所在额文件夹下 IsAssignableFrom() returns false when it should return true

2022/6/15
articleCard.readMore

慎用 CompositionTarget.Rendering

慎用 CompositionTarget.Rendering 在WPF中使用 private void RollLabel_Unloaded(object sender, RoutedEventArgs e) { CompositionTarget.Rendering -= CompositionTarget_Rendering; } private void RollLabel_Loaded(object sender, RoutedEventArgs e) { CompositionTarget.Rendering += CompositionTarget_Rendering; } private void CompositionTarget_Rendering(object? sender, EventArgs e) { InvalidateVisual(); } 虽然保证了动画看上去更丝滑,但这个是按帧执行的,具体一秒多少帧取决于电脑支持的最大帧数。 但是很占用GPU,一个小的移动的动画使用CompositionTarget.Rendering就直接占用60%的GPU 跟 DispatcherTimer 相比较 优势 同样的帧数,占用同样的GPU,明显 CompositionTarget 比 DispatcherTimer 的动画更流畅,DispatcherTimer有明显的卡顿感。 劣势 DispatcherTimer 支持自定义时间间隔,可以减少帧数来减少GPU占用

2022/5/14
articleCard.readMore

c# 重写 c++ 程序笔记:数据初始化

c# 重写 c++ 程序笔记:数据初始化 std::uint32_t 初始化 c++ 数组定义 std::array<std::uint32_t, 7> data; 转成c# 数组 var data = new uint[7]; for (int i = 0; i < 7; i++) { data[i] = 0xcccccccc; } std::uint8_t 初始化 c++ 数组定义 std::array<std::uint8_t, 7> data; 转成c# 数组 var data = new byte[7]; for (int i = 0; i < 7; i++) { data[i] = 0xcc; }

2022/5/14
articleCard.readMore

源码编译 aseprite

源码编译 aseprite 环境 操作系统:windows 11 开发工具:TortoiseGit、Visual Studio 2022 Preview 步骤 通过 Visual Studio Installer 安装 Visual Studio Community 2022, 选择 工作负荷 中的 使用 c++ 的桌面开发,右侧的安装详细信息中勾选 Windows 11 SDK,下载安装即可 通过浏览器访问 aseprite 源码克隆到本地即可、或直接使用命令行克隆代码 git clone --recursive https://github.com/aseprite/aseprite.git 下载 skia 最新文件 Skia-Windows-Release-x64.zip,解压到一个文件夹即可 在 aseprite 的源码文件夹下打开cmd 使用命令安装一些一拉模块,如果出现下载失败,重复执行即可,如果还是失败,检查 laf third_party 文件夹下是否只有一个 .git 文件,删除重试命令即可 git submodule update --init --recursive 在aseprite下新建文件夹 build 获取使用命令,在 build 文件夹下打开 cmd mkdir build cd build 输入命令,注意直接从 vs2022 的工具菜单进入命令行是 x86模式,注意:只要找到 vs 的安装目录下的 Common7\Tools\VsDevCmd.bat 文件即可, call "D:\Microsoft Visual Studio\2022\Preview\Common7\Tools\VsDevCmd.bat" -arch=x64 输入命令,D:\Aseprite\Skia 即第三步下载的 skia 解压的文件夹 cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLAF_BACKEND=skia -DSKIA_DIR=D:\Aseprite\Skia -DSKIA_LIBRARY_DIR=D:\Aseprite\Skia\out\Release-x64 -DSKIA_LIBRARY=D:\Aseprite\Skia\out\Release-x64\skia.lib -G Ninja .. 这一步基本不会出什么问题,如果有问题那就是 第五步 安装模块时不完整,删除出错目录下的文件,执行第五步的命令即可 输入命令生成最终文件 ninja aseprite 这一步可能出错,我的是失败在 FIALLED json11,需要更改 third_party/json11/CMakeLists.txt 文件,删除第27行或者改为 if (NOT MSVC) target_compile_options(json11 PRIVATE -fPIC -fno-rtti -fno-exceptions -Wall) endif() 重新执行ninja aseprite命令即可, 生成成功后复制 build/bin 下的 aseprite.exe 主程序 和 data 文件夹即可,这两个文件就是aseprite的运行文件

2022/4/24
articleCard.readMore

记录一下字符串分隔split各语言之间的不同

今天才发现不同编程语言对字符串的 split 是有差距的 js 'a:b:c:d'.split(':', 2); // 输出结果为 ['a','b'] 是先把所有的都拆分成数组,然后取前面的几个 c# "a:b:c:d".Split(":", 2); // 输出结果为 ['a','b:c:d'] 是拆分前几个,剩余的都原样放在最后一个 php explode(':', 'a:b:c:d', 2); // 输出结果为 ['a','b:c:d']

2022/4/21
articleCard.readMore

c# Gzip解码无头内容

依赖 使用NuGet安装 SharpZipLib 主要代码 using ICSharpCode.SharpZipLib.Zip.Compression; byte[] data; // 要解码内容 using var outputStream = new MemoryStream(); var inflater = new Inflater(); try { inflater.SetInput(data); var buffer = new byte[2048]; while (!inflater.IsFinished) { var count = inflater.Inflate(buffer); outputStream.Write(buffer, 0, count); } } catch (Exception) { inflater.Reset(); } byte[] res = outputStream.ToArray(); //解码结果 与 php gzuncompress 比较 gzuncompress 主要是不必考虑有无gzip头,都能进行解码 而 SharpZipLib 中默认的解码方法 GZipInputStream 并不支持无头内容,

2022/4/6
articleCard.readMore

Windows 10 查看内存占用

Windows 10 查看内存占用 查看内存被哪些进程占用 通过自带的任务管理器查看详细信息即可。 内存占用不正常怎么办? 即在任务管理器中进程占用的内存之和远小于实际被占用的内存。 可以通过 RAMMap 或 vmmap 这两个工具查看。 vmmap 这是查看某一个进程已提交虚拟内存类型的明细。 RAMMap 准确地了解 Windows 如何分配物理内存、在 RAM 中缓存的文件数据量,或者内核和设备驱动程序使用了多少内存。 可以完整的看出内存用到哪里去了! PoolMon 可以查看哪些驱动使用的内存情况。 案例 每次使用Steam 或 Epic 下载游戏时,内存占用越来越高,最后占用99%之后电脑卡死,关闭程序无用,只能重启。 分析 任务管理器 中进程查看不到占用大量内存的进程。 使用RAMMap查看到大量内存被 Nonpaged Pool 占用。 使用 poolmon 执行命令 poolmon.exe /p /d 发现被一个 Tag 为 wfpn 的驱动占用了大量内存。 通过 Bing 搜索 poolmon wfpn 找到了 wfpn 为 Killer Network Manager 网络驱动 解决方法 更新 Killer Network Manager 驱动即可;或禁用 NDNB 参考 Memory leak?

2022/3/5
articleCard.readMore

UWP 使用 win2d:加阴影

加阴影 var bitmap = new CanvasRenderTarget(Control, (float)Width, (float)Height, 96); var effect = new Transform2DEffect() { Source = new ShadowEffect() { Source = bitmap, BlurAmount = 2, }, TransformMatrix = Matrix3x2.CreateTranslation(3, 3) }; private void DrawerCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args) { args.DrawingSession.DrawImage(effect); } 无论内容是什么输出的时一个黑色的框框带阴影效果, 应该加代码输出原版的图像,即可 private void DrawerCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args) { args.DrawingSession.DrawImage(effect); args.DrawingSession.DrawImage(bitmap); }

2021/10/22
articleCard.readMore

清除 PowerShell 历史记录

使用的命令过多,该清理一下不要的命令了。 Remove-Item (Get-PSReadlineOption).HistorySavePath

2021/9/20
articleCard.readMore

c# 调用 c++ 的dll

c# 调用 c++ 的dll c++ 程序的效率比 c# 的效率高很多 第一步,新建“c++动态链接库”项目 第二步,添加方法 struct KeyItem { std::uint32_t x, y, z; }; extern "C" _declspec(dllexport) KeyItem FindKey(char* zipFile, char* zipFileName, char* plainFile, char* plainFileName) { } 第三步,c#调用 复制 dll 到生成目录 [StructLayout(LayoutKind.Sequential)] struct KeyItem { public uint x, y, z; } public static class CrackerDLL { [DllImport("cracker.dll", EntryPoint = "FindKey", CallingConvention = CallingConvention.Cdecl)] internal static extern KeyItem FindKey(string zipFile, string zipFileName, string plainFile, string plainFileName); } 第四步,使用 var res = CrackerDLL.FindKey("c.zip", "c.txt", "plain.zip", "plain.txt"); res.x res.y res.z 注意 生成平台必须选择一样的 x64 或 x86,不能使用 Any CPU,否则会报错

2021/9/20
articleCard.readMore

c# 重写 c++ 程序笔记:遍历

c# 重写 c++ 程序笔记:遍历 倒序遍历 c++ std::vector<byte> items item.begin() item.end() item.rbegin() item.rend() 转 c# IList<byte> items 0 items.Count -1 items.Count -1 遍历c++ for(std::vector<byte>::const_iterator p = items.begin(); p != items.end(); ++p) { *p } 转 c# for (var i = 0; i < items.Count; i ++) { items[i] } 倒序遍历c++ using rit = std::reverse_iterator<std::vector<byte>::const_iterator>; for(rit p = rit(items.begin()); p != items.rend(); ++p) { *p } 转 c# for (var i = -1; i != items.Count - 1; i --) { items[i] } 我的理解: std::reverse_iterator 实际上是把输入的位置往前移一位,并把 + 转成 -,方向反一下

2021/9/20
articleCard.readMore

Net Core 与 UWP 共用类开发

开发环境 VS 2022 需求 一个应用程序分:桌面版和UWP版,实现一个类库能被两个版本使用 解决方法 创建新项目,选择WPF 应用程序(用于创建.NET Core WPF 应用程序的项目),Framework 选择.NET 6.0 在解决方案中新建项目,选择空白应用(通用Windows),选择最低版本 17763 在解决方案中新建项目,选择类库(一个创建用于面向NET Standard 或.NET Core的类库的项目),Framework 选择.NET Standard 2.0 在第一和第二项目中添加对第三个项目的引用即可 关键点 Framework 选择.NET Standard 2.0 支持 .NET Core 、UWP 、 NET Framework 4.8 如需要分不同框架引入不同依赖包 则需要手动修改第三个项目的 .csproj 文件 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks Condition="'$(LibraryFrameworks)'==''">net48;netstandard2.0</TargetFrameworks> <TargetFrameworks Condition="'$(LibraryFrameworks)'!=''">$(LibraryFrameworks)</TargetFrameworks> 修改 TargetFrameworks 内容, 然后就可以添加不同的程序集引用了

2021/9/7
articleCard.readMore

hashcat(二)找回rar解压密码

rar 分卷 功能,任意一卷也是可以测试密码的,不用全部下载才发现密码不正确 第一步,获取rar 的密码哈希 下载 rar2john 下载地址 点击 1.9.0-jumbo-1 64-bit Windows binaries 下载 执行命令 rar2john.exe 在压缩包的 run文件夹下 rar2john.exe 1.rar 输出密码哈希 1.rar:$rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe $rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe 就是密码哈希值了 第二步,使用 hashcat 解密 hashcat.exe -m 13000 -a3 $rar5$16$c8917fd9fbfed20e71f7b58f3633add1$15$174bda4b7f67ee9f224bcbae8bfb277f$8$9b7fcdc3051db3fe ?d?d?d?d 具体规则请查看 【上一篇:hashcat(一)找回office文件密码】 或hashcat 文档

2021/8/25
articleCard.readMore

Godot 学习笔记(一)

语法 方法 func _a(param: int = 10): // TODO 方法 退出游戏 get_tree().quit() 切换场景 get_tree().change_scene("res://scene.tscn") 自定义属性 export var speed: int = 30 自定义信号 signal on_play func _on_do(): emit_signal("on_play")

2021/8/17
articleCard.readMore

升级vue3记录

第一步安装 vue-cli npm install --global @vue/cli@next 创建项目 vue create vue-shop 选择 Manually select features 勾选 Choose Vue version Babel Typescript Progressive Web App (PWA) Support Router Vuex Css Pre-processors 使用了Scss 必选 Linter/Formatter Unit Testing 和 E2E Testing 可选其中一个 选择 vue 版本 3.x 接下来输三个 y yes 然后选择 Sass/SCSS ESLint 规则选择: 选了第一个,碰到很多问题,比如 在 template 中 {{ 1 < 2 }} 小于号居然被提示不符合vue 规则 选择 Lint on save 代码规范检查的时机 保存设置 In dedicated config files 然后 是否保存这个配置方便以后创建其他项目 N 安装一些其他依赖 npm install mitt axios vue-class-component@next vue-property-decorator@rc vuex-class vuex-class-modules axios Api数据请求 mitt 全局事件管理,vue3 取消了 $once 等全局事件 vue-class-component@next vue3 Typescript 项目默认使用vue-class-component, 这里是为了安装最新 vue-property-decorator@rc @Prop() 的使用 vuex-class vuex-class-modules 方便vuex 使用 修改代码 复制一些文件和资源 修改项 修改 @Component 为 @Options axios 注册全局 export default { install(app: any) { app.config.globalProperties.$post = post app.config.globalProperties.$fetch = fetch app.config.globalProperties.$patch = patch app.config.globalProperties.$put = put }, } VueRouter 改为 import { Router } from 'vue-router'; 路由直接放 const routes: Array<RouteRecordRaw> = []; Vuex 使用 import { SET_USER, TOKEN_KEY, SET_TOKEN, } from '../types'; import { IUser, ILogin } from '@/api/model'; import { getSessionStorage, setSessionStorage, removeSessionStorage } from '@/utils'; import { getProfile, login, logout } from '@/api/user'; import { Action, Module, Mutation, VuexModule } from 'vuex-class-modules'; @Module({ generateMutationSetters: true }) export class AuthModule extends VuexModule { token: string | null = null; user: IUser | null = null; get isGuest() { if (this.user) { return false; } const token = getSessionStorage<string>(TOKEN_KEY); return !token; } @Mutation [SET_USER](user: IUser|null) { this.user = user; } @Action logoutUser() { return new Promise<void>((resolve, reject) => { const token = getSessionStorage<string>(TOKEN_KEY); if (!token) { resolve(); return; } logout().then(() => { this[SET_TOKEN](null); this[SET_USER](null); resolve(); }).catch(reject); }); } } import { createStore } from 'vuex'; import { AuthModule } from './modules/auth'; const store = createStore({}); export const authModule = new AuthModule({store, name: 'auth'}); export default store; 具体参考 [Vue.js with Typescript and Decorators](https://davidjamesherzog.github.io/2020/12/30/vue-typescript-decorators/) 6. 修改main.ts ```ts import { createApp } from 'vue'; import App from './App.vue'; import './registerServiceWorker'; import router from './router'; import store from './store'; import emitter from './event'; import './assets/iconfont/iconfont.css'; import http from './utils/http'; createApp(App, { onscroll(e: Event) { emitter.emit('scroll', e); // 传递滚动事件 } }).use(http).use(store).use(router).mount('#app'); $children 被删除了,需要改动使用 setup() 获取子元素 filter 已经删除了,所以只能通过方法调用

2021/8/15
articleCard.readMore

angular 12 显示数学公式

angular 12 显示数学公式 公式的格式 主要有两种写法格式: LaTeX AsciiMath 安装依赖 这里使用的是 KaTeX,默认支持 LaTeX,如果需要支持 AsciiMath 则需要 安装转化工具 asciimath2tex npm i katex npm i asciimath2tex 默认是有 angular 版本的 KaTeX npm i ng-katex 注意 ng-katex 默认根据 $ 和 $$ 来识别处理公式的 例如 $a=x^2$ 代码 但是我不知需要显示公式,还需要处理一些其他的,所以直接使用 KaTeX 进行处理 npm i katex npm i asciimath2tex import * as katex from 'katex'; import AsciiMathParser from 'asciimath2tex'; private formatContent() { const items: IMarkItem[] = []; const content = this.content.trim(); let index = -1; let start = 0; const parser = new AsciiMathParser(); const pushMath = () => { index ++; start = index; while (index < content.length - 1) { if (content.charAt(++index) === '$' && backslashedCount(index - 1) % 2 === 0) { break; } } items.push({ type: 'math', content: this.sanitizer.bypassSecurityTrustHtml( katex.renderToString(parser.parse(content.substring(start, index))) ) }); }; const pushText = (end: number) => { if (end > content.length) { end = content.length; } if (start >= end) { return; } const text = content.substring(start, end); if (text.length < 1) { return; } items.push({ type: 'text', content: text, }); }; const backslashedCount = (i: number) => { let count = 0; while (i >= 0) { if (content.charAt(i --) === '\\') { count ++; continue; } break; } return count; }; while (index < content.length - 1) { const code = content.charAt(++index); if (code === '$' && backslashedCount(index - 1) % 2 === 0) { pushText(index); pushMath(); start = index + 1; continue; } if (code === '\n') { pushText(index); items.push({ type: 'line', }); start = index + 1; continue; } } pushText(index + 1); this.items = items; } 关键代码 import * as katex from 'katex'; import AsciiMathParser from 'asciimath2tex'; katex.renderToString(parser.parse(content)); 根据提取的公式转化成 html 代码

2021/7/16
articleCard.readMore

js 监听按键事件

js 监听按键事件 一个输入框 <input type="text"> 主要事件 onkeydown 按下一个按键时执行 onkeypress 按下键盘按钮时执行,不是适用于所有按键(如: ALT, CTRL, SHIFT, ESC) onkeyup 释放键盘按钮时执行 监听所有按键事件 document.addEventListener('keydown', (event: KeyboardEvent) => { }); 监听输入框按键事件 document.querySelector('input').addEventListener('keydown', (event: KeyboardEvent) => { }); KeyboardEvent 的主要属性 interface KeyboardEvent { readonly altKey: boolean; // 按下了 alt 键 readonly code: string; // 按键的内容 例如 Enter KeyK,请注意手机上按键的确认键无法获取,区分左右按键 AltLeft readonly ctrlKey: boolean; // 是否按住了 ctrl 键 readonly key: string; // 键名 例如 Enter、k 字母区分大小写 readonly keyCode: number; // 键的字符代码,已废弃的属性,不建议试用 readonly shiftKey: boolean; // 是否按住了 shift 键 readonly metaKey: boolean; // 是否按住了 win 键 } 使用建议 获取 字母按键 请使用 event.code 获取其他特殊按键(Enter、Tab) 请使用 event.key 例如:输入框确认事件 document.querySelector('input').addEventListener('keydown', (event: KeyboardEvent) => { if (event.key === 'Enter') { // TODO 确认 } }); 监听复制快捷键 document.addEventListener('keydown', (event: KeyboardEvent) => { if (event.ctrlKey && event.code === 'KeyC') { // TODO 复制 } });

2021/7/9
articleCard.readMore

angular 12 ng-deep 使用注意事项

ng-deep 使用 a.scss ::ng-deep { a { color: #fff; } } ng-deep 表示里面的样式是公共的,需要影响子组件的样式。 可以理解为 vue 中的不带 scoped 属性的 style 标签 上面的样式表示,当打开 a 组件之后,样式开始生效,如果从 a 离开了样式依然会起作用, 注意 ::ng-deep 里面的样式是影响全局的,如果想只作用与一个模块下 请使用一个规则名放在最外面 .page ::ng-deep { a { color: #fff; } }

2021/7/4
articleCard.readMore

angular 16 动态生成组件

angular 16 动态生成组件 需求 在页面中生成一个确认弹窗 新的实现方式 第一步,获取需要的工具 @Injectable({ providedIn: 'root' }) export class DialogService { public containerRef: ViewContainerRef; //需要从Component中获取,然后传递进来 constructor( private injector: Injector, ) { } } ViewContainerRef 的获取方式 第一种在 Component 初始化时获取 @Component({ selector: 'app-dialog-container', template: '', styles: [''], }) export class DialogContainerComponent { constructor( private service: DialogService, private viewContainerRef: ViewContainerRef, ) { this.service.containerRef = this.viewContainerRef; } } 在template 中通过 ng-container 获取 @Component({ selector: 'app-dialog-container', template: '<ng-container #modalVC></ng-container>', styles: [''], }) export class DialogContainerComponent implements AfterViewInit { @ViewChild('modalVC', {read: ViewContainerRef}) private viewContainerRef: ViewContainerRef; constructor( private service: DialogService, ) { } ngAfterViewInit(): void { this.service.containerRef = this.viewContainerRef; } } 主要区别:就是增加的元素节点为兄弟节点,所以,第一种添加的弹窗是在外面的,不受 app-dialog-container 中的样式影响;第二种是作为子元素添加的,受样式影响 第二步,实现动态生成组件 component 为组件的类名 const dialogRef = this.containerRef.createComponent(component, { injector: this.injector }); // dialogRef.instance 就是组件的实例 第三步,移除组件 dialogRef.destroy(); 如果需要在组件初始化,自动注入值或传值,则需要自定义 injector import { InjectFlags, Injector, ProviderToken } from '@angular/core'; // 这个类是传值的载体 export class DialogPackage<T = any> { constructor( public data: T, public dialogId: any, ) { } } // 这是自定义的注射器 export class DialogInjector<T> implements Injector { constructor( private data: DialogPackage, private parentInjector: Injector ) {} get<T>(token: ProviderToken<T>, notFoundValue?: T, option?: InjectOptions): T; get(token: any, notFoundValue?: any); get(token: any, notFoundValue?: any, flags?: any): any { if (token === DialogPackage) { return this.data; } return this.parentInjector.get<T>(token, notFoundValue, flags); } } 则第二步需要修改 const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector); const dialogRef = this.containerRef.createComponent(component, { injector: dialogInjector }); // dialogRef.instance 就是组件的实例 在组件中接收值 export class DialogConfirmComponent { constructor( private data: DialogPackage<DialogConfirmOption>, private service: DialogService, ) { } } 过时实现代码 第一步,获取需要的工具 export class DialogService { constructor( private resolver: ComponentFactoryResolver, private applicationRef: ApplicationRef, private injector: Injector, @Inject(DOCUMENT) private document: Document, ) { } } 第二步,实现动态生成组件 component 为组件的类名 const dialogFactory = this.resolver.resolveComponentFactory(component); const dialogRef = dialogFactory.create(this.injector); // 正式添加到程序和添加到页面上,才能显示 this.applicationRef.attachView(dialogRef.hostView); this.document.body.appendChild(dialogRef.location.nativeElement); // dialogRef.instance 就是组件的实例 第三步,移除组件 this.applicationRef.detachView(dialogRef.hostView); this.document.body.removeChild(dialogRef.location.nativeElement); 如果需要在组件初始化,自动注入值或传值,则需要自定义 injector import { InjectFlags, Injector, ProviderToken } from '@angular/core'; // 这个类是传值的载体 export class DialogPackage<T = any> { constructor( public data: T, public dialogId: any, ) { } } // 这是自定义的注射器 export class DialogInjector<T> implements Injector { constructor( private data: DialogPackage, private parentInjector: Injector ) {} get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T; get(token: any, notFoundValue?: any); get(token: any, notFoundValue?: any, flags?: any): any { if (token === DialogPackage) { return this.data; } return this.parentInjector.get<T>(token, notFoundValue, flags); } } 则第二步需要修改 const dialogFactory = this.resolver.resolveComponentFactory(component); // 这里的option 可以是任何值 const dialogInjector = new DialogInjector(new DialogPackage(option, dialogId), this.injector); const dialogRef = dialogFactory.create(dialogInjector); // 正式添加到程序和添加到页面上,才能显示 this.applicationRef.attachView(dialogRef.hostView); this.document.body.appendChild(dialogRef.location.nativeElement); // dialogRef.instance 就是组件的实例 在组件中接收值 export class DialogConfirmComponent { constructor( private data: DialogPackage<DialogConfirmOption>, private service: DialogService, ) { } }

2021/7/3
articleCard.readMore

angular 12 动画执行完成事件

angular 12 动画执行完成事件 需求 在执行完关闭动画移除组件 代码 第一步,定义一个动画 import { animate, state, style, transition, trigger } from '@angular/animations'; export const DialogAnimation = trigger('dialogOpen', [ state('open', style({ transform: 'translate3d(0, 0, 0)', opacity: 1, })), state('closed', style({ transform: 'translate3d(0, -1000px, 0)', opacity: 0, })), transition('* => closed', [ animate('1s') ]), transition('* => open', [ animate('0.5s') ]), ]); 第二步,使用动画 @Component({ selector: 'app-dialog-confirm', templateUrl: './dialog-confirm.component.html', styleUrls: ['./dialog-confirm.component.scss'], animations: [ DialogAnimation, ], }) export class DialogConfirmComponent { public visible = true; } <div class="dialog-box" [@dialogOpen]="visible ? 'open' : 'closed'"> </div> 第三步,加上动画事件 <div class="dialog-box" [@dialogOpen]="visible ? 'open' : 'closed'" (@dialogOpen.done)="animationDone($event)"> </div> (@动画名.start) 表示动画开始执行 (@动画名.done) 表示动画执行完成 import { AnimationEvent } from '@angular/animations'; @Component({ selector: 'app-dialog-confirm', templateUrl: './dialog-confirm.component.html', styleUrls: ['./dialog-confirm.component.scss'], animations: [ DialogAnimation, ], }) export class DialogConfirmComponent { public visible = true; public animationDone(event: AnimationEvent) { // 获取状态 if (event.toState !== 'closed') { return; } // 表示关闭动画已经执行完成 } }

2021/7/3
articleCard.readMore

angular 12 全局搜索组件

一个全局的 SearchService import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class SearchService { /** * 输入文字发送改变 */ static EVENT_CHANGE = 'change'; /** * 确认搜索 */ static EVENT_CONFIRM = 'confirm'; /** * 根据文字设置搜索建议 */ static EVENT_CHANGE_SUGGEST = 'suggest'; private eventPair: { [trigger: string]: string; } = { [SearchService.EVENT_CHANGE]: SearchService.EVENT_CHANGE_SUGGEST }; private listeners: { [key: string]: Function[]; } = {}; constructor( ) { } public on(event: 'change', cb: (keywords: string) => void|boolean|Observable<any[]>): this; public on(event: 'confirm', cb: (keywords: any) => void|false): this; public on(event: 'suggest', cb: (items: any[]) => void): this; public on(event: string, cb: (...items: any[]) => void|boolean|Observable<any>): this; public on(event: string, cb: any) { if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) { this.listeners[event] = []; } this.listeners[event].push(cb); return this; } public emit(event: 'change', keywords: string): this; public emit(event: 'confirm', keywords: any): this; public emit(event: 'suggest', items: any[]): this; public emit(event: string, ...items: any[]): this; public emit(event: string, ...items: any[]) { if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) { return this; } const pair = this.eventPair[event]; const listeners = this.listeners[event]; for (let i = listeners.length - 1; i >= 0; i--) { const cb = listeners[i]; const res = cb(...items); // 允许事件不进行传递 if (res === false) { break; } if (!res || !pair) { continue; } // 接受订阅 if (res instanceof Observable) { res.subscribe(data => { this.emit(pair, data); }); continue; } this.emit(pair, res); } return this; } public off(...events: string[]): this; public off(event: string, cb: Function): this; public off(...events: any[]) { if (events.length == 2 && typeof events[1] === 'function') { return this.offListener(events[0], events[1]); } for (const event of events) { delete this.listeners[event]; } return this; } /** * 移除搜索框页面的接受事件 */ public offTrigger() { return this.off(SearchService.EVENT_CHANGE_SUGGEST); } /** * 移除搜索结果页面的接受事件 */ public offReceiver() { return this.off(SearchService.EVENT_CHANGE, SearchService.EVENT_CONFIRM); } private offListener(event: string, cb: Function): this { if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) { return this; } const items = this.listeners[event]; for (let i = items.length - 1; i >= 0; i--) { if (items[i] === cb) { items.splice(i, 1); } } return this; } } 注册全局 service export class ThemeModule { static forRoot(): ModuleWithProviders<ThemeModule> { return { ngModule: ThemeModule, providers: [ SearchService, ] }; } } @NgModule({ imports: [ ThemeModule.forRoot(), ] }) export class AppModule { } 添加搜索框 <div class="dialog-search" [ngClass]="{inputting: suggestText.length > 0}" [hidden]="!panelVisible"> <i class="iconfont icon-close dialog-close" (click)="close()"></i> <div class="dialog-body"> <div class="search-input"> <i class="iconfont icon-search input-search"></i> <input type="text" placeholder="请输入关键字,按回车 / Enter 搜索" autocomplete="off" [(ngModel)]="suggestText" (keydown)="suggestKeyPress($event)" (ngModelChange)="onSuggestChange()"> <i class="iconfont icon-close input-clear" (click)="tapClear()"></i> </div> <ul class="search-suggestion"> <li *ngFor="let item of suggestItems;let i = index" [ngClass]="{active: i === suggestIndex}" (click)="tapItem(item)"><span>{{ i + 1 }}</span>{{ formatTitle(item) }}</li> </ul> </div> </div> import { Component, OnDestroy, OnInit } from '@angular/core'; import { SearchService } from '../../theme/services'; @Component({ selector: 'app-search', templateUrl: './search.component.html', styleUrls: ['./search.component.scss'] }) export class SearchComponent implements OnInit, OnDestroy { public panelVisible = false; public suggestItems: any[] = []; public suggestText = ''; public suggestIndex = -1; private asyncHandle = 0; constructor( private searchService: SearchService, ) { } ngOnInit() { this.searchService.on('suggest', items => { this.suggestIndex = -1; this.suggestItems = items; }); } ngOnDestroy() { this.searchService.offTrigger(); } public formatTitle(item: any) { if (typeof item !== 'object') { return item; } // 这里只接受 title 或 name 属性进行显示 return item.title || item.name; } public suggestKeyPress(event: KeyboardEvent) { if (event.key === 'Enter') { this.searchService.emit('confirm', this.suggestIndex >= 0 ? this.suggestItems[this.suggestIndex] : this.suggestText); this.close(); return; } if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { this.suggestIndex = -1; return; } if (this.suggestItems.length < 0) { return; } let i = this.suggestIndex; if (event.key === 'ArrowDown') { i = i < this.suggestItems.length - 1 ? i + 1 : 0; } else if (event.key === 'ArrowUp') { i = (i < 1 ? this.suggestItems.length: i) - 1; } this.suggestIndex = i; this.suggestText = this.formatTitle(this.suggestItems[this.suggestIndex]); } public onSuggestChange() { if (this.suggestIndex >= 0) { return; } this.asyncSuggest(); } public tapItem(item: any) { this.searchService.emit('confirm', item); this.close(); } public tapClear() { this.suggestText = ''; this.suggestItems = []; } public open() { this.panelVisible = true; } public close() { this.panelVisible = false; } private asyncSuggest() { if (this.asyncHandle) { clearTimeout(this.asyncHandle); } this.suggestIndex = -1; this.asyncHandle = window.setTimeout(() => { this.asyncHandle = 0; this.suggestIndex = -1; if (this.suggestText.length < 1) { this.suggestItems = []; return; } this.searchService.emit('change', this.suggestText); }, 300); } } 搜索页面 其他页面,根据搜索关键词跳转搜索页面或详情页。 export class BlogComponent implements OnInit, OnDestroy { constructor( private searchService: SearchService, private service: BlogService, private router: Router, private route: ActivatedRoute, ) { } ngOnInit() { this.searchService.on('change', keywords => { return this.service.suggesttion({keywords}); }).on('confirm', res => { if (typeof res === 'object') { this.router.navigate([res.id], {relativeTo: this.route}); return; } this.router.navigate(['./'], {relativeTo: this.route, queryParams: { keywords: res }}); }); } ngOnDestroy() { this.searchService.offReceiver(); } } 如果当前就是搜索页面,那么可以不用跳转 private searchFn = res => { if (typeof res === 'object') { return; } this.queries.keywords = res; this.tapRefresh(); // 阻止事件传递 return false; }; ngOnInit() { this.searchService.on('confirm', this.searchFn); } ngOnDestroy() { this.searchService.off('confirm', this.searchFn); } 注意 搜索事件并不会自动清除,需要添加 ngOnDestroy 进行清除

2021/6/30
articleCard.readMore

angular 12 中单例 Service 的使用

angular 12 singleton service angular 12 中单例 Service 的使用 service 写法 import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class SearchService { } providedIn: 'root' 表明当前 service 为全局有效的单例模式 注册 service 必须在 AppModule 注册,或者 在任意一个 module 中注册。 但是需要注意,这个 module 只能导入一次, 例如:有一个 module ThemeModule 为公共核心组件,很多其他 module 都会依赖她, 导致很多地方都使用了 imports: [ ..., ThemeModule, ... ] 进行导入 而 单例 service SearchService 是注册在 ThemeModule 中的, 这样就会导致单例不生效,每一个 SearchService 都不同, 这是可以给 ThemeModule 增加一个方法 forRoot() export class ThemeModule { static forRoot(): ModuleWithProviders<ThemeModule> { return { ngModule: ThemeModule, providers: [ SearchService, ] }; } } 把 SearchService 仅注册到 forRoot() 中。 然后在 AppModule 中导入 ThemeModule @NgModule({ ... imports: [ ..., ThemeModule.forRoot(), ... ], ... }) export class AppModule 这样就能生效

2021/6/30
articleCard.readMore

js 实现一个正则替换

js 实现一个正则替换 代码 /** * 正则匹配替换 * @param content * @param pattern * @param cb * @returns */ export function regexReplace(content: string, pattern: RegExp, cb: (match: RegExpExecArray) => string): string { if (content.length < 1) { return content; } const matches: RegExpExecArray[] = []; let match: RegExpExecArray|null; while (null !== (match = pattern.exec(content))) { matches.push(match as RegExpExecArray); } const block: string[] = []; for (let i = matches.length - 1; i >= 0; i--) { match = matches[i]; block.push(content.substr(match.index + match[0].length)); block.push(cb(match)); content = content.substr(0, match.index); } return content + block.reverse().join(''); } 原理 先获取所有的匹配结果,存入一个临时数组中 倒序执行回调方法获取替换的内容 按照匹配到的位置,截断字符串,把匹配到的部分根据长度去除,然后按倒序和替换的字符串存入一个数组, 颠倒数组顺序连接即最终结果 疑问 Q:为什么不在匹配时边匹配边替换? A:原本我这样做的,发现匹配结果出错了,有些没匹配到。因为exec 匹配时正则表达式会记住最后的匹配位置,如果原本的内容长度变化;,就会导致这个位置不正确。 Q:为什么要截断字符串? A: 使用字符串替换会搜索全部,多一些不必要的操作;截断存入数组,就是想最后一起做拼接。 Q: 为什么要倒序替换? A: 因为正序截取的话会导致匹配结果中的位置要进行改变。

2021/6/5
articleCard.readMore

uwp win2d 使用

通过 Canvas.Invalidate(); 触发重绘事件 通过 Canvas_Draw 进行绘制 private void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args) { var progress = Progress; var centerX = (float)ActualWidth / 2; var centerY = (float)ActualHeight / 2; var radius = Math.Min(centerX, centerY); var lineRadius = radius - LineWidth; var inlineRadius = lineRadius - 5; using (var draw = args.DrawingSession) { draw.FillRectangle(new Windows.Foundation.Rect(centerX - radius, centerY - radius, 2 * radius, 2 * radius), Colors.Transparent); draw.FillCircle(centerX, centerY, inlineRadius, InlineBackground); draw.DrawCircle(centerX, centerY, lineRadius, Outline, LineWidth); var deg = (2 * Math.PI / 100 * progress) ; // 圆环的绘制 draw.DrawGeometry(Arc(draw, centerX, centerY, lineRadius, (float)(-.5 * Math.PI), (float)deg), Inline, LineWidth); var x = (float)(centerX + Math.Cos(Math.PI * 2 * (progress - 25) / 100) * lineRadius); var y = (float)(centerY + Math.Sin(Math.PI * 2 * (progress - 25) / 100) * lineRadius); draw.FillCircle(x, y, LineWidth, Inline); } } /// 画圆弧 public CanvasGeometry Arc(ICanvasResourceCreator resourceCreator, float centerX, float centerY, float radius, float startAngle, float endAngle) { var path = new CanvasPathBuilder(resourceCreator); path.BeginFigure(centerX, centerY - radius); path.AddArc(new Vector2(centerX, centerY), radius, radius, startAngle, endAngle); path.EndFigure(CanvasFigureLoop.Open); return CanvasGeometry.CreatePath(path); } 最后必须手动销毁 var canvas = GetTemplateChild("Canvas") as CanvasControl; if (canvas != null) { canvas.RemoveFromVisualTree(); }

2021/5/12
articleCard.readMore

UWP Custom Control自定义控件开发

Custom Control 又名 Templated Control 模板控件 开发 分为两个文件 一个资源文件 Themes/Generic.xaml 里面,主要放默认的模板及初始化属性 一个cs 文件 控件名.cs,主要放 声明属性及事件 一个简单的控件,由一个图标和文字组成的控件 public sealed class IconTag : Control { public IconTag() { this.DefaultStyleKey = typeof(IconTag); } /// <summary> /// 内容 /// </summary> public string Label { get { return (string)GetValue(LabelProperty); } set { SetValue(LabelProperty, value); } } // Using a DependencyProperty as the backing store for Label. This enables animation, styling, binding, etc... public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label", typeof(string), typeof(IconTag), new PropertyMetadata(null)); /// <summary> /// 内容字体图标 /// </summary> public string Icon { get { return (string)GetValue(IconProperty); } set { SetValue(IconProperty, value); } } // Using a DependencyProperty as the backing store for Icon. This enables animation, styling, binding, etc... public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(string), typeof(IconTag), new PropertyMetadata(null)); } <Style TargetType="local2:IconTag"> <Setter Property="FontFamily" Value="Microsoft YaHei"/> <Setter Property="Margin" Value="0,0,10,0"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local2:IconTag"> <Grid Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <FontIcon Glyph="{TemplateBinding Icon}" FontSize="{TemplateBinding FontSize}" VerticalAlignment="Center"/> <TextBlock Text="{TemplateBinding Label}" FontFamily="{TemplateBinding FontFamily}" VerticalAlignment="Center" Grid.Column="1" FontSize="{TemplateBinding FontSize}"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> ControlTemplate就是放默认模板, Setter Property= 就是声明一些初始化的属性 代码获取控件模板中的控件 必须先使用 x:Name 声明名称 <Style TargetType="local2:IconTag"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local2:IconTag"> <Grid Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <FontIcon Glyph="{TemplateBinding Icon}" FontSize="{TemplateBinding FontSize}" VerticalAlignment="Center"/> <TextBlock x:Name="Content" Text="{TemplateBinding Label}" FontFamily="{TemplateBinding FontFamily}" VerticalAlignment="Center" Grid.Column="1" FontSize="{TemplateBinding FontSize}"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> 必须声明模板必须包含 x:Name="Content" 且 类型为 TextBlock [TemplatePart(Name = "Content", Type = typeof(TextBlock))] public sealed class IconTag : Control { public IconTag() { DefaultStyleKey = typeof(IconTag); Loaded += IconTag_Loaded; } private void IconTag_Loaded(object sender, RoutedEventArgs e) { var tb = GetTemplateChild("Content") as TextBlock; } } 通过 GetTemplateChild 获取控件,而且必须等控件加载完了才能获取到。

2021/4/29
articleCard.readMore

UWP 读取应用内资源

需求 在项目里加了一个css 样式文件, 需要把这个样式引用到 webview 中 方法 在项目资源文件里放入样式文件 Assets/markdown.css 选中文件右键“属性” 更改文件属性 复制到输出目录: 始终复制 生成操作:内容 然后和html源码合并给webview private async Task<string> RenderHtmlAsync(string content) { string style; try { var fileUri = new Uri("ms-appx:///Assets/markdown.css", UriKind.Absolute); var file = await StorageFile.GetFileFromApplicationUriAsync(fileUri); style = await FileIO.ReadTextAsync(file); } catch (Exception) { style = string.Empty; } return $"<style>{style}</style><div class=\"markdown\">{content}</div>"; } webView.NavigateToString(await RenderHtmlAsync(data.Content));

2021/4/29
articleCard.readMore

gin 使用笔记(二)出错点

自动绑定表单数据 使用 ShouldBindQuery 或者 ShouldBind 需要注意结构体的格式 例如一个查询分页的参数获取 type Queries struct { Page uint `form:"page" json:"page"` PerPage uint `form:"per_page" json:"per_page"` Keywords string `form:"keywords" json:"keywords"` } func GetList(c *gin.Context) { var query Queries if err := c.ShouldBindQuery(&query); err != nil { // error } } form:"page" json:"page" 这个就是结构体的解析说明又名struct tag 这里的 form 指查询参数或表单提交的参数,如果是 GET 或 表单POST 的数据必须有这个标记,不然会绑定失败 如果是 POST 的 json 则用 json:"page" 如果是 POST 的 xml 则用 xml:"page" ShouldBind 是会自动判断内容的格式,是GET 会匹配网址上的参数,其他则根据 请求头 Content-Type 自动判断 reflect: reflect.Value.SetUint using unaddressable value 只要出现 using unaddressable value 就表明代码中该传引用的地方传了值 CORS 的使用 对与跨域的处理中间件必须放在全局即 *gin.Engine 上面,不能放在某一个路由组上 r := gin.Default() r.Use(middleware.CORS) 原因是跨域请求会产生一个前置 OPTIONS 请求,而这个请求实际是不需要响应内容的,如果没有匹配的路由就会响应404 影响下一步浏览器发出真正的请求 websocket 不能添加请求头 所有登录信息没法通过请求头传递,而网址传递并不安全,所以只能先建立连接,然后通过发送消息传递

2021/4/22
articleCard.readMore

gin 使用笔记(一)基础

关于路由 路由 ` 和/` 可以分开作为两个路由, 但是如果只有一个,就会把没注册的那个重定向到注册的那个 r := gin.Default() r.GET("/home", Index) r.GET("/home/", Abount) 这种方法时有效的 如果只注册一个 r := gin.Default() r.GET("/home", Index) 浏览器访问 http://localhost/home/ 就会响应 301 并跳转到 http://localhost/home 路由分组 可以通过 ` ` 进行更精确的分组 r := gin.Default() blog := r.Group("/blog") blog.GET("", Index) g := blog.Group("") g.Use(middleware.CORS) g.GET("/count", Count) 中间件 gin.HandlerFunc 只能通过 c.Abort 进行中断,否则会继续执行下去 r := gin.Default() r.Use(func(c *gin.Context) { if false { c.AbortWithStatusJSON(400, json.RenderFailure(err.Error())) return } }) c.Next() 不是必须调用,如果需要对输出结果进行操作,这是才需要在内部调用 在中间件中传值 c.Keys["key"] = val 路由方法 c.ShouldBindQuery 可以绑定查询值到模型 c.Get() c.GetInt() c.GetString() 等是获取 c.Keys 注册的值 c.Query 获取查询的值 c.ShouldBind 是绑定post 提交的值到模型 c.PostForm 获取post的值 获取网址中匹配的值 r.GET("/user/:id", func(c *gin.Context) { // a GET request to /user/john id := c.Param("id") // id == "john" }) 模板 r := gin.Default() r.Static("/assets", configs.Config.Asset) // 指定资源文件的路径及文件夹 r.StaticFile("/favicon.ico", configs.Config.Favicon) // 指定网站图标 r.LoadHTMLGlob("templates/**/*") 特别注意: templates/**/* 会匹配 templates 文件夹下的子文件夹中的文件,但是只会注册文件名 例如 有 templates/blog/index.html 和 templates/auth/index.html func Index(ctx *gin.Context) { ctx.HTML(200, "index.html", gin.H{}) } ctx.HTML(200, "blog/index.html", gin.H{}) 这样是访问不到的, ctx.HTML(200, "index.html", gin.H{}) 这样才能访问到,但是访问的是 templates/blog/index.html 如果要两个文件都生效,只能改文件名 templates/auth/auth_index.html ctx.HTML(200, "auth_index.html", gin.H{})

2021/4/18
articleCard.readMore

angular 关于自定义组件事件传递

事实事件有两种定义方式 @Output 声明事件 angular 通常方式,使用 @Output()方式 // 在组件声明一个事件 @Output() public tapped = new EventEmitter(); // 组件中触发事件 this.tapped.emit(); 调用组件 <app-custom (tapped)="onTapped()"> public onTapped() { // TODO } 优点 不需要考虑事件是否有接收,同时可以被接收多次,而且可以接受父页面上的值。 缺点 没办法判断事件是否注册了,传递值只能传一个,可以把多个值合并成一个 object 传递 // 在组件声明一个事件 @Output() public tapped = new EventEmitter<number>(); // 组件中触发事件 this.tapped.emit(1); 调用组件 <app-custom (tapped)="onTapped($event)"> public onTapped(e: number) { // TODO } 直接把方法当值传递 非正常方式,使用 @Input()方式 // 在组件声明一个值 @Input() public tapped: () => void; // 组件中触发事件 this.tapped && this.tapped(); 调用组件 <app-custom [tapped]="onTapped"> public onTapped() { // TODO } 优点 可以同时传递多个值,可以判断是否有事件 缺点 没法同时接受父页面上的值,而且这个方法的内部 this 为子组件,所以就没法调用父组件的方法或属性。 private refresh() public onTapped() { this.refresh(); // error refresh is undefined }

2021/4/14
articleCard.readMore

angular 11 怎么获取 Content-Disposition

需求 在 angular 实现文件下载功能, 默认只能在前端代码中手动添加文件类型及文件名。 export class DownloadService { constructor(private http: HttpClient) { } /** * Blob请求 */ public requestBlob(url: string, data?: any): Observable<any> { return this.http.request('post', url, { body: data, observe: 'response', responseType: 'blob', }); } /** * Blob文件转换下载 */ public downFile(result: any, fileName: string, fileType?: string) { const data = result.body; const blob = new Blob([data], { type: fileType || data.type, }); const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.setAttribute('style', 'display:none'); a.setAttribute('href', objectUrl); a.setAttribute('download', fileName); a.click(); URL.revokeObjectURL(objectUrl); } public export(url: string, data: any, fileName: string, fileType?: any) { this.requestBlob(url, data).subscribe(result => { const headers = result.headers as HttpHeaders; this.downFile(result, fileName, fileType || headers.get('Content-Type')); }); } } 使用 private downloadService: DownloadService this.downloadService.export('http://localhost/export', {}, '流水记录.xlsx'); 突然想到 在响应头中已经有了文件类型和文件名,那么是否可以直接获取呢? 解决 关键是响应头中的 Access-Control-Expose-Headers 在 angular issue 中就有人提处理这个问题, 并且给出了解决方法 Unable to view 'Content-Disposition' headers in Angular4 GET response 里面提供了一个 Java 的解决方案 翻译成普通语言就是: 需要在服务端响应头中加 Access-Control-Expose-Headers 加上 Content-Disposition 值 Access-Control-Expose-Headers: Content-Disposition 基础知识 响应首部 Access-Control-Expose-Headers 列出了哪些首部可以作为响应的一部分暴露给外部。 默认情况下,只有七种 simple response headers (简单响应首部)可以暴露给外部: Cache-Control Content-Language Content-Length Content-Type Expires Last-Modified Pragma 如果想要让客户端可以访问到其他的首部信息,可以将它们在 Access-Control-Expose-Headers 里面列出来。 Access-Control-Expose-Headers: <header-name>, <header-name>, ... 多个用英文逗号分隔 【来源】 最终代码 export class DownloadService { constructor(private http: HttpClient) { } /** * Blob请求 */ public requestBlob(url: string, data?: any): Observable<HttpResponse<Blob>> { return this.http.request('post', url, { body: data, observe: 'response', responseType: 'blob', }); } /** * Blob文件转换下载 */ public downFile(result: HttpResponse<Blob>, fileName?: string, fileType?: string) { fileName = this.parseFileName(result.headers.get('Content-Disposition'), fileName); if (!fileName) { console.log('fileName error'); return; } const data = result.body; const blob = new Blob([data], { type: fileType || data.type, }); const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.setAttribute('style', 'display:none'); a.setAttribute('href', objectUrl); a.setAttribute('download', fileName); a.click(); URL.revokeObjectURL(objectUrl); } public export(url: string, data: any, fileName?: string, fileType?: any) { this.requestBlob(url, data).subscribe((res: HttpResponse<Blob>) => { this.downFile(res, fileName, fileType); }); } private parseFileName(header: string, def?: string): string { if (!header) { return def; } const name = header.split(';')[1].trim().split('=')[1]; return decodeURI(name.replace(/"/g, '')); // 注意中文请在服务端添加url编码 } }

2021/4/13
articleCard.readMore

apache 使用gzip 压缩 js、css

给apache开启gzip,自动对输出文件进行压缩 配置 httpd.conf 开启: 去掉前面的注释#即可 LoadModule headers_module modules/mod_headers.so LoadModule deflate_module modules/mod_deflate.so LoadModule filter_module modules/mod_filter.so 然后在文件的最后面加上以下代码。 <ifmodule mod_deflate.c> DeflateCompressionLevel 6 AddOutputFilterByType DEFLATE text/plain AddOutputFilterByType DEFLATE text/html AddOutputFilterByType DEFLATE text/php AddOutputFilterByType DEFLATE text/xml AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE text/javascript AddOutputFilterByType DEFLATE application/xhtml+xml AddOutputFilterByType DEFLATE application/xml AddOutputFilterByType DEFLATE application/rss+xml AddOutputFilterByType DEFLATE application/atom_xml AddOutputFilterByType DEFLATE application/javascript AddOutputFilterByType DEFLATE application/x-javascript AddOutputFilterByType DEFLATE application/x-httpd-php AddOutputFilterByType DEFLATE application/x-font-ttf AddOutputFilterByType DEFLATE image/svg+xml # 针对图片开启gzip压缩,但压缩率不高,对文本的压缩率最高 AddOutputFilterByType DEFLATE image/gif image/png image/jpe image/swf image/jpeg image/bmp image/webp # 排除不需要压缩的文件包括图片和一些其他文件 BrowserMatch ^Mozilla/4 gzip-only-text/html BrowserMatch ^Mozilla/4\.0[678] no-gzip BrowserMatch \bMSIE !no-gzip !gzip-only-text/html SetEnvIfNoCase Request_URI .(?:html|htm)$ no-gzip dont-varySetEnvIfNoCase #SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png)$ no-gzip dont-vary SetEnvIfNoCase Request_URI .(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary SetEnvIfNoCase Request_URI .(?:pdf|doc)$ no-gzip dont-vary </ifmodule> 注意 在 .htaccess 中加代码是没有用的。 参考来源 Apache 开启Gzip压缩——可压缩js、css等静态文件

2021/4/9
articleCard.readMore

angular 11 返回上一页保留页面数据的思考

起因 有这个需要的页面基本是从列表页点击详情或新建编辑页面。在返回需要回到上一次的列表页面,同时保持页面内容不变 例如:在列表已经翻到了第100页,需要修改某一项值(当然可以做一个弹窗修改,这里讨论的是有必要新增页面去修改的情况), 这是点进去修改之后返回,发现到了第一页,这就麻烦了,再放到第100页就需要浪费时间了。 解决方案 接受分页参数 这时可能想到在网址上加一个可接受的分页属性。 但是这也要手动去输入。 如果还有其他查询参数呢?难道还一个个去输入,这也麻烦。 第一版 /list?page=1 private route: ActivatedRoute this.route.queryParams.subscribe(params => { this.goPage(params.page || 1); }); 第二版 /list?page=1&keywords= private route: ActivatedRoute this.route.queryParams.subscribe(params => { this.goPage(params); }); 缺点 不灵活 直接使用 localStorage 保存页面数据 如果所有的列表页面都使用一个值。这样就会发现页面之间会混乱。 例如: /list 访问到了第10页,在访问其他页面 /list2 发现一进去也到了第10页。 那就分开保存,但是 localStorage 是由存储限制的,页面一多。就会发现localStorage存满了, 缺点 不优雅,会受限制 使用浏览器的网址历史功能 每访问一页就进行保存历史。 /** * 遍历对象属性或数组 */ export function eachObject(obj: any, cb: (val: any, key?: string|number) => any): any { if (typeof obj !== 'object') { return cb(obj, undefined); } if (obj instanceof Array) { for (let i = 0; i < obj.length; i++) { if (cb(obj[i], i) === false) { return false; } } return; } for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { if (cb(obj[key], key) === false) { return false; } } } } export function uriEncode(path: string, obj: any = {}, unEncodeURI?: boolean) { const result = []; for (const name in obj) { if (Object.prototype.hasOwnProperty.call(obj, name)) { const value = obj[name]; result.push(name + '=' + (unEncodeURI ? value : encodeURIComponent(value))); } } if (result.length < 1) { return path; } return path + (path.indexOf('?') > 0 ? '&' : '?') + result.join('&'); } /** * 从当前页面链接获取查询参数 * @param routeQueries * @param def * @returns */ export const getQueries = <T>(routeQueries: any, def: T, ): T => { const queries: any = {}; const parseNumber = (val: any): number => { if (!val) { return 0; } if (typeof val === 'string' && val.indexOf('.') > 0) { return parseFloat(val); } return parseInt(val, 10); }; eachObject(def, (val, key) => { if (!routeQueries || !Object.prototype.hasOwnProperty.call(routeQueries, key)) { queries[key] = val; return; } if (typeof val === 'number') { queries[key] = parseNumber(routeQueries[key]); return; } if (typeof val === 'boolean') { queries[key] = routeQueries[key] === true || routeQueries[key] === '1' || routeQueries[key] === 'true'; return; } queries[key] = typeof routeQueries[key] === 'undefined' || routeQueries[key] === null ? '' : routeQueries[key]; }); return queries; }; /** * 记录查询历史 * @param queries * @param title */ export const applyHistory = (queries: any, title = '查询列表') => { const url = window.location.href; const path = url.split('?', 2)[0]; history.pushState(null, title, uriEncode(path, queries)); document.documentElement.scrollTop = 0; }; 使用 private route: ActivatedRoute this.route.queryParams.subscribe(params => { this.queries = getQueries(res, { keywords: '', term: 0, page: 1, per_page: 20, }); this.goPage(this.queries.page); }); public goPage(page: number) { const queries = {...this.queries, page}; this.service.logList(queries).subscribe(res => { this.items = res.data; this.queries = queries; applyHistory(queries); }); }

2021/4/7
articleCard.readMore

一个简单的HTML音视频播放器

一个简单的HTML音视频播放器 适用场景 本播放器适用场景: 播客类型的文章,需要一个播放器,但是不需要预加载资源文件的。可以尽可能的减少不必要的加载及访客流量浪费。 版本 本播放器有两个版本: 基于 Jquery 的版本。本文的源码属于这个版本。 基于Angular 11 的版本。源码见【Github】需要的自取。这个版本音频视频播放器是分开的,而且视频内置了自动区分 iframe 使用。 注意:播放器中的图标都是字体图标,所以只能参考修改。 代码 interface IPlayerOption { [key: string]: any; src: string; type?: 'audio' | 'video' | 'iframe' } ;(function($: any) { const EVENT_TIME_UPDATE = 'timeupdate'; const EVENT_PLAY = 'play'; const EVENT_PAUSE = 'pause'; const EVENT_ENDED = 'ended'; const EVENT_VOLUME_UPDATE = 'volumeupdate'; const EVENT_TAP_PLAY = 'tap_play'; const EVENT_TAP_PAUSE = 'tap_pause'; const EVENT_BOOT = 'boot'; const EVENT_TAP_VOLUME = 'tap_volume'; const EVENT_TAP_TIME = 'tap_time'; const EVENT_ENTER_FULL_SCREEN = 'full_screen'; const EVENT_EXIT_FULL_SCREEN = 'exit_full_screen'; const screenFull = function() { const fnMap = [ [ 'requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror' ], // New WebKit [ 'webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror' ], // Old WebKit [ 'webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror' ], [ 'mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror' ], [ 'msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError' ] ]; for (const item of fnMap) { if (item && item[1] in document) { return item; } } return false; }(); class MediaPlayer { constructor( public element: JQuery, public options: IPlayerOption ) { if (!this.options.src) { return; } this.init(); this.bindCustomEvent(); } private playerElement: HTMLVideoElement|HTMLAudioElement; private playerBar: JQuery; private booted = false; private volumeLast = 100; private duration = 0; public on(event: string, callback: Function): this { this.options['on' + event] = callback; return this; } public hasEvent(event: string): boolean { return this.options.hasOwnProperty('on' + event); } public trigger(event: string, ... args: any[]) { let realEvent = 'on' + event; if (!this.hasEvent(event)) { return; } return this.options[realEvent].call(this, ...args); } private bindCustomEvent() { this.on(EVENT_BOOT, () => { if (this.booted) { return; } if (this.options.type === 'audio') { this.bindAudioEvent(); return; } if (this.options.type === 'iframe') { this.videoFrame(); this.booted = true; return; } this.videoPlayer(); this.initBar(this.element.find('.player-bar')); this.bindVideoEvent(); }).on(EVENT_TAP_PLAY, () => { this.playerElement.play(); }).on(EVENT_TAP_PAUSE, () => { this.playerElement.pause(); }).on(EVENT_TIME_UPDATE, (p: number, t: number) => { this.duration = t; this.playerBar.find('.time').text(this.formatMinute(p) + '/' + this.formatMinute(t)); const progess = this.playerBar.find('.slider .progress'); progess.attr('title', parseInt(p.toString())); progess.find('.progress-bar').css('width', p * 100 / t + '%'); }).on(EVENT_PLAY, () => { this.playerBar.find('.icon .fa').addClass('fa-pause').removeClass('fa-play'); }).on(EVENT_PAUSE, () => { this.playerBar.find('.icon .fa').removeClass('fa-pause').addClass('fa-play'); }).on(EVENT_ENDED, () => { this.trigger(EVENT_PAUSE); }).on(EVENT_TAP_VOLUME, (v: number) => { if (!this.playerElement) { return; } this.playerElement.volume = v / 100; this.trigger(EVENT_VOLUME_UPDATE, v); }).on(EVENT_VOLUME_UPDATE, (v: number) => { const progess = this.playerBar.find('.volume-slider .progress'); progess.attr('title', parseInt(v.toString())); progess.find('.progress-bar').css('width', v + '%'); let volumeCls = 'fa-volume-up'; if (v <= 0) { volumeCls = 'fa-volume-off'; } else if (v < 60) { volumeCls = 'fa-volume-down'; } this.playerBar.find('.volume-icon .fa').attr('class', 'fa ' + volumeCls); }).on(EVENT_TAP_TIME, (p: number) => { if (!this.playerElement) { return; } this.playerElement.currentTime = p; }).on(EVENT_EXIT_FULL_SCREEN, () => { this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-expand'); this.element.removeClass('player-full'); }).on(EVENT_ENTER_FULL_SCREEN, () => { this.element.addClass('player-full'); this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-compress'); }); } private init() { if (this.options.type === 'audio') { this.initAudio(); return; } this.initVideo(); } private initAudio() { this.audioPlayer(); this.initBar(this.element); } private initBar(bar: JQuery) { this.playerBar = bar; const that = this; bar.on('click', '.icon .fa', function() { if (!that.booted) { that.trigger(EVENT_BOOT); } that.trigger($(this).hasClass('fa-play') ? EVENT_TAP_PLAY : EVENT_TAP_PAUSE); }).on('click', '.volume-icon .fa', function() { const $this = $(this); if ($this.hasClass('fa-volume-mute') || $this.hasClass('fa-volume-off')) { that.trigger(EVENT_TAP_VOLUME, that.volumeLast); return; } if (that.playerElement) { that.volumeLast = that.playerElement.volume * 100; } that.trigger(EVENT_TAP_VOLUME, 0); }).on('click', '.slider .progress', function(event) { const $this = $(this); that.trigger(EVENT_TAP_TIME, (event.clientX - $this.offset().left) * that.duration / $this.width()); }).on('click', '.volume-slider .progress', function(event) { const $this = $(this); that.trigger(EVENT_TAP_VOLUME, (event.clientX - $this.offset().left) * 100 / $this.width()); }).on('click', '.full-icon .fa', function() { if (that.element.hasClass('player-full')) { that.exitFullscreen(); return; } that.fullScreen(); }); } private initVideo() { this.videoMask(); this.element.on('click', '.player-mask', () => { this.trigger(EVENT_BOOT); if (this.playerElement) { this.trigger(EVENT_TAP_PLAY); } }); } private bindAudioEvent() { if (this.booted) { return; } this.booted = true; this.playerElement = document.createElement('audio'); this.playerElement.src = this.options.src; this.playerElement.addEventListener('timeupdate', () => { if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) { this.trigger(EVENT_TIME_UPDATE, 0, 0); return; } this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration); }); this.playerElement.addEventListener('ended', () => { this.trigger(EVENT_ENDED); }); this.playerElement.addEventListener('pause', () => { this.trigger(EVENT_PAUSE); }); this.playerElement.addEventListener('play', () => { this.trigger(EVENT_PLAY); }); this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100); } private bindVideoEvent() { if (this.booted) { return; } this.booted = true; this.playerElement = this.element.find('.player-video')[0] as HTMLVideoElement; this.playerElement.addEventListener('timeupdate', () => { if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) { this.trigger(EVENT_TIME_UPDATE, 0, 0); return; } this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration); }); this.playerElement.addEventListener('ended', () => { this.trigger(EVENT_ENDED); }); this.playerElement.addEventListener('pause', () => { this.trigger(EVENT_PAUSE); }); this.playerElement.addEventListener('play', () => { this.trigger(EVENT_PLAY); }); this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100); if (screenFull) { document.addEventListener(screenFull[4], () => { if (this.checkFull()) { this.trigger(EVENT_ENTER_FULL_SCREEN); return; } this.trigger(EVENT_EXIT_FULL_SCREEN); }); } } private audioPlayer() { this.element.addClass('audio-player'); this.element.html(`<div class="icon" title="播放"> <i class="fa fa-play"></i> </div> <div class="slider"> <div class="progress" title="0"> <div class="progress-bar"></div> </div> </div> <div class="time"> 00:00/00:00 </div> <div class="volume-icon"> <i class="fa fa-volume-up"></i> </div> <div class="volume-slider"> <div class="progress" title="100"> <div class="progress-bar" style="width: 100%;"></div> </div> </div>`); } private videoMask() { this.element.addClass('video-player'); this.element.html(`<div class="player-mask" title="此处有视频,点击即可播放"> <i class="fa fa-play"></i> </div>`); } private videoFrame() { this.element.html(` <iframe class="player-frame" src="${this.options.src}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`); } private videoPlayer() { this.element.html(`<video class="player-video" src="${this.options.src}"></video> <div class="player-bar"> <div class="icon" title="播放"> <i class="fa fa-play"></i> </div> <div class="slider"> <div class="progress" title="0"> <div class="progress-bar"></div> </div> </div> <div class="time"> 00:00/00:00 </div> <div class="volume-icon"> <i class="fa fa-volume-up"></i> </div> <div class="volume-slider"> <div class="progress" title="100"> <div class="progress-bar" style="width: 100%;"></div> </div> </div> <div class="full-icon"> <i class="fa fa-expand"></i> </div> </div>`); } private formatMinute(time: number): string { return this.twoPad(Math.floor(time / 60)) + ':' + this.twoPad(Math.floor(time % 60)); } private twoPad(n: number) { const str = n.toString(); return str[1] ? str : '0' + str; } private fullScreen(element: any = document.documentElement) { if (!screenFull) { return; } element[screenFull[0]](); } private exitFullscreen(element: any = document) { if (!screenFull) { return; } element[screenFull[1]](); } private checkFull(): boolean { return screenFull && Boolean(document[screenFull[2]]); } } $.fn.player = function(option?: IPlayerOption) { return new MediaPlayer(this, option); }; })(jQuery); 样式 .audio-player, .video-player { .progress { height: .4em; border-radius: 0; margin-top: .2em; display: flex; overflow: hidden; line-height: 0; font-size: .75rem; background-color: #e9ecef; .progress-bar { cursor: default; display: flex; flex-direction: column; justify-content: center; overflow: hidden; color: #fff; text-align: center; white-space: nowrap; background-color: #007bff; transition: width .6s ease; } &:hover { height: .8em; margin-top: 0; } } } .audio-player { box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%); display: flex; box-sizing: border-box; line-height: 2.5em; i { font-style: normal; } .icon { width: 2.5em; text-align: center; } .slider { flex: 1; padding-top: 1em; } .time { line-height: 80rpx; font-size: .8em; } .volume-icon { padding-left: .5em; } .volume-slider { width: 3em; padding-top: 1em; padding-right: .5em; } &.player-mini { .volume-icon, .volume-slider { display: none; } } } .video-player { position: relative; box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%); i { font-style: normal; } .player-mask { background-color: #000; text-align: center; padding: 1em; .fa { color: #fff; font-size: 4em; } &:hover { .fa { color: #a10000; } } } .player-frame { border: 0; width: 100%; height: 100vw; max-height: 400px; } .player-video { border: 0; width: 100%; height: 100vw; max-height: 400px; background-color: #000; margin: 0; } .player-bar { display: flex; box-sizing: border-box; line-height: 2.5em; .icon { width: 2.5em; text-align: center; } .slider { flex: 1; padding-top: 1em; } .time { line-height: 80rpx; font-size: .8em; } .volume-icon { padding-left: .5em; } .volume-slider { width: 3em; padding-top: 1em; padding-right: .5em; } .full-icon { width: 2em; text-align: center; } } &.player-full { position: fixed; left: 0; right: 0; bottom: 0; top: 0; z-index: 9999; background-color: #000; box-shadow: none; .player-video { width: 100%; height: 100%; max-height: 100%; } .player-bar { background-color: rgba(43,51,63,.7); color: #fff; position: absolute; bottom: 0; left: 0; right: 0; opacity: 0; transition: 1s opacity; &:hover { opacity: 1; } } } } 使用 <link type="text/css" href="player.css" rel="stylesheet" media="all"> <div id="player"></div> <script src="jquery.player.min.js"></script> <script type="text/javascript"> jQuery(document).ready(function () { $('#player').player({ src: "1616073275141535.mp3", type: "audio" }); }); </script> 请先引入jquery.js 参数说明 src 为资源文件的网络路径 type 可选值 video|audio|iframe ;默认为 video ; audio 即音频;iframe 为其他网址的视频,例如 bilibili 分享的链接 参考资料 【screenfull.js】

2021/4/6
articleCard.readMore

Net Core 实现一个简单的分页功能

主要代码 [HtmlTargetElement("pagination")] public class PagerTagHelper : TagHelper { // 总共有多少条记录 public long Total { get; set; } = 0; // 一页有多少条记录 public int PerPage { get; set; } = 20; // 当前第几页 public int Page { get; set; } = 1; // 分页链接的最多显示个数 public int PageLength { get; set; } = 7; private string url; // 当前网址 public string Url { get { return url; } set { if (value.IndexOf('?') < 0) { url = value + "?page="; return; } url = Regex.Replace(value, @"([\?\&])page=\d+\&*", "$1") + "&page="; } } // 是否显示上一页下一页 public bool DirectionLinks { get; set; } = false; public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "div"; var html = new StringBuilder(); var items = initLinks(out bool canPrevious, out bool canNext); html.Append("<ul class=\"pagination\">"); if (DirectionLinks && canPrevious) { html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{1}{2}\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"sr-only\">上一页</span></a></li>", canPrevious ? "" : " disabled", Url, Page - 1 ); } foreach (var item in items) { if (item < 1) { html.Append("<li class=\"page-item disabled\"><a class=\"page-link\">...</a></li>"); continue; } html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{2}{3}\">{1}</a></li>", item == Page ? " active" : "", item, Url, item ); } if (DirectionLinks && canNext) { html.AppendFormat("<li class=\"page-item{0}\"><a class=\"page-link\" href=\"{1}{2}\" aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"sr-only\">下一页</span></a></li>", canNext ? "" : " disabled", Url, Page + 1 ); } html.Append("</ul>"); output.Content.SetHtmlContent(html.ToString()); } private List<int> initLinks(out bool canPrevious, out bool canNext) { var total = (int)Math.Ceiling((double)Total / PerPage); canPrevious = Page > 1; canNext = Page < total; var items = new List<int>(); if (total < 2) { return items; } items.Add(1); var lastList = (int)Math.Floor((double)PageLength / 2); var i = Page - lastList; var length = Page + lastList; if (i < 2) { i = 2; length = i + PageLength; } if (length > total - 1) { length = total - 1; i = Math.Max(2, length - PageLength); } if (i > 2) { items.Add(0); } for (; i <= length; i++) { items.Add(i); } if (length < total - 1) { items.Add(0); } items.Add(total); return items; } } 使用 这是控制器中的方法,需要传递当前网址到分页类中。 public IActionResult Index(int page = 1) { ViewData["items"] = _repository.GetPage(page); ViewData["fullUrl"] = $"{HttpContext.Request.Path}{HttpContext.Request.QueryString}"; ViewData["pageIndex"] = page; return View(); } NetDream.Web 为当前项目命名空间 Page 为 NPoco 查询获取到的分页数据 @using NPoco; @addTagHelper *, NetDream.Web @{ var items = ViewData["items"] as Page<BlogModel>; var pageIndex = (int)ViewData["pageIndex"]; } <pagination url="@ViewData["fullUrl"]" total="@items.TotalItems" page="@pageIndex"></pagination>

2021/4/4
articleCard.readMore

关于内容中的 @用户 加 话题 的一些想法

关于内容中的 @用户 加 话题 的一些想法 第一种html形式 直接生成 html 加上链接,这种也分预处理和实时处理 但是这种对多平台应用并不友好, 例如web 端和 app 端就不能统一,必须在应用内部再处理。 这就增加了一些变化。不能统一 例如: @user 这是一个召唤用户 #每天一次# 输出的内容为 <a href="/user/1">@user</a>这是一个召唤用户 <a href="/topic/1">#每天一次#</a> 优点 web 端快速 缺点 不能适应多平台应用,增加了其他平台的难度 第二种附加属性 参考搜索对关键词的标注 也可以分预处理和实时处理 返回一段原始文本,附加一些标注参数 例如: @user 这是一个召唤用户 #每天一次# 输出 { "content": "@user 这是一个召唤用户 #每天一次#", "rules": [ { "word": "@user", "rule": "user", "id": 1, }, { "word": "#每天一次#", "rule": "topic", "id": 1, }, ] } 这样由各自应用自行组合,不必进行解析步骤。 优点 方便多平台应用使用 拓展 可以把这段数据由发布时进行保存传输,而不是服务端进行提取。 加入规则字符串的开始结束位置,能更准确的定位。 数据存储思考 直接保存到一张表中。保存到 text 或 json 类型的字段中。 增加一张表,分规则保存。方便关联数据变化进行改动。

2021/3/30
articleCard.readMore

Github Host 更改

特别说明 此方法不稳定,但至少有时能打开 第一步 主要解决 github 加载慢打不开的情况 github网址查询:▷ GitHub.com : GitHub: Where the world builds software · GitHub github域名查询:▷ github.global.ssl.Fastly.net Website statistics and traffic analysis | Fastly | fastly.net github静态资源ip:▷ assets-cdn.Github.com Website statistics and traffic analysis | Github | github.com 第二步 刷新DNS缓存 ipconfig /flushdns

2021/3/19
articleCard.readMore

OBS-Studio 等录屏软件录制显示器内容的黑屏的解决方法

关键点 开始菜单 进入 设置 搜索进入 图形设置 点击 浏览 选择软件的 exe 文件 点击 软件 选项 选择 节能 保存即可 非必要 可以给 软件 加上 以管理员身份运行 兼容属性

2021/3/19
articleCard.readMore

angular 11 FormBuilder 中 FormGroup 和 FormArray 使用

angular 11 FormBuilder 中 FormGroup 和 FormArray 使用 导入模块 @NgModule({ imports: [ ReactiveFormsModule, ], }) export class AppModule {} 注入组件 export class EditComponent { constructor( private fb: FormBuilder, ) { } } 普通表单具体代码 初始化表单 export class EditComponent { public form = this.fb.group({ name: ['', Validators.required], }); constructor( private fb: FormBuilder, ) { } } 绑定表单页面 <form [formGroup]="form" (ngSubmit)="tapSubmit()"> <div class="form-group row"> <label for="name" class="col-md-3 col-form-label">称呼</label> <div class="col-md-9"> <input type="text" class="form-control" formControlName="name" id="name" placeholder="请输入称呼"> </div> </div> <div class="row"> <button class="btn btn-primary offset-md-3">保存</button> </div> </form> 设置值 this.form.patchValue({ name: res.name, }); 特殊表单项:一个可变的数组 FormArray 必须为 FormGroup 的子项 初始化表单 export class EditComponent { public form = this.fb.group({ name: ['', Validators.required], children: this.fb.array([]) }); constructor( private fb: FormBuilder, ) { } get children() { return this.form.get('children') as FormArray; } public addChild() { this.children.push(this.fb.group({ name: ['', Validators.required], label: ['', Validators.required], required: [false], only: [false], })); } public removeChild(i: number) { this.children.removeAt(i); } } 绑定表单页面 <form [formGroup]="form" (ngSubmit)="tapSubmit()"> <div class="form-group row"> <label for="name" class="col-md-3 col-form-label">称呼</label> <div class="col-md-9"> <input type="text" class="form-control" formControlName="name" id="name" placeholder="请输入称呼"> </div> </div> <table class="table table-hover"> <thead> <tr> <th>名称</th> <th>别名</th> <th>是否必填</th> <th>不公开</th> <th></th> </tr> </thead> <tbody> <ng-container *ngFor="let item of children.controls; let i = index"> <tr [formGroup]="item"> <td> <input type="text" formControlName="name" class="form-control"> </td> <td> <input type="text" formControlName="label" class="form-control"> </td> <td> <input type="checkbox" formControlName="required" value="1" > </td> <td> <input type="checkbox" formControlName="only" value="1" > </td> <td> <i class="iconfont icon-close" (click)="removeChild(i)"></i> </td> </tr> </ng-container> </tbody> <tfoot> <tr> <td colspan="5"> <i class="iconfont icon-plus" (click)="addChild()"></i> </td> </tr> </tfoot> </table> <div class="row"> <button class="btn btn-primary offset-md-3">保存</button> </div> </form> 设置值 for (const item of items) { this.children.push(this.fb.group(item)); }

2021/2/17
articleCard.readMore

angular 11 ngrx/effects 使用理解

angular 11 ngrx/effects 使用理解 我的理解:这个模块就是把网络请求获取数据,然后更新数据状态的操作合并到了一起。依然需要手动触发,既不会启动时自动加载,也不会获取时只加载一次。 例子: 有一个全局的数据:站点信息 interface AppState { site: ISite; } 通常的方法是: 网络请求获取数据 更新到Store中 app-component.ts ngOnInit() { this.service.site().subscribe(site => { this.store.dispatch(setSite({site})); }); } app.actions.ts export const selectSite = createSelector( (state: AppState) => state.site ); export const setSite = createAction('[app]SET_SITE', props<{site: ISite}>()); export const getSite = createAction('[app]GET_SITE'); 安装 npm i @ngrx/effects 声明effects @Injectable() export class AppEffects { loadSite$ = createEffect(() => this.actions$.pipe( ofType(getSite), switchMap(() => this.service.site().pipe( map(site => setSite({site})), catchError(() => EMPTY) )) )); constructor( private service: ShopService, private actions$: Actions, ) { } } 请注意 ofType 的参数不能是 setSite 不然或导致无限循环 声明 app.moudule.ts @NgModule({ imports: [ EffectsModule.forRoot([AppEffects]), ], }) 获取并使用 this.store.select(selectSite).subscribe(site => { // TODO }); 到这里并不能自动发送请求获取到信息 还必须请触发 app-component.ts ngOnInit() { this.store.dispatch(getSite()); } 大致流程 dispatch(getSite()) --> AppEffects.loadSite$ --> service 网络请求 --> setSite({site}) 更新到Store --> 响应 select(selectSite)

2021/2/8
articleCard.readMore

angular 11 ngrx/store 使用理解

angular 11 ngrx/store 使用理解 声明 export interface AuthSate { site: ISite; cart: any; } export const authFeatureKey = 'auth'; export interface AppState { [authFeatureKey]: AuthSate; } export const initialState: AuthSate = { site: null, // 初始化时,必须设置值,未设置(undefined)的将无法通知 cart: null, }; // 定义根据action 修改方法 const authReducer = createReducer( initialState, on(setCart, (state, {cart}) => ({...state, cart})), on(setSite, (state, {site}) => ({...state, site})), on(clearAuth, state => Object.assign({}, initialState)), // 返回初始化 ); export function reducer(state: State<AppState> | undefined, action: Action) { return authReducer(state, action); } Actions 定义action 用于修改Store中的数据 export const setCart = createAction('[shop]SET_CART', props<{cart: ICart}>()); export const setSite = createAction('[shop]SET_SITE', props<{site: ISite}>()); 相当于 export const setCart = (cart: ICart) => ({ type: '[shop]SET_CART', payload: {cart}, }); Selectors 定义 selector 用于获取数据 export const selectAuth = createFeatureSelector<AppState, AuthState>(authFeatureKey); export const selectCart = createSelector( selectAuth, (state: AuthState) => state.cart ); export const selectSite = createSelector( selectShop, (state: AuthState) => state.site ); 注册到程序中 注册根 app.module.ts StoreModule.forRoot({ [authFeatureKey]: reducer, }), 这是必须的如果没有全局数据,则 StoreModule.forRoot({}), 也可以为某一个模块注册局部数据 StoreModule.forFeature(authFeatureKey, reducer), 使用 设置 export class AppComponent { constructor( private store: Store<AppState>, ) { } setSite() { this.store.dispatch(setSite({site: {}})); } } 获取数据 export class AppComponent { constructor( private store: Store<AppState>, ) { this.store.select(selectSite).subscribe(site => { // TODO }); } } 特别注意 获取数据是一直实时更新的,除非取消订阅了。 更新的频率是依赖 reducer 的,把 reducer 看作一个对象,对象上的某一个属性更新了,会触发其他的属性更新事件 例如: this.store.select(selectSite).subscribe(site => { // 获取到的 site 的值 this.store.dispatch(setCart({cart: {}})); // 这样会陷入死循环,更新了 cart 的值,也会触发 site 更新的事件 });

2021/2/8
articleCard.readMore

angular 10 直接获取表单值

angular 10 直接获取表单值 在一些情况下只需要简单的获取表单的值,而不需要做一些不必要的值定义 例如: 列表的搜索框, 当然可以一个一个的值进行定义,通过绑定 public keywords = ''; <input [(ngModel)]="keywords"> 但这种情况值是始终保持和输入一直,但并不能同时保持和搜索结果的条件一直。 这时我想到了直接获取搜索表单 <form (ngSubmit)="tapSearch(searchForm.value)" #searchForm="ngForm"> <div class="input-group"> <label for="keywords">标题</label> <input type="text" class="form-control" name="keywords" ngModel id="keywords" placeholder="搜索标题" [value]="keywords"> </div> <button type="submit" class="btn btn-primary">搜索</button> </form> public keywords = ''; public tapSearch(form: any) { this.keywords = form.keywords || ''; // TODO } 这里需要注意 (ngSubmit)="tapSearch(searchForm.value)" #searchForm="ngForm" #searchForm="ngForm" 是获取表单对象 <input name="keywords" ngModel [value]="keywords"> ngModel 进行绑定键值,必须的,不然在 searchForm.value 中无法获取到值 searchForm.value 的具体值为 { keywords: '' }

2021/1/4
articleCard.readMore

angular 10 使用 tinymce 编辑器

使用 tinymce 提供的云编辑器 即真正的 tinymce 编辑器脚本都放到 他们的服务器上, 但访问速度真的不行 使用方法 npm i @tinymce/tinymce-angular 然后在模块里加载 import { EditorModule } from '@tinymce/tinymce-angular'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, EditorModule // 这就是编辑器模块 ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 在页面上使用,这里需要填上你在 www.tiny.cloud 上注册生成的 apiKey <editor apiKey="your-api-key" [init]="{ height: 500, menubar: false, plugins: [ 'advlist autolink lists link image charmap print preview anchor', 'searchreplace visualblocks code fullscreen', 'insertdatetime media table paste code help wordcount' ], toolbar: 'undo redo | formatselect | bold italic backcolor | \ alignleft aligncenter alignright alignjustify | \ bullist numlist outdent indent | removeformat | help' }" ></editor> 本地打包 则需要安装 tinymce 编辑器本体 npm i tinymce 然后配置 angular.json 复制脚本文件,及引用脚本 "assets": [ { "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" } ], "scripts": [ "node_modules/tinymce/tinymce.min.js" ] 在模块加载 import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; /* ... */ @NgModule({ /* ... */ imports: [ EditorModule ], providers: [ { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' } ] }) 使用 <editor [init]="{ base_url: '/tinymce', // Root for resources suffix: '.min' // Suffix to use when loading resources }"></editor> 加载语言包 从 官网 下载语言包 复制 zh_CN.js 到 src/assets/tinymce/langs 目录下 然后再使用时加上配置 <editor [init]="{ base_url: '/tinymce', suffix: '.min', language_url: '../../../../../assets/tinymce/langs/zh_CN.js', language: 'zh_CN', }"></editor> PS:这里的;路径为根目录,如果为二级目录backend,则需要修改为 base_url: '/backend/tinymce', suffix: '.min', language_url: '../../../../../backend/assets/tinymce/langs/zh_CN.js', language: 'zh_CN', 图片上传 更改使用时的配置,添加 imagetools 插件 { height: 500, base_url: '/backend/tinymce', suffix: '.min', language_url: '../../../../../backend/assets/tinymce/langs/zh_CN.js', language: 'zh_CN', plugins: [ 'advlist autolink lists link image imagetools charmap print preview anchor', 'searchreplace visualblocks code fullscreen', 'insertdatetime media table paste code help wordcount' ], toolbar: 'undo redo | formatselect | bold italic backcolor | \ alignleft aligncenter alignright alignjustify | \ bullist numlist outdent indent | removeformat | help', image_caption: true, paste_data_images: true, imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions', images_upload_handler: (blobInfo, success: (url: string) => void, failure: (error: string) => void) => { const form = new FormData(); form.append('file', blobInfo.blob(), blobInfo.filename()); // 这里时是上传的具体方法 this.httpClient.post<any>('upload/image', form).subscribe(res => { success(res.url); }, err => { failure(err.error.message); }); }, } images_upload_handler 为自定义上传方法, 这样就可以上传图片了

2020/10/13
articleCard.readMore

htaccess 搭配 angular 10 放在二级目录

把 angular 项目打包放在二级目录 例如: 把一个项目放在 backend 文件夹下 那么 需要设置 index.html <base href="/backend/"> 最好把所有页面放在一个 backend 的路由下 { path: 'backend', loadChildren: () => import('./backend/backend.module').then(m => m.BackendModule) } 这样在本地使用时生成的网址时 http://localhost:4200/backend/home 打包生成的网址也会是 http://zodream.cn/backend/home 但是这是刷新页面的话并不能指向 angular 程序 需要在网站根目录加上一个 .htaccess 的文件(PS:我用的时apache) <IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews -Indexes </IfModule> RewriteEngine On RewriteRule ^backend/[^\.]+$ backend/index.html </IfModule> 加上一句 RewriteRule ^backend/[^\.]+$ backend/index.html 就把路径指向 angular 程序了。 再次刷新网址 http://zodream.cn/backend/home 就能争取打开了

2020/10/13
articleCard.readMore

微信小程序跨页面传值

微信小程序跨页面传值 需求:跳转到另一个页面选择一些东西然后显示在页面中 例如:进入筛选项页面,选择,然后回到列表页面刷新页面。 第一种方法 在 app.js 里的 app.globalData 上设一个属性,进行传值共享 例如: 设置一个可共享的属性 data App({ globalData: { data: {} } }) 第二个页面 设置值 Page({ tapBack() { getApp().globalData.data = {}; wx.navigateBack(); } }) 第一个页面获取值 Page({ onShow() { const data = getApp().globalData.data; } }) 这种方法简单常用,但是使用不灵活,而且可能需要清理 getApp().globalData 上的属性, 第二种方法 直接通过 wx.navigateBack 调用页面上的方法进行回调 wx.navigateBack({ success() { setTimeout(() => { let page = getCurrentPages().pop(); if (!page) { return; } // TODO }, 10); } }); 这里需要注意 getCurrentPages().pop() 获取到的页面需要加上延迟,不然在真机上会获取到的是未返回的页面 例如: 第二个页面 返回值 Page({ tapBack() { wx.navigateBack({ success() { setTimeout(() => { let page = getCurrentPages().pop(); if (!page) { return; } page.selectedBack({}); }, 10); } }); } }) 第一个页面接收值 Page({ selectedBack(data) { // TODO } })

2020/10/12
articleCard.readMore

js 对 FileList 进行文件过滤上传

js 对 FileList 进行文件过滤上传 默认 FileList 是只读,无法进行修改 但有时候 前端可以进行一些文件过滤 例如: 当上传多个图片时,以拖拽的形式获取文件,但这时获取到的 FileList 除了图片可能还包含其他类型的文件 这是就可以使用 FormData 进行多文件上传 const form = new FormData(); for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.indexOf('image/') < 0) { continue; } form.append('file[]', file, file.name); } post('upload', form); 这样通过 post 方式提交,在服务端获取的依然是多个文件 例如:PHP $_FILES['file']; // 获取到的文件就是多文件 ['name' => ['', ''], ...]

2020/10/3
articleCard.readMore

angular自定义表单组件支持 formControlName

默认的 ngModel 的实现方式为 @Component({ selector: 'app-switch', template: ` <div (click)="tapAdd()">{{ value }}</div> `, styleUrls: ['./switch.component.scss'], }) export class SwitchComponent { @Input() value = 0; @Output() valueChange: EventEmitter<number> = new EventEmitter(); public tapAdd() { this.valueChange.emit(++ this.value); } } 使用 <app-switch [(ngModel)]="value"></app-switch> 但这种方式是不支持 formControlName 绑定的 会提示No value accessor for form control with name: 错误 修改代码 因此必须换种方式 @Component({ selector: 'app-switch', template: ` <div (click)="tapAdd()">{{ value }}</div> `, styleUrls: ['./switch.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SwitchComponent), multi: true } ] }) export class SwitchComponent implements ControlValueAccessor { public value = 0; public disable = false; onChange: any = () => { }; onTouch: any = () => { }; public tapAdd() { if (this.disable) { return; } this.onChange(++ this.value); } writeValue(obj: any): void { this.value = obj; } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouch = fn; } setDisabledState?(isDisabled: boolean): void { this.disable = isDisabled; } } 现在可以就支持两种方式了 <app-switch [(ngModel)]="value"></app-switch> <app-switch formControlName="value"></app-switch> 例子 Example with Input Example with Lazy Loaded Input Example with Button

2020/10/1
articleCard.readMore

基于不同形式的json响应处理

我的json格式 普通json响应 这是普通链接响应json格式,不论是否出错,响应状态码都是 200 { "code": 200, // 200 表示程序处理成功,其他数字自定义 "status": "success", // success 表示成功 failure 表示失败 "message": "", // 成功的消息提示 "errors": "", // 失败的信息 "data": {}, // 成功响应的信息 "url": "", // 搭配 code: 302 实现页面跳转 } 具体实例 成功返回用户信息 { "code": 200, "status": "success", "data": { "id": 1, "name": "zodream" }, } 失败返回提示无权限需要登录 { "code": 302, "status": "failure", "errors": "当前账户无权限,请先登录", "url": "/login?redirect_uri=/" } RESTful 响应 状态码判断是否处理成功 具体实例 成功返回用户信息 HTTP/1.1 200 OK { "id": 1, "name": "zodream" } 成功返回用户列表 HTTP/1.1 200 OK { "data": [ { "id": 1, "name": "zodream" } ] } 失败返回提示无权限需要登录 HTTP/1.1 302 Found { "code": 302, "message": "当前账户无权限,请先登录", } 具体代码实现, 以 netcore 版本为例 public interface IJsonResponse { public object Render(object data); public object RenderData(object data); public object RenderData(object data, string message); public object RenderPage<T>(Page<T> page); public object RenderFailure(string message, int code); public object RenderFailure(string message); } 普通版本的实现 public class JsonResponse : IJsonResponse { public object Render(object data) { return data; } public object RenderData(object data) { return Render(new { code = 200, status = "success", data }); } public object RenderData(object data, string message) { return Render(new { code = 200, status = "success", data, message }); } public object RenderFailure(string message, int code) { return new { code, status = "failure", message }; } public object RenderFailure(string message) { return RenderFailure(message, 404); } public object RenderPage<T>(Page<T> page) { return Render(new { code = 200, status = "success", data = page.Items, paging = new { limit = page.ItemsPerPage, offset = page.CurrentPage, total = page.TotalItems, more = page.CurrentPage < page.TotalPages } }); } } RESTful 的实现 public class PlatformResponse : IJsonResponse { public PlatformModel Platform { get; set; } public object Render(object data) { return data; } public object RenderData(object data) { return Render(new { data, appid = Platform.Appid }); } public object RenderData(object data, string message) { return Render(new { data, message }); } public object RenderFailure(string message, int code) { return new { code, message }; } public object RenderFailure(string message) { return RenderFailure(message, 404); } public object RenderPage<T>(Page<T> page) { return Render(new { data = page.Items, paging = new { limit = page.ItemsPerPage, offset = page.CurrentPage, total = page.TotalItems, more = page.CurrentPage < page.TotalPages } }); } } 通过中间件注入当前请求 public Task InvokeAsync(HttpContext context) { context.Items.Add("json", new JsonResponse()); return _next.Invoke(context); } 再控制器中使用 HttpContext.Items["json"] as IJsonResponse 在这里有一个问题,即状态码并没有实现,可以考虑拓展方法,把HttpContext当作参数传入,内部做状态码输入

2020/9/10
articleCard.readMore

flutter CupertinoPicker 使用不显示

CupertinoPicker是一个ios风格的齿轮滚动的选择器 CupertinoPicker.builder( itemExtent: 40, scrollController: FixedExtentScrollController(initialItem: 0), backgroundColor: Colors.white, onSelectedItemChanged: (index) { // 选择 }, itemBuilder: (context, index) => // items[index] 返回一个部件显示每一项的内容, childCount: items.length, ) 注意事项 在使用 CupertinoPicker 写地区选择器时,发现数据显示不出来,但使用 r 更新就能正常显示。 查找原因: 数据分为三列:即省、市、区, 数据都是网络请求的,因此第一次初始化的时候,数据长度是零。 刚开始,以为是没有setState(() {}); 更新,但多次设定也没用。 然后发现是因为 CupertinoPicker 接受数据长度为零的数据就没办法正常显示。 因此通过判断直接把数据长度为零的不生成CupertinoPicker,具体原因不知。 最终解决方案: List<List<Region>> regionItems = []; List<int> regionIndex = []; List<FixedExtentScrollController> regionController = []; Widget buildPickers() { var items = <Widget>[]; for (var i = 0; i < regionController.length; i++) { // 判断是数据的长度,小于1的那一列就不显示 if (regionItems[i].length < 1) { continue; } items.add(Expanded( child: CupertinoPicker.builder( itemExtent: 40, scrollController: regionController[i], backgroundColor: Colors.white, onSelectedItemChanged: (index) { changeRegion(i, index); }, itemBuilder: (context, index) => buildTextItem(regionItems[i][index].name), childCount: regionItems[i].length, ), )); } return Expanded( child: Row( children: items, ), ); } Widget buildTextItem(String text) { return Container( color: Colors.white, alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Text( text, textAlign: TextAlign.center, style: const TextStyle(fontSize: 14.0), ), ); }

2020/9/3
articleCard.readMore

CC协议

CC协议即版权协议 主要包括五个方面 版权 署名(BY):必须注明来源为创作者 非商业性运用(NC):非商业性目的 制止演绎(ND):不允许任何衍生物或作品的改编 相同方法同享(SA):必须按照相同的条款共享 主要六种不同的许可证类型 CC BY:本许可证允许再使用者以任何媒介或格式分发、混音、改编和建立在材料上,但必须注明来源为创作者。该许可证允许商业使用。 CC BY-SA:本许可证允许再使用者以任何媒介或格式分发、混音、改编和建立在材料上,但必须注明来源为创作者。该许可证允许商业使用。如果您对该材料进行再混合、改编或建立,您必须按照相同的条款对修改后的材料进行许可。 CC BY-NC:本许可证允许再使用者以任何媒介或格式发布、混音、改编和建立在材料基础上的非商业性目的,但前提是必须归功于创作者。 CC BY-NC-SA:本许可证允许再使用者以任何媒介或格式发布、混音、改编和建立在材料基础上的非商业性目的,但前提是必须归功于创作者。如果您对该材料进行再混合、改编或建立,您必须根据相同的条款对修改后的材料进行许可。 CC BY-ND:该许可证允许再使用者以任何媒介或格式复制和分发未经改编的材料,但前提是必须注明出处给创作者。该许可证允许商业使用。 CC BY-NC-ND:该许可证允许再使用者以任何媒介或格式复制和分发该材料,但仅限于非商业目的,且必须注明出处给创作者。 还有一种共享创意公共领域协议 CC0:该许可证是一种公共奉献工具,它允许创作者放弃自己的版权,并将其作品放到全球公共领域。CC0允许再使用者以任何媒介或格式无条件地分发、混音、改编和建立在材料上。 参考 【About CC Licenses】

2020/8/26
articleCard.readMore

flutter margin 负值实现

本身 Container 上的 margin 是不能设为负值 例如 Container( margin: EdgeInsets.only(top: -10), ) 这样会报错 Failed assertion: line 251: 'margin == null || margin.isNonNegative': is not true. 但可以通过 transform 属性实现负值效果 Container( transform: Matrix4.translationValues(0, -10, 0), )

2020/8/22
articleCard.readMore

win10添加删除开机自启项

常见形式 开始菜单启动文件夹 任务计划程序 注册表 启动文件夹 win + R 进入 运行 程序,输入 shell:Startup 打开文件夹 添加 直接创建程序的快捷方式 删除 删除快捷方式即可 任务计划程序 使用任务栏的搜索,搜索 任务计划程序 添加 点击右侧的创建任务 新建 触发器 选择 开始任务 启动时 新建 操作 选择要启动的程序 删除 可以点击右侧的结束、禁用、删除 注册表 win + R 输入 regedit 进入注册表 常见的注册表键值有如下几项 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce 添加 新建 字符串值 输入名称,输入路径(路径请用英文双引号包起来) 在数值数据的最后加上 /background 可以实现后台自启 删除 删除项即可

2020/8/21
articleCard.readMore

Wallpager Engine 删除记录

普通删除 直接选中,右键取消订阅即可, 但是没办法删除其他机器的订阅记录。因为有些记录是不出现在 已安装 列表里的(个人猜测是已下架的壁纸) 导致每一次安装都需要下载大量的文件。 针对这种记录,就需要用特别的方法。 根据文件标题去搜索中心安装取消订阅 按道理是可以的, 但是我没有试过,因为有很多壁纸都搜不到了 这种方法也很麻烦 让这些壁纸出现在已安装列表里即可取消订阅 首先,必须清楚每一个壁纸有一个特定的 编号,删除也是根据这个编号进行删除的 实际上每一个壁纸的文件夹名就是这个编号,只要把这个编号出现在列表里,就能取消订阅了 实际操作方法: 获取编号 找到配置文件(实际是在wallpager 的安装路径下的 bin\workshopcache.json 文件) 打开文件,复制一项,改掉 workshopid 为编号即可,最好 title 也改成编号,不然认不出 打开Wallpager Engine,选中取消订阅即可 重启Wallpager Engine,就删除成功了 提供一个脚本批量生成 import os import json def get_all_dir(dir): ''' :brief: 获取所有的文件夹名 :param dir: string 父文件夹路径 :return: string[] 子文件夹名 ''' for _, dirs, _ in os.walk(dir): return dirs def get_all_workshopid(dir): ''' :brief:获取所有项目的workshopid :param dir: string 父文件夹路径 :return: string[] id列表 ''' data = [] dirs = get_all_dir(dir) for item in dirs: data.extend(get_all_dir(dir + r'\\' + item)) return data def create_config(items, sample): ''' :brief:根据示例生成新的项 :param items: string[] workshopid数组 :param sample: dict :return: dict[] ''' data = [] for item in items: new_item = sample.copy() old = new_item['workshopid'] for (k, v) in new_item.items(): if k == 'title' or k == 'workshopid': new_item[k] = item elif isinstance(v, str): new_item[k] = v.replace(old, item) data.append(new_item) return data # steam 的安装路径 steam_dir = r'D:\Steam\\' # steam下载的文件路径 workshop_dir = steam_dir + r'steamapps\workshop\content' # wallpager 的配置文件路径 wallpager_config_file = steam_dir + r'steamapps\common\wallpaper_engine\bin\workshopcache.json' id_items = get_all_workshopid(workshop_dir) print('总文件个数:%d' %(len(id_items))) with open(wallpager_config_file, 'r+', encoding='utf-8') as fs: content = fs.read() res = json.loads(content) if len(res['wallpapers']) < 1: print('请先订阅一个作为示例') exit(0) sample = res['wallpapers'][0] exist_id = [] for item in res['wallpapers']: exist_id.append(item['workshopid']) data = list(set(id_items).difference(set(exist_id))) if len(data) < 1: print('没有缓存的文件') exit(0) new_items = create_config(data, sample) res['wallpapers'].extend(new_items) content = json.dumps(res, indent=4, ensure_ascii=False) fs.seek(0) fs.write(content) print('成功生成缓存配置文件,可以进行取消订阅') 全选或框选批量取消订阅即可 注意改掉里面的路径 steam_dir 必须先订阅一个壁纸 本来是想找到接口,通过接口删除的,但能力有限,没找到方法

2020/8/4
articleCard.readMore

angular10教程之http 拦截器

请求拦截器 登录令牌注入 token.interceptor.ts @Injectable() export class TokenInterceptor implements HttpInterceptor { constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const auth = this.injector.get(AuthService); const clonedRequest = request.clone({ headers: auth.getTokenHeader(request), url: this.fixUrl(request.url), params: request.params }); return next.handle(clonedRequest); } private fixUrl(url: string) { if (url.indexOf('http://') >= 0 || url.indexOf('https://') >= 0) { return url; } return environment.apiEndpoint + url; } } 响应拦截器 例如登录令牌失效 reponse.interceptor.ts @Injectable() export class ResponseInterceptor implements HttpInterceptor { constructor(private injector: Injector) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req).pipe(catchError((event: HttpEvent<any>) => { if (event instanceof HttpErrorResponse) { if (event.status === 401) { const auth = this.injector.get(AuthService); auth.logoutUser(); } } return throwError(event); })); } } 使用 @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }, // 使用请求拦截器 { provide: HTTP_INTERCEPTORS, useClass: ResponseInterceptor, multi: true }, // 使用响应拦截器 ], }) export class ThemeModule { static forRoot(): ModuleWithProviders<ThemeModule> { return { ngModule: ThemeModule }; } } AuthService const USER_KEY = 'user'; @Injectable() export class AuthService { constructor( private http: HttpClient, @Inject(PLATFORM_ID) private platformId: any) {} /** * 清除本地的token */ public logoutUser() { if (isPlatformBrowser(this.platformId)) { localStorage.clear(); } this.store.dispatch(this.actions.logoutSuccess()); } /** * 生成请求头 * @returns HttpHeaders */ getTokenHeader(request: HttpRequest<any>): HttpHeaders { if (this.getUserToken()) { return new HttpHeaders({ 'Content-Type': 'application/vnd.api+json', Authorization: `Bearer ${this.getUserToken()}`, Accept: '*/*' }); } return new HttpHeaders({ 'Content-Type': 'application/vnd.api+json', Accept: '*/*' }); } private setTokenInLocalStorage(user: any, keyName: string): void { const jsonData = JSON.stringify(user); if (isPlatformBrowser(this.platformId)) { localStorage.setItem(keyName, jsonData); } } public getUserToken() { if (isPlatformBrowser(this.platformId)) { const user: IUser = JSON.parse(localStorage.getItem(USER_KEY)); return user ? user.token : null; } else { return null; } } }

2020/8/4
articleCard.readMore

dpl 文件

解释:.dpl 主要为Potplayer直播源列表文件 文件格式 文件头 DAUMPLAYLIST playname= topindex=0 saveplaypos=0 playname 为当前播放的地址 topindex 为当前播放的序号,即在列表中的位置 列表项 1*file*地址 1*title*名称 序号从 1 开始 地址行为 序号 + *file* + 地址 标题行为 序号 + *title* + 地址 txt 文件转 dpl txt 文件每一行格式为 标题,地址 获取文件编码 移动到正文开始的位置 写入文件头 循环取每一行,转换并写入 Converter(saveFile, sourcefile); 具体转换代码 const string HEADER_TAG = "DAUMPLAYLIST"; const string FILE_TAG = "*file*"; const string TITLE_TAG = "*title*"; const string NEW_LINE = "\n"; public static readonly string[] Headers = new string[3]{ "playname=", // 当前播放的网址 "topindex=0", // 当前播放的序号 "saveplaypos=0" }; /// <summary> /// 转换文件 /// </summary> /// <param name="dist"></param> /// <param name="source"></param> public static void Converter(string dist, string source) { using (var sourceStream = new FileStream(source, FileMode.Open)) { using (var distStream = new FileStream(dist, FileMode.Create)) { Converter(distStream, sourceStream); } } } /// <summary> /// 转换文件 /// </summary> /// <param name="dist"></param> /// <param name="source"></param> public static void Converter(FileStream dist, FileStream source) { Reset(source); var encoder = new TxtEncoder(); var encoding = encoder.GetEncoding(source); source.Seek(encoder.Position, SeekOrigin.Begin); var line = ReadLine(source, encoding); if (line.Trim() == HEADER_TAG) { Reset(source); Copy(dist, source); return; } source.Seek(encoder.Position, SeekOrigin.Begin); Write(dist, HEADER_TAG); Write(dist, NEW_LINE); foreach (var item in Headers) { Write(dist, item); Write(dist, NEW_LINE); } var i = 1; while (null != (line = ReadLine(source, encoding))) { if (string.IsNullOrWhiteSpace(line)) { continue; } var args = line.Split(','); if (args.Length < 2) { continue; } Write(dist, i.ToString()); Write(dist, FILE_TAG); Write(dist, args[1]); Write(dist, NEW_LINE); Write(dist, i.ToString()); Write(dist, TITLE_TAG); Write(dist, args[0]); Write(dist, NEW_LINE); i++; } } /// <summary> /// 移动指针到开始位置 /// </summary> /// <param name="source"></param> private static void Reset(Stream source) { source.Seek(0, SeekOrigin.Begin); } /// <summary> /// 复制流 /// </summary> /// <param name="dist"></param> /// <param name="source"></param> private static void Copy(FileStream dist, FileStream source) { source.CopyTo(dist); } /// <summary> /// 读取一行 /// </summary> /// <param name="source"></param> /// <returns></returns> private static string ReadLine(FileStream source, Encoding encoding) { var bytes = new List<byte>(); int code; bool hasByte = false; while ((code = source.ReadByte()) != -1) { hasByte = true; if (code == 0x0a/* \n */ || code == 0x0d /* \r */) { break; } bytes.Add(Convert.ToByte(code)); } if (!hasByte) { return null; } if (bytes.Count < 1) { return ""; } return encoding.GetString(bytes.ToArray()); } /// <summary> /// 写入字符 /// </summary> /// <param name="dist"></param> /// <param name="line"></param> private static void Write(FileStream dist, string line) { var bytes = Encoding.UTF8.GetBytes(line); dist.Write(bytes, 0, bytes.Length); } 获取编码 /// <summary> /// 用于取得一个文本文件的编码方式(Encoding)。 /// </summary> public class TxtEncoder { /// <summary> /// 正文开始的位置 /// </summary> public int Position { get; private set; } = 0; /// <summary> /// 取得一个文本文件的编码方式。如果无法在文件头部找到有效的前导符,Encoding.Default将被返回。 /// </summary> /// <param name="fileName">文件名。</param> /// <returns></returns> public Encoding GetEncoding(string fileName) { return GetEncoding(fileName, Encoding.Default); } /// <summary> /// 取得一个文本文件流的编码方式。 /// </summary> /// <param name="stream">文本文件流。</param> /// <returns></returns> public Encoding GetEncoding(FileStream stream) { return GetEncoding(stream, Encoding.Default); } /// <summary> /// 取得一个文本文件的编码方式。 /// </summary> /// <param name="fileName">文件名。</param> /// <param name="defaultEncoding">默认编码方式。当该方法无法从文件的头部取得有效的前导符时,将返回该编码方式。</param> /// <returns></returns> public Encoding GetEncoding(string fileName, Encoding defaultEncoding) { var fs = new FileStream(fileName, FileMode.Open); var targetEncoding = GetEncoding(fs, defaultEncoding); fs.Close(); return targetEncoding; } /// <summary> /// 取得一个文本文件流的编码方式。 /// </summary> /// <param name="stream">文本文件流。</param> /// <param name="defaultEncoding">默认编码方式。当该方法无法从文件的头部取得有效的前导符时,将返回该编码方式。</param> /// <returns></returns> public Encoding GetEncoding(FileStream stream, Encoding defaultEncoding) { var targetEncoding = defaultEncoding; if (stream == null || stream.Length < 2) return targetEncoding; //保存文件流的前4个字节 byte byte3 = 0; //保存当前Seek位置 var origPos = stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin); var nByte = stream.ReadByte(); var byte1 = Convert.ToByte(nByte); var byte2 = Convert.ToByte(stream.ReadByte()); if (stream.Length >= 3) { byte3 = Convert.ToByte(stream.ReadByte()); } //根据文件流的前4个字节判断Encoding //Unicode {0xFF, 0xFE}; //BE-Unicode {0xFE, 0xFF}; //UTF8 = {0xEF, 0xBB, 0xBF}; if (byte1 == 0xFE && byte2 == 0xFF)//UnicodeBe { targetEncoding = Encoding.BigEndianUnicode; Position = 2; } else if (byte1 == 0xFF && byte2 == 0xFE && byte3 != 0xFF)//Unicode { targetEncoding = Encoding.Unicode; Position = 3; } else if (byte1 == 0xEF && byte2 == 0xBB && byte3 == 0xBF) //UTF8 { targetEncoding = Encoding.UTF8; Position = 3; } else { stream.Seek(0, SeekOrigin.Begin); int read; while ((read = stream.ReadByte()) != -1) { if (read >= 0xF0) break; if (0x80 <= read && read <= 0xBF) break; if (0xC0 <= read && read <= 0xDF) { read = stream.ReadByte(); if (0x80 <= read && read <= 0xBF) continue; break; } if (0xE0 > read || read > 0xEF) continue; read = stream.ReadByte(); if (0x80 <= read && read <= 0xBF) { read = stream.ReadByte(); if (0x80 <= read && read <= 0xBF) { targetEncoding = Encoding.UTF8; } } break; } } //恢复Seek位置 stream.Seek(origPos, SeekOrigin.Begin); return targetEncoding; } }

2020/8/2
articleCard.readMore

微信小程序开发记录(一)真机无法进入页面

这是一个以前开发的正常上线项目,今天要更新一下,突然出问题了 具体症状 在微信开发者工具中是正常的,使用真机模拟时,发现一直加载中,而且加载的圆已全,不动了,在控制台也没有任何错误输出,在首页的js文件中 onLoad onShow 方法都正常执行了。 解决 先看是不是代码问题,无论删除多少代码都不行, 在看是不是项目配置版本的问题,改到最新也不行, 最后,查到 app.json 这个文件,删除一些配置,试试,才发现是底部导航栏配置的问题, 在以前,是允许导航栏名称为空的,即以下写法是没有问题的 { "pagePath": "page/index/index", "text": "" } 但是现在不行了,必须有字符,需改成 { "pagePath": "page/index/index", "text": " " }

2020/8/1
articleCard.readMore

flutter 跳转页面操作上一页

跳转到登录页,登录成功后返回上一页,并更新上一页 主要通过 Navigator.pop(context, args); 第二个参数就是要传的值 那么该怎么接受呢 Navigator.pushNamed(context, LOGIN_PATH).then((args) { // TODO }); 通过 then 即可接受传过来的值,然后根据值判断是否登录成功等

2020/7/30
articleCard.readMore

Regex Generator 使用指南

操作指南 输入要匹配的内容 输入正则表达式 点击按钮开始匹配,结果会出现在右下角列表里 输入模板语句 双击匹配结果任意一项,弹出组合的结果 点击复制即可 左上角有个灯泡图标,点击查看规则 模板生成语法 1. {} 直接输出 默认为 0, 可以 int 可以用 . 连接使用第二级的内容 2. for 语句 {for} 或 {~} 开始标志 for 无参数时,表示循环输出全部 for(n) 一个参数时,表示从0 开始 共输出 n 个 for(m,) 第二个参数缺省时,表示从m 开始,输出后面所有的 for(m,n) 两个参数时,表示从 m 开始,共输出 n 个 循环体 {} 只能是 int 或 标签 ,不再支持 . 连接,暂不支持 for 循环 {end} 或 {!} 结束标志 3. 整数循环 {1 2 ... 7} 开始标志 {1 ... 7} 从1循环到7 即输出 1 2 3 4 5 6 7 {1,3 ... 7} 从1循环到7 每个数隔2 , 即 1 3 5 7 {16:1 2 ... 15} 从1循环到15的十六进制 即输出 1 2 ... F {} 取值标志 {end} 或者 {!} 结束标志 4. {m~n} 输出 m到n 的随机数 5. 新增驼峰与下划线转换 {studly:} “ ”、“_”、“-” 为分割符,包括首字母转大写成驼峰写法例如 aa_aa 转成 AaAa {lstudly:} 首字母为小写的驼峰写法 例如 aa_aa 转成 aaAa {unstudly:} 驼峰写法转下划线 例如 AaAa 转成 aa_aa

2020/7/26
articleCard.readMore

go init函数

介绍 init 函数用于包的初始化 init 函数执行在 main 之前 一个包 可以有多个 init 函数 同一个包 的 init 函数执行顺序无明确定义,不同包的init函数是根据包导入的依赖关系决定的 执行 使用 import 就会执行 init 如果仅执行包的 init 函数,不导入其他函数,使用 import _ "package" 通过init仅执行一次 import "sync" var once sync.Once func init() { once.Do(func() { // TODO }) }

2020/7/24
articleCard.readMore

angular 9 升级 angular 10

单例服务 Singleton services export class ThemeModule { static forRoot(): ModuleWithProviders { return { ngModule: ThemeModule }; } } 修改为 static forRoot(): ModuleWithProviders<ThemeModule> { return { ngModule: ThemeModule }; } CommonJS or AMD dependencies can cause optimization bailouts warning 解决办法: 配置 CommonJS 依赖项 修改 angular.json projects > 项目名 > architect > build > options 添加 allowedCommonJsDependencies 填入依赖名 "options": { "allowedCommonJsDependencies": [ "lodash", "jsonapi-deserializer" ], }

2020/7/24
articleCard.readMore

kotlin AndroidManifest 注意事项

android:value 为数字则无法通过字符串获取 例如: <meta-data android:name="XXX" android:value="11543906547"/> 获取 packageManager.getApplicationInfo( packageName, PackageManager.GET_META_DATA ).metaData?.getString("XXX"); 返回的值为 null, 因为 Bundle 自动识别为数字 正确的写法是修改 AndroidManifest,前加上 \ (反斜杠和空格)即可 <meta-data android:name="XXX" android:value="\ 11543906547"/>

2020/7/22
articleCard.readMore

对于zodream 框架的优化的思考

php 与其他最大的不同是不支持多进程,本身也就没法实现web程序,需要搭配其他服务软件。 因此每一个访问就是一个全新的进程,没法实现一个内容的共享,造成内存浪费,使得并发数少。 优化方向 路由 目前是实时寻找路由,加载多个文件,时间较长 考虑解决方向 使用路由缓存,加载一个文件即可执行最终控制方法。 可以使用 redis,已最终路径为键,保存。 关于路由重写的,可以在生成时做好映射 配置 目前处于每次都得加载三个文件并进行多维数组合并,考虑进行合并成一个文件。 数据库 查询语句考虑抛弃实时拼接方法 终极方法 使用配置文件,配置路由,配置路由需要的参数,配置路由需要的依赖项,直接就访问控制器方法。最大程度减少加载文件。数据库访问全部使用 redis ,同步到mysql通过其他方式,数据可以共享的都存到 redis 中。

2020/7/22
articleCard.readMore

flutter 页面滚动条

GridView、ListView 嵌套使用需要在子列表添加 shrinkWrap:true, // 处理列表嵌套报错 physics: NeverScrollableScrollPhysics(), // 处理子列表中滑动父级列表无法滑动 CustomScrollView 子项可以是列表或其他部件 如果是列表则使用 SliverList 、 SliverFixedExtentList 、 SliverGrid 如果是其他部件则必须使用 SliverToBoxAdapter SliverList 和 SliverFixedExtentList 的区别,如果知道高度 则推荐用 SliverFixedExtentList 例如 标题栏 SliverFixedExtentList( itemExtent: 50, // 是设置每一个子元素的高度 delegate: SliverChildListDelegate( <Widget>[ Container( color: Colors.white, height: 50, child: Center( child: Text(title), ), ) ], ), ) 两列商品类别 SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 1.0, crossAxisSpacing: 1.0, childAspectRatio: 0.7, ), delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return ProductItem( item: data[index], ); }, childCount: data.length), ),

2020/7/14
articleCard.readMore

flutter swiper 使用

添加项目依赖 flutter_swiper : any 使用 Container( width: MediaQuery.of(context).size.width, height: 200.0, child: Swiper( itemBuilder: (context, index) => CachedNetworkImage(imageUrl: banners[index].content), itemCount: banners.length, autoplay: true, loop: true, ), ) 注意 必须放到 一个父元素了,不能直接放到 <Widget>[] 中作为一个子元素,原因是必须指定尺寸,即宽和高

2020/7/14
articleCard.readMore

flutter API请求

dio 的使用 响应 虽然获取到的数据可以自动进行json解析,但是获取的数据不会自动进行类转换 即 dio.request<T>() T 只能指定为 dynamic 或 Map,无法作为其他类进行转换,例如定义了一个类 @JsonSerializable() class Site extends Object { String name; String version; String logo; int goods; int category; int brand; String currency; Site(this.name, this.version, this.logo, this.goods, this.category, this.brand, this.currency); factory Site.fromJson(Map<String, dynamic> json) => _$SiteFromJson(json); Map<String, dynamic> toJson() => _$SiteToJson(this); } 而实际服务器响应的是json { "name": "zodream", "version": "0.1", "logo": "https://zodream.cn/assets/upload/image/wap_logo.png", "category": 6, "brand": 1, "goods": 133, "currency": "¥" } 使用 dio.request<Site>() 是会报错的, 只能手动转换 var response = dio.request<Map<String, dynamic>>(); return Site.fromJson(response.data);

2020/7/14
articleCard.readMore

flutter 自定义 AppBar

实现 import 'package:flutter/material.dart'; class SearchAppBar extends StatefulWidget implements PreferredSizeWidget { final Widget child; //从外部指定内容 final Color backgroundColor; // 背景颜色 SearchAppBar({this.child, this.backgroundColor}); @override _SearchAppBarState createState() => _SearchAppBarState(); @override Size get preferredSize => new Size.fromHeight(kToolbarHeight); } class _SearchAppBarState extends State<SearchAppBar> { @override Widget build(BuildContext context) { return Container( height: kToolbarHeight + MediaQuery.of(context).padding.top, width: MediaQuery.of(context).size.width, color: widget.backgroundColor, child: SafeArea( top: true, bottom: false, child: widget.child, ), ); } } 使用 SearchAppBar appBar() { return SearchAppBar( backgroundColor: Color(0xFF05A6B1), child: Row( children: <Widget>[ CachedNetworkImage( imageUrl: site.logo, width: 100, ), Expanded( child: Container( child: Row( children: <Widget>[ Icon(Icons.search), Text('搜索商品, 共${site.goods}款好物') ], ), decoration: BoxDecoration( color: Color(0xFFededed), borderRadius: BorderRadius.all(const Radius.circular(2))), ), ), Container( child: IconButton(icon: Icon(Icons.message), onPressed: () {}), width: 54, ) ], ), ); } MediaQuery MediaQuery.of(context) 获取当前设备的信息 属性 说明 size 逻辑像素,并不是物理像素,类似于Android中的dp,逻辑像素会在不同大小的手机上显示的大小基本一样,物理像素 = size*devicePixelRatio。 devicePixelRatio 单位逻辑像素的物理像素数量,即设备像素比。 textScaleFactor 单位逻辑像素字体像素数,如果设置为1.5则比指定的字体大50%。 platformBrightness 当前设备的亮度模式,比如在Android Pie手机上进入省电模式,所有的App将会使用深色(dark)模式绘制。 viewInsets 被系统遮挡的部分,通常指键盘,弹出键盘,viewInsets.bottom表示键盘的高度。 padding 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏。 viewPadding 被系统遮挡的部分,通常指“刘海屏”或者系统状态栏,此值独立于padding和viewInsets,它们的值从MediaQuery控件边界的边缘开始测量。在移动设备上,通常是全屏。 systemGestureInsets 显示屏边缘上系统“消耗”的区域输入事件,并阻止将这些事件传递给应用。比如在Android Q手势滑动用于页面导航(ios也一样),比如左滑退出当前页面。 physicalDepth 设备的最大深度,类似于三维空间的Z轴。 alwaysUse24HourFormat 是否是24小时制。 accessibleNavigation 用户是否使用诸如TalkBack或VoiceOver之类的辅助功能与应用程序进行交互,用于帮助视力有障碍的人进行使用。 invertColors 是否支持颜色反转。 highContrast 用户是否要求前景与背景之间的对比度高, iOS上,方法是通过“设置”->“辅助功能”->“增加对比度”。 此标志仅在运行iOS 13的iOS设备上更新或以上。 disableAnimations 平台是否要求尽可能禁用或减少动画。 boldText 平台是否要求使用粗体。 orientation 是横屏还是竖屏。 参考 【Flutter 强大的MediaQuery控件】

2020/7/14
articleCard.readMore

flutter 主题配置

ThemeData ThemeData({ Brightness brightness, VisualDensity visualDensity, MaterialColor primarySwatch, //备用主题颜色,如果没有设定primaryColor就使用该颜色 Color primaryColor, //主题主色,决定导航栏颜色 Brightness primaryColorBrightness, Color primaryColorLight, Color primaryColorDark, Color accentColor, //主题次级色,决定大多数Widget的颜色,如进度条、开关等。 Brightness accentColorBrightness, Color canvasColor, Color scaffoldBackgroundColor, Color bottomAppBarColor, Color cardColor, //卡片颜色 Color dividerColor, //分割线颜色 Color focusColor, Color hoverColor, Color highlightColor, Color splashColor, InteractiveInkFeatureFactory splashFactory, Color selectedRowColor, Color unselectedWidgetColor, Color disabledColor, Color buttonColor, ButtonThemeData buttonTheme, //按钮主题 ToggleButtonsThemeData toggleButtonsTheme, Color secondaryHeaderColor, Color textSelectionColor, Color cursorColor, Color textSelectionHandleColor, Color backgroundColor, Color dialogBackgroundColor, //对话框背景颜色 Color indicatorColor, Color hintColor, Color errorColor, Color toggleableActiveColor, String fontFamily, TextTheme textTheme, // 字体主题,包括标题、body等文字样式 TextTheme primaryTextTheme, TextTheme accentTextTheme, InputDecorationTheme inputDecorationTheme, IconThemeData iconTheme, IconThemeData primaryIconTheme, IconThemeData accentIconTheme, SliderThemeData sliderTheme, TabBarTheme tabBarTheme, TooltipThemeData tooltipTheme, CardTheme cardTheme, ChipThemeData chipTheme, TargetPlatform platform, MaterialTapTargetSize materialTapTargetSize, bool applyElevationOverlayColor, PageTransitionsTheme pageTransitionsTheme, AppBarTheme appBarTheme, BottomAppBarTheme bottomAppBarTheme, ColorScheme colorScheme, DialogTheme dialogTheme, FloatingActionButtonThemeData floatingActionButtonTheme, NavigationRailThemeData navigationRailTheme, Typography typography, CupertinoThemeData cupertinoOverrideTheme, SnackBarThemeData snackBarTheme, BottomSheetThemeData bottomSheetTheme, PopupMenuThemeData popupMenuTheme, MaterialBannerThemeData bannerTheme, DividerThemeData dividerTheme, ButtonBarThemeData buttonBarTheme}) 使用 Theme.of(context).primaryColor 配置 在 main.dart 中 void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Shop', debugShowCheckedModeBanner: false, theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, secondaryHeaderColor: Color(0x05a6b1), indicatorColor: Color(0xFFB4282D), backgroundColor: Color(0xF4F4F4), ), home: IndexPage(), ); } }

2020/7/14
articleCard.readMore

WPF 使用 WebView2

安装SDK 必须安装 Microsoft Edge (Chromium) Canary channel 浏览器 NuGet 安装 Microsoft.Web.WebView2 预发行版 0.9.538-prerelease,正式版0.9.538安装是没用的 使用 xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" <wv2:WebView2 x:Name="Browser" Source="https://www.baidu.com"/> 相关事件 SourceChanged 可以获取当前的网址,可以更新网页通过 js 修改的网址 NavigationStarting 网页加载开始 NavigationCompleted 网页加载完成 CoreWebView2Ready CoreWebView2 初始化完成,可以进行 CoreWebView2 上的事件绑定 CoreWebView2.NewWindowRequested 加载新标签之前触发,可以通过 e.Handled = true; 阻止触发,e.Uri 为要跳转的网址 代码跳转 Browser.Source = new Uri(url); 获取源代码 主要通过 ExecuteScriptAsync 方法执行 js 代码,返回的值都是 JSON 过的。如果 JSON 编码失败则返回 null using Newtonsoft.Json; var html = await Browser.ExecuteScriptAsync("document.getElementsByTagName('html')[0].innerHTML"); if (string.IsNullOrWhiteSpace(html)) { return html; } return "<!DOCTYPE html><html>" + JsonConvert.DeserializeObject(html) + "</html>"; 获取 cookie using System; using System.ComponentModel; using System.Net; using System.Security; using System.Security.Permissions; using System.Text; /// <summary></summary> /// 取得WebBrowser的完整Cookie。 /// 因为默认的webBrowser1.Document.Cookie取不到HttpOnly的Cookie /// public class FullWebBrowserCookie { [SecurityCritical] public static string GetCookieInternal(Uri uri, bool throwIfNoCookie) { uint pchCookieData = 0; var url = UriToString(uri); const uint flag = (uint)NativeMethods.InternetFlags.INTERNET_COOKIE_HTTPONLY; //Gets the size of the string builder if (NativeMethods.InternetGetCookieEx(url, null, null, ref pchCookieData, flag, IntPtr.Zero)) { pchCookieData++; var cookieData = new StringBuilder((int)pchCookieData); //Read the cookie if (NativeMethods.InternetGetCookieEx(url, null, cookieData, ref pchCookieData, flag, IntPtr.Zero)) { DemandWebPermission(uri); return cookieData.ToString(); } } var lastErrorCode = 0; if (throwIfNoCookie || (lastErrorCode != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS)) { throw new Win32Exception(lastErrorCode); } return null; } private static void DemandWebPermission(Uri uri) { var uriString = UriToString(uri); if (uri.IsFile) { var localPath = uri.LocalPath; new FileIOPermission(FileIOPermissionAccess.Read, localPath).Demand(); } else { new WebPermission(NetworkAccess.Connect, uriString).Demand(); } } private static string UriToString(Uri uri) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } UriComponents components = (uri.IsAbsoluteUri ? UriComponents.AbsoluteUri : UriComponents.SerializationInfoString); return new StringBuilder(uri.GetComponents(components, UriFormat.SafeUnescaped), 2083).ToString(); } } using System; using System.Runtime.InteropServices; using System.Security; using System.Text; internal sealed class NativeMethods { #region enums public enum ErrorFlags { ERROR_INSUFFICIENT_BUFFER = 122, ERROR_INVALID_PARAMETER = 87, ERROR_NO_MORE_ITEMS = 259 } public enum InternetFlags { INTERNET_COOKIE_HTTPONLY = 8192, //Requires IE 8 or higher INTERNET_COOKIE_THIRD_PARTY = 131072, INTERNET_FLAG_RESTRICTED_ZONE = 16 } #endregion #region DLL Imports [SuppressUnmanagedCodeSecurity, SecurityCritical, DllImport("wininet.dll", EntryPoint = "InternetGetCookieExW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)] internal static extern bool InternetGetCookieEx([In] string Url, [In] string cookieName, [Out] StringBuilder cookieData, [In, Out] ref uint pchCookieData, uint flags, IntPtr reserved); #endregion } 使用获取cookie FullWebBrowserCookie.GetCookieInternal(Browser.Source, false)

2020/7/3
articleCard.readMore

网站体检之xss攻击

今天突然想到,虽然sql 注入不了(因为所有前台参数是使用pdo 自带的过滤,比起自己造的过滤器好多了),但是xss 攻击还是有可能的 注册表单检查 用户名注入 在用户名填入以下字符, <script>alert(1);</script> 注册成功后,立马显示弹出框,可以注入 解决:直接对用户名保存进行转换 forum发表主题标题可以xss 标题可以 xss 攻击 主要原因:面包屑没有做转换 code标签可以xss 解决:对标签保存前进行转换 MicroBlog 内容可以xss 解决:在保存前进行转换 Blog 可以xss 标题及简介都可以在显示时转换 内容在markdown 模式下 调用方法即可 (new Parsedown())->setSafeMode(true)

2020/6/26
articleCard.readMore

GitHub 下载慢怎么办

提高加载速度 码云 码云来作为中转站,但是码云对仓库大小有一定限制,所以太大的仓库用这个方法不行 使用cnpmjs镜像 只需要修改你的路径 github.com 为 github.com.cnpmjs.org 但有时必须使用 github 源才行 使用 git clone 时常终止 这时可以使用 github 客户端,虽然对下载速度没有帮助,但是可以保证下载不会中断

2020/6/26
articleCard.readMore

小程序商城开发总结

服务端使用:php 前端使用 ts bug 修复 购物删除商品导致无法选中 原因:在服务端使用了 unset() 移除数组的项导致索引数组变成了关联数组,传到前端变成了对象就没法使用 for 循环了 解决:使用 array_values() 再转成索引数组即可 订单列表删除根据id 移除失败 原因:使用了 === 判断,但两个id 一个是 string 一个是 number 判断失败 解决:改为 == 即可 选择地区导致收货人空白 原因:收货人并没有进行保存,而在更新地区时使用了 this.setData(this.data) 导致收货人也更新了 解决:要么进行收货人输入保存,要么使用 this.setData({region}),只更新一个

2020/6/18
articleCard.readMore

gulp-vue2mini 更新日志

2020-06-28 版本: 1.1.0 新增 html 模板开发 新增 内置ts sass 编译,可以不用gulp vue2mini --watch 监听脚本变动 --input 源码文件或文件夹,默认 `src` 文件夹 --output 目标保存文件夹,默认 `dist` 文件夹 --mini 编译小程序,默认为 编译模板 新增模板html生成(与其他功能不共用) 生效条件 以 @ 开头,前面可以有空格,功能占整行, @ 后面的内容以空格隔开,空格之后为评论内容,不做生成 @@ 为注释,放在首行为声明此文件不生成html页面 @... 加载其他引用此文件的文件内容 @layout/main 加载其他文件,拓展名可以省略 @item@5 加载其他文件重复5次 加载的样式及样式文件自动合并到 head 末尾 加载的脚本及脚本文件自动合并到 body 末尾 支持 ts sass 自动编译 vue2mini --watch 2020-06-12 版本: 1.0.5 修复因html标签内部换行导致的解析错误 增加对 @click.stop 的转化 增加页面直接对方法传递参数的原始参数。即 @click="tap(1)" 允许接收 function tap(i, e: TouchEvent){} 删除一些不必要的 text 标签生成,例如 a 不再内部生成 text 子标签 修复 v-show 解析问题 调整对 vue 模板的解析流程,直接内嵌 rename 文件后缀 最新使用方法 gulp.task('vue', async() => { await gulp.src(getSrcPath('src/**/*.vue')) .pipe(template('tpl')) .pipe(gulp.dest(getDistFolder('dist/'))) .pipe(template('json')) .pipe(gulp.dest(getDistFolder('dist/'))) .pipe(template('scss')) .pipe(template('presass')) .pipe(sass()) .pipe(template('endsass')) .pipe(gulp.dest(getDistFolder('dist/'))) .pipe(template('ts')) .pipe(ts.createProject('tsconfig.json')) .pipe(gulp.dest(getDistFolder('dist/'))); });

2020/6/13
articleCard.readMore

c# 图片处理及ico 生成

根据路径获取图片 var bmp = new Bitmap(fileName); 更改图片尺寸 Image img; var bmp = new Bitmap(width, height); bmp.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (var g = Graphics.FromImage(bmp)) { g.CompositingMode = CompositingMode.SourceCopy; switch (quality) { case SmoothingMode.AntiAlias: g.CompositingQuality = CompositingQuality.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.SmoothingMode = SmoothingMode.AntiAlias; break; case SmoothingMode.HighQuality: g.CompositingQuality = CompositingQuality.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality; break; case SmoothingMode.HighSpeed: g.CompositingQuality = CompositingQuality.HighSpeed; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.PixelOffsetMode = PixelOffsetMode.HighSpeed; g.SmoothingMode = SmoothingMode.HighSpeed; break; } using (var ia = new ImageAttributes()) { ia.SetWrapMode(WrapMode.TileFlipXY); g.DrawImage(source, new System.Drawing.Rectangle(0, 0, width, height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, ia); } } return bmp; png 转 ico ico 图标是由不同尺寸的png 图片组成。因此可以设置不同尺寸下不同的图。 生成不同尺寸的图 /// <summary> /// 修改图片尺寸 /// </summary> /// <param name="width"></param> /// <param name="height"></param> /// <param name="source"></param> /// <returns></returns> public static Bitmap ResizeImage(int width, int height, Image source, SmoothingMode quality) { var bmp = new Bitmap(width, height); bmp.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (var g = Graphics.FromImage(bmp)) { g.CompositingMode = CompositingMode.SourceCopy; switch (quality) { case SmoothingMode.AntiAlias: g.CompositingQuality = CompositingQuality.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.SmoothingMode = SmoothingMode.AntiAlias; break; case SmoothingMode.HighQuality: g.CompositingQuality = CompositingQuality.HighQuality; g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality; break; case SmoothingMode.HighSpeed: g.CompositingQuality = CompositingQuality.HighSpeed; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.PixelOffsetMode = PixelOffsetMode.HighSpeed; g.SmoothingMode = SmoothingMode.HighSpeed; break; } using (var ia = new ImageAttributes()) { ia.SetWrapMode(WrapMode.TileFlipXY); g.DrawImage(source, new System.Drawing.Rectangle(0, 0, width, height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, ia); } } return bmp; } ico 头 /// <summary> /// 写头 /// </summary> /// <param name="count"></param> /// <param name="writer"></param> private static void CreateHeader(int count, BinaryWriter writer) { writer.Write((ushort)0); writer.Write((ushort)1); writer.Write((ushort)count); // } 获取图片数据的长度,及每一个像素,每一个像素又包括 RGBA private static int GetImageSize(Image image) { return image.Height * image.Width * 4; } 写入每张图的位置 private static void CreateDirectory(int offset, Image image, int imageLength, BinaryWriter writer) { var size = imageLength + 40; var width = image.Width >= 256 ? 0 : image.Width; var height = image.Height >= 256 ? 0 : image.Height; var bpp = Image.GetPixelFormatSize(image.PixelFormat); writer.Write((byte)width); writer.Write((byte)height); writer.Write((byte)0); writer.Write((byte)0); writer.Write((ushort)1); writer.Write((ushort)bpp); writer.Write((uint)size); writer.Write((uint)offset); } 写入每张图的数据 private static void CreateBitmap(Image image, int compression, int imageLength, BinaryWriter writer) { writer.Write((uint)40); writer.Write((uint)image.Width); writer.Write((uint)image.Height * 2); writer.Write((ushort)1); writer.Write((ushort)Image.GetPixelFormatSize(image.PixelFormat)); writer.Write((uint)compression); writer.Write((uint)imageLength); writer.Write(0); writer.Write(0); writer.Write((uint)0); writer.Write((uint)0); } private static void CreateDib(Image image, BinaryWriter writer) { for (int i = 0; i < image.Height; i++) { for (int j = 0; j < image.Width; j++) { var color = (image as Bitmap).GetPixel(j, i); writer.Write(color.B); writer.Write(color.G); writer.Write(color.R); writer.Write(color.A); } } } 生成ico IEnumerable<Image> images; Stream stream; var bw = new BinaryWriter(stream); CreateHeader(images.Count(), bw); var offset = 6 + (16 * images.Count()); foreach (var item in images) { var length = GetImageSize(item); CreateDirectory(offset, item, length, bw); } foreach (var item in images) { CreateBitmap(item, colorMode, GetImageSize(item), bw); CreateDib(item, bw); } bw.Dispose();

2020/6/11
articleCard.readMore

UWP TextBlock 换行

开发时,遇到 文字太长未换行,记录一下 手动换行 通过代码设置内容。默认支持 \n 换行 TextBlock1.Text = "a\nb"; 或 TextBlock1.Inlines.Add(new Run("a")); TextBlock1.Inlines.Add(new LineBreak()); TextBlock1.Inlines.Add(new Run("b")); xaml 上通过转义字符
或 <LineBreak/> 换行 <TextBlock Text="a
b"/> <TextBlock> <Run>a</Run> <LineBreak/> <Run>b</Run> </TextBlock> 备注   空格 	 Tab 
 回车 
 换行 自动换行 设置属性 TextWrapping="Wrap" <TextBlock TextWrapping="Wrap"/>

2020/6/9
articleCard.readMore

js 数组排序 sort

使用方法 const items = [3, 1, 12]; const res = items.sort(); // items [1, 12, 3] res.push(4); // items [1, 12, 3, 4] 注意 sort 会改变自身的排序, 并返回自身 默认排序是按字符串升序进行排序,不分数字数组 数字按大小升序 const items = [3, 1, 12]; items.sort((a: number, b: number) => { return a - b; }); // items [1, 3, 12] 数字按大小降序 const items = [3, 1, 12]; items.sort((a: number, b: number) => { return b - a; });

2020/6/8
articleCard.readMore

抛弃create.js 改用pixi.js

今天在 github 看到推荐了 pixi.js,就行进行了了解。 对比 create.js pixi.js 已不更新了 至今更新 js编写的 ts编写的,更喜欢ts 基于canvas 支持canvas和WebGL 支持Adobe Animate图形工具 无图形工具 文档有 文档有(不是最新的) 压缩后235kb 365kb 包含tween 不包含 总结:保持更新及使用 ts 为最重要的选择原因 简单例子 pixie.js 官网 文档 中文文档 npm install pixi.js 这是官方的例子 import * as PIXI from 'pixi.js'; // The application will create a renderer using WebGL, if possible, // with a fallback to a canvas render. It will also setup the ticker // and the root stage PIXI.Container const app = new PIXI.Application(); // The application will create a canvas element for you that you // can then insert into the DOM document.body.appendChild(app.view); // load the texture we need app.loader.add('bunny', 'bunny.png').load((loader, resources) => { // This creates a texture from a 'bunny.png' image const bunny = new PIXI.Sprite(resources.bunny.texture); // Setup the position of the bunny bunny.x = app.renderer.width / 2; bunny.y = app.renderer.height / 2; // Rotate around the center bunny.anchor.x = 0.5; bunny.anchor.y = 0.5; // Add the bunny to the scene we are building app.stage.addChild(bunny); // Listen for frame updates app.ticker.add(() => { // each frame we spin the bunny around a bit bunny.rotation += 0.01; }); }); 插件 周边插件也挺丰富的,动画(spine/dragonbones),粒子系统,物理引擎等等 相关页面

2020/6/8
articleCard.readMore

后台列表页进阶之路

PS: 这是无聊时,嫌弃列表页不好看,之后的改良之路,这只是代表个人审美。 第一版:基本功能 包括: 搜索 列表 分页 从图上可以看出基本功能都存在了,但是感觉不美观,也空洞。 改进1 从搜索框开始 新增 按钮移动右边 有铺满的感觉了 改进2 增加页面提示 页面提示,也可以作为操作指南。 解决了搜索顶格的不适,让搜索不至于处在视野边缘。 改进3 移动分页到中间, 居中,大屏幕下,视野中间,容易找 列表项改进 抛弃传统 table,自适应方面差及屏幕展示的信息少(不是指那种有横向滚动的,而且横向滚动条很烦) 这样主要信息都能查看,而且自适应方便,主题也鲜明了

2020/6/7
articleCard.readMore

局域网文件传输工具

一个用于局域网传输文件的小工具 源码 File-Pass 程序 功能介绍 简单的局域网内部文件传送。 支持文件夹及多文件添加。 支持多文件同时传送,但默认为单进程 支持自动获取本地ip 支持自动获取其他设备ip 缺陷 局域网内部所有ip获取有问题,并不能获取所有ip 文件无具体进度显示 使用方法 接收方 点击 监听 按钮即可 发送方 选择或填写 目标IP(即接收方ip) 点击 选择文件 按钮选择文件夹即可

2020/6/7
articleCard.readMore

angular9教程之PullToRefresh

使用 <app-pull-to-refresh class="items-box" (refreshChange)="tapRefresh()" (moreChange)="tapMore()" [more]="hasMore" [loading]="isLoading"> <ng-container *ngFor="let item of items"> <dl class="book-item" (click)="tapItem(item)"> <dt> <a >{{ item.title }}</a> <span class="book-time">{{ item.created_at }}</span></dt> <dd> <p>{{ item.description }}</p></span> </dd> </dl> </ng-container> </app-pull-to-refresh> refreshChange 下拉刷新事件 moreChange 加载更多事件 more 是否有更多 loading 是否加载中 distance 触底距离 @ViewChild(PullToRefreshComponent) public pullBox: PullToRefreshComponent; public tapRefresh() { this.goPage(1); } public tapMore() { if (!this.hasMore) { return; } this.goPage(this.page + 1); } public goPage(page: number) { if (this.isLoading) { return; } this.isLoading = true; this.pullBox?.startLoad(); this.service.getPage({ page }).subscribe(res => { this.page = page; this.hasMore = res.paging.more; this.isLoading = false; this.items = page < 2 ? res.data : [].concat(this.items, res.data); this.pullBox?.endLoad(); }, () => { this.isLoading = false; this.pullBox?.endLoad(); }); } 推荐使用方法通知,虽然 loading 也可以,但是如果加载没有时间间隔的话,是不起作用的。 startLoad 指定开始加载 endLoad 指定加载完成 监听滚动事件 @HostListener('scroll', [ '$event.target.scrollTop', '$event.target.scrollHeight', '$event.target.offsetHeight', ]) public onDivScroll( scrollY: number, scrollheight: number, offsetHeight: number, ): void { const height = scrollheight; const y = scrollY + offsetHeight; if (this.more && y + this.distance > height) { // 触发加载更多 } } 缺点 第一次不能自动加载,如果有更多,但没有滚动条,也不会自动加载更多 具体代码 请查看【GITHUB】

2020/6/2
articleCard.readMore

c# 添加防火墙例外端口

必须添加 NetFwTypeLib ,在 引用 > 添加引用 > COM /// <summary> /// 添加防火墙例外端口 /// </summary> /// <param name="name">名称</param> /// <param name="port">端口</param> /// <param name="protocol">协议(TCP、UDP)</param> public static void NetFwAddPorts(string name, int port, string protocol) { INetFwMgr netFwMgr = (INetFwMgr)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwMgr")); INetFwOpenPort objPort = (INetFwOpenPort)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwOpenPort")); objPort.Name = name; objPort.Port = port; if (protocol.ToUpper() == "TCP") { objPort.Protocol = NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_TCP; } else { objPort.Protocol = NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_UDP; } objPort.Scope = NET_FW_SCOPE_.NET_FW_SCOPE_ALL; objPort.Enabled = true; bool exist = false; foreach (INetFwOpenPort mPort in netFwMgr.LocalPolicy.CurrentProfile.GloballyOpenPorts) { if (objPort == mPort) { exist = true; break; } } if (!exist) netFwMgr.LocalPolicy.CurrentProfile.GloballyOpenPorts.Add(objPort); }

2020/5/29
articleCard.readMore

c# 获取局域网ip

获取本机局域网ip AddressList必须倒序查找 /// <summary> /// 获取本机IP地址 /// </summary> /// <returns>本机IP地址</returns> public static string GetLocalIP() { try { var hostName = Dns.GetHostName(); //得到主机名 var ipEntry = Dns.GetHostEntry(HostName); var ips = ipEntry.AddressList; for (int i = ips.Length - 1; i >= 0; i--) { //从IP地址列表中筛选出IPv4类型的IP地址 //AddressFamily.InterNetwork表示此IP为IPv4, //AddressFamily.InterNetworkV6表示此地址为IPv6类型 if (ips[i].AddressFamily == AddressFamily.InterNetwork) { return ips[i].ToString(); } } return ""; } catch (Exception ex) { return ''; } } 获取局域网所有ip 这里用的是 ping 方法 ping 255 个地址 private List<string> ipItems = new List<string>(); /// <summary> /// 获取局域网的其他ip /// </summary> /// <param name="baseIp"></param> /// <param name="exsits"></param> private void LoadAllIp(string baseIp, string exsits) { for (int i = 1; i <= 255; i++) { var ip = baseIp + i; if (ip == exsits) { continue; } var ping = new Ping(); ping.PingCompleted += Ping_PingCompleted; ping.SendAsync(ip, 2000, null); } } private void Ping_PingCompleted(object sender, PingCompletedEventArgs e) { if (e.Reply.Status == IPStatus.Success) { ipItems.Add(e.Reply.Address.ToString()); } } 获取的时间为 2-3 秒,获取的也不一定准确

2020/5/29
articleCard.readMore

angular9教程之自定义部件及获取页面元素

组件接受传值 使用 @Input() 定义 @Component({ selector: 'app-page-tip', template: './page-tip.component.html', styleUrls: ['./page-tip.component.scss'] }) export class PageTipComponent { @Input() public title = '提示'; } 使用 <app-page-tip [title]="'标题'"></app-page-tip> 组件事件通知 通过 @Output() 定义事件,使用 .emit() 触发事件通知 @Output() public valueChange = new EventEmitter(); public tap() { this.valueChange.emit(); } 使用 <app-page-tip (valueChange)="tapValue()"></app-page-tip> 双向绑定 使用 [()] 进行双向绑定,那么应该修改事件,保证 事件名为 参数名 + Change 例如 @Component({ selector: 'app-page-tip', template: './page-tip.component.html', styleUrls: ['./page-tip.component.scss'] }) export class PageTipComponent { @Input() public title = '提示'; @Output() public titleChange = new EventEmitter(); public tap() { this.titleChange.emit(); } } 使用 <app-page-tip [(title)]="title"></app-page-tip> 获取页面元素 例如 <div>12312</div> <app-page-tip [(title)]="title"></app-page-tip> 获取 div,必须在div 上加 #+命名 <div #app>12312</div> 然后通过 @ViewChild('命名') 获取 @ViewChild('app') private box: ElementRef; 这是 box.nativeElement 就是 HTMLDivElement,可以想普通元素一样操作。 如果要绑定事件,那么请注意先等页面已经生成,不然获取不到。 ngAfterViewInit 就是在页面初始化后触发。 export class HomeComponent implements AfterViewInit { @ViewChild('app') private box: ElementRef; ngAfterViewInit(): void { const div = this.box.nativeElement as HTMLDivElement; div.addEventListener('click', (event) => { // TODO }); } } 获取页面上的自定义组件 例如 <app-page-tip [(title)]="title"></app-page-tip> 通过 @ViewChild(组件类名) 获取 @ViewChild(PageTipComponent) private box: PageTipComponent;

2020/5/28
articleCard.readMore

angular9教程之表单验证及确认密码验证

FormGroup 的使用 这是一个注册表单, export class RegisterComponent { public registerForm = this.fb.group({ name: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, passwordValidator]], confirm_password: ['', [Validators.required]], agree: [false, Validators.requiredTrue] }, { validators: confirmValidator() }); constructor( private fb: FormBuilder ) { } get email() { return this.registerForm.get('email'); } get password() { return this.registerForm.get('password'); } } 通过 [formGroup] 绑定表单数据源 通过 formControlName 绑定表单项 <form class="form-ico login-form" [formGroup]="registerForm" (ngSubmit)="tapSignUp()"> <div class="input-group"> <input type="text" formControlName="name" placeholder="请输入昵称" required=""> <i class="iconfont icon-user" aria-hidden="true"></i> </div> <div class="input-group" [class]="{error: email.invalid}"> <input type="email" formControlName="email" placeholder="请输入邮箱" required=""> <i class="iconfont icon-at" aria-hidden="true"></i> </div> <div class="input-group" [class]="{error: password.invalid}"> <input type="password" formControlName="password" placeholder="请输入密码" required=""> <i class="iconfont icon-lock" aria-hidden="true"></i> </div> <div class="input-group" [class]="{error: registerForm.errors && registerForm.errors.confirm}"> <input type="password" formControlName="confirm_password" placeholder="请确认密码" required=""> <i class="iconfont icon-check" aria-hidden="true"></i> </div> <div class="input-group"> <div class="checkbox"> <input type="checkbox" formControlName="agree" value="1" id="checkboxInput"> <label for="checkboxInput"></label> </div> 同意《 <a href="https://zodream.cn/agreement">本站协议</a> 》 </div> <button type="submit" class="btn" [disabled]="registerForm.invalid">注册</button> <div class="other-box"> <a routerLink="../">返回登录</a> </div> </form> 获取错误信息 AbstractControl.invalid 获取是否有误 AbstractControl.errors. 获取具体错误信息,注意 .errors 有可能为 undefined 需要先判断 在这里,自定义了两个验证器: 密码验证 passwordValidator 可以验证密码的复杂程度。 export const passwordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { return control.value && control.value.length < 6 ? { password_simple: true } : null; }; 确认密码验证 confirmValidator 可以验证确认密码是否一致 export const confirmValidator = (key: string = 'password', confirmKey: string = 'confirm_password'): ValidatorFn => { return (control: FormGroup): ValidationErrors | null => { return control.get(key).value !== control.get(confirmKey).value ? { confirm : true } : null; }; }; 1). 接受两个参数,为需要比较的两个字段名, 2). 必须放在 FormGroup 上,不然获取不到两个值。 3). 通过 FormGroup.errors && FormGroup.errors.confirm 获取有误信息

2020/5/27
articleCard.readMore

netcore 依赖注入 AddTransient、AddScoped、AddSingleton

使用 Startup.cs public void ConfigureServices(IServiceCollection services) { services.AddScoped<IDatabase>(x => { return new Database(Configuration.GetConnectionString("Default"), DatabaseType.MySQL, MySql.Data.MySqlClient.MySqlClientFactory.Instance); }); services.AddScoped(typeof(UserRepository)); } 区别 AddTransient 每次注入的都是新实例,相当于手动 new。 AddScoped 每次请求,都获取一个新的实例,在一个请求中,获取多次会得到相同的实例。 AddSingleton 每次都获取同一个实例

2020/5/25
articleCard.readMore

createjs 实现封装 drawImage

问题 在 createjs 中,默认是没有封装 canvas 的 drawImage 方法, 想 添加图片到 canvas 上,只能使用 const img: HTMLImageElement; const box = new createjs.Shape(); box.graphics.beginBitmapFill(img).drawRect(0, 0, img.width, img.height); 如果想移动图片的位置,只能使用 box.x 和 box.y, 而改变 drawRect 上的值,就是在图片上裁剪。默认 img 是无限重复的, 即 .drawRect(10, 10, img.width, img.height) 并不是移动到 10,10 再开始画整张图,而是先从 0,0 画,横向重复一次,纵向再重复一次,这是成 田 字排列四张图,然后 选取 10,10 到 img.width + 10, img.height + 10 矩形区域显示 那么,想从 10,10 开始画整张图该怎么办? 解决方法 canvas 默认是有 drawImage 方法,调用就行了 class ImgFill { constructor( private img: HTMLImageElement, private x: number, private y: number, private width: number, private height: number ) { } public exec(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, this.x, this.y, this.width, this.height); ctx.restore(); } } box.graphics.append(new ImgFill(img, 10, 10, img.width, img.height)); 实现一个封装的对象,exec 方法是这个对象必须有的,有这个方法,才能被调用,实现绘制。

2020/5/24
articleCard.readMore

笔记(200521)游戏及玩家分类

本文为阅读 《平衡掌控者游戏数值战斗设计》做的笔记。 游戏分类 角色扮演 RPG 包括:动作角色扮演、模拟角色扮演、策略角色扮演、角色扮演冒险、恋爱角色扮演、角色扮演解谜 动作 ACT 包括:射击(STG)、格斗(FTG)、动作冒险、动作角色扮演(APRG) 冒险 AVG 包括:动作冒险、文字冒险、恋爱冒险 模拟 SIM 或 SLG 包括:策略模拟、模拟经营、模拟养成、战争、飞行、载具模拟 策略 包括:回合战略、回合战术、即时战略、即时战术、解谜 其他 包括:音乐、休闲、体育(SPT)、竞速(RAC)等 玩家分类 基于 MMORPG (大型多人在线角色扮演) 杀手型 通过虚拟世界发泄现实生活压力,包括语言攻击和虐杀对其他人造成心理或精神伤害而获取成就感。 成就型 以提升装备和等级、完成任务为目的。 探索型 包括审美型和学习型,游戏就是获取新素材分享出去。 社交型 游戏就是为了交朋友。

2020/5/22
articleCard.readMore

关于 MasterDetail 模式页面收集

介绍 基本结构 左侧为列表,右边为内容 默认右侧无内容,显示空白或背景 小屏下,默认显示列表,点击列表项才只显示内容,返回回复列表 适用场景 以列表和详情为主的程序 邮件类,参考例子: windows 10 自带的邮件 app 新闻、博客类 消息聊天类 手机版界面转pc,这种感觉上还是不太适合,但这种模式更有感觉,界面不会太变形 实现思路 左右两边有明显分割; 左右宽度最好右边宽,右边才是主体; 右边可以是 iframe 这种内嵌,但是页面少的就不用了,直接放到一个页面就好了,设置这类页面可以直接右侧弹窗

2020/5/22
articleCard.readMore

怎么写日记(一)格式规范

基本格式 必须内容 日期,基本上精确到天,年月日,年可以省略,连续记日记,在进入新的一年时,要写清楚是×年,之后便可以只写×月×日 星期几 天气 正文 可选内容 标题 示例 5月19日   星期二      天气晴 今天做了什么。 日记内容 记的事件要真实。 要多记自己看到或经历过的事和人,多记身边发生过的现实生活中的事和人。只有真实的最能反映事件和心情,真实地叙述问题,才有参考价值,才是有效的积累,才能反映规律。 记的事件内容要具体些,少用概括性叙述,有一定的描写。 写日记主要是练笔。它不同于一般的记录。因此,要通过仔细回忆和重新观察,把事件或人物的细节写出来。 要有选择地记。 一则日记一般只记一件事,不能太杂,不能拖泥带水地在一则日记中什么都级。一则日记要为荣一个中心。这个中心可以是一件事、一个场景、一段对话、一处风景、一个外貌、一种心情、一个动作等。切记不要流水账。 记事件的感受不要牵强附会。 每个人都会对身边发生的事产生一些感受,都会有自己的想法。在记叙过程中,可以穿插自己的感受。但这种感受一定要真实,自己是怎么想的就怎么写,不要老是考虑这个想法对不对。

2020/5/22
articleCard.readMore

关于 "简书" "提示浏览器版本过低" 探究

事先声明 本文只是个人观点,不涉及任何利益之争。 情况 经测试,在手机上使用 edge 浏览器、火狐浏览器、谷歌浏览器,都会出现 您的xx手机浏览器版本过低,请立即免费升级。 垃圾广告既视感! 猜测 先来猜测一下,为什么会这么做?有哪些可能原因 为自己的APP 拉流量 垃圾广告注入,这是主动放的广告 被攻击了,这是被动放的广告 探究 点击弹窗的按钮 确定 进入一个网址页面 那么点击一下 取消 试试,弹出 您确定取消升级吗?, 确定 关闭弹窗,取消 进入下载页面 关键是,每次进入都有弹窗。 先找到弹窗脚本 使用电脑打开谷歌浏览器,切换到手机模拟模式,修改手机型号,苹果手机不会弹出,找个安卓的,Moto G4会弹窗。 找到工具 性能 页,进行录制,刷新页面,关闭弹窗, 停止录制,找到执行脚本 打开脚本 jian.js 发现里面加密了, 下载脚本到本地,根据据关键词 手机浏览器版本过低 可以搜索到,加密了就不继续了, 这个脚本文件是 jian.t58b.com 这个域名下的 按照简书被劫持了吗 说法:这是 简书的广告主 经过三重域名跳转,跳转到一个 APP 下载网站, 关于 最终下载的APP 就不知道是什么了,不敢安装 通过域名查询 发现 通过备案查询发现 t58b.com 为个人,而最终 跳转域名为 微型企业 到此为止,不想继续了 总结 做网站还是得注意选择合适的广告主,虽然要恰饭,但也不能随便恰,逮着狗屎啃,惹了一身骚,导致网站体验不好就不值得了。 选择广告也应该考虑用户的体验,影响用户体验,就不好了。

2020/5/20
articleCard.readMore

angular9教程之路由

主要写在模块的 -routing.module.ts 文件里 const routes: Routes = [ ]; routes 就是放路由的。 基本写法 显示页面,例如: home 路径显示 HomeComponent 的内容 const routes: Routes = [ { path: 'home', component: HomeComponent, }, ]; 未匹配到指定 未找到 页面 const routes: Routes = [ { path: '**', component: NotFoundComponent, }, ]; 指定路由重定向到新的路由 const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, ]; 指定路由到指定模块 const routes: Routes = [ { path: 'blog', loadChildren: () => import('./blog/blog.module').then(m => m.BlogModule) }, ]; 在模块中可以指定路由是否要加载模块公共模板 const routes: Routes = [ { path: '', component: FrontendComponent, children: [ { path: 'home', component: HomeComponent, }, ] }, { path: 'blog', component: HomeComponent, }, ] 注意 frontend.component.html 必须有 <router-outlet></router-outlet> 才能加载子路由 这样 /home 就会先加载 FrontendComponent 再加载 HomeComponent 并把 HomeComponent 的内容 放在 frontend.component.html 的 <router-outlet></router-outlet> 位置 而 /blog 就只会加载 HomeComponent,这样就可以实现模块内不同页面有不同的框架结构。 也可以实现 不同的页面分别由不同的公共模板 const routes: Routes = [ { path: '', component: FrontendComponent, children: [ { path: 'home', component: HomeComponent, }, ] }, { path: 'blog', component: BlogComponent, children: [ { path: '', component: HomeComponent, }, ] }, ]

2020/5/20
articleCard.readMore

PHP 常用框架

php发展到今天为止有很多框架,一般开发一个项目都是先选一个熟悉的框架开始,各种PHP开发框架也让程序开发变的简单有效。每一个开发者都知道,拥有一个强大的框架可以让开发工作变得更加快捷、安全和有效。 对于框架的选择往往从几方面考虑: 文档,完整的文档。 使用难易程度。 配套功能,例如ORM、路由、模板解析。 活跃的社区。 Laravel Github 开源 文档全 有一定的难度 相对更快使用PHP新版本特性 有相关视频教程 对小项目没有加成。 官网:https://laravel.com/ Yii 一款快速、安全和专业的PHP框架。 本身提供一个可视化界面生成代码 官网:https://www.yiiframework.com/ CodeIgniter 一款非常敏捷的开源PHP框架。适合开发一个简单而优雅的工具包。 适合小型项目。 官网:https://www.codeigniter.com/ Zend Framework 顶尖的PHP框架,文档丰富 官网:https://framework.zend.com/ Symfony 是一款可重用的PHP组件,例如 laravel 就基于 Symfony 开发的 官网:https://symfony.com/ Swoole PHP 协程框架,是一个面向生产环境的 PHP 异步网络通信引擎,使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务 目前,国内很多PHP招聘都需要了解swoole 官网:https://www.swoole.com/ Phalcon 运行速度最快的一个PHP框架。主要代码使用 C语言编写。所以配置安装相对麻烦,不一定能适配最新 php 版本(除非你能自己编译c语言编写的php拓展)。 官网:https://phalcon.io/ ThinkPHP 主要用户为国内开发者,作为国内初学者入门框架。 官网:http://www.thinkphp.cn/

2020/5/20
articleCard.readMore

css3 实现汉堡式菜单栏

阅读本文需要了解 scss angular 核心代码 .nav-bar { display:flex; flex-direction:column; .bar-top { overflow-y: auto; flex: 1; } } 这样就实现中间菜单超长滚动,底部菜单自动增高 效果 代码 注意这个代码是使用在 angular 上的,没有做静态页面,可以考虑自己转化一下 使用的图标是 iconfont 图标 navToggle 是 顶部汉堡按钮 控制菜单收缩用的 router-outlet 是 angular 用来加载右侧内容的标签 <div class="page-box" [ngClass]="{'nav-toggle': navToggle}"> <div class="nav-bar"> <i class="iconfont icon-bars nav-toggle-icon" (click)="navToggle = !navToggle"></i> <ul class="bar-top"> <li class="bar-item active"> <a routerLink="/disk"> <i class="iconfont icon-home"></i> <span class="bar-name">首页</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-folder-open-o"></i> <span class="bar-name">全部文件</span> </a> <ul class="bar-children"> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-file-image-o"></i> <span class="bar-name">图片</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-file-word-o"></i> <span class="bar-name">文档</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-file-movie-o"></i> <span class="bar-name">视频</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-gift"></i> <span class="bar-name">种子</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-music"></i> <span class="bar-name">音乐</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-APP"></i> <span class="bar-name">应用</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-file-archive-o"></i> <span class="bar-name">压缩包</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/catalog"> <i class="iconfont icon-file-o"></i> <span class="bar-name">其他</span> </a> </li> </ul> </li> <li class="bar-item"> <a routerLink="/disk/share"> <i class="iconfont icon-share-alt"></i> <span class="bar-name">我的分享</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/trash"> <i class="iconfont icon-trash"></i> <span class="bar-name">回收站</span> </a> </li> </ul> <ul class="bar-bottom"> <li class="bar-item"> <a routerLink="/disk/my"> <i class="iconfont icon-user"></i> <span class="bar-name">zodream</span> </a> </li> <li class="bar-item"> <a routerLink="/disk/setting"> <i class="iconfont icon-cog"></i> <span class="bar-name">设置</span> </a> </li> </ul> </div> <div class="page-body"> <router-outlet></router-outlet> </div> </div> 下面是 css 样式 scrollbar() 这个是设置滚动条样式的,默认的太大了 @mixin scrollbar() { &::-webkit-scrollbar{ height:6px; width:6px; margin-right:5px; background: #f5f5f5; transition:all 0.3s ease-in-out; border-radius:0px } &::-webkit-scrollbar-track { -webkit-border-radius: 0px; border-radius: 0px; } &::-webkit-scrollbar-thumb{ -webkit-border-radius: 0px; border-radius: 0px; background: rgba(0,0,0,0.5); &:hover{ background:rgba(0,0,0,0.6); } &:active{ background:rgba(0,0,0,0.8); } &:window-inactive { background: rgba(0,0,0,0.4); } } } ul, li { margin: 0; padding: 0; } .nav-bar { position: fixed; left: 0; width: 200px; bottom: 0; background-color: #eee; top: 0; z-index: 99; box-shadow: rgba(51,51,51,.7) 0 0 10px; display:flex; flex-direction:column; .nav-toggle-icon { font-size: 30px; padding: 0 10px; display: inline-block; &:hover { background-color: #ccc; } } .bar-top { overflow-y: auto; flex: 1; @include scrollbar(); } a { text-decoration: none; color: #333; } .bar-item { list-style: none; line-height: 40px; text-align: center; font-size: 16px; a { display: block; box-sizing: border-box; position: relative; } .iconfont { position: absolute; left: 10px; top: 0; font-size: 30px; display: block; } &:hover { >a { background-color: #ccc; } } } .bar-children { box-shadow: inset 0 5px 5px -5px #000, inset 0 -5px 5px -5px #000; } .bar-item { &.active { >a { &::before { content: " "; display: block; position: absolute; left: 0; width: 5px; height: 40px; background-color: red; } } } } } .page-body { margin-left: 200px; } .nav-toggle { .nav-bar { width: 50px; .bar-name { display: none; } .bar-item { .iconfont { position: static; } } } .page-body { margin-left: 50px; } } @media screen and (max-width: 769px) { .nav-bar { width: 50px; .bar-name { display: none; } .bar-item { .iconfont { position: static; } } } .page-body { margin-left: 50px; } .nav-toggle { .nav-bar { width: 200px; .bar-name { display: inline-block; } .bar-item { .iconfont { position: absolute; } } } } } 整体项目代码:Angular-ZoDream 当前项目未使用服务端数据对接,所以可以查看实际效果

2020/5/18
articleCard.readMore

angular9教程之监听子路由变化

需求 根据下级路由改变父组件的菜单选中 解决方法 主要通过 this.router.events.subscribe 获取路由的变化 export class FrontendComponent { constructor( private router: Router) { this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { // event.url '/blog' this.routerChanged(event.url); } }); } } 获取当前路由网址进行切换

2020/5/17
articleCard.readMore

angular9教程之添加启动加载动画

第一步 在 src/index.html 加上动画效果 在 <app-root></app-root> 的后面 <style>@-webkit-keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@-moz-keyframes spin{0%{-moz-transform:rotate(0)}100%{-moz-transform:rotate(360deg)}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.spinner{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1003;background: #000000;overflow:hidden} .spinner div:first-child{display:block;position:relative;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;box-shadow:0 3px 3px 0 rgba(255,56,106,1);transform:translate3d(0,0,0);animation:spin 2s linear infinite} .spinner div:first-child:after,.spinner div:first-child:before{content:'';position:absolute;border-radius:50%} .spinner div:first-child:before{top:5px;left:5px;right:5px;bottom:5px;box-shadow:0 3px 3px 0 rgb(255, 228, 32);-webkit-animation:spin 3s linear infinite;animation:spin 3s linear infinite} .spinner div:first-child:after{top:15px;left:15px;right:15px;bottom:15px;box-shadow:0 3px 3px 0 rgba(61, 175, 255,1);animation:spin 1.5s linear infinite}</style> <div id="zo-global-spinner" class="spinner"> <div class="blob blob-0"></div> <div class="blob blob-1"></div> <div class="blob blob-2"></div> <div class="blob blob-3"></div> <div class="blob blob-4"></div> <div class="blob blob-5"></div> </div> 这是一个加载动画 第二步 当加载完需要隐藏 需要在第一个 Component 初始化的时候隐藏 例如 src/app/app.component.ts export class AppComponent { constructor() { // 隐藏 document.getElementById('zo-global-spinner').style.display = 'none'; } }

2020/5/16
articleCard.readMore

angular9教程之分模块开发

为什么分模块 每一个模块都是分开打包的,只有用到模块才会加载模块的资源,因此可以加快第一次打开的时间,减少不必要的加载 生成 生成一个 frontend 模块,并创建路由模块 ng g module frontend --route frontend --module app.module 在 frontend 模块下继续添加模板 ng g module frontend/blog --route blog --module frontend.module 分配路由到模块 src/app/app-routing.module.ts const routes: Routes = [ { path: 'frontend', loadChildren: () => import('./frontend/frontend.module').then(m => m.FrontendModule) }, { path: '', // 默认跳转到 frontend 模块 redirectTo: 'frontend', pathMatch: 'full' }, { path: '**', // 其他没有匹配到的路径都跳转到 frontend 模块 redirectTo: 'frontend' }, ]; 添加公共页面 修改 src/app/frontend/frontend.component.html <header> 导航栏 </header> <router-outlet></router-outlet> <footer> <div class="copyright"> <p>Copyright ©zodream.cn, All Rights Reserved.</p> </div> </footer> <router-outlet></router-outlet> 是必须的,是根据子路由加载页面的 添加首页 ng g component frontend/home 绑定路由 修改 src/app/frontend/frontend-routing.module.ts const routes: Routes = [ { path: '', component: FrontendComponent, children: [ { path: 'home', component: HomeComponent, }, { path: '', redirectTo: 'home', pathMatch: 'full', }, { path: '**', component: HomeComponent, } ] }, ]; 测试 ng serve --open 出现一下内容则表示正常了

2020/5/16
articleCard.readMore

angular9教程之使用

安装Angular CLI npm install -g @angular/cli 生成初始程序 ng new my-app 必须有一个程序名,没办法生成到当前文件夹 运行 cd my-app ng serve --open 使用 bootstrap npm i bootstrap 在 angular.json 修改 projects > my-app > architect > build > options > styles 节点 最前面加上 "node_modules/bootstrap/dist/css/bootstrap.css", "styles": [ "node_modules/bootstrap/dist/css/bootstrap.css", "node_modules/@fortawesome/fontawesome-free/css/all.css", "src/styles.scss" ], 或者使用 ng-bootstrap ng add @ng-bootstrap/ng-bootstrap 这样就不需要手动去添加样式了 使用 fontawesome npm i @fortawesome/fontawesome-free 引用 css 同上 引用其他包的样式和脚本 "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.css", "node_modules/@fortawesome/fontawesome-free/css/all.css", "node_modules/pace-js/templates/pace-theme-flash.tmpl.css", "node_modules/ngx-toastr/toastr.css", "src/styles.scss" ], "scripts": [ "node_modules/pace-js/pace.min.js" ] css 在 angular.json 修改 projects > my-app > architect > build > options > styles 节点加上 css 文件相对路径 js 在 angular.json 修改 projects > my-app > architect > build > options > scripts 节点加上 js 文件相对路径 其他文件例如图片 在 angular.json 修改 projects > my-app > architect > build > options > assets 节点加上文件相对路径

2020/5/16
articleCard.readMore

301跳转https和www

强制使用https访问 Apache 做法 修改网站根目录下的 .htaccess 文件 RewriteEngine On RewriteCond %{SERVER_PORT} 80 RewriteRule ^(.*)$ https://zodream.cn/$1 [R,L] nginx 做法 server { listen 80; server_name zodream.cn; return 301 https://$server_name$request_uri; } iis 做法 修改网站根目录下的 web.config 文件 <?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="HTTP to HTTPS redirect" stopProcessing="true"> <match url="(.*)" /> <conditions> <add input="{HTTPS}" pattern="off" ignoreCase="true" /> </conditions> <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}" /> </rule> </rules> </rewrite> </system.webServer> </configuration> 无www 跳转带www Apache 做法 修改网站根目录下的 .htaccess 文件 RewriteEngine on RewriteCond %{HTTP_HOST} ^zodream.cn [NC] RewriteRule ^(.*)$ http://www.zodream.cn/$1 [L,R=301,NC] nginx 做法 server { listen 80; server_name zodream.cn; return 301 http://www.zodream.cn$request_uri; } iis 做法 修改 无www 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能 <?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <httpRedirect enabled="true" destination="http://www.zodream.cn" httpResponseStatus="Permanent" /> </system.webServer> </configuration> www 跳转无www Apache 做法 修改网站根目录下的 .htaccess 文件 RewriteEngine on RewriteCond %{HTTP_HOST} ^www.zodream.cn [NC] RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC] nginx 做法 server { listen 80; server_name www.zodream.cn; return 301 http://zodream.cn$request_uri; } iis 做法 修改 带www 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能 <?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <httpRedirect enabled="true" destination="http://zodream.cn" httpResponseStatus="Permanent" /> </system.webServer> </configuration> 强制其他域名都跳转到一个域名 Apache 做法 修改网站根目录下的 .htaccess 文件 RewriteEngine on RewriteCond %{HTTP_HOST} !^zodream.cn [NC] RewriteRule ^(.*)$ http://zodream.cn/$1 [L,R=301,NC] nginx 做法 server { listen 80 default_server; server_name _; return 301 http://zodream.cn$request_uri; } iis 做法 修改 默认 * 域名 网站根目录下的 web.config 文件,必须安装 HTTP重定向 功能 <?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <httpRedirect enabled="true" destination="http://zodream.cn" httpResponseStatus="Permanent" /> </system.webServer> </configuration>

2020/5/15
articleCard.readMore

gulp-vue2mini 使用教程

资源 npm gulp-vue2mini npm i gulp-vue2mini 示例 前言 本插件开发初衷,以类似于 vue 的代码语法开发微信小程序 但是,本程序不能直接把vue 程序转化为小程序代码。 那么,这样做有必要吗? 有! 熟悉vue语法的开发者能快速上手 小程序代码本身是 js、css、html、json 分离的,这样看上出去文件繁多,一个小项目干嘛要那么麻烦 本程序是使用 typescript ,有很好的代码提示 使用 本程序提供配套示例代码,这是一个初始化版本, 直接下载本代码, 在 vscode 中打开 npm i 安装依赖 src/app.vue 为程序入口 src/pages/index 为首页 编译程序 npm run build 如果你的 vscode 安装了 Code Runner 扩展,支持右键编译当前单个文件 功能介绍 支持 ts sass 支持拆解html js ts sass css 写在一个文件上的情况 sass 支持ttf文件自动转化为 base64 sass 引用模式自动处理 自动转化html 为 wxml, 自动转化 v-if v-for v-else v-show 支持json自动生成,支持 属性合并 注意:span 标签下不能包含其他标签,否则会自动转换为view 标签属性转化列表 属性名 目标属性 v-if wx:if="{{ }}" v-elseif wx:elif="{{ }}" v-else wx:else v-bind:src src href url @click bindtap v-on:click bindtap (click) bindtap @touchstart bindtouchstart @touchmove bindtouchmove @touchend bindtouchend :key v-show hidden="{{! }}" v-for wx:for="{{ }}" wx:for-index=" " wx:for-item="" v-model value="{{ }}" bind:input=" Changed" 第一个字符为@且值不为空 bind: 第一个字符为: ={{ }} 其他包含@ 支持 对 picker switch slider 执行 v-model 值绑定 支持 :class 数组形式及 {active: true} 形式自动会合并 class 支持 @click 直接赋值及直接传参数 @click="i = 1" @click="tap(i, a)" 定义WxPage WxCommpent WxApp 三个类,增强 setData 的智能提示, export 是为了避免提示未使用,编译时会自动去除 增加自动添加 Page(new Index()) Commpent(new Index()) App(new Index()) 到末尾 增加json配置生成 @WxJson({ usingComponents: { MenuLargeItem: "/components/MenuLargeItem/index", MenuItem: "/components/MenuItem/index" }, navigationBarTitleText: "个人中心", navigationBarBackgroundColor: "#05a6b1", navigationBarTextStyle: "white" }) 自动合并页面相关的json文件 支持自动合并 methods lifetimes pageLifetimes, 如果已有 属性会自动合并 methods @WxMethod lifetimes @WxLifeTime pageLifetimes @WxPageLifeTime 自定义部件自动合并方法到methods属性中 methods = { aa() { } } @WxMethod() tapChange(mode: number) { } 最终生成 methods = { tapChange(mode: number) { }, aa() { } } 标准模板 index.vue <template> <div> </div> </template> <script lang="ts"> import { IMyApp } from '../../app'; const app = getApp<IMyApp>(); interface IPageData { items: number[], } export class Index extends WxPage<IPageData> { public data: IPageData = { items: [] }; onLoad() { this.setData({ items: [] }); } } </script> <style lang="scss" scoped> </style> 最终会处理为3个文件 index.wxml <view></view> index.wxss index.js "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var app = getApp(); var Index = (function () { function Index() { this.data = { items: [], }; } Index.prototype.onLoad = function () { this.setData({ items: [] }); }; return Index; }()); Page(new Index()); 注意 新增了一些指定的声明请参考

2020/5/14
articleCard.readMore

create.js 开发小游戏(二)蒙版

在 createjs 中物体是不会自动超出边界自动隐藏的, 例如,负坐标也不会在视野内隐藏 里面的每一个 DisplayObject 都是没有宽和高限制的 例如 createjs.Container 上面加个会动的 createjs.Shape, 我想让她 在 x: 0, y: 0, width: 600, height: 600 这个范围内可见,跑到其他地方不可见,该怎么办? 下面代码就可以实现, const box = new createjs.Container(); const shape = new createjs.Shape(); box.addChild(shape); // 添加蒙版 const mask = new createjs.Shape(new createjs.Graphics().beginFill("#ffffff").drawRect(box.x, box.y, 600, 600)); box.mask = mask; 但请注意 mark drawRect 使用的时 box 的父坐标系统,不是 box 的坐标系统, 这并不能遮住 box 里面坐标 x 或 y 为负的,可以考虑 mark drawRect(box.x + x, box.y + y, 600 - x, 600 - y) 扩大蒙版的范围

2020/5/14
articleCard.readMore

createjs 游戏结束后重新开始 Tween 补间动画无效

问题描述 当游戏结束了,重新开始发现使用 Tween 的补间动画不起作用了。 解决方法 在 github 的 issues 找到同样的问题 Tween._inited does not reset if scene is being destroyed or removed inside the game and rebuild 意思就是 Tween 上的一个 _inited 私有属性没有更新,只要增加一句代码就行了 Tween._inited = undefined; 但这种方法肯定是不友好的,但是 Tween 又没有提供 reset 方法, 最后通过查看源码,发现是 createjs.Ticker 引起的 当执行 createjs.Ticker.reset() 会清空所有 tick 事件监听,而 Tween 又没有收到通知,没法更新 _inited 属性,导致 Tween 不会重新添加事件监听 createjs.Ticker.addEventListener("tick", Tween) 所以,要么不要使用 createjs.Ticker.reset(),要么使用 createjs.Ticker.reset(); createjs.Tween._inited = undefined;

2020/5/13
articleCard.readMore

create.js 开发小游戏(一)搭建项目

本文使用 typescript 开发 环境准备 node 安装一些依赖 npm i @types/createjs createjs --save npm i gulp gulp-concat gulp-typescript typescript --save-dev 增加文件 tsconfig.json ts 的编译配置信息 { "compilerOptions": { "baseUrl": "./", "target": "es5", "strictNullChecks": true, "noImplicitAny": true, "allowJs": false, "experimentalDecorators": true, "noImplicitThis": true, "noImplicitReturns": true, "alwaysStrict": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "strict": true, "removeComments": true, "pretty": true, "strictPropertyInitialization": true, }, "include": [ "src/**/*.ts" ], "exclude": [ "node_modules", "dist", ] } gulpfile.js gulp 编译方式 var gulp = require('gulp'), ts = require('gulp-typescript'), concat = require('gulp-concat'), tsProject = ts.createProject('tsconfig.json'); gulp.task('copy', async() => { // 复制 createjs 到目标文件夹 await gulp.src('node_modules/createjs/builds/1.0.0/createjs.min.js') .pipe(gulp.dest('dist/')); }); gulp.task('default', gulp.series('copy', async() => { // 合并ts 文件并编译 await gulp.src(['src/core/*.ts', 'src/scene/*.ts', 'src/*.ts']) .pipe(concat('zodream.ts')) .pipe(tsProject()) .pipe(gulp.dest('dist/')); })); 开始 新增 src 文件夹 里面放 主控制 新建 src/core 文件夹 里面放基本定义 新建 src/scene 文件夹 里面放每一个场景 具体代码示例查看 示例 主要代码 初始化场景 let stage = new createjs.Stage(arg: string | HTMLCanvasElement); 设置启用触摸 createjs.Touch.enable(stage); 设置场景的尺寸 (<HTMLCanvasElement>stage.canvas).width = width; (<HTMLCanvasElement>stage.canvas).height = height; 设置场景刷新频率 createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED; createjs.Ticker.framerate = 60; 清空场景,更换场景时使用 stage.removeAllChildren(); 添加物体 stage.addChild(...arg); 刷新生效 createjs.Ticker.addEventListener('tick', function() { stage.update(); }); 运行 新增 index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Catch Cat</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body { margin: 0; padding: 0; } </style> </head> <body> <canvas id="stage" height="600" width="600"></canvas> <script src="dist/createjs.min.js"></script> <script src="dist/zodream.js"></script> <script> App.main('stage'); </script> </body> </html> 打开 index.html 有的浏览器直接打开文件无法加载资源文件,请使用其他程序,例如 iis, 或 npm i static-server server.js var StaticServer = require('static-server'); var server = new StaticServer({ rootPath: '.', // required, the root of the server file tree port: 8081, // required, the port to listen }); server.start(function () { console.log('Server listening to', server.port); }); 启动 node server.js 打开 http://localhost:8081 即可查看效果

2020/5/12
articleCard.readMore

SEO(二)关键词优化试验

可以使用 站长之家 的 百度关键词挖掘,寻找关键字 可以使用 站长之家 的 SEO综合查询,寻找同类型网址的关键字 长尾词 长尾关键词是指网站上的非目标关键词但与目标关键词相关的也可以带来搜索流量的组合型关键词。 长尾词为几个关键词的组合,通常在搜索引擎搜索时出现的智能提示就是长尾词。 首页关键词优化,增加相关关键字,并进行相关文章展示 我的做法 先确定本站的发展方向,选择几个关键词 通过 站长之家-百度关键词挖掘 搜索相关关键词,分别选择几个合适的 长尾词 对 长尾词 进行文章添加 目前效果 未知 关键词 关键词来源:文章内容重点名称的提取 我的做法 对所有文章增加关键词 增加相应的标签进行归类,方便进行文章关联 通过 SEO综合查询 参考其他网址获取关键词 目前效果 未知

2020/5/12
articleCard.readMore

SEO(一)内链优化试验

事先说明 本文为参考网上的相关文章,进行对本站的优化,效果暂时不知道,重点:效果不知道 外部链接是别人对你的投票,内部链接优化建设是自己对自己页面进行投票,页面被投票越多,说明这个页面内容越重要 网站地图 最好给自己的网站建立一个完整的网站地图,把网站地图的链接放在网站首页,这样能更利于让搜索引擎蜘蛛发现地图,抓取网站内容。 一般存放在根目录下并命名sitemap,为爬虫指路,增加网站重要内容页面的收录。 当然,在一些搜索引擎是允许主动提交 sitemap 的。 我的做法 通过程序自动生成sitemap.xml 并把她主动提交到搜索引擎, 目前效果 百度、谷歌、360 都有收录,必应没收录 网站权重传递 一般来说网站结构建设良好,网站首页的权重是最高的,栏目页第二,内容页最差。如果有站长朋友有一些重点推广的页面,可以通过网页的链接提升权重的,使这些页面的权重升高。 保障网站权重在重要页面进行清楚传递 为重点页面制造多个进口传递更多权重 要去掉与网站权重传递无关的元素内容 我的做法 已修改首页,增加多个模块展示更多的文章 目前效果 未知 页面层数 对一般的网站而已,最好能确保从首页开始算,点击2-3次就能到达网站的任何一个页面,最多不要超过4次,点击次数越少越好,更有利于网站优化 网址越大,层数就不好控制,但要保证重点内容能符合标准。 我的做法 文章为中心,所以增加首页文章展示 目前效果 未知 网页相互链接 很多站长会对网站的树形结构有误解,正确的理解是,在不同栏目的网页上链接到其他栏目的相关网页,整个网站的链接最后看起来像蜘蛛网一样既有主干也有页面之间的相互链接。 首页增加文章分类展示 增加文章统计和标签归类 增加文章内容相关文章展示 增加文章内容内链 我的做法 增加标签聚合,增加归档,增加文章相关文章展示 目前效果 未知

2020/5/12
articleCard.readMore

Select2使用

Select2是一款基于JQuery的下拉列表插件,主要用来优化select,支持单选和多选,同时也支持分组显示、列表检索、远程获取数据等功能。 下载 官网 准备 JQuery select2.min.css select2.min.js 使用 普通使用 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="select2.min.css"> </head> <body> <select name="name"> <option value="1">1</option> <option value="2" selected>2</option> </select> <script src="jquery-3.5.1.min.js"></script> <script src="select2.min.js"></script> <script> $(function() { $('select').select2(); }); </script> </body> </html> 提示 $('select').select2({ placeholder: '请选择' }); 加载本地数据 需要两个数据属性id和text var data = [ {id: 1, text: '1'} ]; $('select').select2({ data: data }); 加载远程数据 $('select').select2({ ajax: { url: 'tags', data: function (params) { return { keywords: params.term, // keywords为发送到服务端的参数名,params.term表示输入框中输入的内容 page: params.page || 1 }; }, processResults: function (data) { data = data.data; return { results: data.data.map(item => { return { id: item.id, text: item.name, } }), pagination: { // 是否还有下一页 more: data.paging.more } }; } } }); 设置默认选中 <select name="name[]" multiple> <option value="1" selected>1</option> <option value="2" selected>2</option> </select>

2020/5/12
articleCard.readMore

WordPress使用

预先准备 网站的域名 国内的 【万网】 国外的 【Godadday】 网站空间 分为 虚拟主机 和 服务器 买国内的空间需要给域名备案,不然无法打开。 个人的话对网站内容有限制。 虚拟主机 价格相对便宜,系统和环境不能自定义,只能选指定的,灵活度低,但对新手友好 服务器 系统能更改 运行环境需要自己搭建。 本地软件要求 代码的编辑器: 要注意文件的编码格式,推荐 Vim、NotePad++、Vs Code FTP上传工具:Filezilla 常用博客软件 WordPress 【下载】 环境要求:php7.3 + mysql 5.6 安装 有的虚拟主机包括 wordpress,但版本可能不是最新 从官网下载 WordPress,解压 通过FTP工具将 wordpress 文件夹里面的文件全部上转至你的网站根目录。 配置 浏览器访问你的域名,默认会跳转到 /wp-admin/setup-config.php 第一步,选择语言,wordpress 是只支持单语言 第二步,说明,直接下一步 第三步,配置数据库信息,数据库必须存在,不存在的话请先创建,可以使用 phpMyAdmin 创建 第四步,配置站点信息及管理员账号密码 使用 /wp-login.php 这是登录后台网址 显示语言在 左边菜单 Settings 修改 Site Language 点击 Save Changes 保存 Posts 中发布新的文章 Appearance 下 Themes 可以更改主题模板 Plugins 可以安装各种插件,但安装的越多,打开速度越慢,初期不建议使用

2020/5/11
articleCard.readMore

Redis 使用

windows 安装 Redis 在任务栏搜索 启用或关闭Windows功能 选择 适用于Linux的Windows子系统 在应用商店安装 Ubuntu 安装编译环境 sudo apt-get update sudo apt-get install make sudo apt-get install gcc 安装 wget http://download.redis.io/redis-stable.tar.gz tar xvzf redis-stable.tar.gz cd redis-stable sudo make install sudo cp redis.conf /etc/redis.conf 启动 cd /usr/local/bin redis-server /etc/redis.conf redis-server /etc/redis.conf & #指定配置文件启动redis,且后台启动 安全配置 redis.conf protected-mode yes #打开保护模式 port 6380 #更改默认启动端口 requirepass xxxxxx #设置redis启动密码,xxxx是自定义的密码 来禁用远程修改 DB 文件地址 rename-command FLUSHALL "" rename-command CONFIG "" rename-command EVAL "" 禁止外网访问 Redis bind 127.0.0.1 使用 字符串(String) 可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等,最多可以容纳的数据长度是512M set mykey "this is a test"   //通过set命令为键设置新值,并覆盖原有值。 get mykey incr mykey      //该Key的值递增1 decr mykey      //该Key的值递减1 del mykey      //删除已有键 哈希(Hash) 键值对字典结构 列表(List) 简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边) 集合(Set) 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 有序集合(sorted set) 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 有序集合的成员是唯一的,但分数(score)却可以重复。 使用问题 雪崩 指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。 解决办法: 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。 简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。 缓存穿透 指访问不存在的数据,导致跳过缓存,直接访问数据库。 例如:数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。 解决办法: 每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。 缓存击穿 就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。 解决办法: 可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

2020/5/9
articleCard.readMore

WEB攻防(一)基础

本文为阅读 《黑客攻防技术宝典:Web实战篇(第2版)》 的笔记 黑名单过滤 根据已知的字符串确认数据,排除在黑名单之上的数据。 大小写切换攻击 SELECT 可以尝试 SeleCt 数字更改 or 1=1-- 可以尝试 or 2=2-- 同类函数更换 alert('xss') 可以尝试 prompt('xss') sql 注释 select/*foo*/username,password/*foo*/from/*foo*/users 非标准字符 <img%09onerror=alert(1) src=a> 空字节攻击 在表达式之前插入空字节 %00<script>alert(1)</script> 删除表达式 通过删除特定表达式,过滤内容 删除 <script> 可以尝试 <scr<script>ipt> 递归删除 删除 ../ 删除 ..\ 可以尝试 ....\/ URI 编码 删除省略号 可以尝试 %2527 或 %%2727 HTML 编码 <iframe src=javascript:alert(1) >

2020/5/8
articleCard.readMore

PHP 转义函数

函数名 释义 介绍 使用 htmlspecialchars 将与、单双引号、大于和小于号化成HTML格式 &转成&<br>"转成"<br>' 转成'<br><转成<<br>>转成> html页面输出用户输入内容<br> sitemap.xml 转义链接 htmlentities 所有字符都转成HTML格式 除上面htmlspecialchars字符外,还包括双字节字符显示成编码等。 addslashes 单双引号、反斜线及NULL加上反斜线转义 被改的字符包括单引号 (')、双引号(")、反斜线 backslash (\) 以及空字符NULL。 stripslashes 去掉反斜线字符 去掉字符串中的反斜线字符。若是连续二个反斜线,则去掉一个,留下一个。若只有一个反斜线,就直接去掉。 quotemeta 加入引用符号 将字符串中含有 . \ + * ? [ ^ ] ( $ )等字符的前面加入反斜线 "\" 符号。 nl2br 将换行字符转成<br> strip_tags 去掉HTML及PHP标记 去掉字符串中任何 HTML标记和PHP标记,包括标记封堵之间的内容。注意如果字符串HTML及PHP标签存在错误,也会返回错误。 mysql_real_escape_string 转义SQL字符串中的特殊字符 转义 \x00 \n \r 空格 \ ' " \x1a,针对多字节字符处理很有效。mysql_real_escape_string会判断字符集,mysql_escape_string则不用考虑。 base64_decode base64解码 对使用 MIME base64 编码的数据进行解码 base64_encode base64编码 使用 MIME base64 对数据进行编码 rawurldecode URL解码 对已编码的 URL 字符串进行解码 rawurlencode URL编码 按照 RFC 1738 对 URL 进行编码 urldecode URL解码 解码已编码的 URL 字符串 urlencode URL编码 编码 URL 字符串

2020/5/6
articleCard.readMore

apache自定义文件类型响应头header

第一种修改 httpd.conf 打开配置文件 httpd.conf 找到 IfModule mime_module 配置模块 <IfModule mime_module> AddType application/x-compress .Z 添加方法 AddType + 响应头 + 文件拓展名 第二种修改 mime.types 修改方式 application/vnd.dart dart 响应头 + 文件拓展名 怎么设置文件编码 添加这个全局设置,但只作用文件类型响应头为 text/plain 或 text/html AddDefaultCharset utf-8 其他类型需要在 IfModule mime_module 下使用 AddCharset utf8 .json

2020/5/6
articleCard.readMore

vscode 输出中文乱码

vscode 输出中文乱码的解决方法 直接在任务栏中搜索 区域设置 或 开始->设置->时间和语言右上角相关设置中的 日期、时间和区域格式设置 进入 区域设置 右上角 相关设置中的 其他日期、时间和区域格式设置 区域下的 更改日期、时间或数字格式 管理 选项卡 更改系统区域设置 勾选 使用 Unicode UTF-8 提供全球语言支持 重启电脑即可 参考 vscode输出窗口中文乱码

2020/5/6
articleCard.readMore

网站体检之sqlmap注入

本次使用环境 kali sqlmap 先更新一下软件 sudo su #更新软件源中的所有软件列表 apt-get update #更新软件 apt-get upgrade #更新系统版本 apt-get dist-upgrade 测试 sqlmap 基本参数 -u 网址 "" --dbms= 数据库类型 mysql --cookie= 设置cookie 登录信息 "" --method= 请求方式 POST --headers= 设置请求头 sqlmap -u "http://zodream.localhost/blog?id=19" 结果 parameter 'id' does not seem to be injectable 暂时找不到注入

2020/4/30
articleCard.readMore

phpMyAdmin配置文件中的密文(blowfish_secret)太短。

phpMyAdmin配置文件中的密文(blowfish_secret)太短。 需要修改配置文件 config.inc.php $cfg['blowfish_secret'] = '5d01588e401875b15374662a96261262';/* YOU SHOULD CHANGE THIS FOR A MORE SECURE COOKIE AUTH! */ 随便输入一个32位的字符串即可。 “cookie”身份验证类型使用AES算法加密密码。如果您使用的是“cookie”身份验证类型,请在此输入您选择的随机密码短语。AES算法将在内部使用它:不会提示您输入此密码短语。 这个密钥应该有32个字符长。使用较短的会导致加密cookie的安全性较弱,使用较长的不会造成危害。

2020/4/26
articleCard.readMore

hashcat(一)找回office文件密码

下载地址 hashcat 安装 解压到文件夹即可 使用 具体参数 见【文档】 找回office文件密码 需要下载脚本office2john.py 提取 hash 执行脚本office2john.py获取hash python office2john.py a.xlsx 获得hash $office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e 这是offile 2010 运行程序 直接运行 .\hashcat64.exe -m 9500 '$office$*2010*100000*128*16*e90e9023fa938881fa408c22b25bf721*b2ee73a9b95c490f8e4811e17d' -a 3 ?a?a?a?a?a?a?a 或把hash 保存到文a.txt .\hashcat64.exe -m 9500 a.txt -a 3 ?a?a?a?a?a?a?a -m 哈希类别 9500 表示 MS Office 2010 -a 0字典攻击,-a 1 组合攻击;-a 3掩码攻击 ?a 表示大小写字母数字符号 包含?l?u?d?s,重复几次表示密码有几位 ?d 表示数字 ?l 小写字母 ?u 大写字母 ?s 特殊字符 «space»!"#$%&'()*+,-./:;<=>?@[]^_`{|}~ ?b 0x00 - 0xff ?h 十六进制字符 0123456789abcdef ?H 大写的十六进制字符 0123456789ABCDEF --increment --increment-min 1 --increment-max 8 指定密码范围最小1位最多8位 查看运行进度 s 回车即可 Status...........: Running 表示正在运行中 Status...........: Cracked 表示解密成功 Status...........: Exhausted 表示程序已结束,但没有找到密码 查看密码 当 Status...........: Cracked 出现后,程序运行结束 上一行就会出现破解密码 :123 最后的冒号后的就是密码了 问题 报错 CL_OUT_OF_RESOURCES 解决方法: 新建文本 wddm_timeout_patch.reg,点击即可 Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers] "TdrLevel"=dword:00000000 Hash '$office$20131000002561642f7509323c3d047371ef44fbb0c47b8e7707349': Token length exception No hashes loaded. 这是因为在 powershell 命令行中直接输入,导致hash被当作命令 解决方法: 1. 把hash保存到文件,以文件形式传入 2. 用英文单引号把hash 当作字符串输入,双引号是不行的 INFO: All hashes found in potfile! 表示密码已经找到过了,通过 --show 即可查看

2020/4/26
articleCard.readMore

panic: reflect: call of reflect.Value.Interface on zero Value

panic: reflect: call of reflect.Value.Interface on zero Value 出现场景 在使用 reflect 报错 func Dump(vals ...interface{}) { for _, v := range vals { val := reflect.ValueOf(v) fmt.Printf("[%T]", val.Interface()) } } u := &User{} Dump(u) 解决方案 修改成 Dump(&u) 因为反射看不到未导出的函数 这个错误是没办法通过方法内部解决的,是写代码时的错误,只能修改使用方法的代码。 同类错误 reflect: call of reflect.Value.Call on zero Value

2020/4/25
articleCard.readMore

第一个App终于上线了

体验地址 Win10 UWP 主要功能 本程序开发目的 一开始只有web版,用来约束自己,每次做事都有一个时间统计,合理安排工作与休息时间。 目前已有功能 任务添加 任务计时 任务记录回顾 其他附加功能 扫码登录web (这是我一直想实现的功能,每次登录太麻烦了) 签到 (目前没有签到奖励) 修改个人信息 查看博客文章 下个版本功能 任务包含子任务 任务记录图表统计 加入个人财务管理 具体使用 打开 应用商店 Microsoft Store 搜索 我的时间回忆薄 进入详情,点击获取 点击安装 打开应用 点击左侧 我的 点击头像 进入登录界面 点击注册账户(注:邮箱在注册时可以随意填写,但找回密码和更换邮箱需要发送邮件) 回到首页 点击底部按钮 + 进入 任务列表 点击底部按钮 + 进入添加任务 添加 任务名 任务说明 每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止 点右上角 √ 进行保存 回到任务列表页 点击底部 🖊 编辑按钮 选择几个任务 点击底部 + 添加到今日任务 就会把选中的任务添加到今日任务,可以连续点击,添加多次任务 点击底部的 完成 按钮就会退出编辑模式 底部的 结束任务 可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了 回到首页 点击 一个任务,进入任务执行页面 点击底部的 开始 按钮,开始任务 。。。 等到圈转完 表示此轮任务执行完一次,可以停下来休息3-5分钟 在执行时,可以点击 暂停,表示有其他事打扰,需要暂停一下,处理完后点 开始 继续执行任务 在执行时,可以点击 停止,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务 左侧时间薄可以查看每一天执行的记录 聊聊开发过程 本来,第一个版本是微信小程序版的,当时审核直接被打回,好像时个人无法发布包含用户系统的小程序。 那么由此,我的程序做成 app 也将无法在国内的应用市场上架了,所以只能暂缓flutter 开发 App UWP 是由于几年前就注册了开发者的,但那时没有发布什么app,而且本人业余时间也对 c# 比较熟悉,所以就拿这个程序作为实验版。 UWP 版有想法兼容已经淘汰的Win10 Mobile,因为手上那个还有几个 lumia 手机

2020/4/24
articleCard.readMore

node-sass 安装不了怎么办

今天把 node.js 升到 14.0.0,发现使用 npm update 安装不了 node-sass 使用淘宝镜像 npm install -g cnpm --registry=https://registry.npm.taobao.org 不行,node-sass 依然是从GitHub 下不下来。 这时想到是不是网络问题导致访问不了 github 使用淘宝镜像的 node-sass npm config set sass_binary_site=http://npm.taobao.org/mirrors/node-sass 发现还是不行,点击进去发现没有最新的node-sass 这是回过头看第一种方法,发现是 node-sass 还没有发布适配 node.js 14.0.0 的包 下载 win32-x64-83_binding.node到本地 到 github 上找 node-sass 最新的有一个 适配的 win32-x64-83_binding.node 下载到本地 npm config set sass_binary_path=F:\Downloads\win32-x64-83_binding.node npm i 这样就行了 当然 node-sass 里面有个判断 node.js 版本的报错需要删除 这是临时解决方法,还是等 node-sass 更新吧 删除配置 npm config delete sass_binary_site npm config delete sass_binary_path 最佳方案换成dart-sass npm install --save-dev sass gulp-sass 替换方法 var sass = require("gulp-sass"); sass.compiler = require('sass');

2020/4/24
articleCard.readMore

UWP 使用语言包

UWP 使用语言包 我的程序默认语言为 zh-CN 则新建文件夹 Strings\zh-CN 再添加新建项 资源文件(.resw) 新建文件 Resources.resw 添加 xaml 控件的文字 例如要设置 TextBlock 的 Text <TextBlock Text="开"/> 给 TextBlock 添加属性 x:Uid <TextBlock x:Uid="OnLabel"/> 在 Resources.resw 添加一行 名称 值 OnLabel.Text 开 名称的组成为 {x:Uid}.{属性名} 如果要给一个控件设置多个属性 名称 值 OnLabel.Text 开 OnLabel.Content 开 注意: x:Uid 属性是没法在代码中获取到的 使用代码使用语言包 先添加在一个类上增加一个公开方法: 例如 类名 AppResource #region 语言包获取文字 private static ResourceLoader CurrentResourceLoader { get { return _loader ?? (_loader = ResourceLoader.GetForCurrentView("Resources")); } } private static ResourceLoader _loader; private static readonly Dictionary<string, string> ResourceCache = new Dictionary<string, string>(); /// <summary> /// 获取资源字典的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public static string GetString(string key) { if (ResourceCache.TryGetValue(key, out string s)) { return s; } else { s = CurrentResourceLoader.GetString(key); ResourceCache[key] = s; return s; } } #endregion 在 Resources.resw 添加一行 名称 值 appName 开始 使用 var name = AppResource::GetString("appName"); 注意: 使用 AppResource::GetString("OnLabel.Text") 或 AppResource::GetString("OnLabel") 这种方式是不行,也就是 xaml 上使用的名称没法在代码中使用,只能多增加一条 名称 OnLabel.Text 和 OnLabel 添加是冲突的 多语言 通过 vs 的扩展添加 Multilingual App Toolkit 能自动生成其他语言的翻译 参考 【Win10 UWP 开发系列:使用多语言工具包让应用支持多语言】

2020/4/16
articleCard.readMore

UWP 上传图片

场景 需要一个修改头像的功能, 第一种,直接选择文件上传即可 第二种,选择文件后进行裁剪再上传 选择图片文件 这是第一种,直接选择文件进行上传 using Windows.Web.Http; var filePicker = new FileOpenPicker { ViewMode = PickerViewMode.Thumbnail, SuggestedStartLocation = PickerLocationId.PicturesLibrary, FileTypeFilter = { ".png", ".jpg", ".jpeg" } }; var file = await filePicker.PickSingleFileAsync(); var stream = new HttpStreamContent(await file.OpenReadAsync()); 开始上传 这里是进行具体上传的地方 using Windows.Web.Http; var httpClient = new HttpClient(); var form = new HttpMultipartFormDataContent(); form.Add(stream, "file", "avatar.png"); await httpClient.PostAsync(new Uri(), form); file 为POST表单中文件的键 avatar.png 为文件名 注意: 使用了 HttpMultipartFormDataContent 就不要设置 请求头中 Content-Type,不然服务器端无法解析表单内容 处理图片并进行上传 这是使用 Microsoft.Toolkit.Uwp.UI.Controls 里面的裁剪控件 ImageCropper using Microsoft.Toolkit.Uwp.UI.Controls; using Windows.Web.Http; using Windows.Storage.Streams; var inputStream = new InMemoryRandomAccessStream(); await ImageCropper.SaveAsync(stream, BitmapFileFormat.Png); var stream = new HttpStreamContent(inputStream);

2020/4/16
articleCard.readMore

小程序Mini-Timer开发(四)使用说明

使用此程序必须先登录,支持微信登录 添加任务 首页点右下角 + 进入所有任务 在任务列表 点右下角 + 进入任务添加任务页面 添加 任务名 任务说明 每次任务时间: 0 表示不限时,不会自动停止,其他值表示每次执行多少分钟会自动停止 点右上角 √ 进行保存 添加今日任务 首页点右下角 + 进入所有任务 在任务列表,点击左下角 的 编辑 按钮 进入编辑模式 选择一个或多个 点击底部的 添加到今日任务 就会把选中的任务添加到今日任务,可以连续点击,添加多次任务 点击底部的 完成 按钮就会退出编辑模式 底部的 结束任务 可以把选中的任务结束掉,表示此任务已经完成了,不用再执行了 执行任务 首页点击 一个任务,进入任务执行页面 点击底部的 开始 按钮,开始任务 这时可以把手机放一边了,做事 等时间归 0,手机会震动,表示此轮任务执行完一次,可以停下来休息3-5分钟 在执行时,可以点击 暂停,表示有其他事打扰,需要暂停一下,处理完后点 开始 继续执行任务 在执行时,可以点击 停止,表示必须去处理其他事,这次任务执行无效,需要重新执行,不会减少今日任务

2020/3/30
articleCard.readMore

小程序Mini-Timer开发(三)正式测试

小程序Mini-Timer开发(三)正式测试 问题及解决方法 服务端 请求头 CONTENT_TYPE 接受不到 原因 $_SERVER['HTTP_CONTENT_TYPE'] 没有值 加上一个判断 允许 $_SERVER['HTTP_CONTENT_TYPE] $_SERVER['CONTENT_TYPE'] 两种获取方式 服务端 请求头 Authorization 接受不到 原因 apache 下重写 认是不能获取Authorization信息的 可以使用 getallheaders() 获取 不要从 $_SERVER 上提取,上一个问题也可以用此方法 注意 getallheaders() 获取的是原始请求头 键未大写 ,- 也未处理为 _ 示例 ['Content-Type' => 'application/json;charset=utf-8'] token 突然失效 用户信息 返回的token有问题 birthday 保存不上 服务端未进行 birthday 保存 在任务页面添加了任务返回首页未显示 onLoad 方法只第一执行了, 需要把执行的内容放到 onShow 中,这样每次显示页面都会刷新 任务执行完了但验证失败 原因 服务器时间与本地时间对不上,改为通过已完成时间重新计算开始时间 无法下拉刷新 enablePullDownRefresh: true 未设置 错误响应未显示弹窗 wx.showLoading 与 wx.showToast 是冲突的,必须先关闭 Loading

2020/3/30
articleCard.readMore

小程序Mini-Timer开发(二)http封装

小程序Mini-Timer开发(二)http封装 基本请求封装 Promise 普通请求 interface IRequestOption { headers?: any; mask?: boolean; loading?: boolean; guest?: boolean; // token失效不自动跳转 } interface IRequest extends IRequestOption { url: string; params?: any; // 拼接到url上 data?: any; // post 数据 } export function request<T>(method: 'OPTIONS'| 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT', requestHandler: IRequest, option?: IRequestOption) { if (option) { // 多加一个 option 是方便进一步封装传入自定义配置 requestHandler = Object.assign(requestHandler, option); } let { url, params, data, headers, mask, loading, guest } = requestHandler; if (loading === undefined || loading) { // 自动显示加载中 wx.showLoading && wx.showLoading({title: 'Loading...', mask: mask ? mask : false}) } if (!params) { params = {}; } if (!headers) { headers = {}; } // 这里可以加入自定义接口appid 和签名 const token = wx.getStorageSync(TOKEN_KEY) if (token) { // 加入登录令牌 headers.Authorization = 'Bearer ' + token; } return new Promise<T>((resolve, reject) => { wx.request({ url: util.uriEncode(util.apiEndpoint + url, params), data: data, method: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'].indexOf(method) > -1 ? method : 'GET', header: Object.assign({ 'Content-Type': 'application/json', 'Accept': 'application/json', }, headers), success: function (res) { const { data, statusCode } = res; if (statusCode === 200) { resolve(data as any); return; } if (statusCode !== 401 || !guest) { // 自动显示错误提示 wx.showToast({ title: (data as any).message, icon: 'none', duration: 2000 }); } if (statusCode === 401) { app && app.setToken(); if (!guest) { // 自动跳转到登录页 wx.navigateTo({ url: '/pages/member/login' }); } } // 处理数据 reject(res) }, fail: function () { reject('Network request failed') }, complete: function () { wx.hideLoading && wx.hideLoading() } }) }); } 上传文件 /** * 上传文件 * @param file 要上传文件资源的路径 (本地路径) * @param requestHandler * @param name 上传文件的对应的 key */ export function uploadFile<T>(file: string, requestHandler: IRequest, name: string = 'file'): Promise<T> { let { url, params, data, headers, mask, loading } = requestHandler; if (loading === undefined || loading) { wx.showLoading && wx.showLoading({title: 'Loading...', mask: mask ? mask : false}) } if (!params) { params = {}; } if (!headers) { headers = {}; } // 这里可以加入自定义接口appid 和签名 const token = wx.getStorageSync(TOKEN_KEY) if (token) { // 加入登录令牌 headers.Authorization = 'Bearer ' + token; } return new Promise<T>((resolve, reject) => { wx.uploadFile({ url: util.uriEncode(util.apiEndpoint + url, params), formData: data, filePath: file, name, header: Object.assign({ 'Accept': 'application/json', }, headers), success: function (res) { const { data, statusCode } = res; if (statusCode === 200) { resolve(data as any); return; } wx.showToast({ title: (data as any).message, icon: 'none', duration: 2000 }); if (statusCode === 401) { app && app.setToken(); wx.navigateTo({ url: '/pages/member/login' }); } // 处理数据 reject(res) }, fail: function () { reject('Network request failed') }, complete: function () { wx.hideLoading && wx.hideLoading() } }) }); } 需要的几种请求方式 GET /** * 封装get方法 * @param url * @param data * @param loading 是否显示加载中 * @returns {Promise} */ export function fetch<T>(url: string, params = {}, option?: IRequestOption): Promise<T> { return request<T>('GET', { url, params, }, option); } POST /** * 封装post请求 * @param url * @param data * @param loading 是否显示加载中 * @returns {Promise} */ export function post<T>(url: string, data = {}, option?: IRequestOption): Promise<T> { return request<T>('POST', { url, data, }); } PUT /** * 封装put请求 * @param url * @param data * @param loading 是否显示加载中 * @returns {Promise} */ export function put<T>(url: string, data = {}, option?: IRequestOption) { return request<T>('PUT', { url, data, }, option); } DELETE /** * 删除请求 * @param url * @param loading 是否显示加载中 */ export function deleteRequest<T>(url: string, option?: IRequestOption): Promise<T> { return request<T>('DELETE', { url, }, option); } 请求头添加 自动添加登录令牌 const token = wx.getStorageSync(TOKEN_KEY) if (token) { // 加入登录令牌 headers.Authorization = 'Bearer ' + token; } 响应处理 对令牌过期的处理 清除token 并跳转登录页面,但是一些页面又不想自动跳转到登录页,所以应该能传入设置 if (statusCode === 401) { app && app.setToken(); wx.navigateTo({ url: '/pages/member/login' }); } 登录推出的处理 相应失败的信息弹窗 请求加载进度的显示 有些地方不需要加载进度条,比如下拉加载更多,静默加载更好,不影响阅读

2020/3/30
articleCard.readMore

小程序Mini-Timer开发(一)功能介绍

小程序Mini-Timer开发(一)功能介绍 主要功能 添加任务 有标题 有说明 有每次执行的时间 所有任务 支持批量终止 今日任务 从所有任务页面点击编辑进入编辑模式,可以多选添加到今日任务,可以连续点击添加多次 执行任务 开始:开始任务,并进行计时 暂停:暂停任务,停止计时 终止:中途有事要终止此次任务,计时重置,本次任务恢复 待开发功能 大任务及小任务 任务数据统计 用户相关 注册登录 支持小程序快捷登录 找回密码 个人信息及修改 第三方账户关联 小程序里只支持小程序的绑定,对其他绑定无法进行操作 账户注销 签到 消息列表 只支持查看列表,无法进入查看 扫一扫 支持扫码登录网页端 帮助 文章的搜索 文章的列表 文章的详情 反馈 支持反馈留言 设置 震动开启的设置 屏幕常亮的设置

2020/3/30
articleCard.readMore

typescript 开发小程序编译当前文件

typescript 开发小程序编译当前文件 需求 用 typescript 开发微信小程序时,每次查看效果都很麻烦,编译过程太长了,怎么只编译当前编辑的文件呢? 工具 vs code Code Runner 插件 gulp 第一步 要把当前文件的路径传给 gulp 要在当前工作区增加一个设置 .vscode\settings.json { "code-runner.executorMapByFileExtension": { ".ts": "gulp build --file=$fullFileName", }, } 以下一些语言必须用以下方式才有用,用拓展名是没有用的 java, c, cpp, javascript, php, python, perl, ruby, go, lua, groovy, powershell, bat, shellscript, fsharp, csharp, vbscript, typescript, coffeescript, swift, r, clojure, haxe, objective-c, rust, racket, ahk, autoit, kotlin, dart, pascal, haskell, nim, d, lisp { "code-runner.executorMap": { "typescript": "gulp build --file=$fullFileName", } } Code Runner 有右键菜单项 Run Code 就是执行当前文件的 这样在 ts 文件就会运行命令 gulp build --file= $fullFileName 表示文件的绝对路径 第二步 在 guipfile.js 中增加 build 任务 var gulp = require('gulp'), ts = require("gulp-typescript"), tsProject = ts.createProject('tsconfig.json'); gulp.task('build', async() => { // 这里必须取得相对路径,不然最终的生成文件就会在 dist 文件夹下加全路径 var file = process.argv[3].substr(7).replace(__dirname + '\\', ''); await gulp.src(file) .pipe(tsProject()) .pipe(gulp.dest('dist')); }); 第三步 右键执行 ts 测试以下 增强版 如果我使用一个方法既可以编译所有的,又可以编译一个文件,该怎么做 修改guipfile.js var gulp = require('gulp'), ts = require("gulp-typescript"), tsProject = ts.createProject('tsconfig.json'); // 获取输入的路径 function getSrcPath(src) { if (process.argv.length < 4) { return src; } if (process.argv[2] !== 'build') { return src; } return process.argv[3].substr(7).replace(__dirname + '\\', '').replace('\\', '/'); } // 获取输出的路径 function getDistPath(dist) { if (process.argv.length < 4) { return dist; } if (process.argv[2] !== 'build') { return dist; } return 'dist'; } gulp.task('build', async() => { await gulp.src(getSrcPath('src/**/*.ts')) .pipe(tsProject()) .pipe(gulp.dest(getDistPath('dist/'))); }); 这样执行 gulp build 即可编译所有 ts 文件 右键 Run Code 只编译当前 ts 文件 再也不用等很长时间了

2020/3/26
articleCard.readMore

VS2019 开发PHP 拓展(五)创建 class

VS2019 开发PHP 拓展(五)创建 class 增加一个 application 类 zodream_application.h 定义类的方法 #ifndef ZODREAM_APPLICATION_H #define ZODREAM_APPLICATION_H #ifndef ZO_BOOTED #define ZO_BOOTED ZEND_STRL("booted") #endif // !ZO_BOOTED PHP_METHOD(ZodreamApplication, __construct); PHP_METHOD(ZodreamApplication, __destruct); PHP_METHOD(ZodreamApplication, version); PHP_METHOD(ZodreamApplication, handle); extern zend_class_entry* zo_application_ce; ZEND_MINIT_FUNCTION(zodream_appliation); #endif //ZODREAM_APPLICATION_H zodream_application.c 实现类的方法,并实现初始化 #include "zodream_application.h" zend_class_entry* zo_application_ce; /* {{{ proto ZodreamApplication ZodreamApplication::__construct() Public constructor */ PHP_METHOD(ZodreamApplication, __construct) { // 修改属性 zend_update_property_bool(zo_application_ce, getThis(), ZO_BOOTED, TRUE); } /* }}} */ /* {{{ proto void ZodreamApplication::__destruct() */ PHP_METHOD(ZodreamApplication, __destruct) { } /* }}} */ /* {{{ proto string ZodreamApplication::version() */ PHP_METHOD(ZodreamApplication, version) { zend_string *retval = strpprintf(0, PHP_ZODREAM_VERSION); RETURN_STR(retval); } /* }}} */ /* {{{ proto void ZodreamApplication::handle() */ PHP_METHOD(ZodreamApplication, handle) { zend_bool* booted = (zend_bool *)zend_read_property(zo_application_ce, getThis(), ZO_BOOTED, 0, NULL); RETURN_BOOL(booted); } /* }}} */ zend_function_entry zodream_application_methods[] = { PHP_ME(ZodreamApplication, __destruct, arginfo_void, ZEND_ACC_PUBLIC | ZEND_ACC_DTOR ) PHP_ME(ZodreamApplication, __construct, arginfo_void, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) PHP_ME(ZodreamApplication, version, arginfo_void, ZEND_ACC_PUBLIC) PHP_ME(ZodreamApplication, handle, arginfo_void, ZEND_ACC_PUBLIC) PHP_FE_END }; ZEND_MINIT_FUNCTION(zodream_application) { zend_class_entry ce; INIT_CLASS_ENTRY(ce, "Zodream\\Application", zodream_application_methods); zo_application_ce = zend_register_internal_class_ex(&ce, NULL); zo_application_ce->ce_flags |= ZEND_ACC_FINAL; zend_declare_property_bool(zo_application_ce, ZO_BOOTED, FALSE, ZEND_ACC_PROTECTED); zend_declare_class_constant_string(zo_application_ce, ZEND_STRL("VERSION"), PHP_ZODREAM_VERSION); return SUCCESS; } zodream.c 进行类的初始化 PHP_MINIT_FUNCTION(zodream) { ZEND_MODULE_STARTUP_N(zodream_application)(INIT_FUNC_ARGS_PASSTHRU) return SUCCESS; }

2020/3/24
articleCard.readMore

VS2019 开发PHP 拓展(四)创建 function

VS2019 开发PHP 拓展(四)创建 function 无参无返回 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test1, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() /* {{{ void test1() */ PHP_FUNCTION(test1) { ZEND_PARSE_PARAMETERS_NONE(); php_printf("The extension %s is loaded and working!\r\n", "zodream"); } /* }}} */ static const zend_function_entry zodream_functions[] = { PHP_FE(test1, arginfo_test1) PHP_FE_END }; 1参无返回 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test2, 0, 0, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) ZEND_END_ARG_INFO() /* {{{ void test2() */ PHP_FUNCTION(test2) { char *var = "World"; size_t var_len = sizeof("World") - 1; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_STRING(var, var_len) ZEND_PARSE_PARAMETERS_END(); php_printf("Hello %s\r\n", var); } /* }}} */ static const zend_function_entry zodream_functions[] = { PHP_FE(test2, arginfo_test2) PHP_FE_END }; 多参无返回 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test3, 0, 0, IS_VOID, 2) ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, config, IS_ARRAY, 0) ZEND_END_ARG_INFO() /* {{{ void test3(string $str, array $config) */ PHP_FUNCTION(test3) { zval* config, *entry; zend_ulong num_idx; zend_string* str_idx; char* var = "World"; size_t var_len = sizeof("World") - 1; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_STRING(var, var_len) Z_PARAM_ARRAY(config) ZEND_PARSE_PARAMETERS_END(); php_printf("Hello %s\r\n", var); // 循环打印为字符串的值 ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(config), num_idx, str_idx, entry) { ZVAL_DEREF(entry); if (Z_TYPE_P(entry) == IS_STRING) { php_printf("Hello %s\r\n", Z_STRVAL_P(entry)); } } ZEND_HASH_FOREACH_END(); } /* }}} */ static const zend_function_entry zodream_functions[] = { PHP_FE(test3, arginfo_test3) PHP_FE_END }; 无参有返回 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test4, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() /* {{{ void test4() */ PHP_FUNCTION(test4) { ZEND_PARSE_PARAMETERS_NONE(); zend_string* retval = strpprintf(0, "test 4"); RETURN_STR(retval); } /* }}} */ static const zend_function_entry zodream_functions[] = { PHP_FE(test4, arginfo_test4) PHP_FE_END }; 有参有返回 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_test5, 0, 0, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) ZEND_END_ARG_INFO() /* {{{ void test5() */ PHP_FUNCTION(test5) { char* var = "World"; size_t var_len = sizeof("World") - 1; zend_string* retval; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_STRING(var, var_len) ZEND_PARSE_PARAMETERS_END(); retval = strpprintf(0, "Hello %s", var); RETURN_STR(retval); } /* }}} */ static const zend_function_entry zodream_functions[] = { PHP_FE(test5, arginfo_test5) PHP_FE_END };

2020/3/24
articleCard.readMore

VS2019 开发PHP 拓展(三)一些PHP定义

VS2019 开发PHP 拓展(三)一些PHP定义 宏 PHP_FUNCTION 定义一个方法 RETURN_NULL() 返回null RETURN_LONG(l) 返回整型 RETURN_DOUBLE(d) 返回浮点型 RETURN_STR(s) 返回一个字符串。参数是一个zend_string * 指针 RETURN_STRING(s) 返回一个字符串。参数是一个char * 指针 RETURN_STRINGL(s, l) 返回一个字符串。第二个参数是字符串长度。 RETURN_EMPTY_STRING() 返回一个空字符串。 RETURN_ARR(r) 返回一个数组。参数是zend_array *指针。 RETURN_OBJ(r) 返回一个对象。参数是zend_object *指针。 RETURN_ZVAL(zv, copy, dtor) 返回任意类型。参数是 zval *指针。 RETURN_FALSE 返回false RETURN_TRUE 返回true PHP_MINIT_FUNCTION 初始化module时运行 PHP_MINIT PHP_MSHUTDOWN_FUNCTION 当module被卸载时运行 PHP_MSHUTDOWN PHP_RINIT_FUNCTION 当一个REQUEST请求初始化时运行, return SUCCESS; 返回FALIURE就不会加载这个扩展了 PHP_RINIT PHP_RSHUTDOWN_FUNCTION 当一个REQUEST请求结束时运行 PHP_RSHUTDOWN PHP_MINFO_FUNCTION 这个是设置phpinfo中这个模块的信息 PHP_MINFO PHP_GINIT_FUNCTION 初始化全局变量时 PHP_GSHUTDOWN_FUNCTION 释放全局变量时 ZEND_PARSE_PARAMETERS_NONE 声明方法无参数值 ZEND_PARSE_PARAMETERS_START 获取方法的参数值,第一个参数表示必传的参数个数,第二个参数表示最多传入的参数个数。 Z_PARAM_OPTIONAL 在这个之后的参数都是可选参数 Z_PARAM_STRING 以字符串的形式获取参数值 ZEND_PARSE_PARAMETERS_END 获取参数值结束 方法 php_printf 打印字符串 未完待续

2020/3/24
articleCard.readMore

Git 及 Github 相关操作

Git pull request 问题描述 在 github 上提交了一个 pull request,在作者进行操作前,发现自己某处错了,进行了修改。 这时是关闭这条 pull request 重新发一条,还是有什么操作可以覆盖这次发送的 pull request? 解决方案 push 更新那个分支就行,pull request只和分支名绑定。 直接 push 就会自动追加到到 PR 后面。当然,如果你不希保留旧的 commit 记录,还可以选择本地 git reset 之后 push -f 强行覆盖掉你远程的commit,PR会一并更新。 回退一个版本 git reset HEAD~1 修改代码 强制提交 git push -f 问题描述 如何把已经更新到最新的代码保持跟自己Fork下来的仓库代码保持一致 解决方法 命令查看此时本地仓库对应的远程仓库 git remote -v 为本地仓库添加一个新的远程仓库,指定名字为upstream,指向原项目仓库 git remote add upstream https://github.com/php/php-src.git 这是使用 git remote -v 则可以查看到已经更新了 保持同步更新 git pull upstream master //从原仓库更新代码到的本地master分支 git push origin master //将master推到自己的远程仓库 参考 【从github上fork了一个仓库后保持与原仓库代码同步的操作方法】 【Git实用技巧 pull request修改】 GITHUB 客户端一些操作 撤回 未commit 在 Changes 下,右击想要操作的文件,点击菜单 Discard changes 即可,此操作会删掉对此文件的所有的修改 已commit,但是未push 在 History 下, 右击想要撤回的 commit, Amend commit 是修改本次提交的 描述信息 Undo commit 取消本次提交,可以重新修改文件和描述信息,不会留下记录 Revert changes in commit 撤回本次 commit, 实际上在反向提交一次 commit 删除了上次修改的内容,上次 commit修改的文件就恢复初始状态, 会丢失上次修改的内容,但是会留下记录 已commit,已push 只能 Revert changes in commit, 删除了上次修改的内容,会丢失上次修改的内容,同时会留下记录

2020/3/23
articleCard.readMore

VS2019 开发PHP 拓展(二)创建空拓展

VS2019 开发PHP 拓展(二)创建空拓展 生成空的拓展 命令行进入 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext 这里zodream代表你的php扩展名 php ext_skel.php --ext zodream vs2019 载入项目 打开 Visual Studio 2019 选择 继续但无需代码 打开菜单 文件 -> 新建 -> 从现有代码创建项目 选择 Visual C++ 下一步 项目文件位置(拓展的目录D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\zodream),项目名称 phpzodream,下一步 项目类型选择 动态链接库(DLL)项目,完成 菜单栏选配置 Release x64 右键项目属性 -> C/C++ -> 常规 -> 附加包含目录 -> 编辑,加入目录 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\main D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\TSRM D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\Zend 属性 -> C/C++ -> 预处理器 -> 预处理器定义 -> 编辑 加入以下变量(其中ZODREAM替换成php扩展名) ZEND_DEBUG=0 PHP_EXTENSION PHP_WIN32 ZEND_WIN32 HAVE_ZODREAM=1 COMPILE_DL_ZODREAM 如果为开启线程安全 则加上 ZTS 生成 可能报错 如果提示LINK 1561: 必须定义入口点 属性 -> 常规 -> 配置类型 选择 动态库(.dll) E0020: 未定义标识符arginfo_test1 或者 缺少 zodream_arginfo.h C1083 无法打开包括文件: “zodream_arginfo.h”: No such file or directory 请复制 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\skeleton\skeleton_arginfo.h 为 zodream_arginfo.h 即可 或者在 ext_skel.php 文件中的 copy_sources 方法 加上 'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h' function copy_sources() { global $options; $files = [ 'skeleton.c' => $options['ext'] . '.c', 'skeleton.stub.php' => $options['ext'] . '.stub.php', 'php_skeleton.h' => 'php_' . $options['ext'] . '.h', 'skeleton_arginfo.h' => $options['ext'] . '_arginfo.h' // 加上这行就会自动生成 ]; } LNK2019 无法解析的外部符号 __imp_zend_strpprintf 属性 -> 连接器 -> 输入 -> 附加依赖项 -> 编辑 加入一个php.lib,此文件在正式发布的PHP程序中 例如 D:\zodream\php\php-7.4-nts\dev\php7.lib E0020 未定义标识符 "zif_test2" 因为定义的方式 PHP_FUNCTION(zodream_test2) 但是引入时用的是 PHP_FE(test2, arginfo_test2) 改为 PHP_FUNCTION(test2) { 或者改 D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src\ext\skeleton\skeleton.c /* {{{ string test2( [ string $var ] ) */ PHP_FUNCTION(test2) { char *var = "World"; size_t var_len = sizeof("World") - 1; zend_string *retval; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_STRING(var, var_len) ZEND_PARSE_PARAMETERS_END(); retval = strpprintf(0, "Hello %s", var); RETURN_STR(retval); } 使用 这是在正式时使用 把生成的 phpzodream.dll 复制到正式环境 ext 文件夹下, 修改 php.ini 引入插件 extension=zodream 重启 iis 当前为测试时 按【上一章】编译 php 即可,不需要在ini配置,直接使用即可 在php 代码加入 test1(); echo test2(); 查看效果

2020/3/22
articleCard.readMore

VS2019 开发PHP 拓展(一)环境准备

VS2019 开发PHP 拓展(一)环境准备 准备工作 下载 “php-sdk-binary-tools” 在右边的“Clone or download”点击,选择下方的“Download ZIP” 下载 “PHP源码” 此时最新版本是7.4.4,选择“php-7.4.4-src.zip”下载, 根据 Build your own PHP on Windows Visual C++ 14.0 (Visual Studio 2015) for PHP 7.0 or PHP 7.1. Visual C++ 15.0 (Visual Studio 2017) for PHP 7.2, PHP 7.3 or PHP 7.4. Visual C++ 16.0 (Visual Studio 2019) for master. 即 vs2019 只能编译 php-src 的 master 分支,所以需要下载 master 分支源码,在右边的“Clone or download”点击,选择下方的“Download ZIP” 下载 “Visual Studio 2019” 选择 “社区” 下的 “免费下载” 进行下载, 将 php-sdk-binary-tools-master.zip 解压到 D:\zodream\php-sdk-binary-tools 按住shift在编译目录内点击右键,选择“在此处打开Powershell窗口”; 执行”.\phpsdk-vs16-x64.bat”,成功后提示符从“>”变成“$”; 执行“phpsdk_buildtree phpdev”,成功后目录中会多一个“phpdev”目录,命令行的目录自动切换到“phpdev/vc16/x64”; 在“phpdev/vc16/x64”目录下新建php-src文件夹,将PHP源码复制到此目录; 切换到php-src目录(cd php-src),执行“phpsdk_deps -u”; 在“phpdev/vc16/x64”下建立pecl目录(与PHP源码目录同级),此目录放拓展的源码。如果为自己开发的拓展,请参考【下一章】。 编译流程 将拓展源码复制到该目录下(D:\zodream\php-sdk-binary-toolsphpdev\vc16\x64\pecl); 如果为自己开发的拓展,则不需要这一步,下一步会自动发现这些拓展,直接配置编译选项则可以 在PHP源码目录内(D:\zodream\php-sdk-binary-tools\phpdev\vs16\x64\php-src)执行”buildconf”; configure --help 查看能够使用的配置选项,包括你自己的插件什么的 执行“configure –一些选项”命令配置编译选项,例如”configure –-disable-all –-enable-cli –-enable-cgi –-enable-zlib –-enable-session –-without-gd -–with-bz2 –-enable-yourext”; 编译 php configure --disable-all --enable-cli --enable-$remains 我的编译php 参数 configure --disable-all --enable-cli --with-mysqlnd -–enable-cgi –-enable-zlib –-enable-session --with-bz2 --with-mysqli --enable-pdo --with-pdo-mysql --enable-redis --enable-zodream --enable-fileinfo --with-curl --with-gettext --enable-mbstring --with-openssl --with-imagick --with-pthreads 编译 PECL 拓展, 例如: apcu configure --disable-all --enable-cli --enable-apcu 执行nmake命令编译PHP及拓展, 如果希望压缩生成的 PHP 生成和扩展,则"nmake"之后也会运行:nmake snap 完成一些更改后重新编译, 清理旧的编译二进制文件 nmake clean, 如果需要更新"configure"脚本 buildconf --force, 然后重复编译 3、 4 步 编译成功后,在源码的X64目录下会生成“Release”或“Release_TS”目录,编译好的php.exe及生成的拓展dll均在此目录下。dll的文件名为php_xxxx.dll,例如“php_zodream.dll”。 默认编译出来的拓展是TS(线程安全)的版本(位于Release_TS目录中),如果要编译非线程安全版本,configure时加入“–disable-zts”选项。

2020/3/22
articleCard.readMore

XPath 语法

XPath 语法 选取节点 / 如果是在最前面,代表从根节点选取,否则选择某节点下的某个节点.只查询子一辈的节点 /html 查询到一个结果 /div 查询到0个结果,因为根节点以下只有一个html子节点 /html/body 查询到1个结果 // 查询所有子孙节点 //head/script //div ./ 选取当前节点 ../ 选取当前节点的父节点 @ 选取属性 //div[@id] 选择所有带有id属性的div元素 <div id="s" class="left"> 运算符/特殊字符 说明 / 此路径运算符出现在模式开头时,表示应从根节点选择。 // 从当前节点开始递归下降,此路径运算符出现在模式开头时,表示应从根节点递归下降。 . 当前上下文。 .. 当前上下文节点父级。 * 通配符;选择所有元素节点与元素名无关。(不包括文本,注释,指令等节点,如果也要包含这些节点请用node()函数) @ 属性名的前缀。 @* 选择所有属性,与名称无关。 : 命名空间分隔符;将命名空间前缀与元素名或属性名分隔。 ( ) 括号运算符(优先级最高),强制运算优先级。 [ ] 应用筛选模式(即谓词,包括"过滤表达式"和"轴(向前/向后)")。 [1] 下标运算符;用于在集合中编制索引。1 表示数字 | 两个节点集合的联合,如://messages/message/to - 减法。 div, 浮点除法。 and, or 逻辑运算。 mod 求余。 not() 逻辑非 = 等于 != 不等于 特殊比较运算符 < 或者 <<= 或者 <=> 或者 >>= 或者 >=需要转义的时候必须使用转义的形式,如在XSLT中,而在XMLDOM的scripting中不需要转义。 谓语 谓语是用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中。 //body/div[1] body下的第一个div元素 //body/div[last()] body下的最后一个div元素 //body/div[position()<3] body下的位置小于3的元素 //div[@id] div下带id属性的元素 <div id="s" class="left"> //input[@id="serverTime"] input下id="serverTime"的元素 ||| |---|----| ancestor|选取当前节点的所有先辈(父、祖父等) ancestor-or-self|选取当前节点的所有先辈(父、祖父等)以及当前节点本身 attribute|选取当前节点的所有属性 child|选取当前节点的所有子元素。 descendant|选取当前节点的所有后代元素(子、孙等)。 descendant-or-self|选取当前节点的所有后代元素(子、孙等)以及当前节点本身。 following|选取文档中当前节点的结束标签之后的所有节点。 namespace|选取当前节点的所有命名空间节点 parent|选取当前节点的父节点。 preceding|直到所有这个节点的父辈节点,顺序选择每个父辈节点前的所有同级节点 preceding-sibling|选取当前节点之前的所有同级节点。 self|选取当前节点。 模糊匹配 //div[contains(@class,'f1')] div的class属性带有f1的 通配符 * //body/* body下面所有的元素 //div[@*] 只要有用属性的div元素 //div[@id='footer'] //div 带有id='footer'属性的div下的所有div元素 //div[@class='job_bt'] //dd[@class='job-advantage'] 运算符 //div[@class='job_detail'] and @id='job_tent' //book/title | //book/price 选取 book 元素的所有 title 和 price 元素。 .//a/text() 当前标签下所有a标签的文字内容 //tr[position()>1 and position()<11] 位置大于1小于11 需要注意的知识点 1./和//的区别:/代表子节点,//代表子孙节点,//用的比较多 2.contains有时候某个属性中包含了多个值,那么使用contains函数 //div[contains(@class,'lg')] 3.谓语中的下标是从1开始的,不是从0开始的 常用表达式实例: ||| |----|----| /|Document Root文档根. /|选择文档根下面的所有元素节点,即根节点(XML文档只有一个根节点) /node()|根元素下所有的节点(包括文本节点,注释节点等) /text()|查找文档根节点下的所有文本节点 /messages/message|messages节点下的所有message节点 /messages/message[1]|messages节点下的第一个message节点 /messages/message[1]/self::node()|第一个message节点(self轴表示自身,node()表示选择所有节点) /messages/message[1]/node()|第一个message节点下的所有子节点 /messages/message[1]/[last()]|第一个message节点的最后一个子节点 /messages/message[1]/[last()]|Error,谓词前必须是节点或节点集 /messages/message[1]/node()[last()]|第一个message节点的最后一个子节点 /messages/message[1]/text()|第一个message节点的所有子节点 /messages/message[1]//text()|第一个message节点下递归下降查找所有的文本节点(无限深度) |/messages/message[1] /child::node()| /messages/message[1] /node()| /messages/message[position()=1]/node()| //message[@id=1] /node()|第一个message节点下的所有子节点 //message[@id=1] //child::node()|递归所有子节点(无限深度) //message[position()=1]/node()|选择id=1的message节点以及id=0的message节点 /messages/message[1] /parent::|Messages节点 /messages/message[1]/body/attachments/parent::node()| /messages/message[1]/body/attachments/parent:: | /messages/message[1]/body/attachments/..|attachments节点的父节点。父节点只有一个,所以node()和 返回结果一样。(..也表示父节点. 表示自身节点) //message[@id=0]/ancestor::|Ancestor轴表示所有的祖辈,父,祖父等。向上递归 //message[@id=0]/ancestor-or-self::|向上递归,包含自身 //message[@id=0]/ancestor::node()|对比使用,多一个文档根元素(Document root) /messages/message[1]/descendant::node()| //messages/message[1]//node()|递归下降查找message节点的所有节点 /messages/message[1]/sender/following::|查找第一个message节点的sender节点后的所有同级节点,并对每一个同级节点递归向下查找。 //message[@id=1]/sender/following-sibling::|查找id=1的message节点的sender节点的所有后续的同级节点。 //message[@id=1]/datetime/@date|查找id=1的message节点的datetime节点的date属性 //message[@id=1]/datetime[@date]| //message/datetime[attribute::date]|查找id=1的message节点的所有含有date属性的datetime节点 //message[datetime]|查找所有含有datetime节点的message节点 //message/datetime/attribute::| //message/datetime/attribute::node()| //message/datetime/@|返回message节点下datetime节点的所有属性节点 //message/datetime[attribute::]| //message/datetime[attribute::node()]| //message/datetime[@]| //message/datetime[@node()]|选择所有含有属性的datetime节点 //attribute::|选择根节点下的所有属性节点 //message[@id=0]/body/preceding::node()|顺序选择body节点所在节点前的所有同级节点。(查找顺序为:先找到body节点的顶级节点(根节点),得到根节点标签前的所有同级节点,执行完成后继续向下一级,顺序得到该节点标签前的所有同级节点,依次类推。)注意:查找同级节点是顺序查找,而不是递归查找。 //message[@id=0]/body/preceding-sibling::node()|顺序查找body标签前的所有同级节点。(和上例一个最大的区别是:不从最顶层开始到body节点逐层查找。我们可以理解成少了一个循环,而只查找当前节点前的同级节点) //message[@id=1]//[namespace::amazon]|查找id=1的所有message节点下的所有命名空间为amazon的节点。 //namespace::|文档中的所有的命名空间节点。(包括默认命名空间xmlns:xml) //message[@id=0]//books/[local-name()='book']|选择books下的所有的book节点,注意:由于book节点定义了命名空间<amazone:book>.若写成//message[@id=0]//books/book则查找不出任何节点。 //message[@id=0]//books/[local-name()='book' and namespace-uri()='http://www.amazon.com/books/schema']|选择books下的所有的book节点,(节点名和命名空间都匹配) //message[@id=0]//books/[local-name()='book'][year>2006]|选择year节点值>2006的book节点 //message[@id=0]//books/*[local-name()='book'][1]/year>2006|指示第一个book节点的year节点值是否大于2006.返回xs:boolean: true 参考来源 Xpath语法格式整理

2020/3/15
articleCard.readMore

WPF/UWP 下图标字体文件使用

WPF/UWP 下图标字体文件使用 WPF 下使用FontAwesome 先把 fontawesome.ttf 放到 Fonts 文件夹中,设置文件属性“如果较新则复制” 在 xaml 中声明一个字体资源 <FontFamily x:Key="FontAwesome"> pack://application:,,,/Fonts/#FontAwesome </FontFamily> 绑定属性 <Setter Property="FontFamily" Value="{StaticResource FontAwesome}"/> 使用 <TextBlock Text="" Style="{DynamicResource FontAwesome}" /> tb.Text = "\uf000";//Char('\uf15b'); 或者直接合并234步,按照 /命名空间;component/[路径]#[字体名称] 这个格式写 <TextBlock Text="" FontFamily="/ZoDream;component/Fonts#FontAwesome" /> UWP 下使用FontAwesome <TextBlock Text="" FontFamily="Fonts/FontAwesome.ttf#FontAwesome" /> iconfont 字体使用同理 将 #字体名称 改成 #iconfont 即可 UWP 下iconfont <TextBlock Text="" FontFamily="Fonts/iconfont.ttf#iconfont" />

2020/3/13
articleCard.readMore

登录“记住我”的实现

登录“记住我”的实现 原理 用户登录时勾选 记住我,服务器生成随机token 加用户id 组成字符串永久保存到浏览器cookie 中,下一次从浏览器获取并提取token和id 获取用户登录即可 数据 用户表需要两个关键字段 id 和 remember_token id 是查询用户的主键 remember_token 为随机生成的字符串 程序 需要方法 bool login(User user, bool remember);// 登录 bool logout();// 登出并清除cookie bool setRememberToken(User user);// 生成token 保存到数据库并设置cookie [int, string] getRememberToken();// 从cookie获取 id 和token User findByIdentity(int id);// 根据id 获取用户 User findByRememberToken(int id, string token);// 根据id 和token获取用户

2020/3/12
articleCard.readMore

wordpress 学习(四) 主循环

wordpress 学习(四) 主循环 have_posts() 判断是否有文章 get_post_format() 获取文章的类型 aside chat gallery link image quote status video audio 标准 日志 链接 相册 状态 引语 图像 the_post() the_post()函数则调用 $wp_query->the_post()成员函数前移循环计数器,并且创建一个全局变量$post(不是$posts),把当前的post的所有信息都填进这个$post变量中,以备接下来使用。 简单使用用 the_content 正文 the_title 文章标题 the_time 时间 the_category 文章的分类 the_author_posts_link() 输出作者及链接 the_permalink() 输出文章的网址 The loop starts here: <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?> and ends here: <?php endwhile; else : ?> <p><?php _e( 'Sorry, no posts matched your criteria.' ); ?></p> <?php endif; ?> 隐藏排除显示特定类别 这里排除 3和 8类 <?php $query = new WP_Query( 'cat=-3,-8' ); ?> <?php if ( $query->have_posts() ) : while ( $query->have_posts() ) : $query->the_post(); ?> the_time('Y年m月j日') a代表小写的英语的上下午,如am、pm A代表大写的英语的上下午,如AM、PM d代表英语的日期(小于10仍为两位数写法),如05、12 D代表中文的星期,如五、七 F代表中文的月份(包括“月”这个字),如五月、十二月 g代表英语的小时(小于10为一位数写法),如5、12 G代表英语的小时(小于10仍为两位数写法),如05、12 h代表英语的分钟(小于10为一位数写法),如5、12 H代表英语的分钟(小于10仍为两位数写法),如05、12 j代表英语的日期(小于10为一位数写法),如05、12 l代表中文的星期(包括“星期”这两个字),如星期五、星期七 m代表英语的月份(小于10仍为两位数写法),如05、12 M代表英语的月份(以单词的形式显示),如Jun n代表英语的月份(小于10为一位数写法),如5、12 O代表英语的时区,如+0800 r代表完整的日期时间,如Tue, 06 Jun 2006 18:37:11 +0800 S代表日期的序数后缀,如st、th T代表英语的时区(以单词的形式显示),如CST w代表英语的星期,如5、7 W代表周数,如23 y代表两位数年份,如07、08 Y代表四位数年份,如2007、2008 z代表天数,如156 <?php the_content( $more_link_text, $strip_teaser, $more_file ); ?> get_the_content( $more_link_text, $stripteaser, $more_file ) 参数 $more_link_text (字符串)(可选)“more”链接的链接文本 默认值: '(more...)' $strip_teaser (布尔型)(可选)显示(FALSE)或隐藏(TRUE)more链接前的文本。 默认值:FALSE $more_file (字符串)(可选)more链接所指向的文件 默认值:当前文件 the_title( $before, $after, $echo ); get_the_title() 通过文章ID返回文章标题 $before 字符串型,标题之前放置的文本,默认是空 $after 字符串型,标题之后放置的文件,默认是空 $echo 逻辑型,true表示显示标题,false表示返回它并用在PHP中,默 认为true. the_title_attribute() 也是取出标题,但会过滤一些特殊字符 global $post; echo $post->post_title; 通过post这个全局变量还可以让你获取:ID,post_author,post_date,post_excerpt,comment_count 和其他。 the_category( $separator, $parents, $post_id ) $separator 每个类别的链接之间显示的文本或字符。默认情况下,链接放置在HTML的无序列表。一个空字符串将导致默认行为。默认:空字符串 $parents 'multiple'显示单独指向父表和子类别,展示"父项/子项"关系。▪'single'--仅显示子类别链接,链接文本展示"父项/子项"关系。默认:空字符串注意:默认是一个链接到子类别,没有显示关系。 $post_id 帖子ID来检索类别。 在当前职位的类别列表中的默认值false结果。 comments_popup_link( $zero, $one, $more, $css_class, $none ); 添加一个链接 $zero 没有评论时显示 $one 只有一条评论是显示 $more 用“ % ”显示条数,有多条评论时显示 $css_class class=" " $none 当评论功能被关闭时 显示

2020/3/12
articleCard.readMore

wordpress 学习(三) 添加js

wordpress 学习(三) 添加js wp_enqueue_script($handle,$src,$deps,$ver,$in_footer) $handle (必需)命名 $src 路径 $deps 依赖 $ver 版本号 $in_footer 默认false 放在<head>里, true放在body下方 必需有 wp_footer()函数 wp_localize_script($handle,$object_name,$l10n) 脚本本地化 $handle 脚本名 $object_name 脚本变量名 $l10n 要本地化的数据本身 array() 例如 wp_localize_script( 'nways-script', 'screenReaderText', array( 'expand' => '<span class="screen-reader-text">' . __( 'expand child menu', 'nways' ) . '</span>', 'collapse' => '<span class="screen-reader-text">' . __( 'collapse child menu', 'nways' ) . '</span>', ) );

2020/3/12
articleCard.readMore

wordpress 学习(二)添加css

wordpress 学习(二)添加css wq_enqueue_style($handle,$src,$deps,$ver,$media); $handle (必需)给样式命名 $src 路径 get_stylesheet_uri() 默认会加载 style.css $deps 依赖关系,默认false 不存在依赖, array();接受依赖样式名 $ver 版本号 避免用户端的缓存而使样式不刷新 例如:zx.css?ver=3.2 $media 为指定媒体制定的样式 all @media screen and min width 400{ .hh{ color:#fff; } } wp_register_style( $handle, $src, $deps, $ver, $media ); 注册样式 再使用wp_enqueue_style($handle); 排队加载 也可以使用 function add_stylesheet_to_head() { echo "<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>"; } add_action( 'wp_head', 'add_stylesheet_to_head' ); 但无法检查CSS文件是否已经被包含在页面中 wp_enqueue_scripts 用来在网站前台加载脚本和CSS admin_enqueue_scripts 用来在后台加载脚本和CSS login_enqueue_scripts 用来在WP登录页面加载脚本和CSS 例如:function nways(){ wp_enqueue_style(); } add_action("wp_enqueue_scripts","nways"); 添加动态内联样式:wp_add_inline_style() 如果你的主题有选项可自定义主题的样式,你可以使用 wp_add_inline_style() 函数来打印内置的样式: <?php function mytheme_custom_styles() { wp_enqueue_style( 'custom-style', get_template_directory_uri() . '/css/custom-style.css' ); $bold_headlines = get_theme_mod( 'headline-font-weight' ); // 比方说,它的值是粗体“bold” $custom_inline_style = '.headline { font-weight: ' . $bold_headlines . '; }'; wp_add_inline_style( 'custom-style', $custom_inline_style ); } add_action( 'wp_enqueue_scripts', 'mytheme_custom_styles' ); ?> wp_style_is( $handle, $state ); 检查样式的排队状况 wp_style_add_data($handle,$key,$value) 添加元数据到你的样式中,包括条件注释、RTL的支持和更多! 'conditional' string Comments for IE 6, lte IE 7 etc. 'rtl' bool|string To declare an RTL stylesheet. 'suffix' string Optional suffix, used in combination with RTL. 'alt' bool For rel="alternate stylesheet". 'title' string For preferred/alternate stylesheets. 注销样式文件:wp_deregister_style() 例如 if(wp_style_is("boostop.min","regitered")) { wp_deregister_style('boostop.min'); } wp_dequeue_style() 取消已经排列的样式表

2020/3/12
articleCard.readMore

wordpress学习(一)

wordpress学习(一) __() ,翻译,如果不能就返回原始文本 esc_url() 检查url home_url() 加上域名组成完整网址, <?php bloginfo(’name’); ?> : 博客名称(Title) <?php bloginfo(’stylesheet_url’); ?> : CSS文件路径 <?php bloginfo(’pingback_url’); ?> : PingBack Url <?php bloginfo(’template_url’); ?> : 模板文件路径 <?php bloginfo(’version’); ?> : WordPress版本 <?php bloginfo(’atom_url’); ?> : Atom Url <?php bloginfo(’rss2_url’); ?> : RSS 2.o Url <?php bloginfo(’url’); ?> : 博客 Url <?php bloginfo(’html_type’); ?> : 博客网页Html类型 <?php bloginfo(’charset’); ?> : 博客网页编码 <?php bloginfo(’description’); ?> : 博客描述 get_bloginfo() 返回博客的信息 ||| |---|--| |_e() | 显示翻译文字 |has_nav_menu() | 注册的导航菜单是否已经分配了位置 |wp_nav_menu() | 展示一个导航菜单 |is_active_sidebar() | 判断导航区是否正在使用 |dynamic_sidebar() | 显示动态栏 |get_template_directory() | 得到当前主题的路径 get_permalink() 用来根据固定连接返回文章或者页面的链接。在获取链接时 get_permalink() 函数需要知道要获取的文章的 ID,如果在循环中则自动默认使用当前文章。 用法: <?phpthe_title( $before, $after, $echo ); ?> 参数: $before 字符串型,标题之前放置的文本,默认是空 $after 字符串型,标题之后放置的文件,默认是空 $echo 逻辑型,true表示显示标题,false表示返回它并用在PHP中,默认为true. the_excerpt()函数获取文章摘要 get_post_type() 函数用来获取文章的文章类型 edit_post_link( __( '编辑', 'nways' ), '<span class="edit-link">', '</span>' ); ?>编辑当前文章的连接

2020/3/8
articleCard.readMore

网页缓存考虑整理

网页缓存考虑整理 缓存只是为了保存更新频率不高的页面,以减少对数据库的读写压力及显示效率 根据页面更新频率进行缓存 每一个页面更新的频率不一样,比如文章基本不进行内容修改,这可以进行缓存 局部缓存及整页缓存 局部缓存缺点:无法做到 MVC 分离 整页缓存:一些属性无法更新,比如阅读量 对资源引用 整页缓存需要注意 http 及 https 的变化,https 下浏览器无法使用 http 资源样式 域名变化,比如 同一个顶级域名不带 www 和 带www 页面和资源样式是不能使用的 对路径及影响参数需要加入不同缓存条件 比如 是否登录 是否影响页面 不同语言是否影响多语言的使用 不同id 是否调用不同的文章 评论 评论一定要使用 ajax 异步调用

2020/2/28
articleCard.readMore

vscode 搭配 gcc 进行c语言学习

vscode 搭配 gcc 进行c语言学习 通过 MinGW 安装 gcc 版本较老 安装MinGW 进入 http://www.mingw.org 下载 mingw-get-setup.exe 并进行安装 安装 gcc 在 MinGW Installation Manager 中选择 mingw32-gcc-g++-bin 点击菜单 Installation -> Apply Changes -> Apply 搜索 编辑系统环境变量 在 Path 中添加 MinGW 的bin文件夹, 例如 C:\MinGW\bin 在 cmd 中输入 gcc -v 查看是否安装成功 通过 Msys2 安装 gcc 版本最新 进入 https://www.msys2.org/ 下载 msys2-x86_64 修改pacman源,参考 https://mirrors.tuna.tsinghua.edu.cn/help/msys2/ pacman基本命令 pacman -Sy 更新软件包数据 pacman -Syu 更新所有 pacman -Ss xx 查询软件xx的信息 pacman -S xx 安装软件xx pacman -R xx 删除软件xx 下载make pacman -S make 安装gcc、g++编译器 #查询并找到msys/gcc pacman -Ss gcc #安装 pacman -S msys/gcc vscode 安装相应插件 Code Runner 配置 { "code-runner.executorMap": { "c": "cd $dir && gcc $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt" } } C/C++

2020/2/27
articleCard.readMore

截取html

截取html 需求 从一段html 中截取指定长度的内容, 要求: 长度为不包括html标签的内容长度,即 innerTEXT 长度 保留相关标签,并进行闭合 代码 简单版代码 /** * 截取html, 标签不计入长度,自动闭合标签 * @param string $html * @param int $length * @param string $endWith * @bug 本方法缺陷: 未进行严格标签判断 例如 < <gg data="<a>" * @example ::substr('<p>1111<div/>111<br>111<i class="444">11</i>111 55555</p>', 12) * @return string */ public static function substr(string $html, int $length, string $endWith = '...'): string { if ($length < 1) { return $endWith; } $maxLength = mb_strlen($html); if ($maxLength < $length) { return $html; } $result = ''; $n = 0; $unClosedTags = []; $isCode = false; // 是不是HTML代码 $isHTML = false; // 是不是HTML特殊字符,如 $notClosedTags = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'link', 'meta', 'param', 'embed', 'command', 'keygen', 'source', 'track', 'wbr']; $tag = ''; for ($i = 0; $i < $maxLength; $i++) { $char = mb_substr($html, $i, 1); if ($char == '<') { // 进入标签 $isCode = true; $tag = ''; } else if ($char == '&') { $isHTML = true; } else if ($char == '>' && $isCode) { $n = $n - 1; $isCode = false; $tag = explode(' ', $tag, 2)[0]; if (substr($tag, 0, 1) === '/') { // 判断是否时结束标签, 倒序找到邻近开始标签,进行移除 for ($j = count($unClosedTags) - 1; $j >= 0; $j --) { if ($tag === $unClosedTags[$j]) { $unClosedTags = array_splice($unClosedTags, 0, $j - 1); break; } } $tag = ''; } if (!empty($tag) && substr($tag, strlen($tag) - 1, 1) !== '/' && !in_array(strtolower($tag), $notClosedTags)) { // 不是结束标签且不是自闭合且不是无需闭合把标签加入 $unClosedTags[] = $tag; } $tag = ''; } else if ($char == ';' && $isHTML) { $isHTML = false; } if ($isCode && ($tag !== '' || $char !== '<')) { $tag .= $char; } if (!$isCode && !$isHTML && $char !== ' ') { $n = $n + 1; } $result .= $char; if ($n >= $length) { break; } } $result .= $endWith; for ($j = count($unClosedTags) - 1; $j >= 0; $j --) { $result .= sprintf('</%s>', $unClosedTags[$j]); } return $result; } 此代码存在的问题 未对 < 进行验证是否真的为标签开始 未对标签属性值中可能出现的标记 进行过滤 相关需求 指定行的截取

2019/12/22
articleCard.readMore

MySql 数据库引擎 MyISAM与InnoDB 怎么选择

MySql 数据库引擎 MyISAM与InnoDB 怎么选择 介绍 InnoDB Mysql 5.5 版本开始, InnoDB是默认的表存储引擎, 其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU MyISAM 基于传统的ISAM类型, ISAM是Indexed Sequential Access Method (有索引的顺序访问方法) 的缩写,它是存储记录和文件的标准方法 比较 MyISAM InnoDB 支持事务、回滚 不支持 支持,默认封装成事务提交,多条最好使用一个事务 支持外键 不支持 支持 锁 表级锁 表、行(默认)级锁,行锁是实现在索引上,可能会导致“死锁” 全文索引 支持 5.6 以后支持(使用sphinx插件更好) count(*) 事先保存表的总行数 遍历(加了wehre则一样) 索引、主键 允许没有索引和主键,索引都是保存行的地址 没有则生成一个不可见的6字节的主键 安全性 更安全 高并发 容易表损坏 效率更好 巨大数据量 利用CPU效率更高 查询、更新、插入的效率 更高 加索引查询 更快 加索引更新 慢1/2 慢1/30 内存、空间 占用更大 存储文件 frm是表定义文件,myd是数据文件,myi是索引文件 frm是表定义文件,ibd是数据文件 测试: 测试环境:win10 MySQL8.0 php7.4 CPU 占用差不多、内存占用不高、机械硬盘写入跑满 CREATE TABLE `test_log` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `created_at` INT(10) NULL, PRIMARY KEY (`id`)); |引擎类型|MyISAM|InnoDB|性能相差 -------|-----|----|----|---| 循环插入1万记录|324.83178091049【√】|712.46580886841|1倍 事务插入1万记录|313.85579895973|1.7757570743561【√】|300倍 主键更新100次|2.9796991348267【√】|7.0310151576996|1倍 非主键更新100次|8.5254480838776|9.1009860038757|差不多 查询所有count100次|0.016912937164307【√】|0.25129389762878|20倍 where查询count100次|0.017408847808838|0.021618127822876|差不多 查询单条主键100次|0.018386125564575|0.018260955810547|差不多 查询单条非主键100次|0.27979707717896【√】|0.45667195320129|1倍 like查询100次|0.31684398651123【√】|0.49412107467651|0.5倍 删除单条主键100次|3.5270810127258【√】|7.0672898292542|1倍 删除单条非主键100次|4.6953358650208【√】|11.668270111084|2倍 删除所有|0.23814415931702【√】|0.81685304641724|3倍 use Zodream\Database\DB; use Zodream\Debugger\Domain\Timer; DB::getEngine(); $timer = new Timer(); // 循环插入 10 万 记录 for ($i = 0; $i < 10000; $i ++) { DB::insert(sprintf('INSERT INTO `test_log` (`name`, `created_at`) VALUES (\'test%s\', \'%s\');', $i, time())); } $timer->record('insert 10000'); // 事务插入 10 万 记录 DB::transaction(function (\Zodream\Database\Engine\Pdo $pdo) { for ($i = 0; $i < 10000; $i ++) { $pdo->getDriver()->exec(sprintf('INSERT INTO `test_log` (`name`, `created_at`) VALUES (\'test%s\', \'%s\');', $i, time())); } }); $timer->record('insert trans 10000'); $data = DB::select('SHOW TABLE STATUS where Name=\'test_log\';'); $size = $data[0]['Data_length'] + $data[0]['Index_length']; $timer->record('free '. $size); echo $size; // 更新 主键 for ($i = 0; $i < 100; $i ++) { DB::update(sprintf('UPDATE `test_log` SET `name`=\'test_ttt_%s\' WHERE `id`=\'%s\';', $i, $i * 50 + 5)); } $timer->record('update'); // 更新 非主键 for ($i = 0; $i < 100; $i ++) { DB::update(sprintf('UPDATE `test_log` SET `created_at`=\'%s\' WHERE `name`=\'test%s\';', $i, $i * 30 + 4)); } $timer->record('update name'); // 查询 所有 count for ($i = 0; $i < 100; $i ++) { DB::select('SELECT count(*) as count FROM test_log;'); } $timer->record('select count'); // 查询 where count for ($i = 0; $i < 100; $i ++) { DB::select(sprintf('SELECT count(*) as count FROM test_log WHERE `id`=\'%s\';', $i * 40 + 3)); } $timer->record('select count where'); // 查询 单条主键 for ($i = 0; $i < 100; $i ++) { DB::select(sprintf('SELECT * FROM test_log WHERE `id`=\'%s\';', $i * 40 + 3)); } $timer->record('select one'); // 查询 单条非主键 for ($i = 0; $i < 100; $i ++) { DB::select(sprintf('SELECT * FROM test_log WHERE `name`=\'test%s\';', $i * 40 + 3)); } $timer->record('select one name'); // 查询 like for ($i = 0; $i < 100; $i ++) { DB::select(sprintf('SELECT * FROM test_log WHERE `name` like \'%%%s%%\';', $i * 40 + 3)); } $timer->record('select like'); // 删除 单条主键 for ($i = 0; $i < 100; $i ++) { DB::delete(sprintf('DELETE FROM `test_log` WHERE `id`=\'%s\';', $i * 60 + 3)); } $timer->record('delete'); // 删除 单条非主键 for ($i = 0; $i < 100; $i ++) { DB::delete(sprintf('DELETE FROM `test_log` WHERE `name`=\'test%s\';', $i * 60 + 3)); } $timer->record('delete name'); // 删除所有 DB::delete('DELETE FROM `test_log` WHERE 1'); $timer->record('delete all'); $timer->end(); $timer->log(); 实际选择 思考方向: 数据库是否有外键:innodb 支持 是否需要事务支持:innodb 支持 用什么样的查询模式:如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB 数据有多大 MyISAM恢复起来更困难 日志【MyISAM】 只插入,不修改 小型的应用或项目【MyISAM】 实在不会选,那么直接按项目的大小来选择 参考 MyISAM与InnoDB两者之间区别与选择,详细总结,性能对比 MySQL 8.0 Reference Manual: The MyISAM Storage Engine MySQL 8.0 Reference Manual: Chapter 15 The InnoDB Storage Engine MyISAM与InnoDB 的区别(9个不同点)

2019/12/22
articleCard.readMore

centos + nginx + php + vsftp 实现不同路径

centos + nginx + php + vsftp 实现不同路径 nginx 配置 具体配置请参考 【nginx 子目录匹配不同地方的文件夹】 必须提升权限 nginx.conf user root; 这样访问 vsftp 中的文件 html 或 js 是正常的 但访问 php 文件就会报 File not found 404 错误 开启 nginx 的 error_log 错误日志 会有一条这样的日志 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream. php-fpm 配置 此时应该提升 php-fpm 的权限 php-fpm.conf user = root group = root 但是启动 php-fpm 启动不了 please specify user and group other than root 不允许使用 root 权限,但她有一个启动参数 -R 允许使用 root /etc/init.d/php-fpm start -R 这样还是不行 找到 /etc/init.d/php-fpm 找到 start() 方法, 加上 -R 就行了 daemon --pidfile ${pidfile} /usr/local/php/sbin/php-fpm -R --daemonize 重启 /etc/init.d/php-fpm restart 再次访问php文件,正常了

2019/12/9
articleCard.readMore

php soap 访问.net Web 服务

php soap 访问.net Web 服务 一开始使用soap 连接是报 Couldn't load from 'xxxx' : Premature end of data in tag html line 发现是参数设的不对,SoapClient第一个参数使用的是正式请求网址,SoapHeader 第一个参数设的namespace,不能使用默认的 http://tempuri.org/, 不然 header 和 body 的namespace 相同 ,header 就不会生成加入到xml请求内容中。 __soapCall 的第二个参数传递的是方法的所有参数,类似于 ...args, 所以必须用数组包起来 [参数1, 参数2, ...] $client = new SoapClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]); $header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [ 'User' => 'zodream', 'Password' => 'zodream' ]); $res = $client->__soapCall($path, [$data], null, $header); 但是老是报错 Could not connect to host 按照搜到的方法改了 php.ini soap.wsdl_cache_enabled=0 soap.wsdl_cache_ttl=0 soap.wsdl_cache_limit = 0 发现还不行,再次修改代码 $client = new SoapClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]); $client->soap_defencoding = 'utf-8'; $client->decode_utf8 = false; $client->xml_encoding = 'utf-8'; $header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [ 'User' => 'zodream', 'Password' => 'zodream' ]); $res = $client->__soapCall($path, [$data], null, $header); 还是不行,有尝试过使用 fsockopen 发现连接直接超时 然后用 postman 测试发现需要加上 Content-Type: text/xml;charset=utf-8 服务端才能接收,否则响应 服务器无法为请求提供服务,因为不支持该媒体类型。, 最后没办法只能修改请求方法 $client = new ZoClient('http://zodream.cn/PosWebService.asmx?WSDL', ['trace' => 1, 'exception' => 0]); $client->soap_defencoding = 'utf-8'; $client->decode_utf8 = false; $client->xml_encoding = 'utf-8'; $header = new SoapHeader('http://microsoft.com/webservices/', 'SoapHeader', [ 'User' => 'zodream', 'Password' => 'zodream' ]); $res = $client->__soapCall($path, [$data], null, $header); class ZoClient extends SoapClient { public function __doRequest($request, $location, $action, $version, $one_way = 0) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $location); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml;charset=utf-8', 'SoapAction: '.$action ]); curl_setopt($curl, CURLOPT_POSTFIELDS, $request); $response = curl_exec($curl); curl_close($curl); return $response; } } 虽然可以直接使用curl自己写,但拼接soap的xml有点麻烦,转化响应也麻烦,直接使用soap插件就不麻烦了。

2019/12/5
articleCard.readMore

可视化编辑之组件思路

先进行首页组件化测试 页面增加静态化设置,是否静态化,允许设置更新时间,对缓存页面设置过期时间,这样就不需要访问数据库查询是否过期。 组件内容更新,通过事件通知,更新时通过组件反推相关的页面,进行页面更新。

2019/12/1
articleCard.readMore

vue 返回上一页回到原先滚动位置

vue 返回上一页回到原先滚动位置 在一些页面,比如滚动加载的页面默认无法后退保存之前的滚动位置。但需要这个功能该怎么做? App.vue <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view> router 在路由中需加上 keepAlive: true { name: 'search', path: '/search', component: Search, meta: { keepAlive: true, // 需要缓存 }, }, 页面上 在需要的页面 .vue 里加上事件,mounted 事件只在新进入时触发,或离开页面后缓存页面时触发(这时是获取不到任何页面元素的) data() { return { scrollTop: 0, }; }, beforeRouteLeave (to, from, next) { // this.scrollTop = document.querySelector('.scroll-box').scrollTop; // div内部滚动的 this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop; next(); }, beforeRouteEnter(to, from, next) { next(vm => { // document.querySelector('.scroll-box').scrollTop = vm.scrollTop;// div内部滚动的 document.body.scrollTop = vm.scrollTop; }); }, 注意:以上事件只能放到页面上面,不能放到页面上的部件里 参考 【vue返回上一页面时回到原先滚动的位置】

2019/11/16
articleCard.readMore

nginx 子目录匹配不同地方的文件夹

nginx 子目录匹配不同地方的文件夹 要求 / 对应 /data/www /shop 对应 /home/www/shop /task 对应 /home/task1 /shop/h5 对应 /data/www/shop/h5 /bbs/index.php 对应 /data/bbs/index.php 第一个 server { listen 80 default; server_name zodream.cn; rewrite ^(.*)$ https://${server_name}$1 permanent; # 强制使用https 访问 } server { listen 443 ssl; server_name zodream.cn; index index.html index.htm index.php; root /data/www; ssl_certificate /data/ssl/zodream.cn.pem; ssl_certificate_key /data/ssl/zodream.cn.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ALL:!DH:!EXPORT:!RC4:+HIGH:+MEDIUM:!LOW:!aNULL:!eNULL; location / { } location ~ ^/shop/h5/.+\.php { root /data/www; include php_fcgi.conf; } location ~ ^/shop/h5.* { root /data/www; } location /shop { root /home/www; # 会访问 /home/www/shop 文件下的所有html文件,不能访问其他类型文件 } location ~ ^/shop { root /home/www; # 会访问 /home/www/shop 文件下的所有类型的文件 } location ~ ^/task.* { root /home/task1; rewrite ^/task(.*)$ /$1 break; # 这种方法跟上一种方法效果一样但不需要保持文件名一致 } location ~ ^/bbs/.*\.php.* { # 此方法存在一个问题即默认的 /bbs php程序提示找不到文件 root /data/bbs/; include php_fcgi.conf; set $real_script_name $fastcgi_script_name; if ($fastcgi_script_name ~ "^/bbs/(.+?\.php)(.*)$") { set $real_script_name $1; set $path_info $2; } fastcgi_param SCRIPT_FILENAME /data/bbs/$real_script_name; fastcgi_param SCRIPT_NAME $real_script_name; fastcgi_param PATH_INFO $path_info; } } php_fcgi.conf fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; # PHP only, required if PHP was built with --enable-force-cgi-redirect fastcgi_param REDIRECT_STATUS 200;

2019/11/2
articleCard.readMore

win10 查看已保存的wifi 密码

win10 查看已保存的wifi 密码 右键左下角 徽标 ,点击 Windows PowerShell(管理员) 查看已保存的所有wifi netsh wlan show profile 基本显示 所有用户配置文件 : zodream 后面的就是wifi 名(zodream 即为wifi名替换下面) netsh wlan export profile name="zodream" folder=. key=clear 提示 接口配置文件“zodream”已成功保存在文件“.\WLAN-zodream.xml”中。 输入 cat WLAN-zodream.xml 找到 passPhrase 往下两行就是 <keyMaterial>密码</keyMaterial> 这就是密码了

2019/10/26
articleCard.readMore

第三方支付对接

第三方支付对接 支付 显示支付按钮 直接显示隐藏的表单,点击按钮就提交跳转到支付页面 只显示一个按钮,点击触发 【调起支付】 调起支付 参数: 订单id 支付方式 币种? 服务端生成表单html或调起参数或跳转链接 支付通知 处理通知,完成支付 获取订单信息 验证金额 处理订单并保存第三方支付单号(退款可能用到) 退款 点击退款 退回余额,实时处理 通过订单号生成退款请求,退款的链接或http请求结果,此时退款不能判断是否处理成功 退款通知 获取订单信息 验证金额 处理订单保存第三方退款单号 订单支付状态 未支付 支付中 支付完成 订单退款状态 无 退款中 退款完成 支付流水号 最好不要直接使用订单号去支付,因为如果订单金额发生变动或支付金额受实时汇率影响的话,会导致第三方支付不成功

2019/10/16
articleCard.readMore

net core 正确自定义 TagHelper

net core 正确自定义 TagHelper 自定义一个 友情链接 的tag 创建一个数据源 namespace NetDream.Models { public class FriendLinkModel { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public static List<FriendLinkModel> All() { var data = new List<FriendLinkModel>(); data.Add(new FriendLinkModel() { Name = "ZoDream", Url = "https://zodream.cn", }); return data; } } } 定义 TagHelper namespace NetDream.Base.TagHelpers { public class FriendLinkTagHelper : TagHelper { public string Title = "友情链接"; public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "div"; output.Attributes.Add("class", "friend-link"); var html = new StringBuilder(); html.AppendFormat("<div>{0}</div><div>", Title); var items = FriendLinkModel.All(); foreach (var item in items) { html.AppendFormat("<a href=\"{0}\" target=\"_blank\" rel=\"noopener noreferrer\">{1}</a>", item.Url, item.Name); } html.Append("</div>"); output.Content.SetHtmlContent(html.ToString()); } } } 使用 先在 _ViewImports.cshtml 添加以下代码 @addTagHelper *, netdream 注意 netdream 是本项目的程序集名称,并不是命名空间 没看源码,大致猜测这个是动态引用的即使用时临时检索 程序的dll 引入tagHelper 程序集名称查看方法: 右键项目-> 属性 -> 应用程序 -> 程序集名称(N): 然后在 Home.cshtml 使用 <friend-link title="友情链接"></friend-link> 最终就会输出友情链接了 源码

2019/10/15
articleCard.readMore

【Vue Shop】代码优化

代码优化 变量写法统一 空格控制 类型转化规范 由于使用了 typescript 编写,所以引入了 TSLint 作为代码规范检查工具,在编写过程中并没有太注重代码的规范,导致每次编译的代码提示都刷刷的好几页。 在基本功能完成后进行代码优化工作。 类型转化提示,例如 parseInt 第二个参数必填 10 表示转化为十进制。页面获取传值 this.$route.query 或 this.$route.params 返回多个类型的值,如果需要string 则使用 as string 做类型转化声明,实际在js 代码中不做处理。如果需要 number 则需要使用 parseInt(this.$route.query.a as string, 10) 进行转化 变量统一使用驼峰写法,默认的 ts 规范也是这么规范的,当然也可以改,反正我是习惯用驼峰写法 字符串和引入文件路径使用单引号,当然这也可以改,vscode 默认生成的引用可以在设置里面设置或在 tsconfig.json 中配置 清除空白行中的空格 补全 , 和 ; 匿名函数如果只有一个参数可以不用括号和声明类型,多个参数必须括号和声明类型 一行文字不超过120个,超过截断换行 同意代码风格,页面都使用类的写法 类的所有成员包括属性,方法都加上 public 、private 修饰符 变量声明,尽量使用 const

2019/10/11
articleCard.readMore

【Vue Shop】个人信息修改、实名制、账户注销

个人信息修改、实名制、账户注销 个人信息修改 头像修改,可以增加图片的裁剪 昵称修改,通过底部弹出框修改 生日修改,调用日期选择组件 实名制 此功能主要是用于用户身份证图片上传,审核是后台人工进行审核 账户注销 此功能分为三步: 进入时弹出请用户确认正在操作注销流程 选择注销的理由 提交后台审核,审核通过删除相关订单地址账户记录

2019/10/11
articleCard.readMore

【Vue Shop】订单评价

订单评价 功能介绍 当订单签收后可以进行评价,订单商品为单个评价,当订单中商品都评价后,订单状态更改为已完成

2019/10/11
articleCard.readMore

【Vue Shop】退换货

退换货 功能介绍 退款,已支付未发货订单可以执行退款流程 退货,已发货、已签收、已完成 维修,订单完成之后申请售后 换货, 价格保护,

2019/10/10
articleCard.readMore

【Vue Shop】发票

发票 发票申请有两种方式: 购物车结算时填写发票信息 订单完成后集中申请开票 发票类型: 普票 增票 发票抬头 个人:只能开普票 企业: 需填写税号等 发票方式 纸质发票:需选择收货地址 电子发票: 需填邮箱

2019/10/10
articleCard.readMore

【Vue Shop】优惠券

优惠券 可领取优惠券 显示领取数量 显示是否已领取 我的优惠券 拆分不同状态,显示不同样式 显示优惠券图片及信息

2019/10/10
articleCard.readMore

【Vue Shop】订单及账户开发

订单及账户开发 订单 分状态查询 订单详情 物流信息查询 账户 显示账户余额 账户明细 添加银行卡

2019/10/10
articleCard.readMore

【Vue Shop】支付开发

支付开发 支付方式更改 支付跳转 支付成功失败

2019/10/10
articleCard.readMore

【Vue Shop】收货地址及结算流程

收货地址及结算流程 收货地址 列表的状态:可以选择、不可选择 选择或保存回调地址 地区选择组件的使用 结算 收货地址 分组商品及配送方式 支付方式 优惠券 发票 备注

2019/10/10
articleCard.readMore

【Vue Shop】个人中心页

个人中心页 结构 顶部退出 头像名称 菜单 订单统计 账户统计 帮助 本页面相对简单,一个注意登录与未登录用户头像的显示 特别功能 可以弄一个页面滑动顶部保留头像的功能

2019/10/10
articleCard.readMore

【Vue Shop】购物车加入及显示

加入购物车弹出框的显示 首页及商品列表 商品详情 购物车中的商品 商品分组 优惠 数量加减 商品删除 商品选择 选中商品传递到结算页

2019/10/2
articleCard.readMore

【Vue Shop】商品详情页面开发

商品详情页面开发 页面结构 头部导航 轮播图 商品信息 收藏 商品活动 最新评论 推荐商品 详情 规格 售后保障 底部购买按钮 特别要求 页面滚动时有相应的头部导航切换

2019/10/2
articleCard.readMore

【Vue Shop】登录注册开发及第三方登陆

登录注册开发及第三方登陆 手机号验证码登录 基本需求: 一个输入手机号,必须验证码手机号的正确性 输入验证码,无特殊要求,只是必填 点击发送验证码,发送时有时间限制,显示倒计时,倒计时期间无法点击 手机号密码登录 使用手机号和密码登录, 邮箱登录 邮箱的话就只能密码登录,当然也可以邮箱加邮件验证码登录,不过让用户使用比短信验证码麻烦,不推荐使用 邮箱注册 邮箱注册,填邮箱、用户名、密码等基本信息就行了,可以价格邮箱验证功能,发送验证链接邮件,点击链接就验证成功 手机号注册 手机号注册,就手机号、用户名、密码、短信验证码 邮箱密码找回 第一步填写邮箱,判断是否注册,然后发送找回链接到邮箱中,那这样就跳转到pc 了,没意义了,改为邮件验证码 第三方登陆 大致流程: 点击跳转到服务端 继续重定向到第三方 第三方返回到服务端 服务端获取信息保存登录信息到 cookie 跳转到前端,前端在router 监听事件中获取cookie中的信息

2019/9/30
articleCard.readMore

【Vue Shop】搜索开发

搜索开发 搜索页面可以分两个状态 准备搜索 当搜索条件为空进入显示此页面: 搜索框:包括搜索关键词提示,回车搜索 搜索历史记录 热门搜索关键词 搜索结果 搜索框显示本次搜索内容 搜索筛选 商品列表 涉及接口: 热门关键词接口 关键词提示接口 商品搜索接口:返回商品数据、分页数据、筛选项 技术相关: 搜索历史记录的保存与读取及删除 搜索状态的切换,例如:点搜索结果进入搜索自动填入搜索词,取消显示回原有搜索词 使用上拉刷新下拉加载更多

2019/9/30
articleCard.readMore

【Vue Shop】首页及分类页开发

首页及分类页开发 首页 本次只做一个简单的首页,共分为6部分: 头部搜索框:不是真正的可输入的搜索框,作用是点击跳转到搜索功能页面 轮播图:这里使用的是 mint-ui 中的轮播组件 一级分类菜单栏 新品 热门 精品 涉及到的 API 接口: 商品统计接口:显示在搜索框中,显示商城有多少个商品正在出售 轮播广告图接口 分类接口 商品首页推荐接口:包含新品、热门、精品的商品 技术相关 商品统计在其他页面也需要,可以使用 store 进行缓存 三个推荐商品部分样式一致,可以提取成为组件,传递标题、商品数据即可 头部有一个登录状态的判断,未登录显示登录入口,登录显示消息入口 公共底部导航栏组件 分类页 本页面分三部分: 头部搜索栏,同样是起点击跳转作用,样式与首页有区别,不需要做成组件 左侧一级分类 右侧为左侧选择的分类详情 右侧分为 当前分类的banner 当前分类的名称 当前分类的商品推荐 当前分类的所有子孙分类 涉及到的接口: 分类接口 商品统计接口 分类详情接口:因为本接口允许通过拓展参数设置获取推荐商品及子分类,不需要单独接口获取推荐商品和子分类了 技术相关 左边分类切换,可以考虑缓存当前选中 可以缓存分类详情, 公共底部导航栏组件

2019/9/30
articleCard.readMore

【Vue Shop】客户端身份验证及注入API请求

客户端身份验证及注入API请求 npm i ts-md5 服务端提供 appid secret 公共请求参数 参数名 字段类型 说明 appid string timestamp string 当前时间例如:2019-05-01 01:01:01 sign string 签名 登录的 token 使用headers Authorization: Bearer token 签名方式 md5 签名 Md5.hashStr(appid + timestamp + secret) 注入 axios.interceptors.request 响应过滤 axios.interceptors.response 所有错误提示响应状态码都不是200 响应状态码401 时表示登录过期,需重新登录

2019/9/29
articleCard.readMore

【Vue Shop】API请求构建

API请求构建 本次使用 axios 作为网络请求底层 npm i axios 请求头 post 数据为json格式 接受json格式的数据 Content-Type: application/vnd.api+json Accept: application/json GET 请求 export function fetch<T>(url: string, params = {}): Promise<T> { return new Promise((resolve, reject) => { axios.get(url, { params, }).then((response) => { resolve(response.data) }).catch((err) => { reject(err) }) }) } POST 请求 export function post<T>(url: string, data = {}): Promise<T> { return new Promise((resolve, reject) => { axios.post(url, data) .then((response) => { resolve(response.data) }, (err) => { reject(err) }) }) } DELETE 请求 export function deleteRequest<T>(url: string): Promise<T> { return new Promise<T>((resolve, reject) => { axios.delete(url) .then((response) => { resolve(response.data) }, (err) => { reject(err) }) }) }

2019/9/29
articleCard.readMore

【Vue Shop】页面部件确定及开发

页面部件确定及开发 底部导航 主要功能,根据页面自动判断选中一项 设计思路 定义菜单项结构 interface IMenu { name: string, // 显示的文字 icon: string, // 显示的图标 url: string // 链接 } 定义可接受参数 menus: IMenu[] 通过 $route.name 判断当前的路由,选中菜单 下拉刷新上拉加载 大致分为六个状态 无 下拉过程中 显示下拉刷新 到达下拉距离限制 显示释放刷新 向上脱离下拉 显示停止刷新 直接释放 显示刷新中 底部 显示加载更多 触底 显示加载中 加载完成 显示加载完成 1s 后自动恢复无状态 左滑删除 大致思路 一般的分为左滑和右滑 滑动的距离不能大于可显示的宽度,例如 左滑删除,当删除按钮完全显示后不再继续向左滑 左滑到底是可以继续回撤的(即右滑),如果支持左右滑,那个应该已开始滑的方向为准,不能左滑开始,以实际右滑结束 收起状态, 滑动距离大于1/3可以动画补全但是完成滑动,小于1/3需动画补全恢复原始状态 点击事件,需恢复原始无滑动状态 排异模式,一个开始滑动,应该把其他的恢复到无滑动状态,动画过渡 时间选择 首先确定输入和输出,输入字符串就要输出字符串,输入Date 就要输出Date。 既然要输出字符串,那么就要支持格式化,就要有输入格式化定义 format 一个日期,应该是 年月日时分秒, 那个 年月日 是必须的,时分秒可选 通过 format 判断就行了 还需要有一个选择范围 min max 多语言?暂不考虑 还需要一个触发显示框,不需要外部代码手动触发, <div @click="showCalendar" class="datepicker__input-container"> <slot></slot> </div> 这样就好了,使用方法 <DatePicker v-model="user.birthday" format="yyyy-mm-dd"> <div class="line-item"> <span>生日</span> <span>{{user.birthday}}</span> <i class="fa fa-chevron-right"></i> </div> </DatePicker> import DatePicker from '@/components/DatePicker.vue'; @Component({ components: { DatePicker, }, }) 地区选择(多级滑动选择) 为什么不用多个select 联动? 为了同意样式,全部仿照 ios 底部弹出滑动选择 为了方便,多个 select 占地方而且获取不方便,因为地区是不确定几级的,而且我只要最后的选择id 不确定几级,那么就用一个 v-for 就行了 要通过上下滑动进行选择,加上滑动事件 @touchstart='touchStart' @touchmove='touchMove'

2019/9/29
articleCard.readMore

【Vue Shop】基本架构

基本架构 项目结构 src api 定义实现api接口 model.ts 定义接口数据结构 assets 样式及图片资源 components 公共基本部件 pages 页面 Home Child 本页面用的部件 pipes 自定义过滤器 router 注册的页面路由 store 保存的状态及跨页面传递的数据 utils 辅助方法,http 请求 代码约定 页面及部件文件夹名使用首字母大写的驼峰写法 函数方法名使用驼峰写法 页面及部件全部使用class 类的写法,属性使用驼峰命名 语句结束必须使用; 代码一行不超过120 个字符 对象内部必须使用,结束 html 标签class 类名使用 小写加 - css 尽量不使用 !important scss 嵌套层数不超过5层 具体typescript 规范请查看 tslint.json 网络请求 先定义相应数据模型,基本的模型 // 分页数据 export interface IPaging { limit: number; offset: number; total: number; more: boolean; } // 页面数据 export interface IPage<T> { paging: IPaging; data: T[]; } // 全局相应 export interface IBaseResponse { appid?: string; sign?: string; sign_type?: string; timestamp?: string; encrypt?: string; encrypt_type?: string; } // 失败响应 export interface IErrorResponse { code: number; message?: string; errors?: any; description?: string; } export interface IData<T> extends IBaseResponse { data?: T[]; } export interface IDataOne<T> extends IBaseResponse { data?: T; } 接着封装一个http 请求,包括注入appid 及sign,设置请求头,处理一些全局响应包括token 过期 暴露几个常用的请求方式 get post delete 最后返回一个 Promise<T> 具体页面 [√] 首页 [√] 分类页 [√] 搜索页 [√] 商品详情页 [√] 商品评论显示页 [√] 购物车页 [√] 结算页 [√] 支付页 [√] 个人中心页 [√] 浏览记录页 [√] 个人账户页(包含提现、充值弹窗) [√] 个人账户记录页 [√] 银行卡页 [√] 银行卡绑定页 [√] 发票页 [√] 发票申请页 [√] 发票抬头页 [√] 发票抬头编辑页 [√] 发票记录页 [√] 登录注册页 [√] 订单列表页 [√] 订单详情页 [√] 商品收藏页 [√] 消息页 [√] 账号关联页 [√] 个人信息页 [√] 账户注销页 [√] 登陆设备管理页 [√] 修改密码页 [√] 实名认证页 [√] 收货地址页 [√] 收货地址编辑页 [√] 评论商品页 [√] 发表评论页 [√] 退换货页 [√] 退换货申请页 [√] 文章列表页 [√] 文章分类页 [√] 文章详情页 [√] 推荐页 [√] 推荐规则页 [√] 推荐订单页 [√] 推荐会员页 [√] 推荐二维码页 [√] 优惠券领取页 [√] 我的优惠券页 [√] 签到页

2019/9/28
articleCard.readMore

【Vue Shop】环境准备

环境准备 项目语言选择 Typescript 本人最为熟悉,特别喜欢她的强类型,加入 vscode 这个开发工具的智能提示,简直不要太方便。 scss 喜欢她的嵌套 开发工具 node.js vscode 插件 Paste JSON as Code 根据 json 生成 ts 代码 Vetur vue 的语言服务 Vue VSCode Snippets vue 代码块 安装环境 安装全局vue-cli脚手架 npm install --global @vue/cli 创建项目 vue create my-project-name 选择 Manually select features 选中(上下键+空格键) Babel TypeScript Router Vuex Linter / Formatter 输入三次 y 直接默认回车即可 安装 sass 支持 npm install -D sass-loader node-sass 运行 npm run serve 浏览器打开 http://localhost:8080

2019/9/28
articleCard.readMore

手机版商城客户端开发大纲

手机版商城客户端开发大纲 本项目大致分为 vue、小程序、UWP 版、Flutter版,其他版本暂无时间开发,如果有好的 kotlin、swift 框架推荐请留言。 目前已基本完成 vue 及小程序版,正在开发UWP版及Flutter版,本教程分四部分预计完成时间为三个月,特别纠结,特别时网络请求这一块,必须设计好才能继续下去。 环境准备 【Vue】 【小程序】 【UWP】【Flutter】 基本架构 【Vue】 【小程序】 【UWP】【Flutter】 页面部件确定及开发 【Vue】 【小程序】 【UWP】【Flutter】 API请求构建 【Vue】 【小程序】 【UWP】【Flutter】 客户端身份验证及注入API请求 【Vue】 【小程序】 【UWP】【Flutter】 首页及分类页开发 【Vue】 【小程序】 【UWP】【Flutter】 搜索开发 【Vue】 【小程序】 【UWP】【Flutter】 登录注册开发及第三方登陆 【Vue】 【小程序】 【UWP】【Flutter】 商品详情页面开发 【Vue】 【小程序】 【UWP】【Flutter】 购物车加入及显示 【Vue】 【小程序】 【UWP】【Flutter】 个人中心页 【Vue】 【小程序】 【UWP】【Flutter】 收货地址及结算流程 【Vue】 【小程序】 【UWP】【Flutter】 支付开发 【Vue】 【小程序】 【UWP】【Flutter】 订单及账户开发 【Vue】 【小程序】 【UWP】【Flutter】 优惠券 【Vue】 【小程序】 【UWP】【Flutter】 发票 【Vue】 【小程序】 【UWP】【Flutter】 退换货 【Vue】 【小程序】 【UWP】【Flutter】 订单评价 【Vue】 【小程序】 【UWP】【Flutter】 个人信息修改、实名制、账户注销 【Vue】 【小程序】 【UWP】【Flutter】 代码优化 【Vue】 【小程序】 【UWP】【Flutter】 代码 Vue 小程序 UWP Flutter 本项目初衷 本人主要从事于电商开发及电商系统二次开发,本项目为工作经验的总结前端篇。

2019/9/28
articleCard.readMore

AB 压力测试

今天使用`google search console` 查看网站收录情况发现有很多链接处于 `发现未收录`状态,才想起用工具进行并发测试。 AB (ApacheBench) 是 Apache 自带的一款功能强大的测试工具,可以快速测试基于 HTTP 协议所有 Web 页面的最大负载压力。 下载地址www.apachelounge.com Apache 2.4.39 Win64 本文以 windows 10 为例,使用 PowerShell 执行命令行 加压下载文件到指定文件夹 打开 PowerShell ,通过 cd 进入 bin 目录 cd D:\zodream\Apache24\bin 主要用到 ab.exe 这个程序 .\ab -n 总共发起几次请求 -c 并发数 -t 总共执行时间,到时结束所有请求, -s 最大超时时间,默认30秒 -b TCP 请求发送和接受的字节数 -p POST发送文件 -u PUT发送文件 -T 设置请求头内容类型 -k keep-alive保持连接 最后输入完整网址 带http/https 例如 .\ab -t 10 -c 100 http://zodream.cn/ This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking zodream.cn (be patient) Finished 18 requests # 完成 18个请求 Server Software: Apache Server Hostname: zodream.cn Server Port: 80 Document Path: / Document Length: 24125 bytes # 请求页面大小 Concurrency Level: 100 # 并发请求数量 Time taken for tests: 10.226 seconds # 整个测试耗时 Complete requests: 18 # 完成的请求数 Failed requests: 0 # 失败的请求 Total transferred: 1146475 bytes # 网络传输总量 HTML transferred: 1131655 bytes # HTML 内容传输量 Requests per second: 1.76 [#/sec] (mean) # 每秒的请求平均数 Time per request: 56812.761 [ms] (mean) # 每个请求的平均时间 Time per request: 568.128 [ms] (mean, across all concurrent requests) # 服务器处理请求的平均时间 Transfer rate: 109.48 [Kbytes/sec] received # 网络平均转移率 Connection Times (ms) min mean[+/-sd] median max Connect: 11 14 3.8 13 23 Processing: 5051 6250 1257.6 5386 8892 Waiting: 47 1733 922.1 1300 3888 Total: 5063 6264 1256.8 5398 8906 Percentage of the requests served within a certain time (ms) 50% 5398 66% 6787 75% 7830 80% 7840 90% 7854 95% 8906 98% 8906 99% 8906 100% 8906 (longest request) 发现问题 .\ab -n 100 -c 100 http://zodream.cn/blog.html 使用此命令,发现出现 ...apr_pollset_poll: The timeout specified has expired (70007) 这样的错误,大致意思是请求超时了,可以增加 -k 保持连接 .\ab -n 100 -c 100 -k http://zodream.cn/blog.html

2019/7/31
articleCard.readMore

npm 包开发

准备 nodejs vscode 流程 注册npm账号 新建文件夹 test 在vscode 中打开 ctrl + ` 打开 cmd 模式下登录 npm login 输入账号密码 输入 npm init 输入项目名及说明生成 package.json 文件 npm i @types/node gulp gulp-typescript typescript --save-dev 新建文件夹 src src 下添加 index.ts 根目录添加 gulpfile.js 输入内容 var gulp = require('gulp'), ts = require("gulp-typescript"), tsProject = ts.createProject('tsconfig.json'); gulp.task('default', async() => { await gulp.src('src/**/*.ts') .pipe(tsProject()) .pipe(gulp.dest('dist/')); }); 根目录添加 tsconfig.json { "compilerOptions": { "baseUrl": "./", "declaration": true, "typeRoots": [ "./node_modules/@types" ] }, "include": [ "src/**/*.ts" ], "exclude": [ "node_modules", "dist", "**/*.spec.ts" ] } 修改 package.json "main": "dist/index.js", 将入口指向编译生成的 index.js 文件 添加 "files": [ "dist" ], 只需要 dist 文件夹下的文件就行了,其他源码就不用发布了 src/index.ts function test() { console.log('test'); } export default test; module.exports = test; 执行 gulp 命令 在 dist 下会自动生成 index.js 和 index.d.ts 两文件 npm publish 发布成功 在其他项目 npm i test 安装此包 使用 import test from 'test'; 或 var test = require('test); 添加 bin 添加 bin 文件夹 新建文件 cli.js(文件名随意) #!/usr/bin/env node console.log(1); 然后在 package.json 配置 "bin": { "command-name": "./bin/cli.js" //告诉package.json,我的bin叫 command-name ,它可执行的文件路径是bin/cli.js } bin 命令必须全局安装才能用 npm install . -g 或 npm link

2019/7/12
articleCard.readMore

gorm 使用

开启日志调试sql语句 var db *gorm.DB db.LogMode(true) 去除自带的软删除查询 当 struct 结构中带有 DeletedAt 属性,在查询的时候就会默认加上 deleted_at IS NULL,但是我的数据表 deleted_at 的默认值是 0,因此需要使用 , 去除默认查询加上自己的查询 db.Unscoped().Where("deleted_at=0") 默认查询的表会自动使用 结构名 的复数 可以定义 TableName 方法自定义表名 type User struct { ID uint } func (User) TableName() string { return "blog" } 关联查询 使用 Preload("User") 进行关联查询 type Blog struct { ID uint User User UserID int } var data []Blog db.Preload("User").Find(&data) 使用 Related 进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 FOREIGNKEY 去查找 User, 如果没定义, 则调用时需要指定 db.First(&data) db.Model(&data).Related(&data.User).Find(&data.User) 使用 Association 进行关联查询, 需要把 Blog 查询好, 然后根据 Blog 定义中指定的 AssociationForeignKey 去查找 User, 必须定义 db.First(&data) db.Model(&data).Association("User").Find(&data.User)

2019/6/20
articleCard.readMore

使用 Android Studio 制作 .9.png 图片

重要:四边黑线表示可以伸缩的区域 步骤 将项目的.png图片放到资源文件夹drawable下面,右键 .png 的图片 选择 Create 9-Patch file...,命名默认就行 双击 .9.png 的图片打开,在 9-Patch 窗口下,会出现两个窗口,左边为编辑窗口,右边为预览窗口(拉伸状态下的图片), Zoom 为编辑窗口中的放大比例(无法缩小),Patch scale 为预览窗口中图片放大比例,Show lock鼠标放到原图上,会显示红色斜线部分。表示点9图锁定的区域; Show content:是预览窗口蓝色部分,蓝色表示可以填充内容,白色便是不可填充内容,移动原图中右边和下边的修改可填充内容的区域,规则如上; Show patches:显示编辑窗口中可以缩放的区域,绿色和紫色部分,原图颜色为不伸缩部分;Show bad patches:显示原图中不规范的缩放区域。比如带弧度中部分是不应该缩放的,如下图中红线标记的区域。遇到下面情况,需要向右稍微移动上边的黑线,不标记弧度部分,红线就会取消。 从顶边拉出的线表示线以下的为缩放区域,从底边拉出的线表示线以上的为缩放区域,从左边拉出的线表示线以右的为缩放区域,从右边拉出的线表示线以左的为缩放区域; 在边的区域可以使用鼠标拖拽框选缩放区域 内容居中的做法是,两边的缩放区域等长 内容间距自适应,在间距之间都拉出缩放区域 拉伸四周最快速的方法, zoom 放大3倍,顶边框选两个区域,左边框选两个区域,移动线控制区域到指定位置就行了

2019/6/19
articleCard.readMore

Go 笔记(学习iris)

评论规范写法 // 方法名 说明 // Example 示例 func Example() { } iris 模板语法 公共模版 使用 yield 输出内容 {{ yield }} 传其他部位的内容 先在公共模版声明 此处使用 header 的内容 {{ part "header" }} 再在页面中定义 命名规则 相对于模板根目录的相对路径 -header 文件 blog/index.html {{ define "blog/index-header"}} <link type="text/css" href="/assets/css/blog.css" rel="stylesheet" media="all"> {{ end }} 根据路由名生成链接 homeRoute := app.Get("/", controllers.Index) homeRoute.Name = "home" 使用 {{ urlpath "home" }} 生成链接自动加上域名 rv := router.NewRoutePathReverser(app, router.WithHost("zodream.cn:443")) tmpl.AddFunc("url", rv.URL) 使用 {{ url "home" }}

2019/6/18
articleCard.readMore

项目从vue转微信小程序体验

基于项目Vue-Shop 到Mini-Shop的开发总结 页面修改 底部导航栏修改 图标全部使用图片,在app.json 中配置 头部标题栏修改 移除后退加标题样式,通过自带的标题加背景颜色的json 配置方式 轮播图修改 使用自带的swiper 进行修改 页面标签及事件修改 标签名 目标标签 img image a navigator i span strong font em b text 其他 view 属性名 目标属性 v-if wx:if="{{ }}" v-elseif wx:elif="{{ }}" v-else wx:else v-bind:src src href url @click bindtap v-on:click bindtap (click) bindtap @touchstart bindtouchstart @touchmove bindtouchmove @touchend bindtouchend :key v-show hidden="{{! }}" v-for wx:for="{{ }}" wx:for-index=" " wx:for-item="" v-model value="{{ }}" bind:input=" Changed" 第一个字符为@且值不为空 bind: 第一个字符为: ={{ }} 其他包含@ 样式修改 图标字体修改 将ttf的字体文件转化成base64 放入样式中 import * as fs from 'fs'; export function ttfToBase64(file: string): string { const content = fs.readFileSync(file); return 'url(\'data:font/truetype;charset=utf-8;base64,'+ content.toString('base64') +'\') format(\'truetype\')'; } 标签样式修改 将文件中的标签选择器进行相应转化 程序修改 页面方法修改 created() 转化为 onLoad $route 全部转化为 onLoad(query) 的参数 修改属性值,只能通过 setData 修改 接口调用修改 将 axios 改为 wx.request store 修改 将所有的store 全部放到 app.ts 中 globalData 组件修改 左滑删除 在vue中可以通过$refs 进行排异(只有一个左滑状态,其他自动恢复原状),小程序中可以通过新增自定义组件加入关联关系进行排异 下拉刷新上拉加载更多 启用自定义组件,使用自带的 enablePullDownRefresh: true 地区选择 使用自带的地区选择picker,微信的网络请求有数据大小限制,自定义组件实现无法一次传入所有省市区, 自带地区选择只能获取到省市区名称,因此需要后台接口进行修改 弹出选择 自定义实现监听属性变化 public observe(key: string, callback: (newVal: any, oldVal: any) => void) { let val = this.data[key]; Object.defineProperty(this.data, key, { configurable: true, enumerable: true, set: function(value) { // 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值 callback.call(this, value, val); // value是新值,val是旧值 val = value; }, get: function() { return val; } }) } 总结 总的来说,转化过程没有太大的技术难度,就是每个页面都得改,比较繁琐。

2019/6/3
articleCard.readMore

windows下设置Git 区分文件名大小写

问题 windows 环境 git 下修改文件名大小写无效,虽然不影响读取,但是在一些(比如小程序路径)就无法识别 解决方法 git config core.ignorecase false

2019/4/21
articleCard.readMore

字体反爬与反反爬

说明 一些公司会把一些关键数据进行保密,而网站进行保密(对用户显示,对爬虫隐藏)一种方式就是自定义的字体文件 主要用到的工具 python fonttools 反爬 主要原理 从一个已有的字体文件提取需要加密的文字字形,更改字形索引,保存为新的字体文件,使用新的文字索引使用 反反爬 主要原理 获取字体文件,根据索引获取字形,再根据字形获取真正的文字(前提必须有一个足够丰富的字形库) 晋级 反爬使用指定义的字形(手动修改字形) 反反爬使用类似图像识别进行字形识别 字体处理工具 理想化操作 generate --input font.ttf --font 1234567890 --out new 自动生成 节选的字体文件ttf wof svg 及css html使用例子

2019/4/16
articleCard.readMore

Gulp 插件开发

基本模板 'use strict'; var Transform = require('readable-stream/transform'); module.exports = function (options) { return new Transform({ objectMode: true, transform: function (file, enc, callback) { // TODO callback(null, file) } }); }; file 说明 file.path 完整路径 file.contents 内容 Buffer 或 Stream file.basename 文件名 file.txt file.extname 文件拓展名 .txt file.isNull() 是否为空 file.isBuffer() 是否为Buffer 可以使用 String(file.contents) 转化为字符串, file.contents = Buffer.from(''); 可以修改内容 file.isStream() 是否为Stream 更多请参考 【vinylv】 callback 使用 正常返回 callback(null, file); 返回报错(会中断后续所有文件任务) callback({stack: 'error file'}, file) 中断本次任务,继续操作其他文件 callback() 使用插件 var gulp = require('gulp'); var my = require('./my.js'); gulp.task('default', async() => { await gulp.src('src/**/*') .pipe(my()) .pipe(gulp.dest('dist/')); });

2019/4/6
articleCard.readMore

微信小程序自用框架开发

注意 【项目地址】GITHUB 本程序不做任何引入其他代码做为底层,仅支持原生代码 本框架基于 typescript gulp sass 开发,自动生成原生小程序代码 本框架优化 ts 智能提示,自动转换 html 为 wxml 自动编译 sass 为 wcss 本项目自带转化工具 转化核心 支持 ts sass 支持拆解html js ts sass css 写在一个文件上的情况 sass 引用模式未做处理 自动转化html 为 wxml, 自动转化 v-if v-for v-else v-show 支持json自动生成,支持 属性合并 更新 定义WxPage WxCommpent 两个类,增强 setData 的智能提示, export 是为了避免提示未使用,编译时会自动去除 增加自动添加 Page(new Index()) Commpent(new Index()) 到末尾 增加json配置生成 @WxJson({ usingComponents: { MenuLargeItem: "/components/MenuLargeItem/index", MenuItem: "/components/MenuItem/index" }, navigationBarTitleText: "个人中心", navigationBarBackgroundColor: "#05a6b1", navigationBarTextStyle: "white" }) 自动合并页面相关的json文件 支持自动合并 methods lifetimes pageLifetimes, 如果已有 属性会自动合并 methods @WxMethod lifetimes @WxLifeTime pageLifetimes @WxPageLifeTime 自定义部件自动合并方法到methods属性中 methods = { aa() { } } @WxMethod() tapChange(mode: number) { } 最终生成 methods = { tapChange(mode: number) { }, aa() { } } 标准模板 index.vue <template> <div> </div> </template> <script lang="ts"> import { IMyApp } from '../../app'; const app = getApp<IMyApp>(); interface IPageData { items: number[], } export class Index extends WxPage<IPageData> { public data: IPageData = { items: [] }; onLoad() { this.setData({ items: [] }); } } </script> <style lang="scss" scoped> </style> 最终会处理为3个文件 index.wxml <view></view> index.wxss index.js "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var app = getApp(); var Index = (function () { function Index() { this.data = { items: [], }; } Index.prototype.onLoad = function () { this.setData({ items: [] }); }; return Index; }()); Page(new Index());

2019/4/4
articleCard.readMore

兴业银行支付PHP-SDK 踩坑

需全局使用RSA签名 支付方法默认没有传证书路径及密码给签名方法 Signature 需添加传递 $this->epay_config['mrch_cert'], $this->epay_config['mrch_cert_pwd'] 验签需手动传递证书路径 返回的参数必须去除自定义参数,VerifyMac 方法需传递证书 $this->epay_config['isDevEnv'] ? $this->epay_config['epay_cert_test'] : $this->epay_config['epay_cert_prod'];

2019/4/4
articleCard.readMore

理解Redux

理解Redux Reducer (纯函数)拿到下一个State和之前的State来计算一个新的State。 初始化更改State中的值,并返回State ,接受两个参数 ,第一个为旧的state,第二个为action 根据type 更改state并返回 function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } 可以使用combineReducers合并 import { combineReducers } from 'redux' const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp Store Store 就是把它们联系到一起的对象 let store = createStore(todoApp) // 打印初始状态 console.log(store.getState()) // 每次 state 更新时,打印日志 // 注意 subscribe() 返回一个函数用来注销监听器 const unsubscribe = store.subscribe(() => console.log(store.getState()) ) // 发起一系列 action store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) // 停止监听 state 更新 unsubscribe(); Action 描述State的改变,必须是一个包含type键的对象 export interface Action { type: string; payload?: any; // 这个值可有可无 } 可以封装一个方法来生成action export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter } }

2019/4/1
articleCard.readMore

重新规划数据库读取

大致分为三层 第一层数据库实体(Entity) 文件夹位置 Domain\Entities 主要内容:包含数据库名,表名,主键、字段名、字段验证 保存最原始数据,直接读取的也是最原始数据 第二层数据实体(Model)继承数据库实体 文件夹位置 Domain\Models 主要内容:关联表、追加字段、隐藏显示字段 值转化, 第三层数据仓库(Repository)获取数据实体 文件夹位置 Domain\Repositories 主要内容:读取方法,更新实体 包含获取哪些值,返回数据实体 问题 这样文件变得更复杂、传出来的值都会固定、适应不同接口场景 思考 到底在哪一层操作关联数据库? 使用 <?php namespace Domain\Entities; /** * * @property integer $id * @property string $name * */ class User { public static function tableName() { return 'user'; } protected function rules() { return [ 'name' => 'required|string:0,100', ]; } protected function labels() { return [ 'id' => 'Id', 'name' => '昵称', ]; } } <?php namespace Domain\Models; use Domain\Entities\User as UserEntity; class User extends UserEntity { protected $hidden = ['password']; } <?php namespace Domain\Repositories; use Domain\Models\User; class UserRepository { public function get($id) { return } } <?php namespace Service; use Domain\Models\User; class UserController { protected $user; public function __construct(UserRepository $user) { $this->user = $user; } public function indexAction($id) { return $this->render($this->user->get($id)); } }

2019/4/1
articleCard.readMore

SVN Skipped '' -- Node remains in conflict

SVN Skipped '' -- Node remains in conflict SVN 使用过程中遇到的问题(主要是命令行还不习惯用),这里是把正确的方法记录一下 问题 拉取SVN 更新时遇到 Skipped 'xx' -- Node remains in conflict 解决方法 svn revert --depth=infinity index.html index.html为冲突的文件名 【方法来源】 Node remains in conflict,svn在服务器上显示冲突

2019/3/19
articleCard.readMore

IIS劫持CORS preflight OPTIONS请求

发现问题 刚开机,启动vue 程序,发现所有请求都被浏览器禁止了,提示禁止跨域。 解决问题 明明昨天都正常的,而且服务端程序也启用的允许跨域响应的,再一查看现在的响应头,不对劲,响应头不对了,而且响应服务也变成了 ASP.NET,再一调试服务端,发现请求根本没到程序里来,被iis拦截了 打开iis管理器,选中根节点,进入处理程序映射,点击“查看经过排序的列表”,把PHP(我用的是php环境)上移到`OPTIONSVerbHandler`前面即可,重启iis 【其他参考】 IIS劫持CORS preflight OPTIONS请求(IIS hijacks CORS Preflight OPTIONS request)

2019/3/15
articleCard.readMore

vue 实现左滑删除

更新 支持 :index 为数值或字符串 演示效果 源码 实现原理 需要记录初始位置,滑动方向,是否是滑动(点击不触发touchmove事件) oldLeft: 0, // 记录初始位置 > 0 显示左边控件 < 0 显示右边控件 0 原始状态 left: 0, // 控制滑动位置 startX: 0, // 记录滑动的初始位置 isTouch: false, //判断是否是滑动 touchStart(e: TouchEvent) { this.oldLeft = this.left; this.isTouch = false; this.startX = e.targetTouches[0].clientX; }, 滑动中不能超过可显示的隐藏区域宽度 touchMove(e: TouchEvent) { this.isTouch = true; // 获取滑动距离 const diff = e.targetTouches[0].clientX - this.startX; if (this.oldLeft == 0) { if (diff < 0) { // 左滑显示右边控件 this.left = Math.max(diff, -this.getRightWidth()); return; } // 右滑显示左边控件 this.left = Math.min(diff, this.getLeftWidth()); return; } if (this.oldLeft > 0) { if (diff > 0) { // 已显示左边控件,不能继续右滑 return; } // 左滑隐藏左边控件 this.left = Math.max(this.oldLeft + diff, 0); return; } if (diff < 0) { // 已显示右边控件,不能继续左滑 return; } // 右滑隐藏右边控件 this.left = Math.min(this.oldLeft + diff, 0); } 滑动结束,判断总滑动距离,不超过1/3自动复原 touchEnd(e: TouchEvent) { if (!this.isTouch) { // 点击,并复原 this.animation(this.left, 0); this.$emit('click'); return; } if (this.left == 0) { return; } if (this.left > 0) { const width = this.getLeftWidth(); this.animation(this.left, this.left * 3 > width ? width : 0); return; } const width = - this.getRightWidth(); this.animation(this.left, this.left * 3 < width ? width : 0); } 点击事件需要复原 关于操作一个隐藏其他实现设想 通过父控件记录所有子控件引用,并标记子空间顺序,在父控件调用子元素复原方法或子控件内部调用 子控件在初始化的时刻,生成一个自增唯一标识,然后根据这个挂载到父控件或全局属性上 完整代码 <template> <div class="swipe-row" :style="{left: left + 'px'}"> <div class="actions-left" ref="left"> <slot name="left"></slot> </div> <div :class="['swipe-content', name]" @touchstart='touchStart' @touchmove='touchMove' @touchend='touchEnd'> <slot></slot> </div> <div class="actions-right" ref="right"> <slot name="right"> <i class="fa fa-trash" @click="tapRemove"></i> </slot> </div> </div> </template> <script lang="ts"> import { Vue, Component, Prop, Emit } from 'vue-property-decorator'; @Component export default class SwipeRow extends Vue { @Prop([String, Array]) readonly name!: string| string[]; @Prop([Number, String]) readonly index!: number|string; oldLeft: number = 0; left = 0; startX = 0; isTouch = false; leftBox: HTMLDivElement | null = null; rightBox: HTMLDivElement | null = null; mounted() { this.leftBox = this.$refs.left as HTMLDivElement; this.rightBox = this.$refs.right as HTMLDivElement; } getLeftWidth(): number { if (!this.leftBox) { return 0; } return this.leftBox.clientWidth || this.leftBox.offsetWidth; } getRightWidth(): number { if (!this.rightBox) { return 0; } return this.rightBox.clientWidth || this.rightBox.offsetWidth; } tapRemove(item: any) { this.$emit('remove', item); } touchStart(e: TouchEvent) { this.resetOther(); this.oldLeft = this.left; this.isTouch = false; this.startX = e.targetTouches[0].clientX; } touchMove(e: TouchEvent) { this.isTouch = true; const diff = e.targetTouches[0].clientX - this.startX; if (this.oldLeft == 0) { if (diff < 0) { this.left = Math.max(diff, -this.getRightWidth()); return; } this.left = Math.min(diff, this.getLeftWidth()); return; } if (this.oldLeft > 0) { if (diff > 0) { return; } this.left = Math.max(this.oldLeft + diff, 0); return; } if (diff < 0) { return; } this.left = Math.min(this.oldLeft + diff, 0); } touchEnd(e: TouchEvent) { if (!this.isTouch) { this.animation(this.left, 0); this.$emit('click'); return; } //const diff = e.changedTouches[0].clientX - this.startX; if (this.left == 0) { return; } if (this.left > 0) { const width = this.getLeftWidth(); this.animation(this.left, this.left * 3 > width ? width : 0); return; } const width = - this.getRightWidth(); this.animation(this.left, this.left * 3 < width ? width : 0); } // 补间动画 animation( start: number, end: number, endHandle?: Function) { const diff = start > end ? -1 : 1; let step = 1; let handle = setInterval(() => { start += (step ++) * diff; if ((diff > 0 && start >= end) || (diff < 0 && start <= end)) { clearInterval(handle); this.left = end; endHandle && endHandle(); return; } this.left = start; }, 16); } // 暴露出来给外部调用 public reset() { if (this.left === 0) { return; } this.animation(this.left, 0); } // 复原其他 resetOther() { if (typeof this.index == 'undefined') { return; } const items: SwipeRow[] = this.$parent.$refs.swiperow as SwipeRow[]; if (!items || items.length < 1) { return; } for (let i = 0; i < items.length; i++) { if (items[i].index == this.index) { continue; } items[i].reset(); } } } </script> 使用 <div class="swipe-box"> <SwipeRow v-for="(item, index) in items" :key="index" @remove="tapRemove(item)" :index="index" ref="swiperow"> <div> 这是内容 </div> </SwipeRow> </div> 样式 .swipe-box { overflow: hidden; .swipe-row { width: 100%; position: relative; height: 5rem; padding: 0; margin: 0; transition: left .5s; .swipe-content { width: 100%; display: block; height: 5rem; } .actions-left, .actions-right { position: absolute; height: 5rem; min-height: 5rem; max-height: 5rem; text-align: center; font-size: 1.375rem; top: 0; white-space: nowrap; .fa { font-size: 1.875rem; padding: 1.5625rem 0.9375rem; } a { color: #fff; text-decoration: none; } } .actions-left { color: #fff; background-color: rgb(0, 187, 72); right: 100%; } .actions-right { color: #fff; background-color: #BB0000; left: 100%; } } }

2019/3/15
articleCard.readMore

Vuex 使用心得

为什么用 实现多页面同步共享数据,全局状态管理,也可以当作内存缓存来用 怎么用 简单使用 通过 state 存储数据 通过 computed 实现方法获取 state 中的值或定义getters 获取 定义 mutations 更新 state 数据, 通过 store.commit 触发 mutation 定义 actions 封装 store.commit 触发 多模块 modules 每一个模块实现 state getters mutations actions 如果模块内方法重名,则需要 在模块内加上 namespaced: true, 使用时需要加上模块名才能访问指定模块 state.模块名.属性名;反之直接访问即可 注意 state 并不能默认请求内容,要先 store.commit 设置内容,也可以定义action 异步获取并设置 getCategories(context: {commit: Commit; state: State}) { return new Promise((resolve, reject) => { if (context.state.categories && context.state.categories.length > 0) { resolve(context.state.categories); return; } getCategories().then(res => { context.commit(SET_CATEGORIES, res.data); resolve(res.data); }).catch(reject); }); }, 完整例子 import Vue from 'vue' import Vuex, { Commit, Dispatch } from 'vuex' import { getCategories, } from '@/api' Vue.use(Vuex) export const SET_CATEGORIES = 'SET_CATEGORIES'; export interface State { categories: ICategory[], }; export interface ICategory { id: number, name: string, } interface IActionContext { commit: Commit; state: State; } // initial state const initState: State = { categories: [], }; const getters = {}; const mutations = { [SET_CATEGORIES](state: State, categories: ICategory[]) { state.categories = categories; }, }; // actions const actions = { getCategories(context: IActionContext) { return new Promise((resolve, reject) => { if (context.state.categories && context.state.categories.length > 0) { resolve(context.state.categories); return; } getCategories().then((res: ICategory[]) => { context.commit(SET_CATEGORIES, res.data); resolve(res.data); }).catch(reject); }); }, }; const store = new Vuex.Store({ state: initState, getters, actions, mutations, }); /// 方便typscript 类型推导 export const dispatchCategories = (): Promise<ICategory[]> => store.dispatch('getCategories'); export default store;

2019/3/13
articleCard.readMore

在“应用和功能”中重置了VS2017部署Debug版本UWP,应用消失后无法再部署、安装

问题详情 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 DEP0700: 应用程序注册失败。[0x80073CFB] 另一个用户已安装此应用的未打包版本。当前用户无法将该版本替换为打包版本。冲突程序包为 22bdd128-9c57-4102-b7a4-53d890e28a07,由 CN=ZoDream 发布。 ZoDream 解决方法 已管理员的身份运行 PowerShell 输入命令 Get-AppxPackage "22bdd128-9c57-4102-b7a4-53d890e28a07" -AllUsers | Remove-AppxPackage 参考 【Cannot deploy app from VS2017 after resetting app from 'Apps & Features'】

2019/2/19
articleCard.readMore

.NET Core 多模块

说明 最近准备把本站源码从PHP 迁移到 .NET Core。遇到一个首要问题就是本站是分模块开发的,我也想过分成多个项目来做,但又涉及本站的基础框架,必须所有模块都能随着基础升级,我怕麻烦就整合到一起了。 做法 Areas 是 ASP.NET MVC 功能,在官方文档有介绍【Areas in ASP.NET Core】 第一步 在 Startup.cs 中添加配置 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc(routes => { routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); // routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } 第二步 新建文件夹 Areas/Blog/Controllers 新建控制器 HomeController.cs [Area("Blog")] public class HomeController : Controller { public IActionResult Index() { return View(); } } 文件夹名可以用其他,关键 使用 Area 声明 区域,使路由能指向这里,改了文件夹也要记得改视图的文字 第三步 注册视图位置 services.Configure<RazorViewEngineOptions>(options => { options.AreaViewLocationFormats.Clear(); options.AreaViewLocationFormats.Add("/Categories/{2}/Views/{1}/{0}.cshtml"); options.AreaViewLocationFormats.Add("/Categories/{2}/Views/Shared/{0}.cshtml"); options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml"); }); 默认会自动从这些位置找 /Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml /Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml /Views/Shared/<Action-Name>.cshtml 链接生成 HtmlHelper 语法 @Html.ActionLink("Go to Services Home Page", "Index", "Home", new { area = "Services" }) TagHelper 语法 <a asp-area="Services" asp-controller="Home" asp-action="Index">Go to Services Home Page</a>

2019/1/11
articleCard.readMore

.NetCore 中间件学习

中间件类写法 public class TestMiddleware { private readonly RequestDelegate _next; public TestMiddleware(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext httpContext) { // TO DO return _next(httpContext); } } 注册到宿主上 public static class TestMiddlewareExtensions { public static IApplicationBuilder UseTestMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<TestMiddleware>(); } } 使用 Startup.cs public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //app.Use<TestMiddleware>(); app.UseTestMiddleware(); } 内联中间件 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Use(async (context, next) => { // TO DO await next(); }); } vs 2017 使用中间件模板 右键项目》添加》新建项》已安装》ASP.NET Core》WEB》ASP.NET》中间件类

2019/1/11
articleCard.readMore

隐藏Apache版本号、PHP 版本信息

隐藏apache版本号 文件位置:默认是在 httpd.conf 中,xampp 是在 httpd-default.conf 中 # 找到ServerTokens和ServerSignature并修改为: ServerTokens Prod ServerSignature off # 如果没有找到ServerTokens和ServerSignature可以在最后一行添加 PS: 响应头还是会有 Server: Apache,只是去除了版本号 隐藏php版本号 文件位置:php.ini expose_php=Off 最后重启环境 参考 【Apache、PHP 隐藏版本信息】

2019/1/9
articleCard.readMore

wow.js 搭配 animate.css 实现网页动起来

原由 这篇教程只是给前端看的,简单介绍如何简单实现网页动起来效果。 准备 WOW.js 【官方示例】 Animate.css 【官方示例】 使用 在页面引入js、css文件 <link rel="stylesheet" href="animate.min.css"> <script src="wow.min.js"></script> 启动wow.js,配置样式 <style> .wow { visibility: hidden; } </style> <script> new WOW().init(); </script> 说明:class wow 是wow.js 默认的标记,加上了这个,那这个元素就会动起来,必须设置样式为不可见,才能起到滚动到视窗出现突然出现的效果。 修改需要动画的元素 <section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s"></section> 说明: class wow 是wow.js 默认的标记,可以修改。 class slideInLeft 是 Animate.css 的其中一种动画绑定的class。 data-wow-duration 是指动画执行时间,可以不设,Animate.css 自带默认 data-wow-delay 是指延迟多久执行动画,可以不设,Animate.css 自带默认 最终页面 <link rel="stylesheet" href="animate.min.css"> <style> .wow { visibility: hidden; } </style> <section class="wow slideInLeft" data-wow-duration="2s" data-wow-delay="5s"></section> <section class="wow slideInLeft"></section> <script src="wow.min.js"></script> <script> new WOW().init(); </script>

2019/1/6
articleCard.readMore

ORM 关联查询设计

使用方法 $this->hasOne(class, 'local_key', 'foreign_key'); $this->hasOne(table, table_id, id); $this->hasMany(class, 'local_key', 'foreign_key'); $this->hasMany(table, id, table_id); 流程 with('w') with(['w' => function($query) { $query->select('a') }]) with('w:a') eagerLoad['w'] = function(){} eagerLoad['w'] = function($query) { $query->select('a') } eagerLoad['w'] = function($query) { $query->select('a') } get() $model->w() $this->hasOne(table, table_id, id) $this->hasOne(table, table_id, id)->select(''); Relation{table, local_key, foreign_key} : Query eagerLoad['w'](Relation) Relation(id[])->getResult() w => array

2019/1/6
articleCard.readMore

商城活动构思

单个商品活动显示 获取所有活动 判断是否属于此商品 购物活动显示 获取所有活动 判断是否属于此商品 准备参加活动 获取所有活动 根据购物车中商品能参加的活动 判断用户等级 判断参加活动的金额 判断是否已存在购物车 判断是否参加过此活动 加入活动 参加中的活动 获取购物车中的活动 判断是否过期 根据购物车商品数量改删,判断金额是否还符合 确定参加活动(生成订单) 获取选择的活动 判断是否过期 判断是否含有触发活动的商品 判断选择的商品的金额 优化构思 根据最近活动过期时间缓存 缓存所有的数据,比如活动匹配的商品id,活动的等级,活动的金额,活动的时间

2019/1/6
articleCard.readMore

RBAC 权限管理(一)

介绍 RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限-资源”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,权限与资源之间一般是多对多的关系。 概念性模型 1.RBAC0的模型中包括用户(U)、角色(R)和许可权(P)等3类实体集合。 2.RBAC1,基于RBAC0模型,引入角色间的继承关系,即角色上有了上下级的区别,角色间的继承关系可分为一般继承关系和受限继承关系。一般继承关系仅要求角色继承关系是一个绝对偏序关系,允许角色间的多继承。而受限继承关系则进一步要求角色继承关系是一个树结构,实现角色间的单继承。 3.RBAC2,基于RBAC0模型的基础上,进行了角色的访问控制。添加了责任分离关系。 4.基于RBAC0的基础上,将RBAC1和RBAC2进行整合了。

2019/1/5
articleCard.readMore

多语言网站思考

静态内容的翻译 这些内容基本上是写死在网页文件里的,主要是通过基于 i18n 调用不同语言包实现。 实现原理,通过中间语句去语言包中匹配,然后输出匹配到的目标语言语句。 动态内容的翻译 这些内容是通过后台添加的内容, 可以直接调用翻译接口实时翻译,但是一般翻译不准确 建立一个专门的翻译表,所有需要翻译的内容都放里面。优点可以选择翻译翻译部分,拓展性好 建立附加字段,进行列的增加,一列内容对应一列翻译内容。 分站点。缺点各站点数据关联性差。

2019/1/3
articleCard.readMore

Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.

安装 PHPMyAdmin 4.8 以后,使用浏览器登录不上,报错 Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin. 临时解决方法 换回phpMyAdmin 4.7 使用浏览器的隐私模式访问,谷歌的无痕窗口访问亲测有效

2018/12/29
articleCard.readMore

TCP 连接过程

开始连接(三次握手) 首先确认双方收发功能是否正常 第一次握手,服务端确认客户端的发送能力、服务端的接收能力。客户端发送一个SYN段,并指明客户端的初始序列号,即ISN(c). 第二次握手,客服端确认服务端的接收、发送能力,客户端的接收、发送能力。服务端发送自己的SYN段作为应答,同样指明自己的ISN(s)。为了确认客户端的SYN,将ISN(c)+1作为ACK数值。这样,每发送一个SYN,序列号就会加1. 如果有丢失的情况,则会重传。 第三次握手,服务端确认客户端的接收、发送能力,服务端的发送、接收能力。为了确认服务器端的SYN,客户端将ISN(s)+1作为返回的ACK数值。 确认之后,正式收发数据 结束连接(四次挥手) 第一次挥手,客户端发送一个FIN段,并包含一个希望接收者看到的自己当前的序列号K. 同时还包含一个ACK表示确认对方最近一次发过来的数据。 第二次挥手,服务端将K值加1作为ACK序号值,表明收到了上一个包。这时上层的应用程序会被告知另一端发起了关闭操作,通常这将引起应用程序发起自己的关闭操作。 第三次挥手,服务端发起自己的FIN段,ACK=K+1, Seq=L 第四次挥手,客户端确认。ACK=L+1 DOS 攻击 最基本的DoS攻击就是利用合理的服务请求(发送大量的SYN包)来占用过多的服务资源,从而使合法用户无法得到服务的响应。 DDOS 攻击 分布式拒绝服务攻击采取的攻击手段就是分布式的。 按照TCP/IP协议的层次可将DDOS攻击分为基于ARP的攻击、基于ICMP的攻击、基于IP的攻击、基于UDP的攻击、基于TCP的攻击和基于应用层的攻击。 参考 1.“三次握手,四次挥手”你真的懂吗?

2018/12/29
articleCard.readMore

JSON RPC

基本请求结构 { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 } jsonrpc 为固定参数 method 需要调用的方法 params 方法值,非必须 id 可以是数值或字符串,与响应相对应 基本响应结构 { "jsonrpc": "2.0", "result": 19, "id": 1 } 通知类型响应为空 批量调用请求结构 [ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, {"foo": "boo"}, {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ] 响应结构 [ {"jsonrpc": "2.0", "result": 7, "id": "1"}, {"jsonrpc": "2.0", "result": 19, "id": "2"}, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}, {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} ] 根据 id 确定响应的值 响应错误 code message meaning -32700 Parse error Invalid JSON was received by the server.An error occurred on the server while parsing the JSON text. -32600 Invalid Request The JSON sent is not a valid Request object. -32601 Method not found The method does not exist / is not available. -32602 Invalid params Invalid method parameter(s). -32603 Internal error Internal JSON-RPC error. -32000 to -32099 Server error Reserved for implementation-defined server-errors. JSON-RPC 与 RESTFUL 比较 json rpc 适用与服务器内部通信,例如分布式架构 restful 适用于对外通信,例如与浏览器,APP 等 参考 1.JSON-RPC 2.0 Specification

2018/12/28
articleCard.readMore

VSCode 遭遇typescript language server 初始化失败

症状 同一个项目,在笔记本上打开是正常的,到了台式机上一直在初始化 typscript 语言服务, 输出显示 [Error - 9:59:09 PM] ReaderError RangeError: out of range index at Buffer.copy (buffer.js:602:18) at l.tryReadContent (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:161851) at t.Reader.onLengthData (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:162417) at Socket.t.Reader.i.Disposable.constructor.e.on.e (d:\Microsoft VS Code\resources\app\extensions\typescript-language-features\dist\extension.js:1:162184) at emitOne (events.js:116:13) at Socket.emit (events.js:211:7) at addChunk (_stream_readable.js:263:12) at readableAddChunk (_stream_readable.js:250:11) at Socket.Readable.push (_stream_readable.js:208:10) at Pipe.onread (net.js:594:20) [Error - 9:59:09 PM] TSServer exited with code: 3 解决方法 在设置里增加 "typescript.tsserver.log": "verbose", 重启vscode,再次查看输出,会在第二行出现日志文件路径 等到输出报错后,打开日志文件,查看报错原因 才发现是有.ts视频文件也被识别进去 删除.ts视频文件或在 tsconfig.json 加入排除文件夹 "exclude": [] 报错原因有多种具体请用1、2、3步自行查看或到vscode 提交issues并附上日志内容

2018/12/25
articleCard.readMore

【说明】关于本站《圣诞》主题

《圣诞》主题上线 已知问题 雪花插件未做适应浏览器尺寸变化处理 素材来源 1.背景图片 2.雪花图片来自逐梦博客及百度图片

2018/12/24
articleCard.readMore

重新思考框架架构

当前版本 $app = new Web(); $app->register(); $app->autoResponse(); $response = $app->handle($uri); $app->isAllowDomain(); // 这里会判断是否允许域名访问 $uri = $app->fomat($uri); $app['url']->deRewrite($uri); $route = $app[Router]->handle($uri) new Router() $router->get(''); reurn new Route(); $route->handle($app['request'], $app['response']) $module = $router->getMoudle() $module->boot(); $module->invoke() return $package = $module->getControlerNamespace(); $controller = $route->getController($package) $controller->init(); $controller->invoke($action); $controller->beforeFilter() $controller->$action() $app['view']->render() $response->send(); $response->sendHeader(); $response->sendContent(); 中间件版本 $app = new Web() $app->middleware(Middleware::class) $app->middleware(function($req, $resp, $next)) $response = $app->handle($uri) DomainMiddleware // 是否允许当前域名 CORSMiddleware // 跨域验证 CacheMiddleware // 静态缓存检测 GZIPMiddleware // 压缩输出 RewriteMiddleware // 重写解析还原真实 RouterMiddleware // 路由解析,下一步到控制器 MatchRouteMiddle [any]Middleware ModuleMiddleware DefaultRouteMiddle Controller CSRFMiddleware RuleMiddleware // 控制器中的规则过滤 AuthMiddleware // 登陆验证 HttpMethodMiddleware // 请求方式验证 RoleMiddleware // 角色验证 UriParameterMiddleware // 方法参数过滤 ResponseBodyMiddleware // 加密解密 $response->send(); 目前还不确定是否升级

2018/12/24
articleCard.readMore

页面部件注册思路

app('page')->node('feedback', ['limit' => 12]); // 使用,获取部件对象(单例) // app('page')->feedback(['limit' => 12]); // 另一种使用方式 $page = new Page(); // 未启动是启动 $page->loadNodes(); // 注册所有部件 $page->register('feedback', Feedback::class); // 注册单个部件 $page->__call('feedback', $attrs); // 第二种方式调用 $page->node('feedback', $attrs); $feedback = $page->instance('feedback') // 获取部件单例(返回克隆的对象) $feedback = new Feedback(); // 初始化 $page->on('feedback_list', function () {return $data;}); // 注册需要的资源,避免重复请求数据库 return $feedback $feedback->attr($attrs); // 配置参数 return this; return; return; $feedback->__toString() // <?= ?> 使用时自动调用方法 $feedback->render(); // 开始输出数据 $data = $page->trigger('feedback_list'); // 获取已注册的资源 return $feedback->renderHtml($data); return;

2018/12/24
articleCard.readMore

Canvas 绘图回顾

基本 获取页面元素 let canvas = document.getElementById("canvas"), context = canvas.getContext("2d"); 虚拟缓存(不显示在页面,主要是用于缓存部分场景) let cacheCanvas = document.createElement("canvas"), cacheContext = cacheCanvas.getContext("2d"); 应用缓存 context.drawImage(cacheCanvas, 0, 0); 绘制基本图形 绘制文字 context.font = 'bold 35px Arial'; context.textAlign = 'center'; context.textBaseline = 'bottom'; context.fillStyle = '#ccc'; context.strokeText("Hello Canvas", 150, 100, 200); context.fillText("Hello Canvas", 180, 140); 绘制图片 let img = new Image(); img.src = ''; if(img.complete) { context.drawImage(img, 0, 0); } else { img.onload = function(){ context.drawImage(img, 0, 0); }; img.onerror = function(){ alert('加载失败,请重试'); }; } 高级绘图 绘制游戏画面,先把背景、元素都分成不同的缓存,设定刷新时间,并组成不同的场景

2018/12/22
articleCard.readMore

css3 学习之 attr()

说明 接受两个参数, 第一个参数时主元素的属性名,可以跟转化类型,第二个是默认值, 例如: a::before { color: attr(data-color color, red); } 字符串拼接 a::before { content: "Hi " attr(data-hover); } 一个切换例子 .box { font-size: 55px; overflow: hidden; position: relative; } .box span { display: inline-block; position: relative; transition: transform 0.3s; } .box span:first-child { color: #666; } .box span:nth-child(2) { color: #daa520; } .box span:nth-child(2)::before { bottom: 105%; color: #666; } .box span:first-child::before { top: 105%; color: #daa520; } .box span:first-child::before, .box span:nth-child(2)::before { position: absolute; content: attr(data-hover); } .box:hover span:first-child { transform: translate3d(0, -105%, 0); } .box:hover span:nth-child(2) { transform: translate3d(0, 105%, 0); } <div class="box"> <span data-hover="my">my</span> <span data-hover="friend">friend</span> </div>

2018/12/21
articleCard.readMore

Ueditor 与textarea切换 及 与pjax 结合

页面代码 <textarea id="container">

2018/12/21
articleCard.readMore

Gulp 实现根据参数处理不同任务

需求来源 Gulp 一般使用一个默认的任务,如果需要处理不同的文件就多建几个任务,但是这是一般的做法。 比如本站源码就分成多个模块,项目结构都一样,如果按一般的做法就是要建无数个任务,关键是项目是不断开发中的,不可能每次都新建,所以就想怎么自动切换处理不同文件夹。 解决方法 Gulp 调用不同的任务是通过 gulp task 这个调用task任务的,如果不存在task任务就会报错,那有没有可能 通过 gulp task 调用处理 task 这个文件夹呢? 1.首先获取 参数 var name = ''; if (process.argv && process.argv.length > 2) { name = process.argv[2]; // 这就获取到了参数 } 2.然后根据参数改变目标文件夹 gulp.src(name + '/*.css') 但是 gulp task 是会调用 task 任务的,那么可以 3.临时生成 task 任务 gulp.task(name || 'z', build); name 可能为空,但不能生成一个空命名的任务 4.最终结果 var gulp = require('gulp'), minCss = require('gulp-clean-css'), name = ''; if (process.argv && process.argv.length > 2) { name = process.argv[2]; // 这就获取到了参数 } function cssTask() { return gulp.src('src/' name + "/*.css") .pipe(minCss()) .pipe(gulp.dest('dist/')); } exports.cssTask = cssTask; var build = gulp.series(gulp.parallel(cssTask)); gulp.task(mo, build); gulp.task('default', build); 测试 压缩 test 文件夹下的css gulp test

2018/12/20
articleCard.readMore

box-shadow实现四周阴影

第一种 这个只是简单的进行四个方向的阴影,会出现阴影效果不真实,缺角不连贯 /*说明:(以上部边为例进行说明) 1. 对于上边,沿x轴方向的偏移量显然没有意义,设为0px; 2. 沿y轴正方向阴影进入div内部,不显示,因此写为负数; 3. 扩展半径不要写,或者写成0px,这样就不会影响其他的边; 4. 颜色自定; 5. 模糊程度按需要自定; 6. 下、左、右边阴影按规律类推。 */ box-shadow: 0px -10px 0px 0px #ff0000, /*上边阴影 红色*/ -10px 0px 0px 0px #3bee17, /*左边阴影 绿色*/ 10px 0px 0px 0px #2279ee, /*右边阴影 蓝色*/ 0px 10px 0px 0px #eede15; /*下边阴影 黄色*/ 第二种 真正意义上的全阴影,但是阴影的效果相对于单边阴影距离减半,所以要设得更大 div{ width:250px; height:250px; background:greenyellow; box-shadow:black 0px 0px 10px;//将颜色提到前面,且将h-shadow,v-shadow设为0px,实现四周阴影 } 【参考】 1.box-shadow实现四周阴影 2.DIV四个边框分别设置阴影样式

2018/12/20
articleCard.readMore

那些可以一用的浏览器api

page lifecycle(网页生命周期) document.visibitilityState 来监听网页可见度,是否卸载 focus 事件 focus事件在页面获得输入焦点时触发。 blur 事件 blur事件在页面失去输入焦点时触发。 visibilitychange 事件 visibilitychange事件在网页可见状态发生变化时触发. window.addEventListener('visibilitychange',() => { // 通过这个方法来获取当前标签页在浏览器中的激活状态。 switch(document.visibilityState){ case'prerender': // 网页预渲染 但内容不可见 case'hidden': // 内容不可见 处于后台状态,最小化,或者锁屏状态 case'visible': // 内容可见 case'unloaded': // 文档被卸载 } }); freeze 事件 freeze事件在网页进入挂起状态时触发。 resume 事件 resume事件在网页离开挂起,恢复时触发。 pageshow 事件 pageshow事件在用户加载网页时触发。这时,有可能是全新的页面加载,也可能是从缓存中获取的页面。如果是从缓存中获取,则该事件对象的event.persisted属性为true,否则为false pagehide 事件 pagehide事件在用户离开当前网页、进入另一个网页时触发。它的前提是浏览器的 History 记录必须发生变化,跟网页是否可见无关。 beforeunload 事件 beforeunload事件在窗口或文档即将卸载时触发。 unload 事件 unload事件在页面正在卸载时触发 online state(网络状态) window.addEventListener('online',onlineHandler) window.addEventListener('offline',offlineHandler) Vibration(震动) // 可以传入一个大于0的数字,表示让手机震动相应的时间长度,单位为ms navigator.vibrate(100) // 也可以传入一个包含数字的数组,比如下面这样就是代表震动300ms,暂停200ms,震动100ms,暂停400ms,震动100ms navigator.vibrate([300,200,100,400,100]) // 也可以传入0或者一个全是0的数组,表示暂停震动 navigator.vibrate(0) device orientation(陀螺仪) window.addEventListener('deviceorientation',e => { console.log('Gamma:',e.gamma); // 设备沿着Y轴的旋转角度 console.log('Beta:',e.beta); //设备沿着X轴的旋转角度 console.log('Alpha:', e.alpha); //设备沿着Z轴的旋转角度 }) 注意 alpha 是以手机自带指南针为标准,及手机认为的南方为 0 ,需要注意手机不一定支持指南针,所以会出现偏差 battery status(电量) // 通过这个方法来获取battery对象 navigator.getBattery().then(battery => { // battery 对象包括中含有四个属性 // charging 是否在充电 // level 剩余电量 // chargingTime 充满电所需事件 // dischargingTime 当前电量可使用时间 const { charging, level, chargingTime, dischargingTime } = battery; // 同时可以给当前battery对象添加事件 对应的分别时充电状态变化 和 电量变化 battery.onchargingchange = ev => { const { currentTarget } = ev; const { charging } = currentTarget; }; battery.onlevelchange = ev => { const { currentTarget } = ev; const { level } = ev; } }) 【参考】 你(可能)不知道的web api Page Lifecycle API 教程

2018/12/20
articleCard.readMore

Angular CORS 跨域请求解决方法

CORS:跨域资源共享 实际分两步请求: 第一步.预检: (无论响应什么数据都不会传到js程序里,只是浏览器做判断用,可以根据 请求方式 OPTIONS 和 Origin 请求头判断是否是预检) 请求头:OPTIONS Access-Control-Request-Headers: content-type Access-Control-Request-Method: GET Origin: http://localhost:4200 Access-Control-Request-Headers 告诉服务端第二步将带的请求头 Access-Control-Request-Method 第二步将使用的请求方式 Origin 当前的域名 允许的响应头: Access-Control-Allow-Credentials: Access-Control-Allow-Headers: * Access-Control-Allow-Methods: * Access-Control-Allow-Origin: * Access-Control-Max-Age: 0 Access-Control-Allow-Credentials 告诉浏览器第二步是否允许带上cookie 默认不带, true 为带上 Access-Control-Allow-Headers 允许带的请求头,必须全匹配才行, * 表示允许任何请求头 Access-Control-Allow-Methods 允许的请求方式 * 都允许 Access-Control-Allow-Origin 允许的请求域名 * 不限制 Access-Control-Max-Age 这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久。 注:需要注意的是Access-Control-Max-Age的设置针对完全一样的url,如果url加上路径参数,其中一个url的Access-Control-Max-Age设置对另一个url没有效果 第二步.正式请求: (如果第一步没有获取到允许的响应头就不会发生第二步) 本次依然需要加上允许的跨域的响应头,js程序才能接收到相应的数据

2018/12/7
articleCard.readMore

.NetCore 使用 Mysql

准备 NUGET 添加依赖项 MySql.Data.EntityFrameworkCore 使用 创建数据库映射 public class user { public int id { get; set; } public string name { get; set; } public string email { get; set; } public string avatar { get; set; } } 新建 DbContext类 public class DBContext : DbContext { public DBContext(DbContextOptions options) : base(options) { } public DbSet user { get; set; } //string str = @"Data Source=;Database=;User ID=;Password=;pooling=true;CharSet=utf8;port=3306;sslmode=none"; //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => // optionsBuilder.UseMySQL(str); } appsettings.json里添加连接字符串 { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "MysqlConnection": "Data Source=localhost;Database=zodream;User ID=root;Password=root;pooling=true;CharSet=utf8;port=3306;sslmode=none" } } Startup.cs文件的ConfigureServices方法注册 public void ConfigureServices(IServiceCollection services) { var connection = Configuration.GetConnectionString("MysqlConnection"); services.AddDbContext(options => options.UseMySQL(connection)); services.AddMvc(); } 使用 private readonly DBContext _db; //通过.NET Core框架自动为我们做构造函数依赖注入IOC。 public HomeController(DBContext db) { _db = db; } public ActionResult Index() { var list= _db.student.ToList(); return View(); } 【参考】 1.《.NetCore中EFCore for MySql》

2018/12/7
articleCard.readMore

windows server 2016 安装 IIS 、PHP 7.2、Mysql 8.0

准备: windows server 2016 Notepad++ 下载 PHP 下载 MYSQL 下载 iis rewrite 下载 HeidiSQL 下载 cacert.pem 下载 安装 第一步,安装notepad++ 第二步,安装PHP 解压PHP 到指定文件夹, 例如:C:\Program Files\PHP 修改文件名为 php.ini 配置 修改拓展文件夹路径 extension_dir = "ext" 启用插件 extension=curl extension=fileinfo extension=gd2 extension=gettext extension=mbstring extension=mysqli extension=openssl extension=pdo_mysql 配置时区 date.timezone = PRC 配置openssl证书 openssl.cafile=cacert.pem 附xdebug配置(需下载php_xdebug.dll) [Xdebug] zend_extension="ext/php_xdebug.dll" xdebug.auto_trace=1 xdebug.collect_params=1 xdebug.collect_return=1 xdebug.trace_output_dir="D:/zodream/xdebug/trace" xdebug.profiler_enable=1 xdebug.profiler_output_dir="D:/zodream/xdebug/profiler" xdebug.var_display_max_children=1280 xdebug.var_display_max_data=5120 xdebug.var_display_max_depth=200 第三步,安装IIS 启用“web服务器(IIS)”,启用“iis 可承载web核心”,启用“CGI” 加载PHP 在iis主页 点击“处理程序映射”,右击空白处点击“添加模块映射”,配置 添加默认文档:index.php 安装 iis rewrite 第四步,安装MySQL 解压 mysql.zip 文件到指定文件夹,例如:C:\Program Files\MYSQL 添加文件 my.ini(把{path}全部替换为当前文件夹路径) [mysqld] # 设置3306端口 port=3306 # 设置mysql的安装目录 basedir="{path}" # 设置mysql数据库的数据的存放目录 datadir="{path}\data" # 允许最大连接数 max_connections=200 # 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统 max_connect_errors=10 # 服务端使用的字符集默认为UTF8 character-set-server=utf8 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB # 默认使用“mysql_native_password”插件认证 default_authentication_plugin=mysql_native_password [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8 [client] # 设置mysql客户端连接服务端时默认使用的端口 port=3306 default-character-set=utf8 以管理员打开CMD,进入mysql 的bin 文件夹下 初始化 mysqld --initialize --console 安装服务 mysqld -install 启动服务 net start mysql 第五步,安装HeidiSQL 解压到任意文件,双击运行 heidisql.exe 运行 登录mysql, 第一次登录会要求新设密码 第六步,测试 在 C:\inetpub\wwwroot 新建 index.php, <?php phpinfo(); 使用浏览器访问 http://localhost 【注意】 不同php、mysql 版本依赖不同的vc,大致需要 vc9、vc11、vc12、vc14,也分x86和x64,本次安装不需要额外安装

2018/10/22
articleCard.readMore

GO Methods 中指针和引用

package main import ( "fmt" ) type Hub struct { width, height int } func (r Hub) size() int { return r.width * r.height } func (r *Hub) bound() int { return r.width * r.height } func (r Hub) setX(x int) { r.width = x } func (r *Hub) setY(y int) { r.height = y } func main() { hub := Hub{width: 1, height: 2} fmt.Println(hub) fmt.Println(hub.size()) fmt.Println(hub.bound()) hub.setX(10) hub.setY(20) fmt.Println(hub) fmt.Println(hub.size()) fmt.Println(hub.bound()) } 输出 {1 2} 2 2 {1 20} 20 20 在 struct 的方法中尽可能使用 指针 ,除非不希望方法中改变影响主体内容才使用引用 引用实际上是复制主体,会花费相对较多的系统开销(内存和时间) 在方法中改变参数会改变指针主体参数,不改变引用主体参数

2018/9/22
articleCard.readMore

架构、框架、模式、模块、组件、插件、控件、中间件

架构 软件架构,也成称为软件体系结构,简单地说就是一种设计方案,将用户的不同需求抽象成组件,且能够描述组件之间的通信和调用。软件架构会分析工程中的问题,针对问题设计解决方案,针对解决方案分析应具有的功能,针对功能设计软件系统的层次和模块及层次模块之间的逻辑交互关系,确定各个功能如何由这些逻辑实现。开发人员可以根据软件架构分析出来的层次和架构进行软件编写。 【理解】:综合需求和能力对有关软件整体结构与组件的抽象描述 框架 软件框架,是软件开发过程中提取软件的共性部分形成的体系结构。框架不是现成可用的应用系统,而是一个半成品,是一个提供了诸多服务,供开发人员进行二次开发,实现具体功能的程序实体。 框架与架构的关系:框架不是架构,框架比架构更具体,更偏重于技术,而架构更偏重于设计;架构可以通过多种框架来实现。 【理解】:对普遍使用的方法进行的归类、封装,不进行具体业务处理实现,但提供全面的处理方法 模式 设计模式强调的是一个设计问题的解决方法,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 框架与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,一个模式可应用于不同的框架和被不同的程序语言所实现;而框架则是一个应用的体系结构,是一种或多种设计模式和代码的混合体。设计模式的思想可以在框架设计中进行应用。 架构与设计模式的关系:设计模式研究的是对单一问题的设计思路和解决方法,范畴比较小;而架构是高层次的针对体系结构的一种设计思路,范畴比较大。一个架构中可能会出现多个设计模式的思想。 【理解】:对程序的整体结构的概括 模块 根据不同的标准,通常会说程序模块或功能模块,程序模块指的是一段能够实现某个目标的成员代码段,功能模块则用来说明一个功能所包含的系统行为。定义模块的原则是:高内聚和低耦合。 【理解】:实现大型软件系统的一部分功能的程序 组件 组件是封装了一个或多个程序模块的实体。组件强调的是封装,利用接口进行交互。组件也称为构建。插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。 【理解】:对数据和方法简单封装的对象 插件 插件属于组件,插件是组件的一个子类,就是将组件中具有某些特点的组件归为插件。插件是一种电脑程序,通过和应用程序的互动,来为应用程序增加一些特定的功能,仅靠插件是无法正常运行的,需要依赖于应用程序才能发挥自身功能。插件和应用程序之间通过接口进行交互。 【理解】:根据规范的接口编写的不能脱离平台单独运行的程序 控件 对数据和方法封装的可视化组件。 【理解】:接受输入数据的组件 中间件 主要解决异构网络环境下分布式应用软件的互连与互操作问题,提供标准接口、协议,屏蔽实现细节,提高应用系统易移植性。 【理解】:从不同的输入来源转化成标准的数据,通过标准的接口进入不同的底层执行处理

2018/7/4
articleCard.readMore

CentOs 7 安装apche php mysql

Apache yum 安装Apache yum install httpd 设置服务器开机自动启动Apache systemctl enable httpd.service 手动启动Apache systemctl start httpd.service 在浏览器中输入IP地址即可验证是否启动成功 手动重启Apache systemctl restart httpd.service 手动停止Apache systemctl stop httpd.service 开放80端口 开启端口 firewall-cmd --zone=public --add-port=80/tcp --permanent 命令含义: --zone #作用域 --add-port=80/tcp #添加端口,格式为:端口/通讯协议 --permanent #永久生效,没有此参数重启后失效 重启防火墙 firewall-cmd --reload 查看状态 firewall-cmd --state MySQL 首先检查 MySQL 是否已安装 yum list installed | grep mysql 如果有的话 就全部卸载 yum -y remove +数据库名称 MySQL 依赖 libaio,所以先要安装 libaio yum search libaio 检索相关信息 yum install libaio 安装依赖包 下载 MySQL Yum Repository 地址为 http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm PS:如果提示-bash: wget: 未找到命令,请先执行 yum install wget 安装 wget 添加 MySQL Yum Repository 添加 MySQL Yum Repository 到你的系统 repository 列表中,执行 yum localinstall mysql-community-release-el7-5.noarch.rpm 验证下是否添加成功 yum repolist enabled | grep "mysql.*-community.*" 选择要启用 MySQL 版本 查看 MySQL 版本,执行 yum repolist all | grep mysql 可以看到 5.5, 5.7 版本是默认禁用的,因为现在最新的稳定版是 5.6 yum repolist enabled | grep mysql 查看当前的启动的 MySQL 版本 通过 Yum 来安装 MySQL 执行 yum install mysql-community-server Yum 会自动处理 MySQL 与其他组件的依赖关系. 启动和关闭 MySQL Server 启动 MySQL Server systemctl start mysqld 查看 MySQL Server 状态 systemctl status mysqld 关闭 MySQL Server systemctl stop mysqld MySQL 安全设置 服务器启动后,可以执行 mysql_secure_installation; 安装Mysql 8 yum仓库下载MySQL: sudo yum localinstall https://repo.mysql.com//mysql80-community-release-el7-1.noarch.rpm yum安装MySQL: sudo yum install mysql-community-server 启动MySQL服务: sudo service mysqld start 检查MySQL服务状态: sudo service mysqld status 查看初始密码(如无内容直接跳过): sudo grep 'temporary password' /var/log/mysqld.log 本地MySQL客户端登录: mysql -uroot -p 输入密码为第5步查出的,如果没有,直接回车,然后输入命令 flush privileges 修改root登录密码: ALTER USER 'root'@'localhost' IDENTIFIED BY '密码'; (注意要切换到mysql数据库,使用use mysql) 防火墙设置 远程访问 MySQL, 需开放默认端口号 3306. 执行 firewall-cmd --permanent --zone=public --add-port=3306/tcp firewall-cmd --permanent --zone=public --add-port=3306/udp 这样就开放了相应的端口。 执行 firewall-cmd --reload PHP 安装epel-release yum -y install epel-release 安装PHP7 rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm 成功获取PHP7的yum源,然后再执行: yum install php70w 源码安装PHP7 下载源码 wget http://php.net/get/php-7.2.6.tar.gz/from/a/mirror 解压 tar -zxvf php-7.2.6.tar.gz 进入解压包安装一些必要的依赖 yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel 安装gcc yum install gcc 安装 yum -y install libxslt-devel* yum -y install perl* yum -y install httpd-devel find / -name apxs 得到的路径是:/usr/bin/apxs 于是得到--with-apsx2的路径是/usr/bin/apxs 配置 ./configure --prefix=/usr/local/php7 --with-curl --with-freetype-dir --with-gd --with-gettext --with-iconv-dir --with-kerberos --with-libdir=lib64 --with-libxml-dir --with-mysqli --with-openssl --with-pcre-regex --with-pdo-mysql --with-pdo-sqlite --with-pear --with-png-dir --with-xmlrpc --with-xsl --with-zlib --enable-fpm --enable-bcmath -enable-inline-optimization --enable-gd-native-ttf --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-shmop --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-zip --enable-pcntl --with-curl --with-fpm-user=nginx --enable-ftp --enable-session --enable-xml --with-apxs2=/usr/bin/apxs 编译 make 安装 make install 添加环境变量 vi /etc/profile 在末尾加入: PATH=$PATH:/usr/local/php7/bin export PATH 使改动立即生效 source /etc/profile 查看php版本 php -v (如果有问题 请检查添加的环境变量是否是PHP安装目录里的bin目录) 生成必要文件 cp php.ini-production /usr/local/php7/etc/php.ini cp sapi/fpm/php-fpm /usr/local/php7/etc/php-fpm cp /usr/local/php7/etc/php-fpm.conf.default /usr/local/php7/etc/php-fpm.conf cp /usr/local/php7/etc/php-fpm.d/www.conf.default /usr/local/php7/etc/php-fpm.d/www.conf 配置 如果报错 请敲这行查报错信息 可以查到哪个文件第几行出错: systemctl status httpd.service 修改Apache默认欢迎页: vi /etc/httpd/conf.d/welcome.conf 将/usr/share/httpd/noindex 修改为/var/www 修改Apache配置: vi /etc/httpd/conf/httpd.conf DocumentRoot "/var/www/" (请注意,/var/www这个路径是自定义,在配置文件中有好几处这个路径,如果更改,请全局搜索一下都改掉) 找到 AddType application/x-compress .Z AddType application/x-gzip .gz .tgz 在后面添加 AddType application/x-httpd-php .php AddType application/x-httpd-php-source .php7 搜索<IfModule dir_module>下面这一块添加上index.php <IfModule dir_module> DirectoryIndex index.html index.php </IfModule> 搜索有没有下面这一行: LoadModule php7_module modules/libphp7.so 如果没有 请手动添加 否则 会出现运行php文件变成下载 在最下面配置域名 <VirtualHost *:80> DocumentRoot /var/www ServerName www.你的域名.com ServerAlias 你的域名.com <Directory /phpstudy/www> Options +Indexes +FollowSymLinks +ExecCGI AllowOverride All Order Deny,Allow Allow from all </Directory> </VirtualHost> 测试 【参考】 【Centos7 下安装Apache2 + MySQL + PHP7】 【CentOS7使用yum安装MySQL8.0】 【CentOS 7.3下配置 Apache2.4 + MySQL5.7 + PHP7.1.8】 【centos7下源码编译配置 apache2.4+mysql5.6+php7.1】

2018/6/20
articleCard.readMore

vs 2017 编写第一个 C 程序

正式开始学习C语言。 准备: 《C Primer Plus》第六版 windows 10 + vs2017 社区版 第一次写的程序当然是 《Hello World》 第一步打开vs2017 第二步新建项目 第三步添加源文件 第四步写代码 #include <stdio.h> int main(void) { printf("Hello World!"); return 0; } 第五步编译 控制台会一闪而过,就表示编译成功! 好了,第一个c程序就这么简单完成了。

2018/3/30
articleCard.readMore

input file控件限制上传文件类型

直接设置文件拓展名 多个用英文逗号隔开 <input type="file" accept=".xls,.doc,.txt,.pdf" /> 通过文件类型 文件 类型 *.3gpp audio/3gpp *.ac3 audio/ac3 *.asf allpication/vnd.ms-asf *.au audio/basic *.css text/css *.csv text/csv *.doc application/msword *.dot application/msword *.dtd application/xml-dtd *.dwg image/vnd.dwg *.dxf image/vnd.dxf *.gif image/gif *.htm text/html *.html text/html *.jp2 image/jp2 *.jpe image/jpeg *.jpeg image/jpeg *.jpg image/jpeg *.js text/javascript *.json application/json *.mp2 audio/mpeg *.mp3 audio/mpeg *.mp4 audio/mp4 *.mpeg video/mpeg *.mpg video/mpeg *.mpp application/vnd.ms-project *.ogg application/ogg *.pdf application/pdf *.png image/png *.pot application/vnd.ms-powerpoint *.pps application/vnd.ms-powerpoint *.ppt application/vnd.ms-powerpoint *.rtf application/rtf *.svf image/vnd.svf *.tif image/tiff *.tiff image/tiff *.txt text/plain *.wdb application/vnd.ms-works *.wps application/vnd.ms-works *.xhtml application/xhtml+xml *.xlc application/vnd.ms-excel *.xlm application/vnd.ms-excel *.xls application/vnd.ms-excel *.xlt application/vnd.ms-excel *.xlw application/vnd.ms-excel *.xml text/xml *.zip aplication/zip 当前还有一些多文件的通配符 文件 类型 image/* 匹配所有类型图片 audio/* 匹配所有类型音频 video/* 匹配所有类型视频 注意 这都只是针对普通用户有效的显示, 比如直接修改html代码可以避开 或直接通过其他工具上传也可以避开 或修改文件拓展名也可以避开 后端程序还是需要做其他验证

2018/12/23
articleCard.readMore

kindeditor 加入七牛云上传

七牛云上传主要有两种: 服务端上传 前端上传,前端又分两种返回方式: 1).重定向返回,可以解决ajax跨域的问题 2).回调返回,七牛云先向服务端要返回数据,再由七牛云返回前端,解决不支持重定向的请求方式,比如小程序上传 本次使用的是 七牛云 php sdk; composer require qiniu/php-sdk 在Kindeditor/php 下添加 config.php 主要是配置参数 <?php error_reporting(0); defined('ROOT_PATH') || define('ROOT_PATH', dirname(__DIR__).'/'); defined('QINIU_ACCESS_KEY') || define('QINIU_ACCESS_KEY', ''); defined('QINIU_SECRET_KEY') || define('QINIU_SECRET_KEY', ''); defined('QINIU_TEST_BUCKET') || define('QINIU_TEST_BUCKET', '七牛云空间名'); defined('QINIU_BUCKET_DOMAIN') || define('QINIU_BUCKET_DOMAIN', '七牛云空间网址'); defined('CALLBACK_URL') || define('CALLBACK_URL', '域名/kindeditor/php/callBack.php'); defined('RETURN_URL') || define('RETURN_URL', '域名/kindeditor/php/returnBack.php'); require_once ROOT_PATH."vendor/autoload.php"; 在Kindeditor/php 下添加 qiniu_token.php 主要是生成上传用的 token <?php use Qiniu\Auth; require_once __DIR__."/config.php"; // 构建鉴权对象 $auth = new Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); $data = [ 'returnUrl' => RETURN_URL, ]; if (isset($_REQUEST['is_call'])) { $data = [ 'callbackUrl' => CALLBACK_URL, 'callbackBody' => 'key=$(key)&hash=$(etag)&w=$(imageInfo.width)&h=$(imageInfo.height)' ]; } // 生成上传 Token $token = $auth->uploadToken(QINIU_TEST_BUCKET, null, 3600, $data); echo json_encode([ 'error' => 0, 'token' => $token ]); 在Kindeditor/php 下添加 callBack.php 主要是回调用 <?php use Qiniu\Auth; require_once __DIR__."/config.php"; $_body = file_get_contents('php://input'); $auth = new Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); //回调的contentType $contentType = 'application/x-www-form-urlencoded'; //回调的签名信息,可以验证该回调是否来自七牛 $authorization = $_SERVER['HTTP_AUTHORIZATION']; $isQiniuCallback = $auth->verifyCallback($contentType, $authorization, CALLBACK_URL, $_body); if (!$isQiniuCallback) { echo json_encode([ 'error' => 2, 'message' => '验证失败' ]); die(); } $body = $_POST; $qiniu_url = QINIU_BUCKET_DOMAIN; if (!empty($body['key'])) { echo json_encode([ 'error' => 0, 'url' => $qiniu_url.$body['key'] ]); die(); } echo json_encode([ 'error' => 1, 'message' => '视频上传出错' ]); 在Kindeditor/php 下添加 returnBack.php 主要是重定向接收地址 <?php use Qiniu\Auth; require_once __DIR__."/config.php"; $upload_ret = base64_decode($_GET['upload_ret']); $upload_ret = json_decode($upload_ret, true); $qiniu_url = QINIU_BUCKET_DOMAIN; if (!empty($upload_ret['key'])) { echo json_encode([ 'error' => 0, 'url' => $qiniu_url.$upload_ret['key'] ]); die(); } echo json_encode([ 'error' => 1, 'message' => '视频上传出错' ]); 接下来是前端更改,我改的时视频上传 Kindeditor/plugins/media/media.js KindEditor.plugin('media', function(K) { var self = this, name = 'media', lang = self.lang(name + '.'), allowMediaUpload = K.undef(self.allowMediaUpload, true), allowFileManager = K.undef(self.allowFileManager, false), formatUploadUrl = K.undef(self.formatUploadUrl, true), extraParams = K.undef(self.extraFileUploadParams, { 'token': ''//添加token }), filePostName = K.undef(self.filePostName, 'file'), //更改文件上传名 uploadJson = K.undef(self.uploadJson, ' //更改上传地址,我用的时华东区的空间使用https .................... function getQToken() { $.getJSON('/includes/kindeditor/php/qiniu_token.php', function (data) { K('[name="token"]', div).val(data.token); }); } // 获取设置上传token getQToken(); if (allowMediaUpload) { var uploadbutton = K.uploadbutton({ button : K('.ke-upload-button', div)[0], fieldName : filePostName, extraParams : extraParams, url : uploadJson,//去除添加参数 afterUpload : function(data) { ... }); 这要就可以上传视频到七牛云了。

2017/11/11
articleCard.readMore

centos7 xampp 配置Let's Encrypt 证书

centos7 xampp 配置Let's Encrypt 证书 本次使用的是 Let's Encrypt 推荐的快速安装工具 Certbot 尝试 一开始使用文档中的安装方法 yum install certbot-apache 但是发现报错,原来是程序不是最新的 然后根据 certbot: ImportError: ‘pyOpenSSL’ module missing required functionality, 使用pip 安装 清除之前操作 yum remove certbot pyOpenSSL 准备 安装epel扩展源, 安装pip并更新到最新 yum -y install epel-release yum -y install python-pip pip install --upgrade pip 安装 安装最新版 pip install pyOpenSSL pip install certbot 申请ssl 绑定路径 certbot certonly --webroot -w /data/www/html -d zodream.cn 如果有多个域名 那么只要在加上 -d 域名即可,即使是临时增加一个域名,也要把所有域名都加上才行, certbot certonly --webroot -w /data/www/html -d zodream.cn -d test.zodream.cn 如果不同域名在不同文件夹,只需要设一个 -w 作为验证权限即可 更改 apache 站点配置,重启xampp <VirtualHost *:443> ServerName zodream.cn DocumentRoot "/data/www/html" ServerAdmin webmaster@zodream.cn ServerName zodream.cn ServerAlias www.zodream.cn SSLEngine on SSLCertificateFile /etc/letsencrypt/live/zodream.cn/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/zodream.cn/privkey.pem ErrorLog "logs/dummy-www.zodream.cn-error_log" CustomLog "logs/dummy-www.zodream.cn-access_log" common </VirtualHost> <Directory "/data/www/html"> DirectoryIndex index.php index.html index.htm Options FollowSymLinks Includes ExecCGI AllowOverride All Order allow,deny Allow from all </Directory> 使用 certbot renew --dry-run 安装自动更新证书 也可以手动更新证书 certbot renew 参考: certbot.eff.org certbot-importerror-pyopenssl-module-missing-required-functionality

2017/10/29
articleCard.readMore

jquery插件系列之延迟加载

思路 当浏览器滚动到指定元素进行加载 可以加载多次 先上代码 enum LazyMode { once, every } class LazyItem { constructor( public element: JQuery, public callback: Function, public mode: LazyMode = LazyMode.once, public diff: number|Function = 0 ) { element.on('lazy-refresh', () => { this.refresh(); }); } private _lastHeight: number; // 上次执行的高度 /** * 重新刷新 */ public refresh() { this._lastHeight = undefined; } /** * 判断能否执行 * @param height * @param bottom */ public canRun(height: number, bottom: number): boolean { if (this.mode == LazyMode.once && this._lastHeight != undefined) { return false; } if (this.element.parent().length < 1) { // 判断元素是否被移除 return false; } if (typeof this.diff == 'function') { return this.diff.call(this, height, bottom); } let top = this.element.offset().top; return top + this.diff >= height && top < bottom; } public run(height: number, bottom: number, index: number = 0): boolean { // if (!this.canRun(height, bottom)) { // return false; // } this.callback.call(this, this.element, height, bottom, index); this._lastHeight = height; return true; } } class Lazy { constructor( public element: JQuery, options ? : LazyOptions ) { this.options = $.extend({}, new LazyDefaultOptions(), options); let $window = $(window); let instance = this; this._init(); $window.scroll(function () { instance.scrollInvote(); }); // 首次执行 this.scrollInvote(); } public options: LazyOptions; private _data: Array<LazyItem>; /** * 页面滚动触发更新 */ public scrollInvote() { let $window = $(window); let height = $window.scrollTop(); let bottom = $window.height() + height; this.run(height, bottom); } public run(height: number, bottom: number) { if (!this._data) { return; } let index: number = 0; for (let i = 0, length = this._data.length; i < length; i ++) { let item = this._data[i]; if (item.canRun(height, bottom)) { item.run(height, bottom, index ++); } // if (item.run(height, bottom) && item.mode == LazyMode.once) { // this._data.splice(i, 1); // } } } // 暂时只做一次 private _init() { this._data = []; let instance = this; this.element.each(function (i, ele) { let item = new LazyItem( $(ele), typeof instance.options.callback != 'function' ? Lazy.getMethod(instance.options.callback) : instance.options.callback, instance.options.mode, instance.options.diff); instance._data.push(item); }); $.each(this.options.data, (i, item: any) => { if (item instanceof LazyItem) { this._data.push(item); return; } if (typeof i == 'string') { item['tag'] = i; } $(item.tag).each(function (i, ele) { let lazyItem = new LazyItem( $(ele), typeof item.callback != 'function' ? Lazy.getMethod(item.callback) : item.callback, item.mode || LazyMode.once, item.diff || 0 ); instance._data.push(lazyItem); }) }); } /** * 全局方法集合 */ public static methods: {[name: string]: Function} = {}; /** * 添加方法 * @param name * @param callback */ public static addMethod(name: string, callback: Function) { this.methods[name] = callback; } /** * 获取方法 * @param name */ public static getMethod(name: string): Function { return this.methods[name]; } } /** * 加载图片,如需加载动画控制请自定义 */ Lazy.addMethod('img', function (imgEle: JQuery) { let img = imgEle.attr('data-src'); $("<img />") .bind("load", function () { if (imgEle.is('img') || imgEle.is('video')) { imgEle.attr('src', img); return; } imgEle.css('background-image', 'url(' + img + ')'); }).attr('src', img); }); /** * 加载模板,需要引用 template 函数 */ Lazy.addMethod('tpl', function (tplEle: JQuery) { let url = tplEle.attr('data-url'); tplEle.addClass('lazy-loading'); let templateId = tplEle.attr('data-tpl'); $.get(url, data => { let html = ''; if (typeof data === 'object') { if (data.code != 200) { return; } html = typeof data.data != 'string' ? template(templateId, data.data) : data.data; } else { html = data; } tplEle.removeClass('lazy-loading'); tplEle.html(html); tplEle.trigger('lazyLoaded'); }, typeof templateId === 'undefined' ? null : 'json'); }); /** * 滚动加载模板,需要引用 template 函数 */ Lazy.addMethod('scroll', function (moreEle: JQuery) { let page: number = parseInt(moreEle.attr('data-page') || '0') + 1; let url = moreEle.attr('data-url'); let templateId = moreEle.attr('data-tpl'); let target = moreEle.attr('data-target'); $.getJSON(url, { page: page }, function (data) { if (data.code != 200) { return; } if (typeof data.data != 'string') { data.data = template(templateId, data.data); } $(target).html(data.data); moreEle.attr('data-page', page); }); }); interface LazyOptions { [setting: string]: any, data ? : {[tag: string]: string | Object} | Array <Object> | Array < Lazy > , tag ? : string | JQuery, callback ? : string | Function, // 回调 mode ? : LazyMode, //执行模式 diff ? : number|Function, //距离可视化区域的距离 } class LazyDefaultOptions implements LazyOptions { mode: LazyMode = LazyMode.once; diff: number = 0 } ; (function ($: any) { $.fn.lazyload = function (options ? : LazyOptions) { return new Lazy(this, options); }; })(jQuery); 本插件使用 typescript 编写, js 请查看 GITHUB 本插件内置两个方法: 1.简单的图片加载。可以参考增加加载动画 给 img 的 src 设置一张 加载动态图片 <img src="loading.gif" data-src="image.jpg" class="lazy"> <script> $("img.lazy").lazyload({ callback: 'img' }); </script> 2.局部加载。依赖 template 函数(参考 art-template) <div class="templateLazy lazy-loading" data-url="1" data-tpl="temp_tpl">div> 主要有两个参数 : data-url 请求网址 data-tpl 模板元素id (lazy-loading 为加载动画) <script id="temp_tpl" type="text/template"> <div>{{ id }}</div> </script> $(".templateLazy").lazyload({ callback: 'tpl' }); 请注意,本插件依赖 jquery

2017/10/29
articleCard.readMore

使用 typescript 写 jQuery 插件

思路 可以接受的参数 默认的参数 类 挂载在 JQuery 上 步骤 参数接口 配置的接口,在js中无用,这里只是为了以后 TS 使用方便,方便智能提示和书写, interface CarouselOptions { range?: number, // 每次移动的距离,默认一个item宽度 itemTag?: string, // 子代的标签 boxTag?: string, // 盒子的标签 spaceTime?: number, // 停顿的时间 animationTime?: string|number, // 动画执行时间 animationMode?: string, // 动画效果 previousTag?: string, // 可点击向前的元素 nextTag?: string, // 可点击向后的元素 thumbMode?: string // 缩略图模式 } 默认参数 class CarouselDefaultOptions implements CarouselOptions { itemTag: string = 'li'; boxTag: string = '.carousel-box'; spaceTime: number = 3000; animationTime: string|number = 1000; animationMode: string = "swing"; previousTag: string = '.carousel-previous'; nextTag: string = '.carousel-next'; } 类 插件具体功能实现 /// class Carousel { constructor( public element: JQuery, options?: CarouselOptions ) { this.options = $.extend({}, new CarouselDefaultOptions(), options); // 合并参数 this._init(); // 初始化,包括长度不足循环补足 this._addEvent(); // 绑定事件 } // 下一张 public next(range: number = this.options.range) { this.goLeft(this._left - range); } // 上一张 public previous(range: number = this.options.range) { this.goLeft(this._left + range); } } 挂载在 JQuery 上 ;(function($: any) { $.fn.carousel = function(options ?: CarouselOptions) { return new Carousel(this, options); }; })(jQuery); --- 用typescript 写 js 一目了然

2017/10/29
articleCard.readMore

UWP 解压 GZIP

准备工作: 通过 NUGET 安装 Microsoft.Bcl.Compression ; 使用命名空间 using System.IO.Compression ; public static async Task Get(string url) { WebRequest request = WebRequest.CreateHttp(new Uri(url)); //创建WebRequest对象 request.Method = "GET"; //设置请求方式为GET request.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"; request.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate"; //设置接收的编码 可以接受 gzip var response = await request.GetResponseAsync(); Stream stream = null; stream = response.Headers[HttpRequestHeader.ContentEncoding].Equals("gzip", StringComparison.CurrentCultureIgnoreCase) ? new GZipStream(response.GetResponseStream(), CompressionMode.Decompress) : response.GetResponseStream(); var ms = new MemoryStream(); var buffer = new byte[1024]; while (true) { if (stream == null) continue; var sz = stream.Read(buffer, 0, 1024); if (sz == 0) break; ms.Write(buffer, 0, sz); } var bytes = ms.ToArray(); var html = GetEncoding(bytes, response.Headers[HttpRequestHeader.ContentType]).GetString(bytes); await stream.FlushAsync(); return html; } 获取编码: public static Encoding GetEncoding(byte[] bytes, string charSet) { var html = Encoding.UTF8.GetString(bytes); var regCharset = new Regex(@"charset\b\s*=\s*""*(?<charset>[^""]*)"); if (regCharset.IsMatch(html)) { return Encoding.GetEncoding(regCharset.Match(html).Groups["charset"].Value); } if (string.IsNullOrEmpty(charSet)) { return Encoding.UTF8; } try { // 解决 gbk gb2312 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); return Encoding.GetEncoding(charSet); } catch (Exception) { return Encoding.UTF8; } } 虽然使用 HttpClient 更简单 var http = new HttpClient(); http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"); http.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate"); var response = await http.GetAsync(new Uri(url)); response.EnsureSuccessStatusCode();//确保请求成功 但是它的响应头并没有 Content-Encoding ,所以无法直接判断需不需要 Gzip 解压。

2017/10/29
articleCard.readMore

第六课 Model介绍

最新文档 具体文档请查看【zodream】 介绍 主要用于连接数据库,默认使用单例模式通过PDO连接,其他连接方式有MYSQL、MYSQLI。 使用 默认通过继承 Zodream\Database\Model\Model 扩展,添加具体的方法 <?php namespace Domain\Model; use Domain\Model\Model; /** * Class LogModel * @property integer $id * @property integer $type * @property float $number * @property string $remark * @property integer $created_at * @property integer $updated_at */ class LogModel extends Model { public static function tableName() { return 'log'; } protected function rules() { return [ 'id' => 'required|int', 'type' => 'int:0,9', 'number' => '', 'remark' => '', 'created_at' => 'int', 'updated_at' => 'int', ]; } protected function labels() { return [ 'id' => 'Id', 'type' => 'Type', 'number' => 'Number', 'remark' => 'Remark', 'created_at' => 'Created At', 'updated_at' => 'Updated At', ]; } } 一般是控制器调用方法传给视图。

2017/10/29
articleCard.readMore

ZoDream 主程序下载

主程序地址: zodream , Demo地址: PHP-ZoDream , 文档地址:zodream 其他获取方式: composer: composer require zodream/zodream

2017/10/29
articleCard.readMore

第五课 生成功能介绍

最新文档 具体文档请查看【zodream】 介绍 自动生成程序,主要分为三部分:获取数据库、模板、生成程序。其中生成程序又分为:Config、Controller、Model、Module、View; 函数介绍 make() 主入口,启动程序 getDatabase() 获取所有数据库名 getTable() 获取数据库下的所有表名 getColumn() 获取表下的所有列名 makeController($name) 生成控制器 $name 为控制器名 makeModel($name, $columns) 生成数据 $name 数据名 $columns 所有列名及详细信息 makeModule($module, $table) 生成模块名及使用的数据表 makeConfig($configs, $module = APP_MODULE) 生成配置信息文件,$configs 配置信息数组,$module 文件名 makeView($name = 'Home', $column = array()) 生成视图模板文件,$name 文件夹名,$column 所有列名 使用 在配置文件中手动添加,在本地运行时自动开启 'modules' = array( //模块 'gzo' => 'Zodream\Module\Gzo' ) 输入网址 http://localhost/gzo 已完成功能 web 界面操作 通过简单的操作就能自动生成对应的代码

2017/10/17
articleCard.readMore

登陆后立即掉线

session 经常变动,导致登录不了, 原因: session_id 获取不到,cookie 中的PHPSESSIONID 变成了 ,_PHPSESSIONID 解决方法: session_name('ZoDream'); 更改cookie中的SESSION_NAME

2016/3/3
articleCard.readMore

第四课 视图模板

最新文档 具体文档请查看【zodream】 位置 位于 UserInterface 下,以文件夹区分模块 使用 在控制中 $this->show(); 表示使用 UserInterface/模块/控制器/方法.php $this->show('index'); 表示使用 UserInterface/模块/index.php $this->show('home.index'); 表示使用 UserInterface/模块/home/index.php 主要函数 $this->ech($name, $default); 输出$name的值,如果为空则输出 $default ,默认为 null ,当$name为 null 时,以字符串的形式输出数组; $this->get($name, $default); 同理返回; $this->set($name, $value); 传递参数值 $this->extend($view, $script); 加载其他共享视图,并传递脚本路径; $this->jcs($arg, ...); 生成js css引用, 可以为 匿名函数,以 @ 开头是绝对路径 $this->url($url); 输出绝对路径 $this->asset($file); 输出绝对资源路径 例如 (所有文件在 UserInterface/模块 文件夹下) layouts/header.php <?php defined('APP_DIR') or exit(); use Zodream\Template\View; /** @var $this View */ ?> <!DOCTYPE html> <html lang="<?=$this->get('language', 'zh-CN')?>"> <head> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="Description" content="<?=$this->description?>" /> <meta name="keywords" content="<?=$this->keywords?>" /> <title><?=$this->title?></title> <?=$this->header();?> </head> <body> layouts/footer.php <?php defined('APP_DIR') or exit(); use Zodream\Template\View; /** @var $this View */ ?> <?=$this->footer()?> </body> </html> index.php <?php defined('APP_DIR') or exit(); use Zodream\Template\View; /** @var $this View */ $this->title = 'ZoDream'; $this->extend('layouts/header'); ?> Alert body ... <?php $this->extend('layouts/footer');?>

2017/10/29
articleCard.readMore

第三课 控制器使用

最新文档 具体文档请查看【zodream】 步骤 第一步 在 Service 文件夹下新建 Home 文件夹,并新建 HomeController.php 文件作为默认控制器 第二步 <?php namespace Service\Home; use Zodream\Route\Controller\Controller; class HomeController extends Controller { public function indexAction() { return $this->show(); } } 解释 第一句,声名命名空间; 第二句,使用控制器基类; 第三句,使本控制器继承至基类; 第四句,声名方法,能根据网址使用的必须带Action后缀; 第五句,显示视图; 参数说明 $this->show($name, $data); $name 为空或 null 时,根据路由得到的控制器名和方法名自动生成路径 Home/index.php 为数组时,此时得到的值作为要传递的参数,路径同上; 为匿名方法,直接执行,当有返回值则输出匿名函数的返回值; 为 @ 开头的字符串,直接输出字符串; 其他则作为路径解析 $data 为空 为字符串是,参数则以 data 命名; 为数组,并入参数

2016/3/3
articleCard.readMore

第二课 安装及入口配置

最新文档 具体文档请查看【zodream】 基本骨架 请下载 PHP-ZoDream 中的 starter 分支,这个分支只包含基本的骨架,适合新项目开发 主分支为框架配套的开发项目,包括本站的源码和一些开发中的模块 搭建完整架构 请配合composer.phar 在命令行执行 php composer.phar install 入口配置 html/index.php <?php use Zodream\Service\Web; require_once dirname(__DIR__).'/Service/Bootstrap.php'; $app = new Web(APP_DIR); $app->autoResponse(); 解释 这里主要加载主程序 ,具体引入文件在 Service/Bootstrap.php <?php if (version_compare(PHP_VERSION, '7.1.0', '<')) { die('require PHP > 7.1.0 !'); } defined('DEBUG') or define('DEBUG', true); //是否开启测试模式 define('APP_DIR', dirname(__DIR__)); //定义路径 require_once APP_DIR.'/vendor/autoload.php'; 第一句,是指要求 PHP 版本最低要求为 7.1 ; 第二句,是指是否开启测试模式,在上线时#必须#改为 FALSE 或注释此句,不然会爆出错误并提示路径; 第三句,是指定义路径,是指当前程序所在的位置,如果本入口文件与其他文件夹不处于同级目录,请指向其他文件夹所处的父目录; 第四句,是指定义组件名,这个主要是为了方便多个网址同时存在,并共用同一个主程序; 第五句,载入 Composer 的自动加载程序,方便以后使用其他类; 第六句,正式启动程序,手动控制是为了能够手动注册其他路由;

2016/3/3
articleCard.readMore

第一课 介绍

最新文档 具体文档请查看【zodream】 整体说明 在本程序中,主要使用的是领域驱动设计模式,在主程序中主要分为四部分:用户层、服务层、领域层、基础层。 用户层主要面向用户,包括界面,负责展示给用户看,是面向浏览器端。 服务层主要负责接收用户信息,分配领域层执行具体操作,并将结果返回给用户。 领域层负责具体执行流程,是整个模式的核心。 基础层主要负责底层方法,提供基础设施,为领域层的执行提供底层方法。 基本步骤 在本程序中, 从浏览器接受网址, 启动指定的脚本文件(例如index.php),启动主程序(Zodream/Service/Web.php),进入路由导航(Zodream/Route/Router.php -> Route.php),根据配置文件信息路由驱动解析网址(例如:优雅链接),分配到具体的控制器(Service/Home/HomeCcontroller.php),执行控制器中的指定方法(indexAction),返回具体的界面($this->show('index') -> UserInterface/Home/index.php) 路由介绍 在整个流程中,路由的原理是: 第一步,判断是否是首页; 第二步,判断是否是在配置文件中注册的路由; 第三步,进行自动判断,先判断网址中包含控制器和方法,再判断只包控制器(方法为默认 index),再判断只包含方法(控制器为默认Home),最后报错; 其中网址可能包含参数,参数以数字分割,如果提取的参数是单数,第一个数字则为分隔符,忽略,然后把参数进行配对,奇数为参数名,偶数为参数值。 控制器介绍 控制器中包括变量和方法,变量是 $rules ,指定方法的规则,基本规则有 * 无要求 * ? 必须是游客, * @ 必须已经登录, * p 必须POST提交, * ! 未开放不能访问, * 其他 要求通过验证权限; 方法,方法名必须加上APP_ACTION定义的后缀(避免与普通方法混淆), 加载数据或插件, 可以通过 use、include、include_once、require、require_once , 也可通过 $this->loader->model()、$this->loader->library()、$this->loader->plugin() 加载, 然后通过 $this->__() 使用; 通过 $this->show() 指向界面, 如果第一个参数不是 string 则,根据路由解析出来的控制器名和方法名自动加载界面(例如 HomeController::indexAction -> Home/index.php), 如果是指定则可以用户 . 代替 / ,对界面传参数, 可以用 $this->send() 传任意值(如果不是数组则自动加在 data 下)或 $this->show() 第一个参数为数组或第二个参数; View介绍 主要函数 $this->ech($name, $default); 输出$name的值,如果为空则输出 $default ,默认为 null ,当$name为 null 时,以字符串的形式输出数组; $this->get($name, $default); 同理返回; $this->set($name, $value); 传递参数值 $this->extend($view, $script); 加载其他共享视图,并传递脚本路径; $this->jcs($arg, ...); 生成js css引用, 可以为 匿名函数,以 @ 开头是绝对路径 $this->url($url); 输出绝对路径 $this->asset($file); 输出绝对资源路径 结束语 基本介绍就这么多。

2016/3/3
articleCard.readMore