用了一段时间 iOS 的 dark mode 感觉良好,特别是夜间看手机不会很刺眼。周末给网站加了暗黑模式功能,检测用户设备对用户切换到暗黑模式,不需要插件,只要一小段js和css代码即可。

实现原理

  1. 通过js获取当前访问设备的主题色设置模式做判断给html页面增加样式;
  2. 利用增加的样式和css的自定义属性切换css属性值。

利用js代码获取当前访问设备的主题色设置模式,并在页面html的body标签上增加css类dark

if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches{
document.querySelector("body").classList.add("dark")
};

其中,prefers-color-scheme CSS 媒体特性用于检测用户是否有将系统的主题色设置为亮色或者暗色,详情请见prefers-color-scheme

处理css样式:第一步,定义暗黑模式和正常模式下的css样式

.dark{ /*这里定义暗黑模式下的样式*/
--dark-background:#333;
--dark-color:#333;
}
:root{ /*root伪类定义正常模式下的样式*/
--normal-background:1px solid #e5e5e5;
--normal-color:1px solid #555;
}

处理css样式:第二步,修改暗黑模式下需要调整的css样式

这里需要用到var()函数的备用值功能来处理正常模式下的样式,否则在正常模式下获取不到样式。

.wrapper{
background:var(--dark-background,var(--normal-background));
color:var(--dark-color,#666);
color:#666;/*原有样式可保留使之兼容不支持css自定义属性的浏览器,如IE浏览器*/
}

什么是css自定义属性

自定义属性(有时候也被称作CSS变量或者级联变量)是由CSS作者定义的,它包含的值可以在整个文档中重复使用。

使用css自定义属性

css自定义属性使用起来非常简单和方便,如下:
由自定义属性标记设定值,如: --main-color: #000;
由var() 函数来获取值,如: color: var(--main-color);
以上效果等同于color:#000;
var() 函数可以定义多个备用值(fallback value),当给定值未定义时将会用备用值替换,语法为:

var( <custom-property-name> [, <declaration-value> ]? )

例如:color: var(--main-color,#999)即,当--main-color未定义或是为无效值时,取#999
color: var(--main-color,var(--main-color-b))即,当--main-color未定义或是为无效值时,取--main-color-b
灵活使用css自定义属性会来带意想不到的效果。
更多请见:使用CSS自定义属性(变量)

浏览器兼容性

本文用到的css自定义属性prefers-color-scheme均需注意浏览器兼容性问题,关于浏览器兼容性详情请见各自的文档说明:

  1. prefers-color-scheme
  2. 使用CSS自定义属性(变量)

单独制作了一个日记本来记录关于孩子的一些事情,为方便浏览需要将 post 发布日期改成类似 QQ 亲子相册那样,根据孩子出生日期和照片拍摄日期计算出来年龄,形如:生出、7天、1个月、 3岁5个月、5岁生日。如下图:

QQ亲子相册年龄计算

这个功能实现起来需要指定出生日期、获取Post的发布日期,剩下的就是日期判断,代码大概如下,放置于主题的 function.php 文件中:

//brave_years_of_age
function brave_years_of_age() {
$birth_date = date_create('2016-5-18'); //出生日期
$post_date = date_create(get_the_time('Y-m-d')); //当前 post 的发布日期
$interval = date_diff($birth_date, $post_date); //发布日期和出生日期的间隔

if ( $interval->format('%y') == 0 ) {
if ( $interval->format('%m')== 0 ) {
if ( $interval->format('%d') == 0 ) {
echo $interval->format('出生');
} else if ( $interval->format('%d') > 0 ) {
echo $interval->format('%d天');
}
} else if ( $interval->format('%m')> 0 ) {
echo $interval->format('%m个月');
}
} else if ( $interval->format('%y') > 0 ) {
if ( $interval->format('%m')== 0 ) {
if ( $interval->format('%d') == 0 ) {
echo $interval->format('%y岁生日');
} else if ( $interval->format('%d') > 0 ) {
echo $interval->format('%y岁');
}
} else if ( $interval->format('%m')> 0 ) {
echo $interval->format('%y岁%m个月');
}
}
}

在需要显示的地方加上下面这段输出即可:

<?php echo brave_years_of_age(); ?>

极个别情况会发一下私密的东西,有时疏忽会忘记设置私密,以下代码可以实现在特定形式在发布时自动设置私密的功能,这段代码在浏览器上是生效的,在 WordPress APP 上却无法正常工作,不知何故。

//aside 形式发布时自动置为私密
function set_post_to_private( $data, $postarr ) {
$brave_post_format = get_post_format();
if ( $brave_post_format == 'aside' && $data['post_status'] == 'publish' ) {
$data['post_status'] = 'private'; //发布时状态设置为私密
}
return $data;
}
add_filter( 'wp_insert_post_data', 'set_post_to_private', 99, 2 );

wp_insert_post_data

Android 平台的 WordPress app 有这样一个功能:预置一段自定义的话,发布时会自动附加在文末,如“发自 WordPress for Android”,见下图,iOS平台始终没有这个功能。最近给网站添加了类似功能,实现在发布时后台自动获取当前终端信息,获取到的终端信息被添加到自定义字段中,在页面上想要展示的位置展示之。加到自定义字段的好处是可以自由控制,实现更多个性化的功能。

实现的关键是 Mobile_Detect 插件和 save_post 钩子,步骤如下:

获取 Mobile_Detect 插件

第一步,需要用到插件 Mobile_Detect
以获取发布终端的设备信息,这个插件就一个文件,名为 Mobile_Detect.php,点此下载最新的 Mobile_Detect.php 文件,解压找到 Mobile_Detect.php 文件放至 WordPress 主题目录。

save_post 处理

第二步,将以下两段代码放到在用主题的 functions.php 文件中,保存。第一段代码的作用是发布时获取终端设备信息并存入当前日志的自定义字段;第二段代码的作用是定义存放于自定义代码中的设备信息在前端界面的展示逻辑。

// Auto_save post device meta data
function brave_post_device_meta( $post_id ) {

//自动保存不处理
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}

$current_post_device_name = get_post_meta($post->ID, 'post_device_name', true);
$current_post_device_ver = get_post_meta($post->ID, 'post_device_ver', true);
//已存在记录的已发布的/私密的不做更新
if ( current_user_can( 'edit_post', $post_id ) && ! in_array( get_post_status ( $post_id ) , array( 'private', 'publish') )) {
//加载插件
include_once( get_template_directory() . '/p/Mobile_Detect.php' );
$detect = new Mobile_Detect;
if ( $detect->isMobile() && !$detect->isTablet()) {
//手机
if ( $detect->isiPhone() ){
$device_name = 'iPhone';
$device_ver = $detect->version('iOS');
} else if ( $detect->isAndroidOS() ){
$device_name = 'Android';
$device_ver = $detect->version('Android');
}
} else if ( $detect->isMobile() && $detect->isTablet() ) {
//平板
if ( $detect->isiPad() ){
$device_name = 'iPad';
$device_ver = $detect->version('iOS');
} else if ( $detect->isKindle() ){
$device_name = 'Kindle';
} else if ( $detect->isAndroidOS() ){
$device_name = 'Android Tablet';
$device_ver = $detect->version('Android');
}
} else {
//桌面
if ( $detect->version('Chrome') ){
$device_name = 'Chrome';
$device_ver = $detect->version('Chrome');
} else if ( $detect->version('Firefox') ){
$device_name = 'Firefox';
$device_ver = $detect->version('Firefox');
} else if ( $detect->version('Safari') ){
$device_name = 'Safari';
$device_ver = $detect->version('Safari');
}
}
$current_post_device_name = $device_name;
$current_post_device_ver = $device_ver;
update_post_meta( $post_id, 'post_device_name', $current_post_device_name );
update_post_meta( $post_id, 'post_device_ver', $current_post_device_ver );
} else {
return;
}
}
add_action( 'save_post', 'brave_post_device_meta');

//前端展示,可自由调整样式

//brave_post_device
function brave_post_device() {
global $post;
$post_device_name = get_post_meta($post->ID, 'post_device_name', true);
$post_device_ver = get_post_meta($post->ID, 'post_device_ver', true);
$post_device_ver = str_replace(array('_', ' ', '/'), '.', $post_device_ver);//版本号替换,将_空格/的间隔统一替换为.
$post_device_ver = ' '.substr($post_device_ver,0,strpos($post_device_ver,'.'));//移除第一个小数点后的数字
$post_device_pfx = 'via '; //前缀
$post_device = $post_device_pfx . $post_device_name;

if ( $post_device_name == "iPhone" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "Android" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "iPad" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "Android Tablet" ) {
echo "<span class='ml'>Android平板</span>";
} else if ( $post_device_name == "Kindle" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "Firefox" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "Chrome" ) {
echo "<span class='ml'>$post_device</span>";
} else if ( $post_device_name == "Safari" ) {
echo "<span class='ml'>$post_device</span>";
}
}

第三步,在你希望展示的地方加上下面代码,行了,最终效果见下图。

<?php echo brave_pub_device(); ?>

这里只是粗略地检测设备平台,mobiledetect 插件功能非常强大,颗粒度能够做的更细,可以输出平台和版本号,如:iOS/Android、iPhone OS 13.6、Firefox/Chrome。

参考:
mobiledetect:http://mobiledetect.net
save_post:https://developer.wordpress.org/reference/hooks/save_post/

最近,在业务上遇到一个问题:公司的会员卡号要支持合并了,一位顾客注册了A、B两张会员卡,系统支持将A会员卡合并到B会员卡,A会员卡的会员权益和相关记录将在合并发生后转移到B会员卡。同时,A到B的合并关系会记录在一张数据表里。现在,要实现通过sql查询出A会员卡合并到B会员卡这样的关系。

若仅仅是由A合并到B,则很容易实现。偏偏这里的合并未做限制,由于羊毛党猖獗,加上营销部门有时为了数据好看,现实中往往存在这样的合并路径:A合并到B,B合并到C、E合并到C、C合并到D,这样A最后合并到了D,E最后也合并到了D。通过SQL查询这样的合并结构,这里需要用到递归查询。

with 查询

使用递归查询前先了解下 with 查询,with 提供了一种方式来书写在一个大型查询中使用的辅助语句。这些语句通常被称为公共表表达式或CTE,它们可以被看成是定义只在一个查询中存在的临时表。例如:

with a as ( --先用 with 创建一个名为a的对班级男同学的查询
select stu.stu_num,stu.stu_name from student as stu
where stu.stu_sex='男'
) stu
--接着引用这个临时表找到名字叫“李岩”的这位同学
select stu_num,stu_name from a where stu_name='李岩';

在查询中使用 with 有诸多好处:

  1. 使sql语句结构简单,层次分明:特别是零售数据类的查询,动辄几百行,这种情况下出现几个长嵌套,不要说给别人看了,半个月之后连自己都搞不清楚了,这时若使用 with 查询则模块清晰,层次分明,易于阅读和理解;
  2. 提升效率:with 查询的一个有用的特性是在每一次父查询的执行中它们通常只被计算一次,即使它们被父查询或兄弟 with 查询被多次引用。 因此,在多个地方需要的昂贵计算可以被放在一个 with 查询中来避免冗余工作。比如要对某组指定的会员做不同维度的数据分析,那么,对“这组指定人员”就可用使用 with 查询,其他兄弟查询直接引用结果即可,当需要对“这组指定人员”进行调整时,单独调整 with 查询就行了。

递归查询

通过对 with 语句增加 RECURSIVE 修饰符实现递归查询的目的,常用于层级结构查询,如组织层级、人员层级、物料清单等场景,递归查询格式如下:

with RECURSIVE recursive_name as (
非递归查询 --必须
union --必须,也可以是 union all
递归查询 --必须
)
select * from recursive_name;

下面以本次面对的问题展开:
创建会员合并记录表 cust_merge_dtl

CREATE table if not EXISTS cust_merge_dtl (
merge_id integer, --id
src_cust_id integer, --合并前的会员卡
desc_cust_id integer, --合并后的会员卡
op_time TIMESTAMP --合并时间
);

写入几条会员合并记录

insert into cust_merge_dtl (merge_id,src_cust_id,desc_cust_id,op_time)
VALUES ('1','10056','10071','2019-09-21 8:38:17');
insert into cust_merge_dtl (merge_id,src_cust_id,desc_cust_id,op_time)
VALUES ('2','10071','10092','2020-03-12 16:18:55');
insert into cust_merge_dtl (merge_id,src_cust_id,desc_cust_id,op_time)
VALUES ('3','10088','10092','2020-11-11 10:08:27');
insert into cust_merge_dtl (merge_id,src_cust_id,desc_cust_id,op_time)
VALUES ('4','10092','10112','2020-12-30 18:30:06');
insert into cust_merge_dtl (merge_id,src_cust_id,desc_cust_id,op_time)
VALUES ('5','10010','10112','2021-03-12 12:44:46');

查询会员合并记录表 cust_merge_dtl

select * from cust_merge_dtl;


如上图所示,以10056会员卡为例,它只有一条合并记录,即10056->10071,但是10071紧接着又做了一次合并,10071->10092,最后10092合并到了10112,推算出10056最终合并到了10112,完整的合并路径是这样的:10056 --> 10071 --> 10092 --> 10112

现在用递归查询生成这种合并关系。

with recursive cust_merge as (
select dtl1.src_cust_id as original_cust_id,--初始会员卡
dtl1.src_cust_id,--合并前的会员卡
dtl1.desc_cust_id,--合并后的会员卡
dtl1.op_time
from cust_merge_dtl dtl1 where dtl1.src_cust_id='10056' --非递归查询,此处的查询结果构成整个递归查询的基本结果形式
union
select cm.original_cust_id,dtl2.src_cust_id,dtl2.desc_cust_id,dtl2.op_time
from cust_merge_dtl dtl2
inner join cust_merge cm on cm.desc_cust_id = dtl2.src_cust_id --递归查询
)
select * from cust_merge;

得到原始会员卡及过程中每一次的变动记录。

递归查询的执行过程是这样的:

  1. 非递归查询的查询结果在union去重后作为输出(cust_merge输出),供递归查询部分引用;
  2. 供递归查询引用上一步的cust_merge输出作为输入,进行递归查询,生成新的cust_merge输出(每次递归查询生成的cust_merge输出都将被保存在一个中间表中,并做union去重处理);
  3. 新的cust_merge输出不为空时,则回到步骤2,作为输入,循环执行,直到cust_merge输出为空时停止执行;
  4. 最后的结果是步骤1的查询结果+步骤2的中间表的结果的并集,即两者union去重复(如使用 union all 整个过程不做去重处理)。

同理如果不关注中间的过程只想知道这些合并的会员卡最终合并到哪个会员卡了,稍作调整即可:

with recursive cust_merge as (
select dtl1.src_cust_id as original_cust_id,dtl1.src_cust_id,dtl1.desc_cust_id,dtl1.op_time
from cust_merge_dtl dtl1 --where dtl1.src_cust_id='10056'
union all
select cm.original_cust_id,dtl2.src_cust_id,dtl2.desc_cust_id,dtl2.op_time
from cust_merge_dtl dtl2
inner join cust_merge cm on cm.desc_cust_id = dtl2.src_cust_id
),
last_record as (
--原始会员对应的最新迭代合并记录
select cm.original_cust_id,max(op_time) as last_merge_time
from cust_merge cm
GROUP BY cm.original_cust_id
)
select cm.original_cust_id,cm.desc_cust_id
from cust_merge cm
where exists (
select 1 from last_record lr where lr.last_merge_time=cm.op_time and lr.original_cust_id=cm.original_cust_id
);

参考:http://postgres.cn/docs/12/queries-with.html