博客 (50)

通常,我们在更新一条数据库记录时,EF 先取出这一条记录:

var log = DbContext.MyTable.Find(id);

然后赋值字段并保存:

log.Result = "OK";
DbContext.SaveChanges();

这样就会产生两次数据库查询。

正确的做法是用 Attach 方法。

先创建一个仅包含主键的对象:

var log  = new MyTable { Id = id };

将对象附加到上下文:

DbContext.MyTable.Attach(log);

然后更新需要更新的字段:

log.Result = "OK";
DbContext.SaveChanges();

这样,能在保留其它字段值的前提下,减少一次数据库查询。

但是需要注意的是:

当某非 null 字段需要恢复默认值时,EF 会忽略这个更改。(可能会因 EF 版本等原因有不同的结论)

举个例子:

某记录有个 int 型字段 a,在数据库中这个记录的 a 的值为 1,但 C# 中 int 型的默认值为 0,所以当 Attach 附加这个对象后,如果设置 log.a 为 0,那么保存后 a 的值仍为 1。

xoyozo 6 天前
126

本文记录于 2024 年 11 月。



升级前期望(最新正式版)最终选择
操作系统Alibaba Cloud Linux 3Alibaba Cloud Linux 3Alibaba Cloud Linux 3
管理面板宝塔面板 Linux 版 9.2.0宝塔面板 Linux 版 9.2.0宝塔面板 Linux 版 9.2.0
Web 服务nginx 1.24nginx 1.24nginx 1.24
脚本语言PHP 7.4PHP 8.2PHP 8.2
数据库
PolarDB MySQL 5.6RDS MySQL 9.1PolarDB MySQL 8.0
论坛程序
Discuz! X3.4 GBKDiscuz! X5.0Discuz! X3.5 UTF8


版本选择原因:

  • Discuz! X5 刚刚发布,生态尚未成熟,考虑到是老论坛升级,所以选择 X3.5。

  • Discuz! X3.5 目前最高支持 PHP 8.2、MySQL 8.0。


完整升级步骤:

  1. 购买新的 ECS、PolarDB,具体环境配置步骤参此文

  2. 安装宝塔面板并配置;

  3. 安装 nginx 及 PHP;

  4. 创建网站、配置 SSL、伪静态、防盗链、可写目录禁执行仅允许部分入口文件执行等(.conf);

  5. 配置 hosts;

  6. 如果是正式升级阶段,关闭论坛,防止产生新数据。

  7. 备份原网站程序、PolarDB 数据库;

  8. PolarDB 创建快照。

    测试阶段:从快照还原到新实例(MySQL 5.6 不能直接恢复到 MySQL 8.0),然后从 5.6 迁移到 8.0.x;

    正式阶段:全量模式直接从原实例迁移到 8.0.x,若增量模式且存在触发器,建议从快照还原;

  9. 上传原网站程序到新的站点目录下;

  10. 按 Discuz! X 升级文档升级 X3.4 至 X3.5;详情见下文 ↓;

  11. 升级完成后切换到 PHP 8;

  12. 配置 OSS、Redis、更新缓存等;

  13. 测试论坛基本功能是否正常;检查附件是否能够正常上传检查附件是否正常显示;全面检查控制台配置;

  14. 逐个开启插件并检查兼容性(短信通、马甲引擎等);

  15. 按二开备忘录逐个按需进行二开;

  16. 逐个修改调用论坛接口的项目及直接调用论坛数据库的项目;

  17. 调试 MAGAPP 接口;

  18. 尝试强制 https 访问;

  19. 将以上所有修改后的程序保留备份;发布升级公告并关闭论坛;重复以上步骤;修改域名解析;开启论坛;

  20. 配置 IP 封禁、定时器、日志、自动备份、配置其它 ECS 的 hosts 等;

  21. 查看搜索引擎中收录的地址,是否有无法访问的情况;

  22. 尝试将历史遗留的本地附件全部转移到 OSS;

  23. 建议 在新服上创建 3 个网站:

    1. 第 1 个用来尝试迁移、升级、二开,并测试所有功能

    2. 第 2 个用来再次重复这些步骤,最终保留程序代码及 UGC 文件,作为最终的网站程序,以正式域名作为网站根目录路径;

    3. 第 3 个用来正式升级,主要功能是升级最新数据库。完成后修改第 2 个网站的数据库连接指向到最新的数据库,差异化同步新增的 UGC 文件到第 2 个网站。


Discuz! X 升级步骤及注意点:

  • 因 PolarDB MySQL 不支持压缩,所以应移除 Discuz! 和 UCenter 代码中所有的 MYSQLI_CLIENT_COMPRESS,将 , MYSQLI_CLIENT_COMPRESS 替换为 /*, MYSQLI_CLIENT_COMPRESS*/

  • 升级前务必先修改 ./config/ 目录下的数据库/缓存连接信息,以防出现新站连接老库的情况;

  • 官方文档进行升级,升级前先修改一下:

  • 【UC】先升级 UCenter 1.6 至 1.7。

    将 /uc_server/data 权限改为 777 并递归。

    打开 update_ucenter_adult.php 修改 $limit 值为 10000 以免执行超时

    因通信失败的“发送通知失败”可以直接改 URL 参数跳过就完成了,原因可能是因数据量大,发送改名通知执行改名时超时。等DZ升级完成后UC会自动重试改名通知,或单独写个 php 文件将 UCenter 的用户名同步到应用的数据库。

  • 【DZ】运行到 /install/update_adult.php?step=innodb&table=pre_common_member_grouppm 时报错:Duplicate key name 'gpmid' ALTER TABLE common_member_grouppm ADD INDEX gpmid(gpmid);

    解决方法:先删除索引,因保存时提示失败,应同时取消 gpmid 字段的自增,转换成功后再设置自增。

  • 【DZ】运行到 /install/update_adult.php?step=file 一片空白而停止

    正在解决

  • 【问题】common_menber 表用户名字段编码转换失败

    原因是部分用户名包含特殊字符(如全角空格),用以下语句查看两个表同一 uid 的不同用户:

    SELECT 
        u.uid, 
        u.username AS ucenter_username, 
        c.username AS common_username
    FROM 
        pre_ucenter_members u
    JOIN 
        pre_common_member c ON u.uid = c.uid
    WHERE 
        u.username <> c.username

    Discuz! 用户登录都是以 UCenter 中的用户名为主,所以可以写个小程序直接将 pre_ucenter_members 的用户名同步到 pre_common_member 中,另外 pre_ucenter_members 的用户数少于 pre_common_member 是正常的。若 pre_common_member_archive 表遇到错误同理。

  • 如果用户登录慢,或打开 UCenter 慢,是因为 UC 正在通知 DZ 改用户名,每次打开一个页面会更改一个用户名,具体可以查看 pre_ucenter_notelist 中 closed 为 0 的队列,或进入 UC 后台-数据列表-通知列表 查看。

  • 【问题】发布主题遇到错误:(1062) Duplicate entry '*' for key 'pid'

    【原因】forum_post 中的 pid 不是自动增长的,而是由表 forum_post_tableid 中自动增长的 pid 生成的。如果生成的 pid 值已在 forum_post 表中存在,则会出现此错误。

  • 【解决】迁移数据库时应关闭论坛,以防止 forum_post 表有新数据插入。

  • 【问题】打开帖子页面 ./thread-***-1-1.html 显示 404 Not Found,而 ./forum.php?mod=viewthread&tid=*** 可以正常打开

    【原因】未配置伪静态(可在宝塔面板中选择)

  • 【问题】打开 UCenter 时报错:UCenter info: MySQL Query Error SQL:SELECT value FROM [Table]vars WHERE name='noteexists'

    【解决】打开文件 ./uc_server/data/config.inc.php 配置数据库连接

  • 【问题】打开登录 UCenter 后一片空白

    【解决】将目录 ./uc_server/data/ 设为可写

  • 需要将原来安装的插件文件移回 ./source/plugin/ 目录,并设置可写;

  • 界面-表情管理,界面-编辑器设置-Discuz!代码


后续 Discuz! X3.5 小版本升级注意事项:

  1. 确认插件是否支持新版本(如短信通)

  2. 先创建一个新网站测试二开代码

  3. 保留 /config/、/data/、/uc_client/data/、/uc_server/data/、/source/plugin/,其它移入 old

  4. 上传文件

  5. 移回其它需要的文件,如:

  6. -- 勋章/loading/logo/nv 等:/static/image/common/

  7. -- 表情:/static/image/smiley/

  8. -- 水印:/static/image/common/watermark.*

  9. -- 风格:/template/default/style/t2/nv.png 等

  10. -- 默认头像:/uc_server/images/noavatar_***.gif

  11. -- 根目录 favicon.ico 等

  12. -- 及其它非 DZ 文件

  13. 再次检查可写目录的写入权限和禁止运行 PHP 效果。


xoyozo 6 个月前
710

宝塔面板中-安全-系统防火墙 功能非常丰富,特别是“地区规则”用来屏蔽来自国外的请求非常有用,那些通过 URL 来找网站漏洞的恶意请求大多来自国外。但是添加规则经常无反应

image.png

于是找到这个功能对应的配置文件:/etc/firewalld/zones/public.xml

<?xml version="1.0" encoding="utf-8"?>
<zone>
  <short>Public</short>
  <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
  <service name="ssh"/>
  <service name="dhcpv6-client"/>
  <port protocol="tcp" port="80"/>
  <port protocol="tcp" port="443"/>
  <masquerade/>
  <rule family="ipv4">
    <source address="43.131.232.135"/>
    <drop/>
  </rule>
  <rule family="ipv4">
    <source address="103.150.11.45"/>
    <drop/>
  </rule>
  <rule>
    <source ipset="US"/>
    <drop/>
  </rule>
  <rule>
    <source ipset="GB"/>
    <drop/>
  </rule>
</zone>

修改完成后重启防火墙。

但是看到的效果好像并不一致,甚至有时候服务器重启后防火墙是关闭的,但是有时候又是生效的。

关键的问题是CPU占用异常高。

反正宝塔的系统防火墙功能挺好挺强大,但是用起来不稳定不顺手,有些暴殄天物了。

相关阅读:

系统防火墙添加规则转圈卡死

2024年8月30日补充 :宝塔面板中配置禁用IP等规则时,提示保存成功或未提醒是否成功,但效果是未成功。其实在宝塔自己的配置里已经记录了(包含备注),但在系统防火墙配置文件中未更改成功,手动修改系统防火墙配置文件就能完全修改成功。(系统防火墙配置文件中没有备注信息,根据IP地址等规则与宝塔自己的配置文件里的记录关联后,在宝塔面板安全模块中展示出来就有备注了。)

2024年8月30日下午补充:在宝塔的软件商店找到这个应用,看到红色的说明,啥都明白了:

image.png

于是果断关闭了系统防火墙,开始研究其它替代方案。

xoyozo 8 个月前
693

因 MySQL 8 默认使用 utf8mb4 字符集,如果是从 MySQL 5-7 等低版本迁移的,那么仍然是 utf8(相当于 utf8mb3)。

这在保存字符串时有可能会报错:

An error occurred while saving the entity changes. See the inner exception for details.

Incorrect string value: '\xF0\xA1\x90\x93\xE6\x9D...' for column 'Html' at row 1

即使连接字符串上加上 CharSet=utf8 或 CharSet=utf8mb3 或 CharSet=utf8mb4 也没用。

那么只能把数据库的字符集改为 utf8mb4。


更改字符集和排序方式前必须先备份数据库!


  1. 更改现有数据库的字符集和排序规则

    ALTER DATABASE mydatabase 
    CHARACTER SET utf8mb4 
    COLLATE utf8mb4_unicode_ci;
  2. 更改现有表的字符集和排序规则

    ALTER TABLE mytable 
    CONVERT TO CHARACTER SET utf8mb4 
    COLLATE utf8mb4_unicode_ci;
  3. 更改现有列的字符集和排序规则

    当列单独设置了字符集时执行,未设置的不需要执行,会继承表的字符集

    ALTER TABLE mytable 
    MODIFY mycolumn VARCHAR(255) 
    CHARACTER SET utf8mb4 
    COLLATE utf8mb4_unicode_ci


数据库中可能存在许多表,数据表中可能存在许多列,使用下面的命令可以生成批量执行的命令:

  1. 更改所有表的默认字符集和排序规则

    USE database_name; -- 替换为你的数据库名
    
    SELECT CONCAT('ALTER TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') 
    FROM information_schema.TABLES 
    WHERE TABLE_SCHEMA = 'database_name'; -- 再次替换为你的数据库名
  2. 更改所有列的字符集和排序规则

    USE database_name; -- 使用你的数据库名
    
    SELECT CONCAT('ALTER TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '` MODIFY `', COLUMN_NAME, '` ', COLUMN_TYPE, ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') 
    FROM information_schema.COLUMNS 
    WHERE TABLE_SCHEMA = 'database_name'; -- 使用你的数据库名


另外给出几条查询语句:

  1. 查看特定数据库的字符集和排序规则

    SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
    FROM information_schema.SCHEMATA
    WHERE SCHEMA_NAME = 'your_database_name';
  2. 查看特定表的字符集和排序规则

    SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION
    FROM information_schema.TABLES
    WHERE TABLE_SCHEMA = 'your_database_name';
  3. 查看特定列的字符集和排序规则

    SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME 
    FROM information_schema.COLUMNS 
    WHERE TABLE_SCHEMA = 'your_database_name' AND TABLE_NAME = 'your_table_name';
xoyozo 1 年前
2,109

如果手机开启了自动横屏,那么网页就会跟着旋转,导致网页宽度变大,高度变小,页面布局不友好,我们可以通过判断屏幕方向 api,用 css 设置旋转。


HTML:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>保持竖屏示例</title>
</head>
<body>
    <div id="app">
        <div class="page">
            <div>把手机横过来试试</div>
            <div>页面永远保持竖屏方向</div>
        </div>
    </div>
</body>
</html>

CSS:

body { margin: 0; }
.page { text-align: center; padding-top: 100px; }

@media screen and (orientation: landscape) {
    body { width: 100vh; height: 100vw; overflow: hidden; }
    .page.landscape-primary { transform: rotate(-90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 100%; left: 0; }
    .page.landscape-secondary { transform: rotate(90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 0; left: 100%; }
}

JS:

function fn_set_orientation() {
    var orientation = window.screen.orientation || window.screen.mozOrientation || window.screen.msOrientation;
    const div = document.querySelector('.page');
    console.log(orientation.type)
    switch (orientation.type) {
        case 'landscape-primary': {
            div.classList.remove('landscape-secondary');
            div.classList.add('landscape-primary');
            break;
        }
        case 'landscape-secondary': {
            div.classList.remove('landscape-primary');
            div.classList.add('landscape-secondary');
            break;
        }
        default: {
            div.classList.remove('landscape-primary');
            div.classList.remove('landscape-secondary');
            break;
        }
    }
}
window.addEventListener("orientationchange", function () {
    fn_set_orientation();
});
fn_set_orientation();

示例:

https://xoyozo.net/Demo/Orientation


xoyozo 1 年前
1,057

在 Vue 中,当你将一个布尔对象双向绑定到 radio 输入上时,确保 radio 的 value 属性仍然保持布尔类型是很重要的。如果 value 属性是字符串,那么 Vue 会将其解释为字符串而不是布尔值。以下是一个示例,演示如何正确地将布尔对象双向绑定到 radio 按钮,并确保 value 属性保持布尔类型:

<template>
  <div>
    <input type="radio" v-model="myBoolean" :value="true" /> True
    <input type="radio" v-model="myBoolean" :value="false" /> False
    <p>myBoolean: {{ myBoolean }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      myBoolean: false, // 布尔对象
    };
  },
};
</script>

在这个示例中,我们使用了 v-model 指令将 myBoolean 属性与两个 radio 按钮进行了双向绑定。通过将 value 属性设置为 true 和 false,我们确保了 myBoolean 属性保持布尔类型。

这样,当用户选择其中一个 radio 按钮时,myBoolean 属性将保持布尔值而不是字符串。确保 value 属性的类型与要绑定的数据类型匹配,以确保绑定的数据类型保持一致。

同样的问题出现在 select 上也是一样:

<select v-model="cat.colorId" required>
    <option :value="null">不选择</option>
    <option v-for="color in colors" :value="color.id">{{color.name}}</option>
</select>


xoyozo 2 年前
1,568

语言集成查询(LINQ)为 C# 和 VB 提供语言级查询功能和高阶函数 API,让你能够编写具有很高表达力度的声明性代码。

LINQ 有两种写法:查询语法和方法语法,查询语法又称查询表达式语法。

查询语法:

from 变量名 in 集合 where 条件 select 结果变量

方法语法:

集合.Where(变量名 => 条件)


LINQ 的标准查询运算符及语法示例

类型操作符功能方法语法查询语法
投影操作符Select用于从集合中选择指定的属性或转换元素
cats.Select(cat => cat.color)
from cat in cats
select cat.color
SelectMany用于在嵌套集合中选择并平铺元素
families.SelectMany(family => family.members
.Select(member => member.name))
from family in families
from member in family.members
select member.name
限制操作符Where根据指定的条件筛选集合中的元素
cats.Where(cat => cat.color == "white")
from cat in cats
where cat.color == "white"
select cat
排序操作符

OrderByOrderByDescending、ThenBy、ThenByDescending

用于对集合中的元素进行排序
cats.OrderBy(cat => cat.age)
from cat in cats
order by cat.age
select cat
Reverse将集合中的元素顺序反转
cats.Reverse()


联接操作符

Join

GroupJoin

用于在两个集合之间执行内连接(Join)操作,或者对一个集合进行分组连接(GroupJoin)操作内联接
families.Join(
    members,
    family => family.familyId,
    member => member.familyId,
    (family, member) => new
    {
        family.familyId,
        member.name,
    })

左连接

families.GroupJoin(
    members,
    family => family.familyId,
    member => member.familyId,
    (family, familyMembers) => new
    {
        family.familyId,
        name = familyMembers.FirstOrDefault()?.name
    })


内联接
from family in families
join member in members on family.familyId equals member.familyId
select new
{
    family.familyId,
    member.name,
}

左连接

from family in families
join member in members on family.familyId equals member.familyId into g
from member in g.DefaultIfEmpty()
select new
{
    family.familyId,
    name = member?.name,
}


分组操作符GroupBy根据指定的键对集合中的元素进行分组

串联操作符Concat将两个集合连接成一个新集合

聚合操作符

AggregateAverage、Count、LongCount、Max、Min、Sum

Aggregate 可以用于在集合上执行自定义的累积函数,其他方法用于计算集合中的元素的平均值、总数、最大值、最小值和总和

集合操作符

DistinctUnion、Intersect、Except

用于执行集合间的不同操作,Distinct 移除重复元素,Union 计算两个集合的并集,Intersect 计算两个集合的交集,Except 计算一个集合相对于另一个集合的差集

生成操作符

EmptyRange、Repeat

Empty 创建一个空集合,Range 创建一个包含一系列连续数字的集合,Repeat 创建一个重复多次相同元素的集合

转换操作符

AsEnumerableCast、OfType、ToArray、ToDictionary、ToList、ToLookup

这些方法用于将集合转换为不同类型的集合或字典

元素操作符

DefaultIfEmptyElementAtElementAtOrDefaultFirst、Last、FirstOrDefault、LastOrDefault、Single、SingleOrDefault

这些方法用于获取集合中的元素,处理可能的空集合或超出索引的情况

相等操作符SequenceEqual用于比较两个集合是否包含相同的元素,顺序也需要相同

量词操作符All、Any、Contains用于检查集合中的元素是否满足特定条件,All 检查是否所有元素都满足条件,Any 检查是否有任何元素满足条件,Contains 检查集合是否包含特定元素

分割操作符Skip、SkipWhile、Take、TakeWhile用于从集合中跳过一些元素或只取一部分元素,可以结合特定条件进行操作

了解立即执行延迟执行可以大大改善性能。

xoyozo 2 年前
1,709


本文过程较为复杂,且部分内容已无法实现,建议点击这里查阅最新的操作方法!


前言:本文操作需要你具备浏览器安装和使用扩展插件的能力、以及简单的使用命令行的能力。


第一步:下载视频

首先我使用 Edge 浏览器(Chrome 操作类似,不过安装扩展需要科学上网)。

2023 年初的时候,用 FetchV 这个扩展是非常方便的,它会自动嗅到网页中的视频,即使没有嗅到也可以用录制的方式来保存。

但到了过了一两个月发现 FetchV(及其马甲)经常打不开,或者无法嗅到视频流,更别提录制了。

所以我找到了另一款专业视频下载神器

image.png

当然它的马甲们用法也是大同小异,主界面是这样的:

image.png

开启捕获,同意下载多个文件,然后播放视频,耐心等待。

心急的朋友可以用修改播放速度的扩展(如 视频加速减速控制),例如用 16 倍速,那么一个 16 分钟的视频用 1 分钟就播放完成了。(或者在 F12 的控制台中使用 JS 代码加速:document.querySelector('video').playbackRate = 16; )

等小浮框提示“捕获完成 点击下载”的时候就可以保存到磁盘上了。

image.png


第二步:音频修复

下载后它会有两个 .mp4 文件保存到电脑上,其中较大的是视频部分,较小的是音频部分。

但是有个小问题是,这个音频文件用 Windows 自带播放器播放正常,用 potplayer 等第三方播放器或者一些视频编辑软件播放就会有问题。

我在 Microsoft Store 中找了一款叫 Movie Maker - Video Editor 的应用,

apps.39913.13859945615656852.b1baa2a9-3899-402f-856c-99b5767be0e6.png

在这个软件中添加刚才的只有音轨的视频文件会提示转码,转码后的 .mp4 文件音轨就正常了。

具体步骤是依次点击“Create New Project”,“Add clip”,“Photo/Video”,选择文件后“Transcode”,保存以后默认会在文件名后加上“ (Transcoded).mp4


第三步:音视频合成

接下来是合成视频和音频,将视频文件命名为 v.mp4,音频文件命名为 a.mp4。

FFmpeg 官网下载 Windows 版,然后使用这个命令从音频文件中提取音轨:

ffmpeg -i a.mp4 -vn -acodec copy a.aac

再用这个命令将 v.mp4 的视频和 a.aac 的音频合成一个新的文件

ffmpeg -i v.mp4 -i a.aac -c:v copy -c:a copy -map 0:v:0 -map 1:a:0 output.mp4

相比于其它的视频转换工具,ffmpeg 直接提取合并的速度是极快的。


Tips:

  • 小鹅通中学习过的课程再次打开会从上次关闭的地方开始播放,这会导致捕获不全,可以将进度条手动拖到末尾,这样它会停止播放,再次刷新就会从头开始播放。

  • 浮框中“点击下载”可能没反应,估计是在合成文件,过几秒钟多点几下,不会重复下载。

  • 如果要下载的视频比较长或者比较多,可以像我一样在虚拟机里进行,把视频播放器的音量开到最大,把操作系统的声音关闭。

xoyozo 2 年前
20,709

不知道从哪个版本的 Chrome 或 Edge 开始,我们无法通过 ctrl+v 快捷键将时间格式的字符串粘贴到 type 为 date 的 input 框中,我们想办法用 JS 来实现。


方式一、监听 paste 事件:

const input = document.querySelector('input[type="date"]');
input.addEventListener('paste', (event) => {
    input.value = event.clipboardData.getData('text');
});

这段代码实现了从页面获取这个 input 元素,监听它的 paste 事件,然后将粘贴板的文本内容赋值给 input。

经测试,当焦点在“年”的位置时可以粘贴成功,但焦点在“月”或“日”上不会触发 paste 事件。


方式二、监听 keydown 事件:

const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
    if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
        event.preventDefault();
        var clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
        input.value = clipboardData.getData('text');
    }
});

测试发现报错误:

Uncaught TypeError: Cannot read properties of undefined (reading 'getData')

Uncaught TypeError: Cannot read properties of undefined (reading 'clipboardData')

看来 event 中没有 clipboardData 对象,改为从 window.navigator 获取:

const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
    if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
        event.preventDefault();
        window.navigator.clipboard.readText().then(text => {
            input.value = text;
        });
    }
});

缺点是需要用户授权:

image.png

仅第一次需要授权,如果用户拒绝,那么以后就默认拒绝了。


以上两种方式各有优缺点,选择一种适合你的方案就行。接下来继续完善。


兼容更多时间格式,并调整时区

<input type="date" /> 默认的日期格式是 yyyy-MM-dd,如果要兼容 yyyy-M-d 等格式,那么:

const parsedDate = new Date(text);
if (!isNaN(parsedDate.getTime())) {
    input.value = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
}

以 text 为“2023-4-20”举例,先转为 Date,如果成功,再转为英国时间格式“20-04-2023”,以“/”分隔,逆序,再以“-”连接,就变成了“2023-04-20”。

当然如果希望支持中文的年月日,可以先用正则表达式替换一下:

text = text.replace(/\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*/, "$1-$2-$3");


处理页面上的所有 <input type="date" />

const inputs = document.querySelectorAll('input[type="date"]');
inputs.forEach((input) => {
    input.addEventListener(...);
});


封装为独立域

避免全局变量污染,使用 IIFE 函数表达式:

(function() {
  // 将代码放在这里
})();

或者封装为函数,在 jQuery 的 ready 中,或 Vue 的 mounted 中调用。


在 Vue 中使用

如果将粘贴板的值直接赋值到 input.value,在 Vue 中是不能同步更新 v-model 绑定的变量的,所以需要直接赋值给变量:

<div id="app">
    <input type="date" v-model="a" data-model="a" v-on:paste="fn_pasteToDateInput" />
    {{a}}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
    const app = Vue.createApp({
        data: function () {
            return {
                a: null,
            }
        },
        methods: {
            fn_pasteToDateInput: function (event) {
                const text = event.clipboardData.getData('text');
                const parsedDate = new Date(text);
                if (!isNaN(parsedDate.getTime())) {
                    const att = event.target.getAttribute('data-model');
                    this[att] = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
                }
            },
        }
    });
    const vm = app.mount('#app');
</script>

示例中 <input /> 添加了 data- 属性,值同 v-model,并使用 getAttribute() 获取,利用 this 对象的属性名赋值。 

如果你的 a 中还有嵌套对象 b,那么 data- 属性填写 a.b,方法中以“.”分割逐级查找对象并赋值

let atts = att.split('.');
let target = this;
for (let i = 0; i < atts.length - 1; i++) {
    target = target[atts[i]];
}
this.$set(target, atts[atts.length - 1], text);


xoyozo 2 年前
1,746

本文以 vue 3 为例,vue 2 同理。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>clipboard.js 在 vue.js 中使用</title>
</head>
<body>
    <div id="app">
        <ul>
            <li v-for="item in list">
                {{item}}
                <button class="btn-cp" :value="item">复制</button>
            </li>
        </ul>
    </div>
    <script src="//xxx/vue/core-3.x.x/vue.global.js"></script>
    <script src="//xxx/clipboard/clipboard.js-2.x.x/dist/clipboard.min.js"></script>
    <script>
        const app = Vue.createApp({
            data: function () {
                return {
                    list: ["aaa", "bbb", "ccc"]
                }
            },
            mounted: function () {
                var clipboard = new ClipboardJS('.btn-cp', {
                    text: function (trigger) {
                        return trigger.getAttribute('value');
                    }
                });
                clipboard.on('success', function (e) {
                    alert('已复制“' + e.text + '”到粘贴板');
                    e.clearSelection();
                });
            }
        });
        const vm = app.mount('#app');
    </script>
</body>
</html>


xoyozo 3 年前
2,046