feat: 公共表更名 common/member 前缀 + 插件命令自动加载 + 版本 1.1.1

- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零
- Attachment 模型补  表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患
- install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空
- AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过)
- 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整
- 框架版本 1.1.0 -> 1.1.1
This commit is contained in:
ywxapp
2026-08-20 20:19:15 +08:00
parent 303f548664
commit 1d49e6f5ee
395 changed files with 13342 additions and 2012 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ namespace addon\haonav;
use think\facade\Config;
use think\facade\Db;
use ywxapp\Addons;
use ywxapp\AddonBase;
use ywxapp\model\BaseModel;
/**
@@ -23,7 +23,7 @@ use ywxapp\model\BaseModel;
* favorites / favorite_shares / ads / apply / click_stats);
* 卸载时按统一前缀清理全部表;升级时经 callAddonHook('upgrade') 收敛表结构。
*/
class Addon extends Addons
class Addon extends AddonBase
{
/**
* 安装钩子(框架自动导入 install.sql 后调用)。
+44 -9
View File
@@ -93,10 +93,17 @@ class Index extends FrontendBase
*/
public function index()
{
$data = CategoryModel::with(['links' =>
function ($query) {
$query->where('status', 1)->limit(10)->order('click_count', 'desc');
}])->where('status', 1)->order('sort', 'desc')->select();
$data = CategoryModel::with([
'children' => function ($query) {
$query->with(['subLinks' => function ($q) {
$q->limit(12)->order('click_count', 'desc');
}]);
},
'links' => function ($query) {
// 首页不按二级分组,直接展示该一级分类下的全部链接(含已归属二级的)
$query->where('status', 1)->order('click_count', 'desc')->limit(30);
},
])->where('pid', 0)->where('status', 1)->order('sort', 'desc')->select();
$hotspot = LinksModel::where('is_hot', 1)->where('status', 1)->limit(18)->order('click_count', 'desc')->select();
$links = LinksModel::where('is_recommend', 1)->where('status', 1)->limit(10)->order('sort', 'desc')->select();
@@ -441,11 +448,39 @@ class Index extends FrontendBase
*/
public function category($id = 0)
{
$data = CategoryModel::with(['links' =>
function ($query) {
$query->where('status', 1)->order('click_count', 'desc');
}])->find($id);
$this->view->assign('info', $data);
$info = CategoryModel::with([
'children' => function ($query) {
$query->with(['subLinks' => function ($q) {
$q->order('click_count', 'desc');
}]);
},
])->where('status', 1)->find($id);
if (!$info) {
return redirect('/haonav/index.html');
}
// 顶级分类且有子分类:按二级子分类分组展示
if ($info->pid == 0 && $info->children && $info->children->count() > 0) {
// 未细分二级的链接(sub_cid=0)归入「常用」分组,排在最后
$others = LinksModel::where('cid', $id)
->where('sub_cid', 0)
->where('status', 1)
->order('click_count', 'desc')
->select();
$this->view->assign('others', $others);
$this->view->assign('mode', 'group');
} else {
// 二级分类 / 叶子分类:直接列出该分类下的全部链接
$links = ($info->pid == 0)
? LinksModel::where('cid', $id)->where('status', 1)
: LinksModel::where('sub_cid', $id)->where('status', 1);
$links = $links->order('click_count', 'desc')->select();
$this->view->assign('links', $links);
$this->view->assign('mode', 'flat');
}
$this->view->assign('info', $info);
$this->assignSeo();
return $this->view->fetch();
}
+5 -4
View File
@@ -57,10 +57,11 @@ class Category extends HaonavBackend
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 20);
$data = $this->model->withAttr('cate')
->when($title, fn($q, $t) => $q->whereLike('title', "%{$t}%"))
->paginate(['page' => $page, 'list_rows' => $limit]);
$this->result->setCount($data->total());
$this->result->success($data->items());
->when($title, fn($q, $t) => $q->whereLike('title', "%{$t}%"))->select();
// ->paginate(['page' => $page, 'list_rows' => $limit]);
// $this->result->setCount($data->total());
// $this->result->success($data->items());
$this->result->success($data);
}
return $this->view->fetch('category/index');
}
+2 -2
View File
@@ -6,7 +6,7 @@ return [
'intro' => '简洁实用的网址导航,支持分类管理、链接收录、热门/推荐与点击统计。',
'author' => 'ywxapp',
'website' => 'https://github.com',
'version' => '1.0.9',
'version' => '1.0.10',
'state' => 1,
'url' => '/haonav',
'license' => '',
@@ -34,6 +34,6 @@ return [
],
'services' => [
],
'install_time' => 1785818546,
'install_time' => 1786890830,
'update_time' => 1786365870,
];
+3035 -4
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -41,6 +41,24 @@ class Category extends BaseModel
return $this->hasMany(Links::class, 'cid', 'id');
}
/**
* 二级分类(pid 指向自身)
*/
public function children()
{
return $this->hasMany(self::class, 'pid', 'id')->where('status', 1)->order('sort', 'desc');
}
/**
* 按二级分类分组的链接(首页/分类页用)
* 返回二级分类及其下属链接,sub_cid=0 的链接归入「常用」
*/
public function subLinks()
{
return $this->hasMany(Links::class, 'sub_cid', 'id')
->where('status', 1)->order('click_count', 'desc');
}
/**
* 删除前保护:存在子分类或下属链接时禁止删除,避免产生孤儿数据
* @param \think\Model $model
+2 -1
View File
@@ -3,7 +3,7 @@
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-07-31 13:28:15
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-10 23:40:51
* @LastEditTime: 2026-08-20 12:27:18
* @Description:
* @FilePath: \ywxapp_dev\addon\haonav\route\app.php
* @CustomString: Copyright (c) 2026 YwxApp
@@ -84,6 +84,7 @@ Route::group('backend', function () {
Route::rule('links/:action', 'Links/:action');
Route::rule('category', 'Category/index') ;
Route::rule('category/index', 'Category/index') ;
Route::rule('category/update', 'Category/update') ;
Route::rule('configs', 'Configs/index');
Route::rule('configs/:action', 'Configs/:action');
Route::rule('ad', 'Ad/index');
+80
View File
@@ -16,3 +16,83 @@ INSERT IGNORE INTO `__PREFIX__haonav_config` (`id`, `name`, `group`, `title`, `t
(2, 'enable_submit', 'site', '开启网址投稿', '关闭后前台投稿将直接上架,无需审核', 'bool', '1', '', '', '', ''),
(3, 'enable_hot_api', 'site', '启用外部热榜', '开启后将尝试聚合外部热搜(可能较慢,失败自动降级)', 'bool', '0', '', '', '', ''),
(4, 'check_token', 'task', '死链检测令牌', 'cron 调用 /haonav/task/checklinks?token= 时校验,留空则拒绝执行', 'string', '', '', '', '', '');
-- 3. 链接表新增二级分类字段
ALTER TABLE `__PREFIX__haonav_links`
ADD COLUMN `sub_cid` int unsigned NOT NULL DEFAULT '0' COMMENT '所属二级分类ID,0表示未细分' AFTER `cid`;
ALTER TABLE `__PREFIX__haonav_links`
ADD INDEX `idx_sub_cid` (`sub_cid`);
-- 4. 写入二级分类(pid 指向一级,已存在则忽略)
INSERT IGNORE INTO `__PREFIX__haonav_category` (`id`, `pid`, `title`, `icon`, `summary`, `keywords`, `description`, `sort`, `status`, `website_count`, `click_count`, `create_at`, `update_at`) VALUES
(101, 1, '综合社交', '💬', '', '', '微博、朋友圈等综合社交平台', 100, 1, 0, 0, NULL, NULL),
(102, 1, '即时通讯', '📱', '', '', '微信、QQ、Telegram 等通讯工具', 90, 1, 0, 0, NULL, NULL),
(103, 1, '兴趣社区', '🏘️', '', '', '豆瓣、贴吧、知乎等兴趣社区', 80, 1, 0, 0, NULL, NULL),
(104, 1, '职场交友', '🤝', '', '', '脉脉、探探、Soul 等职场与交友', 70, 1, 0, 0, NULL, NULL),
(105, 2, '长视频', '🎬', '', '', '腾讯视频、爱奇艺、优酷等长视频', 100, 1, 0, 0, NULL, NULL),
(106, 2, '短视频', '📱', '', '', '抖音、快手、B站等短视频', 90, 1, 0, 0, NULL, NULL),
(107, 2, '动漫漫画', '🎨', '', '', '哔哩漫画、腾讯动漫、快看', 80, 1, 0, 0, NULL, NULL),
(108, 2, '直播娱乐', '🎥', '', '', '直播平台与综合娱乐', 70, 1, 0, 0, NULL, NULL),
(109, 3, '综合电商', '🛍️', '', '', '淘宝、京东、拼多多等综合电商', 100, 1, 0, 0, NULL, NULL),
(110, 3, '特卖二手', '♻️', '', '', '唯品会、闲鱼、转转等特卖与二手', 90, 1, 0, 0, NULL, NULL),
(111, 3, '本地生活', '🍜', '', '', '美团、大众点评、旅游出行', 80, 1, 0, 0, NULL, NULL),
(112, 3, '汽车房产', '🚗', '', '', '汽车之家、链家、贝壳等', 70, 1, 0, 0, NULL, NULL),
(113, 5, '在线课程', '📝', '', '', 'MOOC、网易云课堂、极客时间', 100, 1, 0, 0, NULL, NULL),
(114, 5, '学术检索', '🔬', '', '', '知网、万方、Google Scholar', 90, 1, 0, 0, NULL, NULL),
(115, 5, '基础教育', '📐', '', '', '可汗学院、学堂在线等', 80, 1, 0, 0, NULL, NULL),
(116, 6, '综合新闻', '📰', '', '', '人民日报、新华社、央视网', 100, 1, 0, 0, NULL, NULL),
(117, 6, '财经科技', '💡', '', '', '财新、36氪、界面新闻', 90, 1, 0, 0, NULL, NULL),
(118, 6, '健康医疗', '🩺', '', '', '丁香医生、好大夫在线', 80, 1, 0, 0, NULL, NULL),
(119, 7, '游戏平台', '🕹️', '', '', 'Steam、TapTap、腾讯游戏', 100, 1, 0, 0, NULL, NULL),
(120, 7, '小游戏', '🎲', '', '', '4399、7k7k 等小游戏', 90, 1, 0, 0, NULL, NULL),
(121, 8, '搜索引擎', '🔍', '', '', '百度、Google、Bing 等', 100, 1, 0, 0, NULL, NULL),
(122, 8, '门户邮箱', '📧', '', '', '新浪、搜狐、QQ邮箱、Gmail', 90, 1, 0, 0, NULL, NULL),
(123, 8, '翻译地图', '🗺️', '', '', '翻译、地图等工具门户', 80, 1, 0, 0, NULL, NULL),
(124, 9, '代码托管', '📦', '', '', 'GitHub、GitLab、Gitee', 100, 1, 0, 0, NULL, NULL),
(125, 9, '技术社区', '👨‍💻', '', '', 'CSDN、掘金、Stack Overflow', 90, 1, 0, 0, NULL, NULL),
(126, 9, '云服务', '☁️', '', '', '阿里云、腾讯云、Cloudflare', 80, 1, 0, 0, NULL, NULL),
(127, 9, '设计素材', '🎨', '', '', '站酷、花瓣、摄图网', 70, 1, 0, 0, NULL, NULL),
(128, 10, '网络文学', '📖', '', '', '起点、晋江、纵横等', 100, 1, 0, 0, NULL, NULL),
(129, 10, '电子阅读', '📚', '', '', '微信读书、掌阅、番茄小说', 90, 1, 0, 0, NULL, NULL),
(130, 11, '办公套件', '📊', '', '', 'WPS、Office 等办公套件', 100, 1, 0, 0, NULL, NULL),
(131, 11, '企业协同', '👥', '', '', '钉钉、飞书、企业微信', 90, 1, 0, 0, NULL, NULL),
(132, 11, '云存储', '💾', '', '', '百度网盘、阿里云盘', 80, 1, 0, 0, NULL, NULL),
(133, 11, '音乐音频', '🎵', '', '', '网易云、QQ音乐、Spotify', 70, 1, 0, 0, NULL, NULL),
(134, 12, '证券理财', '📈', '', '', '东方财富、同花顺、雪球', 100, 1, 0, 0, NULL, NULL),
(135, 12, '支付银行', '🏦', '', '', '支付宝、招商银行等', 90, 1, 0, 0, NULL, NULL);
-- 5. 已装库旧数据按 URL 特征归入二级(仅更新 sub_cid=0 的记录,安全可重复执行)
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 102 WHERE `sub_cid` = 0 AND (`url` LIKE '%weixin.qq%' OR `url` LIKE '%im.qq%' OR `url` LIKE '%telegram%' OR `url` LIKE '%whatsapp%' OR `url` LIKE '%skype%' OR `url` LIKE '%discord%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 103 WHERE `sub_cid` = 0 AND (`url` LIKE '%douban%' OR `url` LIKE '%tieba%' OR `url` LIKE '%zhihu%' OR `url` LIKE '%weibo%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 104 WHERE `sub_cid` = 0 AND (`url` LIKE '%maimai%' OR `url` LIKE '%tantan%' OR `url` LIKE '%soulapp%' OR `url` LIKE '%immomo%' OR `url` LIKE '%jiayuan%' OR `url` LIKE '%yidui%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 105 WHERE `sub_cid` = 0 AND (`url` LIKE '%v.qq%' OR `url` LIKE '%iqiyi%' OR `url` LIKE '%youku%' OR `url` LIKE '%mgtv%' OR `url` LIKE '%sohu.com%tv%' OR `url` LIKE '%miguvideo%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 106 WHERE `sub_cid` = 0 AND (`url` LIKE '%douyin%' OR `url` LIKE '%kuaishou%' OR `url` LIKE '%bilibili%' OR `url` LIKE '%ixigua%' OR `url` LIKE '%haokan%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 107 WHERE `sub_cid` = 0 AND (`url` LIKE '%manga%' OR `url` LIKE '%manhua%' OR `url` LIKE '%ac.qq%' OR `url` LIKE '%u17%' OR `url` LIKE '%dmzj%' OR `url` LIKE '%manhuadb%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 108 WHERE `sub_cid` = 0 AND (`url` LIKE '%douyu%' OR `url` LIKE '%huya%' OR `url` LIKE '%live%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 109 WHERE `sub_cid` = 0 AND (`url` LIKE '%taobao%' OR `url` LIKE '%tmall%' OR `url` LIKE '%jd.com%' OR `url` LIKE '%pinduoduo%' OR `url` LIKE '%suning%' OR `url` LIKE '%amazon%' OR `url` LIKE '%mi.com%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 110 WHERE `sub_cid` = 0 AND (`url` LIKE '%vip.com%' OR `url` LIKE '%dangdang%' OR `url` LIKE '%goofish%' OR `url` LIKE '%zhuanzhuan%' OR `url` LIKE '%xianyu%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 111 WHERE `sub_cid` = 0 AND (`url` LIKE '%meituan%' OR `url` LIKE '%ele.me%' OR `url` LIKE '%dianping%' OR `url` LIKE '%ctrip%' OR `url` LIKE '%fliggy%' OR `url` LIKE '%qunar%' OR `url` LIKE '%mafengwo%' OR `url` LIKE '%12306%' OR `url` LIKE '%ly.com%' OR `url` LIKE '%kfc%' OR `url` LIKE '%mcdonalds%' OR `url` LIKE '%xiachufang%' OR `url` LIKE '%meishij%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 112 WHERE `sub_cid` = 0 AND (`url` LIKE '%autohome%' OR `url` LIKE '%dongchedi%' OR `url` LIKE '%yiche%' OR `url` LIKE '%lianjia%' OR `url` LIKE '%ke.com%' OR `url` LIKE '%anjuke%' OR `url` LIKE '%5i5j%' OR `url` LIKE '%guazi%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 113 WHERE `sub_cid` = 0 AND (`url` LIKE '%icourse%' OR `url` LIKE '%study.163%' OR `url` LIKE '%ke.qq%' OR `url` LIKE '%xuetangx%' OR `url` LIKE '%imooc%' OR `url` LIKE '%geekbang%' OR `url` LIKE '%jikexueyuan%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 114 WHERE `sub_cid` = 0 AND (`url` LIKE '%cnki%' OR `url` LIKE '%wanfang%' OR `url` LIKE '%cqvip%' OR `url` LIKE '%scholar.google%' OR `url` LIKE '%xueshu.baidu%' OR `url` LIKE '%smartedu%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 115 WHERE `sub_cid` = 0 AND (`url` LIKE '%khanacademy%' OR `url` LIKE '%coursera%' OR `url` LIKE '%edx%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 116 WHERE `sub_cid` = 0 AND (`url` LIKE '%people.com%' OR `url` LIKE '%news.cn%' OR `url` LIKE '%cctv%' OR `url` LIKE '%chinanews%' OR `url` LIKE '%huanqiu%' OR `url` LIKE '%cankaoxiaoxi%' OR `url` LIKE '%thepaper%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 117 WHERE `sub_cid` = 0 AND (`url` LIKE '%caixin%' OR `url` LIKE '%jiemian%' OR `url` LIKE '%36kr%' OR `url` LIKE '%tmtpost%' OR `url` LIKE '%leiphone%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 118 WHERE `sub_cid` = 0 AND (`url` LIKE '%dxy.com%' OR `url` LIKE '%haodf%' OR `url` LIKE '%jk.cn%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 119 WHERE `sub_cid` = 0 AND (`url` LIKE '%steampowered%' OR `url` LIKE '%taptap%' OR `url` LIKE '%game.qq%' OR `url` LIKE '%game.163%' OR `url` LIKE '%epicgames%' OR `url` LIKE '%wegame%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 120 WHERE `sub_cid` = 0 AND (`url` LIKE '%4399%' OR `url` LIKE '%7k7k%' OR `url` LIKE '%2144%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 121 WHERE `sub_cid` = 0 AND (`url` LIKE '%baidu.com%' OR `url` LIKE '%google.com%' OR `url` LIKE '%bing%' OR `url` LIKE '%sogou%' OR `url` LIKE '%yahoo%' OR `url` LIKE '%duckduckgo%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 122 WHERE `sub_cid` = 0 AND (`url` LIKE '%sina%' OR `url` LIKE '%sohu%' OR `url` LIKE '%mail.qq%' OR `url` LIKE '%mail.163%' OR `url` LIKE '%outlook%' OR `url` LIKE '%gmail%' OR `url` LIKE '%foxmail%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 123 WHERE `sub_cid` = 0 AND (`url` LIKE '%fanyi%' OR `url` LIKE '%map.baidu%' OR `url` LIKE '%amap%' OR `url` LIKE '%translate%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 124 WHERE `sub_cid` = 0 AND (`url` LIKE '%github%' OR `url` LIKE '%gitlab%' OR `url` LIKE '%gitee%' OR `url` LIKE '%bitbucket%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 125 WHERE `sub_cid` = 0 AND (`url` LIKE '%csdn%' OR `url` LIKE '%juejin%' OR `url` LIKE '%stackoverflow%' OR `url` LIKE '%oschina%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 126 WHERE `sub_cid` = 0 AND (`url` LIKE '%aliyun%' OR `url` LIKE '%cloud.tencent%' OR `url` LIKE '%huaweicloud%' OR `url` LIKE '%qiniu%' OR `url` LIKE '%cloudflare%' OR `url` LIKE '%cloud.baidu%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 127 WHERE `sub_cid` = 0 AND (`url` LIKE '%zcool%' OR `url` LIKE '%huaban%' OR `url` LIKE '%ui.cn%' OR `url` LIKE '%699pic%' OR `url` LIKE '%58pic%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 128 WHERE `sub_cid` = 0 AND (`url` LIKE '%qidian%' OR `url` LIKE '%jjwxc%' OR `url` LIKE '%zongheng%' OR `url` LIKE '%hongxiu%' OR `url` LIKE '%xxsy%' OR `url` LIKE '%heiyan%' OR `url` LIKE '%fanqie%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 129 WHERE `sub_cid` = 0 AND (`url` LIKE '%weread%' OR `url` LIKE '%zhangyue%' OR `url` LIKE '%read.douban%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 130 WHERE `sub_cid` = 0 AND (`url` LIKE '%wps%' OR `url` LIKE '%office%' OR `url` LIKE '%yozosoft%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 131 WHERE `sub_cid` = 0 AND (`url` LIKE '%dingtalk%' OR `url` LIKE '%feishu%' OR `url` LIKE '%work.weixin%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 132 WHERE `sub_cid` = 0 AND (`url` LIKE '%pan.baidu%' OR `url` LIKE '%aliyundrive%' OR `url` LIKE '%lanzou%' OR `url` LIKE '%cloud.189%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 133 WHERE `sub_cid` = 0 AND (`url` LIKE '%music.163%' OR `url` LIKE '%y.qq%' OR `url` LIKE '%kugou%' OR `url` LIKE '%kuwo%' OR `url` LIKE '%spotify%' OR `url` LIKE '%music.apple%' OR `url` LIKE '%ximalaya%' OR `url` LIKE '%qtfm%' OR `url` LIKE '%lizhi%' OR `url` LIKE '%kg.qq%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 134 WHERE `sub_cid` = 0 AND (`url` LIKE '%eastmoney%' OR `url` LIKE '%10jqka%' OR `url` LIKE '%xueqiu%' OR `url` LIKE '%finance.sina%' OR `url` LIKE '%hexun%' OR `url` LIKE '%dzh%');
UPDATE `__PREFIX__haonav_links` SET `sub_cid` = 135 WHERE `sub_cid` = 0 AND (`url` LIKE '%alipay%' OR `url` LIKE '%cmbchina%' OR `url` LIKE '%icbc%' OR `url` LIKE '%ccb%' OR `url` LIKE '%boc.cn%');
+282 -329
View File
@@ -1,59 +1,21 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{$title | default='YwxApp'}</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href="/assets/layui/css/layui.css" rel="stylesheet">
<link href="/assets/ywxapp/css/ywxapp.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements. All other JS at the end of file. -->
<!--[if lt IE 9]>
<script src="__CDN__/assets/js/html5shiv.js"></script>
<script src="__CDN__/assets/js/respond.min.js"></script>
<![endif]-->
<script type="text/javascript">
var config = {
root: "/",
};
var require = {
};
</script>
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-form-item">
<div class="layui-inline">
<input type="text" name="title" placeholder="请输入标题" autocomplete="off" class="layui-input">
</div>
<div class="layui-inline">
<button class="layui-btn layuiadmin-btn-useradmin" lay-submit lay-filter="tableSearchButton">
<i class="layui-icon layui-icon-search layuiadmin-button-btn"></i>
</button>
</div>
</div>
</div>
<div class="layui-card-header"> </div>
<div class="layui-card-body">
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
</div>
<script type="text/html" id="dataBarTpl">
<script type="text/html" id="dataBarTpl">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm layui-btn-primary" title="编辑节点" lay-event="update"><i class="layui-icon layui-icon-edit"></i> </a>
<a class="layui-btn layui-btn-sm layui-btn-primary" title="添加子节点" lay-event="create"><i class="layui-icon layui-icon-add-1"></i> </a>
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
</div>
</script>
<!-- 添加/编辑菜单表单模板 -->
<script type="text/html" id="dataFormTpl">
<!-- 添加/编辑菜单表单模板 -->
<script type="text/html" id="dataFormTpl">
<form class="layui-form layui-form-pane" lay-filter="wxapp-form" id="wxapp-form">
<input type="hidden" name="id" value="{{= d.id || '' }}">
<input type="hidden" name="pid" value="{{= d.pid || 0 }}">
@@ -105,11 +67,11 @@
</div>
</form>
</script>
<!-- 状态开关 -->
<script type="text/html" id="statusTpl">
<!-- 状态开关 -->
<script type="text/html" id="statusTpl">
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-text="启用|禁用" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
</script>
<script type="text/html" id="dataRecybinTpl">
<script type="text/html" id="dataRecybinTpl">
<div class="layui-card">
<div class="layui-card-header"> </div>
<div class="layui-card-body">
@@ -118,303 +80,294 @@
</div>
</script>
</script>
<script src="/assets/layui/layui.js"></script>
<script src="/assets/ywxapp/ywxapp.js" module="backend"></script>
<script>
layui.use(['layer', 'http'], function () {
var $ = layui.$;
var treeTable = layui.treeTable;
var form = layui.form;
var layer = layui.layer;
var laytpl = layui.laytpl;
var http = layui.http;
console.log('layui.treeTable:', layui.setter.layerArea()); // 调试输出
treeTable.render({
defaultToolbar: [{
title: '创建资源', layEvent: 'dataCreate', icon: 'layui-icon-add-1'
}, {
title: '删除资源', layEvent: 'dataDelete', icon: 'layui-icon-delete'
}, {
title: '资源回收站', layEvent: 'dataRecybin', icon: 'layui-icon-home'
}, 'filter', 'exports', 'print'
],
elem: '#dataTable',
url: 'category/index',
parseData: function (res) { // 预处理返回数据
console.log('原始数据:', res); // 调试输入
return {
code: res.code === 0 ? 0 : 1, // 0 标识成功
data: res.data || [], // 数据数组
msg: res.message || '' // 错误信息
};
},
tree: {
customName: {
children: "children",
isParent: "is_parent",
name: "title",
id: "id",
pid: "pid",
icon: "icon"
},
data: { isSimpleData: true, rootPid: 0 },
view: {},
async: {},
callback: {}
},
height: 'full-100',
toolbar: '#tableBarTpl',
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: '名称', width: 180, fixed: 'left' },
{ field: 'icon', title: '图标', width: 80 },
{ field: 'sort', title: '排序', width: 80, sort: true },
{ field: 'status', title: '状态', width: 96, align: 'center', templet: '#statusTpl' },
{ fixed: "right", title: "操作", align: "center", toolbar: "#dataBarTpl", fixed: 'right' }
]],
page: true,
done: function (res, curr, count, origin) { }
});
treeTable.on('toolbar(dataTable)', function (obj) {
var options = obj.config;
switch (obj.event) {
case 'dataCreate':
active.dataCreate({ pid: 0, type: 1 });
break;
case 'dataDelete':
var checkStatus = treeTable.checkStatus('dataTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
active.dataDelete(ids);
break;
case 'dataRecybin':
active.dataRecybin();
break;
<script>
layui.use(['layer', 'http'], function () {
var $ = layui.$;
var treeTable = layui.treeTable;
var form = layui.form;
var layer = layui.layer;
var laytpl = layui.laytpl;
var http = layui.http;
var cols = [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: '名称', width: 180, fixed: 'left' },
{ field: 'icon', title: '图标', width: 80 },
{ field: 'sort', title: '排序', width: 80, sort: true },
{ field: 'status', title: '状态', width: 96, align: 'center', templet: '#statusTpl' },
{ fixed: "right", title: "操作", align: "center", toolbar: "#dataBarTpl", fixed: 'right' }
]];
treeTable.render({
elem: '#dataTable',
url: 'index',
parseData: function (res) { // 预处理返回数据
console.log('原始数据:', res); // 调试输入
return {
code: res.code === 0 ? 0 : 1, // 0 标识成功
data: res.data || [], // 数据数组
msg: res.message || '' // 错误信息
};
},
tree: {
customName: {
children: "children",
isParent: "is_parent",
name: "title",
id: "id",
pid: "pid",
icon: "icon"
},
data: { isSimpleData: true, rootPid: 0 },
view: {},
async: {},
callback: {}
},
height: 'full-100',
toolbar: '#tableBarTpl',
cols: cols,
page: true,
done: function (res, curr, count, origin) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
}
});
treeTable.on('toolbar(dataTable)', function (obj) {
var options = obj.config;
switch (obj.event) {
case 'dataCreate':
active.dataCreate({ pid: 0, type: 1 });
break;
case 'dataDelete':
var checkStatus = treeTable.checkStatus('dataTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
active.dataDelete(ids);
break;
case 'dataRecybin':
active.dataRecybin();
break;
};
});
treeTable.on('tool(dataTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'update':
active.dataEdit(data);
break;
case 'create':
active.dataCreate({ pid: data.id });
break;
case 'delete':
active.dataDelete([data.id]);
break;
default:
break;
}
});
// 状态开关
form.on('switch(statusSwitch)', function (obj) {
var data = this.value;
var status = obj.elem.checked ? 1 : 0;
console.log(data);
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该数据吗?', function (index) {
layui.http.put('update', { id: data.id,title:data.title, status: status }).then(function (res) {
if (res.code === 0) {
layer.msg(res.msg, { icon: 1 });
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg, { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
});
layer.close(index);
}, function () {
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
treeTable.on('tool(dataTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'update':
active.dataEdit(data);
break;
case 'create':
active.dataCreate({ pid: data.id });
break;
case 'delete':
active.dataDelete([data.id]);
break;
default:
break;
});
var dataFromFun = function (data, callback, done) {
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
layer.open({
title: data.id ? '编辑数据' : (data.pid ? '添加子数据' : '添加根数据'),
content: formHtml,
anim: "slideLeft",
offset: "r",
btnAlign: "l",
area: ['520px', '98%'],
shade: 0.1,
shadeClose: true,
btn: ['确定', '取消'],
success: function (layero, index) {
callback(layero, index);
form.render();
},
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem);
layui.off('submit(wxapp-form)', 'from');
return false;
});
layero.contents().find("#wxapp-form-submit").trigger('click');
}
});
form.on('submit(tableSearchButton)', function (data) {
var field = data.field;
treeTable.reload('dataTable', {
where: field
});
});
// 状态开关
form.on('switch(statusSwitch)', function (obj) {
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该数据吗?', function (index) {
layui.http.put('update', { id: id, status: status }).then(function (res) {
if (res.code == 0) {
layer.msg(res.msg, { icon: 1 });
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg, { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
});
};
//事件
var active = {
dataCreate: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.status = field.status ? 1 : 0;
http.post('save', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataEdit: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.status = field.status ? 1 : 0;
http.put('update', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataDelete: function (ids, force = 0) {
if (ids.length === 0) {
return layer.msg('请选择数据');
}
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index) {
layer.close(index);
}, function () {
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
layer.confirm('确定删除吗?', function (index) {
http.delete('delete', { ids: ids.join(','), force: force })
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
layer.msg('已删除');
treeTable.reload('dataTable', {}, true);
});
});
});
var dataFromFun = function (data, callback, done) {
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
},
dataRecybin: function (data = {}) {
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
layer.open({
title: data.id ? '编辑数据' : (data.pid ? '添加子数据' : '添加根数据'),
title: '资源回收站',
content: formHtml,
anim: "slideLeft",
offset: "r",
btnAlign: "l",
area: layui.setter.layerArea(),
area: ['60%', '99%'],
shade: 0.1,
shadeClose: true,
btn: ['确定', '取消'],
success: function (layero, index) {
callback(layero, index);
form.render();
},
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem);
layui.off('submit(wxapp-form)', 'from');
return false;
});
layero.contents().find("#wxapp-form-submit").trigger('click');
}
});
};
//事件
var active = {
dataCreate: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.icon = 'layui-icon ' + field.icon;
field.status = field.status ? 1 : 0;
http.post('save', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataEdit: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.icon = 'layui-icon ' + field.icon;
field.status = field.status ? 1 : 0;
http.put('update', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataDelete: function (ids, force = 0) {
if (ids.length === 0) {
return layer.msg('请选择数据');
}
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index) {
layer.close(index);
layer.confirm('确定删除吗?', function (index) {
http.delete('delete', { ids: ids.join(','), force: force })
.then((res) => {
if (res.code === 0) {
layer.close(index);
treeTable.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
layer.msg('已删除');
treeTable.reload('dataTable', {}, true);
});
});
},
dataRecybin: function (data = {}) {
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
layer.open({
title: '资源回收站',
content: formHtml,
anim: "slideLeft",
offset: "r",
area: ['60%', '99%'],
shade: 0.1,
shadeClose: true,
success: function (layero, index) {
layui.table.render({
elem: '#dataRecybinTable',
url: 'recyclebin',
height: 'full-100',
defaultToolbar: [{
title: '批量删除数据',
layEvent: 'dataDelete',
icon: 'layui-icon-delete',
onClick: function (obj) {
var checkStatus = treeTable.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
console.log(ids);
}
}, 'filter', 'exports', 'print'],
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: '标题', width: 180, fixed: 'left' },
{
fixed: "right", title: "操作", width: 120, align: "center", templet: function (d) {
return `<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" title="恢复数据" lay-event="restore" > <i class="layui-icon layui-icon-edit"></i> </a >
<a class="layui-btn layui-btn-sm" title="删除数据" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
</div >`;
}
}
]],
page: true,
done: function (res, curr, count, origin) {
layui.table.on('tool(dataRecybinTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'restore':
active.dataRestore([data.id]);
break;
case 'delete':
active.dataDelete([data.id], 1);
break;
}
});
layui.table.on('toolbar(dataRecybinTable)', function (elem) {
switch (elem.event) {
case 'dataDelete':
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let idss = checkData.map((item, index, array) => { return item.id; });
active.dataDelete(idss, 1);
break;
case 'dataRestore':
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
active.dataRestore(ids);
break;
};
});
layui.table.render({
elem: '#dataRecybinTable',
url: 'recyclebin',
height: 'full-100',
defaultToolbar: [{
title: '批量删除数据',
layEvent: 'dataDelete',
icon: 'layui-icon-delete',
onClick: function (obj) {
var checkStatus = treeTable.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
console.log(ids);
}
});
}
});
},
dataRestore: function (ids) {
if (ids.length === 0) {
return layer.msg('请选择数据');
}
http.put('restore', { ids: ids.join(',') })
.then((res) => {
if (res.code === 0) {
layer.msg('恢复成功');
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '恢复失败');
}, 'filter', 'exports', 'print'],
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: 'title', width: 180, fixed: 'left' },
{
fixed: "right", title: "操作", width: 120, align: "center", templet: function (d) {
return `<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" title="恢复数据" lay-event="restore" > <i class="layui-icon layui-icon-edit"></i> </a >
<a class="layui-btn layui-btn-sm" title="删除数据" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
</div >`;
}
}
]],
page: true,
done: function (res, curr, count, origin) {
layui.table.on('tool(dataRecybinTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'restore':
active.dataRestore([data.id]);
break;
case 'delete':
active.dataDelete([data.id], 1);
break;
}
});
layui.table.on('toolbar(dataRecybinTable)', function (elem) {
switch (elem.event) {
case 'dataDelete':
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let idss = checkData.map((item, index, array) => { return item.id; });
active.dataDelete(idss, 1);
break;
case 'dataRestore':
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
active.dataRestore(ids);
break;
};
});
}
});
}
});
},
dataRestore: function (ids) {
if (ids.length === 0) {
return layer.msg('请选择数据');
}
};
});
</script>
</body>
http.put('restore', { ids: ids.join(',') })
.then((res) => {
if (res.code === 0) {
layer.msg('恢复成功');
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '恢复失败');
}
});
}
};
});
</html>
</script>
+56 -11
View File
@@ -62,18 +62,63 @@
<div class="section-title">
<h2>📋 {$info.title}</h2>
</div>
<div class="website-list">
{volist name="info.links" id="link"}
<a href="/haonav/site/{$link.id}.html" class="website-item">
<span class="website-item-icon"><img src="{$link.show_icon}" loading="lazy" alt="{$link.title}"></span>
<span class="website-item-info">
<h3>{$link.title}</h3>
<p>{$link.description|default='暂无描述'}</p>
</span>
<span class="qr-btn js-qr" data-url="{$link.url}" title="二维码">▦</span>
</a>
{/volist}
{if $mode == 'group'}
<!-- 顶级分类:按二级子分类分组展示 -->
{volist name="info.children" id="sub"}
<div class="sub-category">
<div class="sub-title"><span>{$sub.title}</span></div>
<div class="website-list">
{volist name="sub.sub_links" id="link"}
<a href="/haonav/site/{$link.id}.html" class="website-item">
<span class="website-item-icon"><img src="{$link.show_icon}" loading="lazy" alt="{$link.title}"></span>
<span class="website-item-info">
<h3>{$link.title}</h3>
<p>{$link.description|default='暂无描述'}</p>
</span>
<span class="qr-btn js-qr" data-url="{$link.url}" title="二维码">▦</span>
</a>
{/volist}
</div>
</div>
{/volist}
<!-- 未细分二级的常用链接(sub_cid=0) -->
{notempty name="others"}
<div class="sub-category">
<div class="sub-title"><span>常用</span></div>
<div class="website-list">
{volist name="others" id="link"}
<a href="/haonav/site/{$link.id}.html" class="website-item">
<span class="website-item-icon"><img src="{$link.show_icon}" loading="lazy" alt="{$link.title}"></span>
<span class="website-item-info">
<h3>{$link.title}</h3>
<p>{$link.description|default='暂无描述'}</p>
</span>
<span class="qr-btn js-qr" data-url="{$link.url}" title="二维码">▦</span>
</a>
{/volist}
</div>
</div>
{/notempty}
{else /}
<!-- 二级/叶子分类:直接列出该分类下的全部链接 -->
<div class="sub-category">
<div class="sub-title"><span>{$info.title}</span></div>
<div class="website-list">
{volist name="links" id="link"}
<a href="/haonav/site/{$link.id}.html" class="website-item">
<span class="website-item-icon"><img src="{$link.show_icon}" loading="lazy" alt="{$link.title}"></span>
<span class="website-item-info">
<h3>{$link.title}</h3>
<p>{$link.description|default='暂无描述'}</p>
</span>
<span class="qr-btn js-qr" data-url="{$link.url}" title="二维码">▦</span>
</a>
{/volist}
</div>
</div>
{/if}
</div>
<div class="footer">
+19 -12
View File
@@ -143,21 +143,28 @@
<!-- 分类区块 -->
<div class="category-block" id="cat-{$k}">
<div class="section-title">
<h2>📋 <a href="/haonav/category/{$vo.id}.html" target="_blank"> {$vo.title} </a></h2>
<h2>📋 <a href="/haonav/category/{$vo.id}.html" target="_blank"> {$vo.title} </a></h2> <div class="sub-title"><span>全部</span> <a class="sub-more" href="/haonav/category/{$vo.id}.html">更多 ›</a></div>
</div>
<div class="website-grid">
{volist name="vo.links" id="site"}
<div class="website-card" data-sid="{$site.id}">
<span class="fav-btn js-fav" data-id="{$site.id}" data-title="{$site.title}" data-url="{$site.url}" data-icon="{$site.show_icon}" title="收藏">★</span>
<span class="qr-btn js-qr" data-url="{$site.url}" title="二维码">▦</span>
<a href="/haonav/site/{$site.id}.html" target="_blank" title="{$site.title}" style="text-decoration:none;color:inherit;display:block;">
<div class="website-icon"><img src="{$site.show_icon}" loading="lazy" alt="{$site.title}"></div>
<div class="website-name">{$site.title}</div>
{if $site.rating_count > 0}<div class="website-rating">⭐ {$site.rating}<span class="rc">({$site.rating_count})</span></div>{/if}
</a>
<!-- 首页只展示一级分类下的全部链接,二级分类在分类页展示 -->
{notempty name="vo.links"}
<div class="sub-category">
<div class="website-grid">
{volist name="vo.links" id="site"}
<div class="website-card" data-sid="{$site.id}">
<span class="fav-btn js-fav" data-id="{$site.id}" data-title="{$site.title}" data-url="{$site.url}" data-icon="{$site.show_icon}" title="收藏">★</span>
<span class="qr-btn js-qr" data-url="{$site.url}" title="二维码">▦</span>
<a href="/haonav/site/{$site.id}.html" target="_blank" title="{$site.title}" style="text-decoration:none;color:inherit;display:block;">
<div class="website-icon"><img src="{$site.show_icon}" loading="lazy" alt="{$site.title}"></div>
<div class="website-name">{$site.title}</div>
{if $site.rating_count > 0}<div class="website-rating">⭐ {$site.rating}<span class="rc">({$site.rating_count})</span></div>{/if}
</a>
</div>
{/volist}
</div>
{/volist}
</div>
{/notempty}
</div>
{/volist}