用户 2026-10-03 提的四条:
① 家谱卡片重做
· 去掉「⇄ 姻亲」按钮,改成**每个人**卡片右下角都有「以 TA 为中心」
(点了以他重画一张家谱,涵盖原来的姻亲场景)。
· 头像外框改**长方形**(96×72,宽度贴合头像),object-fit: contain 不裁切,
姓名/称呼在头像下方。
· 修「点右下角按钮误进成员页」:卡片不再整张是 <a>,
只有姓名是链接,右下角是独立的 <button>(物理上分开,不抢点击)。
② 家谱文案与按钮清理
· 去掉副标题与下方图例说明;整块去掉「看全部 / 回到我 / 回到我的家谱」
(顺带消掉"出现两个回到我的家谱")。镜头由库自动 fit,拖拽/滚轮即可。
③ 首页控制台
· 标题下显示个人 slogan;去掉右侧三个统计;「我的资料/我的家谱」换成图标按钮;
去掉与导航条重复的「名册」按钮。
· 去掉「接下来的日子」(生日归到"未来七天"里,不再重复)。
· 新增统计信息块:按时段随机问候 + 今日天气(Open-Meteo,免 key,可留空不显示)
+ 近七天日程/待办/逾期数。
· 新增各模块小结卡(任务/日程/纪念日/观影/旅程/财务/名册/家谱/资源)。
· 个人设置新增「首页上的模块」:可勾选与拖动排序(每人一份,存 user_settings)。
· 保留「往年的今天」。
④ 名册改列表
· 卡片网格 → **竖向列表**:左头像、中间姓名+与我的关系+手机号+生日。
· 修拖动「只能往后不能往前」:落点改为显式记录"插到谁前面/后面",
并排除被拖元素自身。
· 去掉那句拖动提示(能力保留)。
· 搜索新增**姓名首字母**检索(y / yy / yyq 都命中「杨永全」):
新 app/Pinyin.php,intl 优先 + 内置常用字表兜底(本地无 intl 也能测);
纯字母/数字查询词不走 SQL LIKE(拼音与加密手机号在库里都比不了)。
附带上两轮的收尾:
· 菜单点外部自动关闭(全局事件委托,覆盖所有 details.menu)。
· 旅程 is_public:列表与日历只显示公开的;详情页不拦(不丢数据)。
· 重复日程:新 app/Repeat.php(RRULE 解析/生成/展开,含"工作日"),
新表 calendar_event_skips(RFC 的 EXDATE);日历按规则展开;
编辑支持三档「全部 / 这一次及以后 / 仅此一次」。
· 结束时间可添加/去掉,并随开始时间联动。
验证:端到端 772 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 26、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
299 lines
17 KiB
PHP
299 lines
17 KiB
PHP
<?php
|
||
/**
|
||
* 汉字 → 拼音首字母(用户 2026-10-03 第 4 条)。
|
||
*
|
||
* 用途:名册搜索支持「姓名首字母」——
|
||
* 输入 y / yy / yyq 都能搜到「杨永全」。
|
||
*
|
||
* 两条路(**必须都有**,因为本地与线上环境不同):
|
||
*
|
||
* ① `Intl` 扩展的 `Collator('zh-Hans-CN-u-co-pinyin')`:拿一张
|
||
* 「a..z 各挑一个锚字」的表做二分,就能判断任意汉字落在哪个字母段。
|
||
* 线上 PHP 7.4/8.1 都有 intl,走这条最准、零数据。
|
||
* ② 没有 intl 时的兜底:**常用姓氏/人名用字表**。
|
||
* 本地开发环境没有 intl(实测 `intl: 无`),没有兜底就等于
|
||
* 「这个功能本地永远测不了」—— 那是最糟的情况,只能上线才发现问题。
|
||
*
|
||
* ⚠️ 兜底表覆盖不到的生僻字会**原样保留首字符参与匹配**(不产字母),
|
||
* 也就是说:搜不到时是"少匹配",绝不会错匹配。
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App;
|
||
|
||
final class Pinyin
|
||
{
|
||
/** 拼音排序用的锚字(a..z 各一个;没有 i/u/v 开头的常用字,所以是 23 个) */
|
||
private const ANCHORS = '阿八擦搭娥发嘎哈击喀垃妈拿哦啪期然撒塌挖昔压匝';
|
||
private const LETTERS = 'abcdefghjklmnopqrstwxyz';
|
||
|
||
/**
|
||
* 常用姓氏 + 常见人名字的**首字母表**(兜底用)。
|
||
* 只列"作为名字第一个字"出现频率高的;查不到就交给下一档处理。
|
||
* 这一份不可能穷尽,所以它只是兜底 —— 线上有 intl 时走的是锚字二分。
|
||
*/
|
||
private const TABLE = [
|
||
// 姓氏(覆盖绝大多数中国姓氏)
|
||
'赵a' => 1, '钱q' => 1, '孙s' => 1, '李l' => 1, '周z' => 1, '吴w' => 1, '郑z' => 1,
|
||
'王w' => 1, '冯f' => 1, '陈c' => 1, '褚c' => 1, '卫w' => 1, '蒋j' => 1, '沈s' => 1,
|
||
'韩h' => 1, '杨y' => 1, '朱z' => 1, '秦q' => 1, '尤y' => 1, '许x' => 1, '何h' => 1,
|
||
'吕l' => 1, '施s' => 1, '张z' => 1, '孔k' => 1, '曹c' => 1, '严y' => 1, '华h' => 1,
|
||
'金j' => 1, '魏w' => 1, '陶t' => 1, '姜j' => 1, '戚q' => 1, '谢x' => 1, '邹z' => 1,
|
||
'喻y' => 1, '柏b' => 1, '水s' => 1, '窦d' => 1, '章z' => 1, '云y' => 1, '苏s' => 1,
|
||
'潘p' => 1, '葛g' => 1, '奚x' => 1, '范f' => 1, '彭p' => 1, '郎l' => 1, '鲁l' => 1,
|
||
'韦w' => 1, '昌c' => 1, '马m' => 1, '苗m' => 1, '凤f' => 1, '花h' => 1, '方f' => 1,
|
||
'俞y' => 1, '任r' => 1, '袁y' => 1, '柳l' => 1, '酆f' => 1, '鲍b' => 1, '史s' => 1,
|
||
'唐t' => 1, '费f' => 1, '廉l' => 1, '岑c' => 1, '薛x' => 1, '雷l' => 1, '贺h' => 1,
|
||
'倪n' => 1, '汤t' => 1, '滕t' => 1, '殷y' => 1, '罗l' => 1, '毕b' => 1, '郝h' => 1,
|
||
'邬w' => 1, '安a' => 1, '常c' => 1, '乐y' => 1, '于y' => 1, '时s' => 1, '傅f' => 1,
|
||
'皮p' => 1, '卞b' => 1, '齐q' => 1, '康k' => 1, '伍w' => 1, '余y' => 1, '元y' => 1,
|
||
'卜b' => 1, '顾g' => 1, '孟m' => 1, '平p' => 1, '黄h' => 1, '和h' => 1, '穆m' => 1,
|
||
'萧x' => 1, '尹y' => 1, '姚y' => 1, '邵s' => 1, '湛z' => 1, '汪w' => 1, '祁q' => 1,
|
||
'毛m' => 1, '禹y' => 1, '狄d' => 1, '米m' => 1, '贝b' => 1, '明m' => 1, '臧z' => 1,
|
||
'计j' => 1, '伏f' => 1, '成c' => 1, '戴d' => 1, '谈t' => 1, '宋s' => 1, '茅m' => 1,
|
||
'庞p' => 1, '熊x' => 1, '纪j' => 1, '舒s' => 1, '屈q' => 1, '项x' => 1, '祝z' => 1,
|
||
'董d' => 1, '梁l' => 1, '杜d' => 1, '阮r' => 1, '蓝l' => 1, '闵m' => 1, '席x' => 1,
|
||
'季j' => 1, '麻m' => 1, '强q' => 1, '贾j' => 1, '路l' => 1, '娄l' => 1, '危w' => 1,
|
||
'江j' => 1, '童t' => 1, '颜y' => 1, '郭g' => 1, '梅m' => 1, '盛s' => 1, '林l' => 1,
|
||
'刁d' => 1, '钟z' => 1, '徐x' => 1, '邱q' => 1, '骆l' => 1, '高g' => 1, '夏x' => 1,
|
||
'蔡c' => 1, '田t' => 1, '樊f' => 1, '胡h' => 1, '凌l' => 1, '霍h' => 1, '虞y' => 1,
|
||
'万w' => 1, '支z' => 1, '柯k' => 1, '昝z' => 1, '管g' => 1, '卢l' => 1, '莫m' => 1,
|
||
'房f' => 1, '裘q' => 1, '缪m' => 1, '干g' => 1, '解x' => 1, '应y' => 1, '宗z' => 1,
|
||
'丁d' => 1, '宣x' => 1, '邓d' => 1, '单s' => 1, '杭h' => 1, '洪h' => 1, '包b' => 1,
|
||
'诸z' => 1, '左z' => 1, '石s' => 1, '崔c' => 1, '吉j' => 1, '钮n' => 1, '龚g' => 1,
|
||
'程c' => 1, '嵇j' => 1, '邢x' => 1, '滑h' => 1, '裴p' => 1, '陆l' => 1, '荣r' => 1,
|
||
'翁w' => 1, '荀x' => 1, '羊y' => 1, '於y' => 1, '惠h' => 1, '甄z' => 1, '曲q' => 1,
|
||
'封f' => 1, '芮r' => 1, '羿y' => 1, '储c' => 1, '靳j' => 1, '汲j' => 1, '邴b' => 1,
|
||
'糜m' => 1, '松s' => 1, '井j' => 1, '段d' => 1, '富f' => 1, '巫w' => 1, '乌w' => 1,
|
||
'焦j' => 1, '巴b' => 1, '弓g' => 1, '牧m' => 1, '隗k' => 1, '山s' => 1, '谷g' => 1,
|
||
'车c' => 1, '侯h' => 1, '宓m' => 1, '蓬p' => 1, '全q' => 1, '郗x' => 1, '班b' => 1,
|
||
'仰y' => 1, '秋q' => 1, '仲z' => 1, '伊y' => 1, '宫g' => 1, '宁n' => 1, '仇q' => 1,
|
||
'栾l' => 1, '暴b' => 1, '甘g' => 1, '厉l' => 1, '戎r' => 1, '祖z' => 1, '武w' => 1,
|
||
'符f' => 1, '刘l' => 1, '景j' => 1, '詹z' => 1, '束s' => 1, '龙l' => 1, '叶y' => 1,
|
||
'幸x' => 1, '司s' => 1, '韶s' => 1, '郜g' => 1, '黎l' => 1, '蓟j' => 1, '薄b' => 1,
|
||
'印y' => 1, '宿s' => 1, '白b' => 1, '怀h' => 1, '蒲p' => 1, '邰t' => 1, '从c' => 1,
|
||
'鄂e' => 1, '索s' => 1, '咸x' => 1, '籍j' => 1, '赖l' => 1, '卓z' => 1, '蔺l' => 1,
|
||
'屠t' => 1, '蒙m' => 1, '池c' => 1, '乔q' => 1, '阴y' => 1, '郁y' => 1, '胥x' => 1,
|
||
'能n' => 1, '苍c' => 1, '双s' => 1, '闻w' => 1, '莘s' => 1, '党d' => 1, '翟z' => 1,
|
||
'谭t' => 1, '贡g' => 1, '劳l' => 1, '逄p' => 1, '姬j' => 1, '申s' => 1, '扶f' => 1,
|
||
'堵d' => 1, '冉r' => 1, '宰z' => 1, '郦l' => 1, '雍y' => 1, '郤x' => 1, '璩q' => 1,
|
||
'桑s' => 1, '桂g' => 1, '濮p' => 1, '牛n' => 1, '寿s' => 1, '通t' => 1, '边b' => 1,
|
||
'扈h' => 1, '燕y' => 1, '冀j' => 1, '郏j' => 1, '浦p' => 1, '尚s' => 1, '农n' => 1,
|
||
'温w' => 1, '别b' => 1, '庄z' => 1, '晏y' => 1, '柴c' => 1, '瞿q' => 1, '阎y' => 1,
|
||
'充c' => 1, '慕m' => 1, '连l' => 1, '茹r' => 1, '习x' => 1, '宦h' => 1, '艾a' => 1,
|
||
'鱼y' => 1, '容r' => 1, '向x' => 1, '古g' => 1, '易y' => 1, '慎s' => 1, '戈g' => 1,
|
||
'廖l' => 1, '庾y' => 1, '终z' => 1, '暨j' => 1, '居j' => 1, '衡h' => 1, '步b' => 1,
|
||
'都d' => 1, '耿g' => 1, '满m' => 1, '弘h' => 1, '匡k' => 1, '国g' => 1, '文w' => 1,
|
||
'寇k' => 1, '广g' => 1, '禄l' => 1, '阙q' => 1, '东d' => 1, '欧o' => 1, '殳s' => 1,
|
||
'沃w' => 1, '利l' => 1, '蔚w' => 1, '越y' => 1, '夔k' => 1, '隆l' => 1, '师s' => 1,
|
||
'巩g' => 1, '厍s' => 1, '聂n' => 1, '晁c' => 1, '勾g' => 1, '敖a' => 1, '融r' => 1,
|
||
'冷l' => 1, '訾z' => 1, '辛x' => 1, '阚k' => 1, '那n' => 1, '简j' => 1, '饶r' => 1,
|
||
'空k' => 1, '曾z' => 1, '毋w' => 1, '沙s' => 1, '乜n' => 1, '养y' => 1, '鞠j' => 1,
|
||
'须x' => 1, '丰f' => 1, '巢c' => 1, '关g' => 1, '蒯k' => 1, '相x' => 1, '查z' => 1,
|
||
'后h' => 1, '荆j' => 1, '红h' => 1, '游y' => 1, '竺z' => 1, '权q' => 1, '逯l' => 1,
|
||
'盖g' => 1, '益y' => 1, '桓h' => 1, '公g' => 1,
|
||
// 名字里高频的字
|
||
'永y' => 1, '全q' => 1, '博b' => 1, '为w' => 1, '明m' => 1, '华h' => 1, '建j' => 1,
|
||
'国g' => 1, '文w' => 1, '军j' => 1, '平p' => 1, '志z' => 1, '强q' => 1, '磊l' => 1,
|
||
'洋y' => 1, '勇y' => 1, '艳y' => 1, '杰j' => 1, '娟j' => 1, '涛t' => 1, '超c' => 1,
|
||
'敏m' => 1, '静j' => 1, '丽l' => 1, '秀x' => 1, '英y' => 1, '娜n' => 1, '丹d' => 1,
|
||
'玲l' => 1, '芳f' => 1, '燕y' => 1, '红h' => 1, '霞x' => 1, '梅m' => 1, '兰l' => 1,
|
||
'琴q' => 1, '云y' => 1, '飞f' => 1, '鹏p' => 1, '宇y' => 1, '浩h' => 1, '鑫x' => 1,
|
||
'亮l' => 1, '刚g' => 1, '辉h' => 1, '峰f' => 1, '龙l' => 1, '波b' => 1, '斌b' => 1,
|
||
'春c' => 1, '雨y' => 1, '晨c' => 1, '阳y' => 1, '雪x' => 1, '悦y' => 1, '欣x' => 1,
|
||
'怡y' => 1, '佳j' => 1, '思s' => 1, '雨y' => 1, '子z' => 1, '小x' => 1, '大d' => 1,
|
||
'老l' => 1, '爸b' => 1, '妈m' => 1, '哥g' => 1, '姐j' => 1, '弟d' => 1, '妹m' => 1,
|
||
'爷y' => 1, '奶n' => 1, '叔s' => 1, '姨y' => 1, '舅j' => 1, '嫂s' => 1, '儿e' => 1,
|
||
'女n' => 1, '宝b' => 1, '乐l' => 1, '安a' => 1, '康k' => 1, '福f' => 1, '寿s' => 1,
|
||
'喜x' => 1, '顺s' => 1, '和h' => 1, '美m' => 1, '好h' => 1, '新x' => 1, '中z' => 1,
|
||
'正z' => 1, '长c' => 1, '海h' => 1, '江j' => 1, '河h' => 1, '山s' => 1, '林l' => 1,
|
||
'木m' => 1, '金j' => 1, '玉y' => 1, '石s' => 1, '田t' => 1, '力l' => 1, '学x' => 1,
|
||
'生s' => 1, '成c' => 1, '立l' => 1, '德d' => 1, '仁r' => 1, '义y' => 1, '礼l' => 1,
|
||
'智z' => 1, '信x' => 1, '忠z' => 1, '孝x' => 1, '友y' => 1, '爱a' => 1, '慈c' => 1,
|
||
// 家谱称呼里高频的那批字(父/母/伯/叔/姑/舅/姨/岳…)——
|
||
// 名册搜索经常会拿这些字当关键词,兜底表里缺了就会出现
|
||
// "本地搜 s 搜不到『张三』"这种与环境相关的怪现象。
|
||
'父f' => 1, '母m' => 1, '祖z' => 1, '孙s' => 1, '外w' => 1, '表b' => 1, '堂t' => 1,
|
||
'侄z' => 1, '甥s' => 1, '婿x' => 1, '媳x' => 1, '伯b' => 1, '姑g' => 1, '岳y' => 1,
|
||
'公g' => 1, '婆p' => 1, '妻q' => 1, '夫f' => 1, '丈z' => 1, '娘n' => 1, '爹d' => 1,
|
||
'三s' => 1, '四s' => 1, '五w' => 1, '六l' => 1, '七q' => 1, '八b' => 1, '九j' => 1,
|
||
'十s' => 1, '百b' => 1, '千q' => 1, '万w' => 1, '一y' => 1, '二e' => 1, '零l' => 1,
|
||
'不b' => 1, '的d' => 1, '了l' => 1, '在z' => 1, '有y' => 1, '是s' => 1, '我w' => 1,
|
||
'人r' => 1, '家j' => 1, '门m' => 1, '户h' => 1, '先x' => 1, '后h' => 1, '晚w' => 1,
|
||
'长c' => 1, '幼y' => 1, '大d' => 1, '小x' => 1, '多d' => 1, '少s' => 1, '高g' => 1,
|
||
'矮a' => 1, '胖p' => 1, '瘦s' => 1, '黑h' => 1, '白b' => 1, '花h' => 1, '草c' => 1,
|
||
'树s' => 1, '叶y' => 1, '果g' => 1, '苗m' => 1, '阳y' => 1, '光g' => 1, '月y' => 1,
|
||
'星x' => 1, '风f' => 1, '雪x' => 1, '雨y' => 1, '雷l' => 1, '电d' => 1, '冰b' => 1,
|
||
'州z' => 1, '城c' => 1, '村c' => 1, '镇z' => 1, '县x' => 1, '市s' => 1, '省s' => 1,
|
||
'华h' => 1, '青q' => 1, '岛d' => 1, '京j' => 1, '津j' => 1, '冀j' => 1, '鲁l' => 1,
|
||
'思s' => 1, '念n' => 1, '想x' => 1, '记j' => 1, '忘w' => 1, '知z' => 1, '道d' => 1,
|
||
'说s' => 1, '话h' => 1, '听t' => 1, '看k' => 1, '走z' => 1, '来l' => 1, '去q' => 1,
|
||
'回h' => 1, '到d' => 1, '给g' => 1, '拿n' => 1, '做z' => 1, '用y' => 1, '买m' => 1,
|
||
'卖m' => 1, '吃c' => 1, '喝h' => 1, '睡s' => 1, '起q' => 1, '坐z' => 1, '站z' => 1,
|
||
'跑p' => 1, '跳t' => 1, '唱c' => 1, '写x' => 1, '读d' => 1, '画h' => 1, '算s' => 1,
|
||
'作z' => 1, '课k' => 1, '班b' => 1, '校x' => 1, '师s' => 1, '医y' => 1, '院y' => 1,
|
||
'药y' => 1, '病b' => 1, '健j' => 1, '体t' => 1, '身s' => 1, '心x' => 1, '手s' => 1,
|
||
'脚j' => 1, '眼y' => 1, '耳e' => 1, '鼻b' => 1, '口k' => 1, '头t' => 1, '发f' => 1,
|
||
];
|
||
|
||
/** 首字母查表:'杨' => 'y'。首次调用时从 TABLE 摊平,之后走内存缓存。 */
|
||
private static ?array $map = null;
|
||
|
||
public static function initialMap(): array
|
||
{
|
||
if (self::$map === null) {
|
||
self::$map = [];
|
||
foreach (self::TABLE as $kv => $_) {
|
||
$k = (string)$kv;
|
||
if (mb_strlen($k, 'UTF-8') >= 1) {
|
||
$ch = mb_substr($k, 0, 1, 'UTF-8');
|
||
$ch = mb_substr($k, 0, mb_strlen($k, 'UTF-8') - 1, 'UTF-8');
|
||
$letter = mb_substr((string)$kv, mb_strlen((string)$kv, 'UTF-8') - 1, 1, 'UTF-8');
|
||
if ($ch !== '' && $letter !== '') {
|
||
self::$map[$ch] = strtolower($letter);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return self::$map;
|
||
}
|
||
|
||
/**
|
||
* 一个字的拼音首字母(小写)。认不出时返回 ''。
|
||
* 有 intl 走锚字二分;没有就走内置表。
|
||
*/
|
||
public static function initial(string $ch): string
|
||
{
|
||
if ($ch === '') {
|
||
return '';
|
||
}
|
||
// 已经是 ASCII 字母 → 直接用
|
||
if (preg_match('/^[a-zA-Z]$/', $ch)) {
|
||
return strtolower($ch);
|
||
}
|
||
if (preg_match('/^[0-9]$/', $ch)) {
|
||
return $ch;
|
||
}
|
||
// ① intl 拼音排序(线上有)
|
||
$viaIntl = self::initialViaIntl($ch);
|
||
if ($viaIntl !== null) {
|
||
return $viaIntl;
|
||
}
|
||
// ② 内置表兜底(本地无 intl 时)
|
||
return self::initialMap()[$ch] ?? '';
|
||
}
|
||
|
||
/** 用 Intl 的拼音 collation + 锚字二分定位;没有 intl 返回 null */
|
||
private static function initialViaIntl(string $ch): ?string
|
||
{
|
||
static $coll = false;
|
||
if ($coll === false) {
|
||
$coll = null;
|
||
if (class_exists('\\Collator')) {
|
||
$c = new \Collator('zh-Hans-CN-u-co-pinyin');
|
||
// 排序没真的生效时(compare('阿','匝') 不 < 0)就别用,免得给出错结果
|
||
if ($c->compare('阿', '匝') < 0) {
|
||
$coll = $c;
|
||
}
|
||
}
|
||
}
|
||
if ($coll === null) {
|
||
return null;
|
||
}
|
||
$anchors = self::ANCHORS;
|
||
$len = mb_strlen($anchors, 'UTF-8');
|
||
$lo = 0;
|
||
$hi = $len - 1;
|
||
$best = -1;
|
||
while ($lo <= $hi) {
|
||
$mid = intdiv($lo + $hi, 2);
|
||
$a = mb_substr($anchors, $mid, 1, 'UTF-8');
|
||
if ($coll->compare($ch, $a) >= 0) {
|
||
$best = $mid;
|
||
$lo = $mid + 1;
|
||
} else {
|
||
$hi = $mid - 1;
|
||
}
|
||
}
|
||
if ($best < 0) {
|
||
return null;
|
||
}
|
||
return mb_substr(self::LETTERS, $best, 1, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* 一串字 → 首字母串(小写)。认不出的字**跳过**(不产字母,也不报错)。
|
||
* 例:'杨永全' → 'yyq'。
|
||
*/
|
||
public static function initials(string $s): string
|
||
{
|
||
$out = '';
|
||
$n = mb_strlen($s, 'UTF-8');
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$out .= self::initial(mb_substr($s, $i, 1, 'UTF-8'));
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* q 是否是 s 的**子序列**(用于"输入 y 也能搜到 yyq"这类前缀式检索)。
|
||
*
|
||
* 为什么用子序列而不是前缀:
|
||
* 用户可能只记得中间某个字(比如"永全" → yq),或干脆跳着打;
|
||
* 名册这种小列表宁可宽松一点,也别让人以为搜索坏了。
|
||
*/
|
||
public static function isSubsequence(string $q, string $s): bool
|
||
{
|
||
$q = strtolower(trim($q));
|
||
$s = strtolower($s);
|
||
if ($q === '') {
|
||
return true;
|
||
}
|
||
$i = 0;
|
||
$qn = strlen($q);
|
||
for ($j = 0, $sn = strlen($s); $j < $sn && $i < $qn; $j++) {
|
||
if ($s[$j] === $q[$i]) {
|
||
$i++;
|
||
}
|
||
}
|
||
return $i === $qn;
|
||
}
|
||
|
||
/**
|
||
* 这个人是否匹配搜索词。
|
||
*
|
||
* 覆盖:姓名 / 称呼 / 邮箱 / 手机号 的**模糊**包含,以及姓名首字母的**子序列**匹配。
|
||
*
|
||
* @param array $row 名册行(含 name / email / phone_plain,可选 my_nickname)
|
||
* @param string $q 搜索词
|
||
*/
|
||
public static function matches(array $row, string $q): bool
|
||
{
|
||
$q = trim($q);
|
||
if ($q === '') {
|
||
return true;
|
||
}
|
||
$needle = mb_strtolower($q, 'UTF-8');
|
||
foreach (['name', 'my_nickname', 'nickname', 'email', 'phone_plain'] as $k) {
|
||
$v = mb_strtolower(trim((string)($row[$k] ?? '')), 'UTF-8');
|
||
if ($v !== '' && mb_strpos($v, $needle) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
// 首字母:只对**字母/数字**查询词做子序列匹配,
|
||
// 中文查询词走上面的"包含"(否则"永全"会被当成首字母串去比对,永远匹配不上)
|
||
if (preg_match('/^[a-z0-9]+$/', $needle)) {
|
||
$ini = self::initials((string)($row['name'] ?? ''));
|
||
if ($ini !== '' && self::isSubsequence($needle, $ini)) {
|
||
return true;
|
||
}
|
||
// 称呼也参与首字母(比如称呼是"小宝" → xb)
|
||
$nickIni = self::initials((string)($row['my_nickname'] ?? ''));
|
||
if ($nickIni !== '' && self::isSubsequence($needle, $nickIni)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
} |