Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 246
白荼日记 - 前端 https://blog.iletter.top/tag/%E5%89%8D%E7%AB%AF/ typecho博客Hello world插件增强 https://blog.iletter.top/archives/424.html 2025-08-07T13:59:32+08:00 后台点击用户名称旁边的hello word会出现提示。直接将该代码替换掉原本的代码即可:<?php namespace TypechoPlugin\HelloWorld; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Typecho\Widget\Helper\Form\Element\Text; use Widget\Options; use Widget\Stat; use Typecho\Db; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Hello World * * @package HelloWorld * @author 白荼 * @version 1.0.0 */ class Plugin implements PluginInterface { /** * 激活插件方法,如果激活失败,直接抛出异常 */ public static function activate() { \Typecho\Plugin::factory('admin/menu.php')->navBar = __CLASS__ . '::render'; // 添加CSS和JS \Typecho\Plugin::factory('admin/header.php')->header = __CLASS__ . '::header'; } /** * 禁用插件方法,如果禁用失败,直接抛出异常 */ public static function deactivate() { } /** * 获取插件配置面板 * * @param Form $form 配置面板 */ public static function config(Form $form) { /** 分类名称 */ $name = new Text('word', null, 'Hello World', _t('说点什么')); $form->addInput($name); } /** * 个人用户的配置面板 * * @param Form $form */ public static function personalConfig(Form $form) { } /** * 在header中添加CSS和JS */ public static function header($header) { $customCssJs = ' <!-- HelloWorld Plugin CSS --> <style> .helloworld-tooltip { position: absolute; background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 9999; min-width: 280px; display: none; font-size: 12px; } .helloworld-tooltip h4 { margin: 0 0 10px 0; padding: 0; color: #333; border-bottom: 1px solid #eee; padding-bottom: 8px; font-size: 14px; } .helloworld-tooltip .comparison { background: #f8f9fa; border-left: 3px solid #007cba; padding: 8px 12px; margin: 10px 0; border-radius: 0 4px 4px 0; font-size: 13px; } .helloworld-tooltip ul { list-style: none; padding: 0; margin: 0; } .helloworld-tooltip li { padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.5; } .helloworld-tooltip li:last-child { border-bottom: none; } .helloworld-tooltip strong { color: #666; font-weight: normal; } .helloworld-trigger { cursor: pointer; border-bottom: 1px dotted #BBBBBB; } .helloworld-trigger:hover { color: #666 !important; border-bottom-color: #666; } </style> <!-- HelloWorld Plugin JS --> <script> (function() { function initHelloWorldTooltip() { var trigger = document.querySelector(".helloworld-trigger"); var tooltip = document.querySelector(".helloworld-tooltip"); if (trigger && tooltip) { trigger.addEventListener("click", function(e) { e.preventDefault(); e.stopPropagation(); // 计算位置 var rect = trigger.getBoundingClientRect(); var tooltipWidth = tooltip.offsetWidth || 280; var tooltipHeight = tooltip.offsetHeight || 250; // 默认位置 var left = rect.left + window.scrollX; var top = rect.bottom + window.scrollY + 5; // 边界检测 if (left + tooltipWidth > window.innerWidth + window.scrollX) { left = window.innerWidth + window.scrollX - tooltipWidth - 10; } if (top + tooltipHeight > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - tooltipHeight - 5; } tooltip.style.left = left + "px"; tooltip.style.top = top + "px"; tooltip.style.display = "block"; }); // 点击其他地方关闭 document.addEventListener("click", function(e) { if (!tooltip.contains(e.target) && e.target !== trigger) { tooltip.style.display = "none"; } }); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initHelloWorldTooltip); } else { initHelloWorldTooltip(); } })(); </script>'; return $header . $customCssJs; } /** * 根据字数获取对应的文学著作比较 */ private static function getLiteraryComparison($wordCount) { // 文学著作字数对照表(字数 => 著作名称) $literaryWorks = [ // 百字级 50 => '几行诗句', 100 => '一段朋友圈文字', 200 => '一条微博内容', 500 => '一篇微博长文', // 千字级 1000 => '一页A4纸手写内容', 1500 => '一页打印稿', 2000 => '一篇标准作文', 3000 => '一篇博客文章', 5000 => '一篇短篇小说', 8000 => '一篇中篇小说开头', // 万字级(中国现代文学) 10000 => '《呐喊》', 12000 => '《彷徨》', 15000 => '《朝花夕拾》', 20000 => '《野草》', 25000 => '《故事新编》', 30000 => '《热风》', 35000 => '《坟》', 40000 => '《而已集》', 45000 => '《三闲集》', // 万字级(其他现代文学) 50000 => '《沉沦》(郁达夫)', 55000 => '《春风沉醉的晚上》', 60000 => '《迟桂花》', 65000 => '《茑萝集》', 70000 => '《过去》', 75000 => '《薄奠》', 80000 => '《微雪的早晨》', // 万字级(长篇小说选段) 85000 => '《骆驼祥子》前半部分', 90000 => '《二马》', 95000 => '《离婚》', 100000 => '《骆驼祥子》', 110000 => '《四世同堂》第一部分', 120000 => '《边城》', 130000 => '《湘行散记》', 140000 => '《长河》', 150000 => '《家》', 160000 => '《憩园》', 170000 => '《第四病室》', 180000 => '《春》', 190000 => '《秋》', 200000 => '《寒夜》', // 20万字级 210000 => '《激流三部曲》选段', 220000 => '《爱情三部曲》选段', 230000 => '《抗战三部曲》选段', 240000 => '《人间词话》', 250000 => '《人间喜剧》选段', 260000 => '《巴黎圣母院》', 270000 => '《悲惨世界》选段', 280000 => '《九三年》', 290000 => '《海上劳工》', 300000 => '《笑面人》', // 30万字级 310000 => '《约翰·克利斯朵夫》选段', 320000 => '《红与黑》', 330000 => '《巴马修道院》', 340000 => '《吕西安·娄凡》', 350000 => '《阿尔芒斯》', 360000 => '《法尼娜·法尼尼》', 370000 => '《瓦尼娜·瓦尼尼》', 380000 => '《红屋骑士》', 390000 => '《卡斯特罗修道院院长》', 400000 => '《意大利遗事》', // 40万字级 410000 => '《三个火枪手》', 420000 => '《基督山伯爵》', 430000 => '《黑郁金香》', 440000 => '《二十年后》', 450000 => '《布拉热洛纳子爵》', 460000 => '《玛戈王后》', 470000 => '《蒙梭罗夫人》', 480000 => '《四十五卫士》', 490000 => '《侠隐记》', 500000 => '《红楼梦》(精简版)', // 50万字级 520000 => '《水浒传》选段', 540000 => '《西游记》选段', 560000 => '《三国演义》选段', 580000 => '《儒林外史》', 600000 => '《聊斋志异》选段', 620000 => '《阅微草堂笔记》', 640000 => '《子不语》', 660000 => '《夜雨秋灯录》', 680000 => '《萤窗异草》', 700000 => '《耳食录》', // 70万字级 720000 => '《浮生六记》', 740000 => '《影梅庵忆语》', 760000 => '《秋灯琐忆》', 780000 => '《香艳丛书》选段', 800000 => '《太平广记》选段', 820000 => '《夷坚志》选段', 840000 => '《剪灯新话》', 860000 => '《剪灯余话》', 880000 => '《觅灯因话》', 900000 => '《喻世明言》', // 百万字级 920000 => '《警世通言》', 940000 => '《醒世恒言》', 960000 => '《初刻拍案惊奇》', 980000 => '《二刻拍案惊奇》', 1000000 => '《红楼梦》', 1050000 => '《镜花缘》', 1100000 => '《老残游记》', 1150000 => '《孽海花》', 1200000 => '《官场现形记》', 1250000 => '《二十年目睹之怪现状》', // 百万字级以上 1300000 => '《资本论》第一卷', 1400000 => '《莎士比亚十四行诗集》', 1500000 => '《战争与和平》', 1600000 => '《安娜·卡列尼娜》', 1700000 => '《复活》', 1800000 => '《静静的顿河》', 1900000 => '《被开垦的处女地》', 2000000 => '《一个人的遭遇》', 2100000 => '《静静的顿河》全本', 2200000 => '《钢铁是怎样炼成的》', 2300000 => '《青年近卫军》', 2400000 => '《日瓦戈医生》', 2500000 => '《追忆似水年华》第一卷', 2600000 => '《约翰·克利斯朵夫》', 2700000 => '《马丁·伊登》', 2800000 => '《美国悲剧》', 2900000 => '《嘉莉妹妹》', 3000000 => '《资本论》全三卷', 3100000 => '《莎士比亚全集》悲剧部分', 3200000 => '《莎士比亚全集》喜剧部分', 3300000 => '《莎士比亚全集》历史剧部分', 3400000 => '《莎士比亚十四行诗全集》', 3500000 => '《莎士比亚全集》', 3600000 => '《追忆似水年华》第二卷', 3700000 => '《追忆似水年华》第三卷', 3800000 => '《追忆似水年华》第四卷', 3900000 => '《追忆似水年华》第五卷', 4000000 => '《追忆似水年华》', 4200000 => '《堂吉诃德》', 4400000 => '《神曲》', 4600000 => '《荷马史诗》', 4800000 => '《失乐园》', 5000000 => '《大英百科全书》一卷', 5500000 => '《大英百科全书》两卷', 6000000 => '《牛津英语词典》一卷', 6500000 => '《不列颠百科全书》', 7000000 => '《中国大百科全书》一卷', 7500000 => '《辞海》', 8000000 => '《康熙字典》', 9000000 => '《中华大字典》', 10000000 => '《大英百科全书》全套', 12000000 => '《四库全书》选段', 15000000 => '《永乐大典》残卷', 20000000 => '《古今图书集成》选段' ]; // 找到最接近的著作 $closestWork = '几行文字'; $closestWords = 0; foreach ($literaryWorks as $words => $work) { if ($wordCount >= $words) { $closestWork = $work; $closestWords = $words; } else { break; } } // 计算倍数 if ($closestWords > 0) { $multiple = round($wordCount / $closestWords, 1); if ($multiple >= 2) { return "约等于 {$multiple} 本 {$closestWork} 的字数"; } else { return "约等于 1 本 {$closestWork} 的字数"; } } else { return "约等于几行文字的字数"; } } /** * 获取统计信息 */ private static function getStats() { $db = Db::get(); $user = \Typecho\Widget::widget('Widget_User'); // 最近登录时间 $lastLogin = $user->logged; $lastLoginText = $lastLogin ? date('Y-m-d H:i:s', $lastLogin) : '未知'; // 文章数量 $postCount = $db->fetchObject($db->select(['COUNT(cid)' => 'num']) ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid))->num; // 字数统计(简单统计标题和内容长度) $posts = $db->fetchAll($db->select('title', 'text') ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid)); $totalWords = 0; foreach ($posts as $post) { $totalWords += mb_strlen($post['title'], 'UTF-8'); $totalWords += mb_strlen(strip_tags($post['text']), 'UTF-8'); } // 最近文章时间 $lastPost = $db->fetchRow($db->select('created') ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid) ->order('created', Db::SORT_DESC) ->limit(1)); $lastPostTime = $lastPost ? date('Y-m-d H:i:s', $lastPost['created']) : '暂无文章'; // 获取文学著作比较 $literaryComparison = self::getLiteraryComparison($totalWords); return [ 'lastLogin' => $lastLoginText, 'postCount' => $postCount, 'totalWords' => $totalWords, 'lastPostTime' => $lastPostTime, 'literaryComparison' => $literaryComparison ]; } /** * 插件实现方法 * * @access public * @return void */ public static function render() { $stats = self::getStats(); $word = Options::alloc()->plugin('HelloWorld')->word; // 获取当前登录用户的信息 $user = \Typecho\Widget::widget('Widget_User'); $userName = $user->screenName ?: $user->name; // 优先使用昵称,如果没有则使用登录名 echo '<span class="helloworld-trigger" style="color:#BBBBBB">' . htmlspecialchars($word) . '</span>'; echo '<div class="helloworld-tooltip"> <h4>欢迎回来,' . htmlspecialchars($userName) . '</h4> <div style="margin-bottom:20px;"> 您已经写了 ' . $stats['postCount'] . ' 篇文章,一共 ' . $stats['totalWords'] . ' 字,' . $stats['literaryComparison'] . '。 </div> <ul> <li><strong>最近登录:</strong>' . $stats['lastLogin'] . '</li> <li><strong>最近发文:</strong>' . $stats['lastPostTime'] . '</li> </ul> </div>'; } } typecho给后台admin界面添加图表分析 https://blog.iletter.top/archives/406.html 2025-07-27T02:32:00+08:00 觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php感兴趣的可以直接下载index.php文件覆盖/admin 下面的index.php文件下载地址:https://wonder1999.lanzouu.com/ikRiy32sfnah<?php include 'common.php'; include 'header.php'; include 'menu.php'; $stat = \Widget\Stat::alloc(); ?> <script src="https://blog.iletter.top/usr/blog_img/chart.js"></script> <!-- 引入 Font Awesome 图标库 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <style> /* 自定义美化样式 */ .typecho-dashboard { padding: 20px 0; } .card { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; } .card-title { margin-top: 0; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #333; font-size: 1.2em; } /* 欢迎区域 */ .welcome-card { background: linear-gradient(135deg, #1f1f20 0%, #0076bb 100%); color: white; } .welcome-card h3 { color: white; margin-top: 0; } .welcome-card p { font-size: 1.1em; margin-bottom: 20px; } .welcome-card a { color: #fff; text-decoration: underline; } .welcome-card a:hover { text-decoration: none; opacity: 0.9; } /* 快捷链接 */ #start-link { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 10px; } #start-link li { margin: 0; } #start-link a { display: inline-block; padding: 8px 16px; background-color: rgba(255, 255, 255, 0.2); border-radius: 4px; color: white !important; text-decoration: none; transition: background-color 0.3s ease; } #start-link a:hover { background-color: rgba(255, 255, 255, 0.3); } #start-link .balloon { background-color: #ff6b6b; color: white; border-radius: 10px; padding: 2px 6px; font-size: 0.8em; margin-left: 5px; } /* 统计卡片 */ .stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } .stat-card { background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; text-align: center; transition: transform 0.2s ease, box-shadow 0.2s ease; } .stat-icon { font-size: 2em; margin-bottom: 10px; color: #000000; /* 可根据不同类型调整颜色 */ } .stat-number { font-size: 1.8em; font-weight: bold; margin: 5px 0; color: #495057; } .stat-label { font-size: 0.9em; color: #6c757d; } /* 最近列表 */ .latest-link ul { list-style: none; padding: 0; margin: 0; } .latest-link li { padding: 8px 0; border-bottom: 1px solid #eee; } .latest-link li:last-child { border-bottom: none; } .latest-link .title { margin-left: 5px; } .latest-link span { color: #6c757d; font-size: 0.9em; } /* 图表区域 - 重新规划 */ .chart-container { max-width: 1200px; /* 限制图表区域最大宽度 */ margin: 0 auto; /* 水平居中 */ display: flex; flex-direction: column; gap: 20px; } .chart-row { display: flex; gap: 20px; flex-wrap: wrap; } .chart-box { flex: 1; min-width: 250px; /* 调整最小宽度以适应更多屏幕 */ background-color: #fff; border-radius: 8px; /*box-shadow: 0 2px 4px rgba(0,0,0,0.1);*/ padding: 15px; display: flex; flex-direction: column; /* 确保内部元素垂直排列 */ } .chart-box h4 { margin: 0 0 10px 0; /* 调整标题边距 */ text-align: center; color: #333; flex-shrink: 0; /* 防止标题被压缩 */ } .chart-box canvas { width: 100% !important; /* 确保画布宽度占满容器 */ max-height: 300px; /* 限制图表最大高度 */ flex-grow: 1; /* 画布占据剩余空间 */ } /* 响应式调整 */ @media (max-width: 768px) { .stat-cards { grid-template-columns: repeat(2, 1fr); } .chart-row { flex-direction: column; /* 小屏幕时堆叠 */ } .chart-box { min-width: 100%; /* 小屏幕时占满宽度 */ } #start-link { flex-direction: column; } } /* 中等屏幕调整 - 让图表在中等屏幕也能两列显示 */ @media (min-width: 769px) and (max-width: 1100px) { .chart-row { /* 在这个范围内,允许换行,但.chart-box的flex行为会使其尽可能并排 */ } .chart-box { /* 可以微调 min-width 来控制换行点 */ min-width: calc(50% - 10px); } } </style> <div class="main"> <div class="container typecho-dashboard"> <?php include 'page-title.php'; ?> <div class="row typecho-page-main"> <!-- 欢迎卡片 --> <div class="col-mb-12" role="main"> <div class="card welcome-card"> <h3><?php _e('欢迎使用 Typecho'); ?></h3> <p><?php _e('目前有 <em>%s</em> 篇文章, 并有 <em>%s</em> 条关于你的评论在 <em>%s</em> 个分类中.', $stat->myPublishedPostsNum, $stat->myPublishedCommentsNum, $stat->categoriesNum); ?></p> <ul id="start-link" class="clearfix"> <?php if ($user->pass('contributor', true)): ?> <li><a href="<?php $options->adminUrl('write-post.php'); ?>"><i class="fas fa-pen"></i> <?php _e('撰写新文章'); ?></a></li> <?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->waitingCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><i class="fas fa-comments"></i> <?php _e('待审核的评论'); ?></a> <span class="balloon"><?php $stat->waitingCommentsNum(); ?></span> </li> <?php elseif ($stat->myWaitingCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><i class="fas fa-comments"></i> <?php _e('待审核评论'); ?></a> <span class="balloon"><?php $stat->myWaitingCommentsNum(); ?></span> </li> <?php endif; ?> <?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->spamCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><i class="fas fa-trash-alt"></i> <?php _e('垃圾评论'); ?></a> <span class="balloon"><?php $stat->spamCommentsNum(); ?></span> </li> <?php elseif ($stat->mySpamCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><i class="fas fa-trash-alt"></i> <?php _e('垃圾评论'); ?></a> <span class="balloon"><?php $stat->mySpamCommentsNum(); ?></span> </li> <?php endif; ?> <?php if ($user->pass('administrator', true)): ?> <li><a href="<?php $options->adminUrl('manage-posts.php'); ?>"><i class="fas fa-list"></i> <?php _e('文章管理'); ?></a></li> <li><a href="<?php $options->adminUrl('plugins.php'); ?>"><i class="fas fa-plug"></i> <?php _e('插件管理'); ?></a></li> <li><a href="<?php $options->adminUrl('options-general.php'); ?>"><i class="fas fa-cog"></i> <?php _e('系统设置'); ?></a></li> <?php endif; ?> <?php endif; ?> </ul> </div> </div> <!-- 统计卡片 --> <div class="col-mb-12"> <div class="stat-cards"> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-posts.php'); ?>"> <div class="stat-icon"><i class="fas fa-file-alt"></i></div> <div class="stat-number"><?php echo $stat->myPublishedPostsNum; ?></div> <div class="stat-label"><?php _e('文章'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-comments.php'); ?>"> <div class="stat-icon"><i class="fas fa-comment"></i></div> <div class="stat-number"><?php echo $stat->myPublishedCommentsNum; ?></div> <div class="stat-label"><?php _e('评论'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-categories.php'); ?>"> <div class="stat-icon"><i class="fas fa-folder"></i></div> <div class="stat-number"><?php echo $stat->categoriesNum; ?></div> <div class="stat-label"><?php _e('分类'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-tags.php'); ?>"> <div class="stat-icon"><i class="fas fa-tags"></i></div> <div class="stat-number"><?php echo $stat->tagsNum; ?></div> <div class="stat-label"><?php _e('标签'); ?></div> </a> </div> <!-- 可根据需要添加更多统计 --> </div> </div> <!-- 左侧内容:最近文章 --> <div class="col-mb-12 col-tb-6" role="complementary"> <div class="card"> <h3 class="card-title"><?php _e('最近发布的文章'); ?></h3> <?php \Widget\Contents\Post\Recent::alloc('pageSize=10')->to($posts); ?> <ul class="latest-link"> <?php if ($posts->have()): ?> <?php while ($posts->next()): ?> <li> <span><?php $posts->date('n.j'); ?></span> <a href="<?php $posts->permalink(); ?>" class="title"><?php $posts->title(); ?></a> </li> <?php endwhile; ?> <?php else: ?> <li><em><?php _e('暂时没有文章'); ?></em></li> <?php endif; ?> </ul> </div> </div> <!-- 右侧内容:最近评论 --> <div class="col-mb-12 col-tb-6" role="complementary"> <div class="card"> <h3 class="card-title"><?php _e('最近得到的回复'); ?></h3> <ul class="latest-link"> <?php \Widget\Comments\Recent::alloc('pageSize=10')->to($comments); ?> <?php if ($comments->have()): ?> <?php while ($comments->next()): ?> <li> <span><?php $comments->date('n.j'); ?></span> <a href="<?php $comments->permalink(); ?>" class="title"><?php $comments->author(false); ?></a>: <?php $comments->excerpt(35, '...'); ?> </li> <?php endwhile; ?> <?php else: ?> <li><?php _e('暂时没有回复'); ?></li> <?php endif; ?> </ul> </div> </div> <!-- 图表显示区域 --> <div class="col-mb-12" role="complementary"> <div class="card"> <h3 class="card-title" style="color: #333;"><?php _e('文章统计数据'); ?></h3> <div class="chart-container"> <div class="chart-row"> <!-- 月度发布图表 --> <div class="chart-box"> <h4><?php _e('月度文章发布趋势'); ?></h4> <canvas id="monthlyPostChart" height="250"></canvas> </div> <!-- 状态分布图表 --> <div class="chart-box"> <h4><?php _e('文章状态分布'); ?></h4> <canvas id="statusChart" height="250"></canvas> </div> </div> <div class="chart-row"> <!-- 分类文章统计 --> <div class="chart-box"> <h4><?php _e('各分类文章数量'); ?></h4> <canvas id="categoryChart" height="250"></canvas> </div> <!-- 标签文章统计 --> <div class="chart-box"> <h4><?php _e('热门标签文章数量 (Top10)'); ?></h4> <canvas id="tagChart" height="250"></canvas> </div> </div> </div> </div> </div> </div> </div> </div> <?php include 'copyright.php'; include 'common-js.php'; ?> <script> $(document).ready(function() { <?php // ==================== PHP 数据计算部分 ==================== // 1. 月度文章统计数据 $db = Typecho_Db::get(); // 月度文章统计 $select = $db->select() ->from('table.contents') ->where('type = ?', 'post') ->where('status = ?', 'publish'); $posts = $db->fetchAll($select); $monthlyData = []; $monthLabels = []; $currentYear = date('Y'); $currentMonth = date('n'); // 初始化最近12个月的数据 for ($i = 11; $i >= 0; $i--) { $year = $currentYear; $month = $currentMonth - $i; if ($month <= 0) { $month += 12; $year--; } $key = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); $monthlyData[$key] = 0; $monthLabels[$key] = $year . '年' . $month . '月'; } // 统计实际数据 foreach ($posts as $post) { $created = $post['created']; $year = date('Y', $created); $month = date('m', $created); $key = $year . '-' . $month; if (isset($monthlyData[$key])) { $monthlyData[$key]++; } } // 2. 文章状态统计 $published = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.contents') ->where('type = ?', 'post') ->where('status = ?', 'publish'))->count; $draft = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.contents') ->where('type = ?', 'post_draft') // 注意:Typecho 草稿通常在 contents 表中 type 为 post_draft // ->where('status = ?', 'publish') // 草稿状态通常不是 publish,可以省略或检查实际结构 )->count; // 3. 分类文章统计 $categories = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'category')); $categoryData = []; $categoryLabels = []; foreach ($categories as $category) { $count = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.relationships') ->where('mid = ?', $category['mid']))->count; if ($count > 0) { // 只显示有文章的分类 $categoryLabels[] = $category['name']; $categoryData[] = (int)$count; } } // 4. 标签文章统计 (取前10个) $tags = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'tag')); $tagData = []; $tagLabels = []; foreach ($tags as $tag) { $count = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.relationships') ->where('mid = ?', $tag['mid']))->count; if ($count > 0) { $tagLabels[] = $tag['name']; $tagData[] = (int)$count; } } // 按数量排序,取前10个 array_multisort($tagData, SORT_DESC, $tagLabels); $topTagLabels = array_slice($tagLabels, 0, 10); $topTagData = array_slice($tagData, 0, 10); // 准备图表数据 $chartMonthlyData = [ 'labels' => array_values($monthLabels), 'values' => array_values($monthlyData) ]; $chartStatusData = [ 'published' => (int)$published, 'draft' => (int)$draft ]; $chartCategoryData = [ 'labels' => $categoryLabels, 'values' => $categoryData ]; $chartTagData = [ 'labels' => $topTagLabels, 'values' => $topTagData ]; ?> // ==================== JavaScript 图表渲染部分 ==================== // 1. 月度文章图表 var monthlyData = <?php echo json_encode($chartMonthlyData); ?>; if (monthlyData.labels && monthlyData.labels.length > 0) { var ctx1 = document.getElementById('monthlyPostChart').getContext('2d'); new Chart(ctx1, { type: 'line', data: { labels: monthlyData.labels, datasets: [{ label: '每月发文数', data: monthlyData.values, borderColor: 'rgb(54, 162, 235)', // 调整为蓝色系 backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, // 稍微增加曲线张力 fill: true }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' // 网格线颜色 } }, x: { grid: { color: 'rgba(0, 0, 0, 0.05)' } } }, plugins: { legend: { display: false // 隐藏图例,因为只有一个数据集 }, tooltip: { mode: 'index', intersect: false } } } }); } // 2. 文章状态图表 var statusData = <?php echo json_encode($chartStatusData); ?>; if (statusData) { var ctx2 = document.getElementById('statusChart').getContext('2d'); new Chart(ctx2, { type: 'doughnut', data: { labels: ['已发布', '草稿'], datasets: [{ data: [statusData.published, statusData.draft], backgroundColor: [ 'rgba(75, 192, 192, 0.8)', // 已发布 - 青色 'rgba(255, 205, 86, 0.8)', // 草稿 - 黄色 ], borderWidth: 1, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { return context.label + ': ' + context.raw; } } } }, cutout: '60%' // 增加中间空心部分 } }); } // 3. 分类文章图表 var categoryData = <?php echo json_encode($chartCategoryData); ?>; if (categoryData.labels && categoryData.labels.length > 0) { var ctx3 = document.getElementById('categoryChart').getContext('2d'); new Chart(ctx3, { type: 'bar', data: { labels: categoryData.labels, datasets: [{ label: '文章数量', data: categoryData.values, backgroundColor: 'rgba(153, 102, 255, 0.7)', // 紫色系 borderColor: 'rgba(153, 102, 255, 1)', borderWidth: 1, borderRadius: 4, // 条形圆角 barPercentage: 0.7, // 调整条形宽度 categoryPercentage: 0.8 }] }, options: { indexAxis: 'x', // 保持为垂直柱状图 responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' } }, x: { grid: { display: false // 隐藏 X 轴网格线 } } }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } } } }); } // 4. 标签文章图表 var tagData = <?php echo json_encode($chartTagData); ?>; if (tagData.labels && tagData.labels.length > 0) { var ctx4 = document.getElementById('tagChart').getContext('2d'); new Chart(ctx4, { type: 'bar', data: { labels: tagData.labels, datasets: [{ label: '文章数量', data: tagData.values, backgroundColor: 'rgba(255, 159, 64, 0.7)', // 橙色系 borderColor: 'rgba(255, 159, 64, 1)', borderWidth: 1, borderRadius: 4, barPercentage: 0.7, categoryPercentage: 0.8 }] }, options: { indexAxis: 'x', responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' } }, x: { grid: { display: false } } }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } } } }); } }); </script> <script> $(document).ready(function () { }); </script> <?php include 'footer.php'; ?> 其他插件推荐:[post cid="405" /] 去除宝塔界面一些烦人的东西 https://blog.iletter.top/archives/400.html 2025-07-25T13:18:00+08:00 更新之后越来越难用了,自己懒得配置nginx,mysql,这些环境,开心版的又怕后门,自己改后端的js文件看的脑瓜子疼,反正都是界面,直接js删掉一些dom元素就行了。有需要的可以拿走。// ==UserScript== // @name 宝塔面板优化 - 界面美化 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 移除指定 class 元素、替换特定 span 文字内容,并删除包含“需求反馈”的 span // @match http://152.136.153.72:9999/* // @grant none // ==/UserScript== (function () { 'use strict'; console.log('⚠ 脚本准备加载'); // 定义需要移除的 class 组合 const selectorsToRemove = [ '.h-auto.mt-\\[1\\.2rem\\]', // h-auto mt-[1.2rem] '.svgtofont-desired.\\!mr-4px.\\!text-\\[16px\\]', // svgtofont-desired !mr-4px !text-[16px] '.icon-end-time-free.icon-unpaid-ltd', // icon-end-time-free icon-unpaid-ltd '.el-card.is-always-shadow.is-always-shadow.\\!border-\\[\\#efefef\\]', // el-card is-always-shadow is-always-shadow !border-[#efefef] '.icon-end-time.icon-unpaid-ltd' // icon-end-time icon-unpaid-ltd ]; // 移除指定 class 的元素 const removeElementsByClass = () => { selectorsToRemove.forEach(selector => { document.querySelectorAll(selector).forEach(el => el.remove()); }); }; // 替换指定 span 的文字内容 const replaceSpanText = () => { const spanSelector = 'span.mr-1rem.ml-\\[-1\\.5rem\\]'; document.querySelectorAll(spanSelector).forEach(span => { span.textContent = '精简版'; }); }; // 删除 span/button 包含 指定字符 的 span 元素 const removeSpansWithText = () => { const spans = document.querySelectorAll('span'); spans.forEach(span => { if (span.textContent.trim() == '需求反馈') { span.remove(); } }); const buttons = document.querySelectorAll('button'); buttons.forEach(span => { if (span.textContent.trim() == '立即体验') { span.remove(); } }); }; // 删除指定class 包含置顶字符的元素 const removeClassWithText =()=>{ const parents = document.querySelectorAll('.header-child-tab'); parents.forEach(span => { if ( span.textContent.trim() == '安全检测' || span.textContent.trim() == '违规词检测' || span.textContent.trim() == 'PHP网站安全' || span.textContent.trim() == '入侵防御' || span.textContent.trim() == '系统加固' || span.textContent.trim() == '扫描感知' || span.textContent.trim() == '日志审计' || span.textContent.trim() == 'SSH登录日志' ) { span.remove(); } }); const classparents = document.querySelectorAll('.el-collapse-item'); classparents.forEach(span => { if ( span.textContent.trim().includes('top5') ) { span.remove(); } }); } // 删除包含特定子元素的 el-col 元素 const removeParentIfChildExists = () => { // 获取所有 el-col.el-col-6.wrapper-item 元素 const parents = document.querySelectorAll('.el-col.el-col-6.wrapper-item'); parents.forEach(parent => { // 检查子元素是否包含指定的 class const child = parent.querySelector('.absolute.-left-1\\.5.-top-1'); if (child) { parent.remove(); // 移除父元素 } }); }; // 综合执行函数 const removeElements = () => { removeElementsByClass(); replaceSpanText(); removeSpansWithText(); removeParentIfChildExists(); removeClassWithText(); }; // 等待dom加载完毕后执行综合函数 let timeoutId; const debouncedRemove = () => { clearTimeout(timeoutId); timeoutId = setTimeout(removeElements, 200); }; let runCount = 0; const maxRuns = 5; const observer = new MutationObserver((mutations, obs) => { if (runCount >= maxRuns) { obs.disconnect(); return; } obs.disconnect(); debouncedRemove(); runCount++; obs.observe(document.body, { childList: true, subtree: true }); }); observer.observe(document.body, { childList: true, subtree: true }); // 点击一次执行一次综合函数 let clickTimeout; document.addEventListener('click', () => { clearTimeout(clickTimeout); clickTimeout = setTimeout(removeElements, 200); }); // 初始执行(延迟 1 秒,等待 DOM 加载) setTimeout(() => { removeElements(); }, 1000); })(); nvm命令自查(node版本管理) https://blog.iletter.top/archives/324.html 2024-11-10T14:22:19+08:00 nvm 命令命令解释nvm --help展示帮助nvm --version已安装的nvm版本nvm install version下载对应的 node 版本(version)mvn install --reinstall-packages-from=重新安装对应的 node 版本nvm install --lts仅从LTS版本中选择安装nvm install --lts=仅从特定LTS系列的版本中选择nvm install --skip-default-packages跳过默认软件包文件nvm install --latest-npm安装后,在给定的节点版本上升级到最新的npmnvm install --no-progress没有下载进度条nvm uninstall卸载对应的 node 版本nvm uninstall --lts卸载LTS版本nvm uninstall --lts=卸载指定的LTS 版本nvm use使用对应的 node 版本nvm use --lts使用LTS 版本nvm use --lts=使用指定的LTS 版本nvm list展示安装的 node 版本(可以简写为nvm ls)nvm current显示当前节点的激活版本nvm version展示当前的 node 版本nvm exec [--silent] []使用指定的 版本运行 command命令nvm run [--silent] []使用指定的版本运行 argsnvm alias对 version 版本设置一个别名nvm unalias删除这个别名nvm install-latest-npm在当前node 版本上升级最新的npm版本nvm reinstall-packages将version版本中的全局安装包安装到当前版本中nvm unload从shell中卸载nvmnvm on开启node版本管理nvm off关闭node版本管理nvm node_mirror [url]设置node镜像nvm npm_mirror [url]设置npm 镜像 typecho的handsome主题微调 https://blog.iletter.top/archives/322.html 2024-11-06T16:48:00+08:00 用国typecho和wordpress之后,个人还是比较中意typecho这个,handsome主题相对全面,所以就选择使用它了。但是总有一些自己小瑕疵需要自己去调整,比如归档页面的文章目录等等,handsome版本: 9.2.1(个人是php菜鸡,没学过,一边百度一边改。)取消归档页面的文章目录找到主题文件下面的sidebar.php然后打开编辑,搜索到<!--非文章页面-->这个之后,然后会看到<?php echo PostContent::returnTOC($this->is('page'),false) ?>这个代码,这个就是非文章界面的文章目录显示。<!--非文章页面--> <?php if (!($this->is('post'))) : ?> <section id="tag_cloud" class="widget widget_tag_cloud wrapper-md padder-v-none clear"> <h5 class="widget-title m-t-none"><?php _me("标签云") ?></h5> <div class="tags l-h-2x panel wrapper-sm padder-v-ssm"> <?php Typecho_Widget::widget('Widget_Metas_Tag_Cloud','ignoreZeroCount=1&limit=30')->to($tags); ?> <?php if($tags->have()): ?> <?php while ($tags->next()): ?> <a href="<?php $tags->permalink();?>" class="label badge"><?php $tags->name(); ?></a> <?php endwhile; ?> <?php endif; ?> </div> </section> <!--<?php echo PostContent::returnTOC($this->is('page'),false) ?>--> <?php // 获取当前页面的路径 $currentUrlPath = parse_url($this->permalink, PHP_URL_PATH); // 判断是否为页面,且 URL 中不包含 "archive" if ($this->is('page') && substr($currentUrlPath, -12) !== 'archive.html' ) { echo PostContent::returnTOC(true, false); } ?> <?php else: ?>如果你想对具体方法进行操作,可以找到libs/content/PostContent.php 这个就是相应的右侧边栏的一些用到的方法。去除掉友情链接的内页链接友情链接嘛,就是links,那我们就在主题文件夹下搜索links<ul class="nav no-padder b-b"> <li class="nav-item active"><a class="nav-link" href data-toggle="tab" data-target="#my-info"><?php _me("申请友链") ?></a></li> <!--<li class="nav-item"><a class="nav-link" href data-toggle="tab" data-target="#tab_2"><?php _me("内页链接") ?></a></li>--> <li class="nav-item"><a class="nav-link" href data-toggle="tab" data-target="#tab_4"><?php _me("全站链接") ?></a></li> <li class="nav-item"><a class="nav-link" href data-toggle="tab" data-target="#tab_3"><?php _me("推荐链接") ?></a></li> </ul>这里就是对链接的显示,注释掉你不喜欢的就可以了。注释完毕后,如果你有强迫症的话,可以想我一样进行接下来的更改。针对后台管理的友情链接的操作。找到插件里面的handsome插件,找到Plugin.php这个文件,里面有个form函数,就是他,你若找到的话可以看到该函数下面有一部分的代码。注释掉你不喜欢的就可以了。$sort = new Typecho_Widget_Helper_Form_Element_Select('sort', array( 'ten' => '全站链接,首页左侧边栏显示', // 'one' => '内页链接,在独立页面中显示(需要新建独立页面<a href="https://handsome2.ihewro.com/#/plugin" target="_blank">友情链接</a>)', 'good' => '推荐链接,在独立页面中显示', 'others' => '失效链接,不置输出,标注暂时失效的友链' ), 'ten', _t('链接输出位置*'), '选择友情链接输出的位置');左侧边栏导航 配置打开主题的后台,可以参考本博客的配置进行修改,左侧边栏的图标用的是feather图标,点击链接跳转查看,https://feathericons.com/)[ { "name": "云盘", "feather": "cloud", "link": "https://alist.ittoolman.com/", "target": "_blank" }, { "name": "图床", "feather": "inbox", "link": "https://img.ittoolman.com/", "target": "_blank" }, { "name": "相册", "feather": "image", "link": "https://blog.iletter.top/index.php/category/image/", "target": "_self" }, { "name": "分类", "feather": "layout", "sub": [ { "name": "随笔", "feather": "edit", "target": "_self", "link": "https://blog.iletter.top/index.php/category/sui-bi/" }, { "name": "技术笔记", "feather": "monitor", "target": "_self", "link": "https://blog.iletter.top/index.php/category/ji-shu-bi-ji/" }, { "name": "文章收藏", "feather": "scissors", "target": "_self", "link": "https://blog.iletter.top/index.php/category/wen-zhang/" } ] }, { "name": "归档", "feather": "archive", "link": "https://blog.iletter.top/index.php/archive.html", "target": "_self" }, { "name": "时光机", "feather": "clock", "link": "https://blog.iletter.top/index.php/cross.html", "target": "_self" }, { "name": "友人帐", "feather": "users", "link": "https://blog.iletter.top/index.php/links.html", "target": "_self" }, { "name": "关于我", "feather": "coffee", "link": "https://blog.iletter.top/index.php/start-page.html", "target": "_self" } ] uni-app微信小程序属性为0 https://blog.iletter.top/archives/162.html 2024-06-04T23:52:00+08:00 前几日在封装一个组件的时候,接收数据的时候遇到一个属性等于0,但是,前端死活不显示这里是有值的,但到了前端就没有了,卧槽,太神奇了是不是。打印一下看看打印出来传入值竟然是undefined而不是0,一定是闹鬼了。。。最后再不断定位错误的时候,发现是这里的毛病post: item.post || '', // 添加 post 属性,默认值为空字符串原因是当数值为0的时候自己就默认是false0在布尔上下文中被认为是假值,因此 item.post||‘’ 会在 item.post 为 0 时返回空字符串。所以说为了保留0,需要改成 item.post !== undefined ? item.post : ''来确保仅在 item.post 是 undefined 时返回空字符串。下面贴一下源代码<template> <view> <!-- 绑定个人信息 --> <u-popup :show="localBindCompShow" @close="onClickOverlayClose" @open="localBindCompShow =true" mode="center" round="20" :closeOnClickOverlay="true"> <view class="bindCompCard"> <view class="header-container"> <view class='header-button'> <u-button type="info" size="5" icon="close" @click="onClickOverlayClose" :plain="true" :hairline="true"></u-button> </view> <view class="header-text">绑定企业信息</view> <u-tabs :list="bindCompTabList" @click="selectTab" class="tabs"></u-tabs> </view> <view v-if="!bindCompNewShow"> <view class="supplier-container"> <view v-for="(item, index) in itemsToDisplay" :key="index" class="supplier-card"> <view class="supplier-details"> <view class="company-name">{{ item.companyName }}</view> <view class="supplier-name">政采账号:{{ item.supplierAccessName }}</view> </view> <view class="bind-button"> <view v-if="!item.isShow"> <view class="post-select"> {{ getPostText(item.post) }} </view> <button size="default" type="default" style="margin-top: 20rpx;color:#ffffff;background-color:#189f33;border-color:#ffffff;border-radius: 40rpx;height: 60rpx;display: flex;align-items: center;justify-content: center;">已绑定</button> </view> <view v-if="item.isShow"> <view class="post-select" v-if="item.isShow"> <uni-data-select v-model="item.post" :localdata="postList" placeholder="选择职位"></uni-data-select> </view> <button size="default" type="default" @click="submitBindCompCard(item)" style="margin-top: 20rpx;color:#ffffff;background-color:#4874cb;border-color:#ffffff;border-radius: 40rpx;height: 60rpx;display: flex;align-items: center;justify-content: center;">提交绑定</button> </view> </view> </view> </view> </view> <view v-if="bindCompNewShow"> <u--form labelPosition="left" :model="formBindComp"> <u-form-item label="政采网账号" prop="formBindComp.supplierAccessName" borderBottom labelWidth='200'> <u--input v-model="formBindComp.supplierAccessName" border="none"></u--input> </u-form-item> <u-form-item label="职位" prop="formBindComp.post" borderBottom labelWidth='200'> <uni-data-select v-model="formBindComp.post" :localdata="postList" placeholder="选择职位"></uni-data-select> </u-form-item> </u--form> <!-- <u-button type="success">提交</u-button> --> <button size="default" type="default" @click="submitBindComp" style="margin-top: 40rpx;color:#ffffff;background-color:#4874cb;border-color:#ffffff;border-radius: 40rpx;height: 60rpx;display: flex;align-items: center;justify-content: center;">提交绑定</button> </view> </view> </u-popup> </view> </template> <script> import { mapState, mapMutations } from 'vuex'; import { getFieldList, postBindEnterprise, getUserBindComp, getUserHaveBindComp, } from "@/api/user.js"; export default { name: 'BindComp', props: { bindCompShow: { type: Boolean, default: true }, userId: { type: String, }, itemList: { type: Array, default: () => [] } }, computed: { ...mapState("user", ["token", "tokenExpire", "userInfo"]), localBindCompShow: { get() { return this.bindCompShow; }, set(val) { this.$emit('update:bindCompShow', val) } }, itemsToDisplay() { let list = this.itemList.length > 0 ? this.itemList : this.supplierList; // 处理 list,添加 post 和 isShow 属性 let processedList = list.map(item => ({ ...item, post: item.post !== undefined ? item.post : '', // 添加 post 属性,保留原值或默认值为空字符串 isShow: item.isShow !== undefined ? item.isShow : true // 添加 isShow 属性,默认值为 true })); return processedList; } }, data() { return { supplierList: [], bindCompNewShow: false, bindCompTabList: [{ name: '政采企业信息绑定', }, { name: '绑定其他企业', } ], postList: [], formBindComp: { supplierAccessName: '', post: '', userId: '', } }; }, mounted() { this.fieldListGet(); if (this.userInfo.id != undefined) { this.userBindCompGet(); } }, created() { }, methods: { ...mapMutations("user", ["setUserInfo"]), async submitBindComp() { this.formBindComp.userId = this.userId if (!this.formBindComp.userId) { this.formBindComp.userId = this.userInfo.id } if (this.formBindComp.userId == "") { uni.$u.toast('绑定失败,请尝试重新登陆后绑定!'); return; } if (this.formBindComp.supplierAccessName === "") { uni.$u.toast('政采网账号不能为空!'); return; } if (this.formBindComp.post === "") { uni.$u.toast('职位信息不能为空!'); return; } const { code, msg, data } = await postBindEnterprise(this.formBindComp); if (code === 0) { // console.log(data) uni.$u.toast('绑定企业成功!'); setTimeout(() => { this.$emit('close-popup'); uni.reLaunch({ url: '/pages/index/index' }); }, 1000); } }, async submitBindCompCard(item) { if (item.post === undefined || item.post === "") { uni.$u.toast('企业职位不能为空!'); return; } let userID = this.userId; if (!userID) { userID = this.userInfo.id } const params = { supplierAccessName: item.supplierAccessName, post: item.post, userId: userID, } console.log(params) const { code, msg, data } = await postBindEnterprise(params); if (code === 0) { // console.log(data) // 更新 item 的 isShow 属性 this.$set(item, 'isShow', false); uni.$u.toast('绑定' + item.companyName + '成功!'); // 更新绑定企业 this.userBindCompGet(); } }, onClickOverlayClose() { this.$emit('close-popup'); }, //获取用户下面的政采企业信息 async userBindCompGet() { try { // 如果resBindComp里面包含resHaveBind的数据,那么就设置该企业已经绑定好了 const resHaveBind = await getUserHaveBindComp(); const resBindComp = await getUserBindComp(); if (resHaveBind.code === 0 && resBindComp.code === 0) { const haveBindMap = new Map(); resHaveBind.data.forEach(item => { haveBindMap.set(item.supplierId, item.post); }); this.supplierList = resBindComp.data.map(item => { if (haveBindMap.has(item.supplierId)) { return { ...item, post: haveBindMap.get(item.supplierId), isShow: false }; } return item; }); console.log(this.supplierList); } } catch (error) { console.error('获取用户绑定企业信息失败', error); } }, //获取职位信息 async fieldListGet() { const { code, msg, data } = await getFieldList({ fieldName: "register_post" }); if (code === 0 && data) { // console.log(data) this.postList = data.map(post => { return { value: post.dictCode, text: post.dictName } }); // console.log(this.postList) } }, getPostText(postValue) { const postItem = this.postList.find(post => post.value === postValue); // console.log(postValue, postItem) return postItem ? postItem.text : '未知职位'; }, selectTab(item) { // console.log(item.name) if (item.name == "绑定其他企业") { this.bindCompNewShow = true; } else { this.bindCompNewShow = false; } }, }, } </script> <style scoped lang="less"> .bindCompCard { width: 650rpx; padding: 20rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; } .header-container { display: flex; flex-direction: column; align-items: center; width: 100%; margin-bottom: 20rpx; } .header-button { position: absolute; width: 80rpx; top: 0; right: 5px; margin: 10rpx; } .header-text { font-size: 36rpx; margin-bottom: 10rpx; text-align: center; width: 100%; } .tabs { width: 100%; } // ------------------------ .supplier-container { display: flex; flex-direction: column; gap: 20rpx; padding: 20rpx; height: 600rpx; overflow: auto; } .supplier-card { background: #fff; border-radius: 10rpx; box-shadow: 0 2rpx 5rpx rgba(0, 0, 0, 0.1); padding: 20rpx; display: flex; // flex-direction: column; gap: 10rpx; } .supplier-details { margin-top: 27rpx; width: 360rpx; display: flex; flex-direction: column; gap: 10rpx; } .company-name, .supplier-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; } .company-name { font-size: 32rpx; font-weight: bold; color: #333; } .supplier-name { font-size: 32rpx; color: #666; } .post-select { margin-top: 10rpx; width: 222rpx; } .bind-button { margin-left: 10rpx; } </style> 前端实现div的高度拖拽改变 https://blog.iletter.top/archives/153.html 2023-11-03T23:49:00+08:00 实现代码html <div class="contactPerson_bottom_div" id="fuDiv"> <div class="dragDiv" @mousedown="dragEagle" > <el-icon><SemiSelect /></el-icon> </div> </div> scriptdragEagle(e) { var targetDiv = document.getElementById('fuDiv'); //得到点击时该地图容器的宽高: var targetDivHeight = targetDiv.offsetHeight; var startX = e.clientX; var startY = e.clientY; document.onmousemove = function (e) { e.preventDefault(); //得到鼠标拖动的宽高距离:取绝对值 var distY = Math.abs(e.clientY - startY); //往上方拖动: if (e.clientY < startY) { targetDiv.style.height = (targetDivHeight + distY) + 'px'; } //往下方拖动: if (e.clientX < startX && e.clientY > startY) { targetDiv.style.height = targetDivHeight - distY + 'px'; } if (parseInt(targetDiv.style.height) >= 800) { targetDiv.style.height = 800 + 'px'; } if (parseInt(targetDiv.style.height) <= 320) { targetDiv.style.height = 320 + 'px'; } } document.onmouseup = function () { document.onmousemove = null; } },css.contactPerson_bottom_div { position: absolute; bottom: 0; left: 0; right: 0; border-top-style: solid; border-color: black; background: rgb(255, 255, 255); color: rgb(0, 0, 0); z-index: 99; height: 320px; overflow-y: auto;/* 允许垂直滚动 */ } .dragDiv{ height:10px; display: flex; align-items: center; justify-content: center; } .dragDiv:hover { /* background-color: #666; */ cursor: pointer;/* 在hover的时候,鼠标指针变成手 */ }首先针对需要设置的div添加一个id,再通过getElementById获取到目标div。然后在内部添加一个div,可以设置成自己想要的形式,做明显的标记使用。针对这个div设置操作活动@mousedown。也就是当鼠标按下去的时候调用此dragEagle方法,在此之前一定要传入对象参数e。var targetDivHeight = targetDiv.offsetHeight;: 这行代码获取了目标 div 元素的当前高度,并将其存储在 targetDivHeight 变量中。var startX = e.clientX; 和 var startY = e.clientY;: 这两行代码分别记录了鼠标点击位置的横坐标(X坐标)和纵坐标(Y坐标),用于后续计算鼠标拖动的距离。document.onmousemove = function (e) { ... }: 这是一个鼠标移动事件处理程序,当用户按住鼠标左键并移动时,会触发这个事件处理程序。在这个处理程序中,计算了鼠标拖动的距离,并根据拖动的方向来改变目标 div 元素的高度。e.preventDefault();: 这一行代码用于防止浏览器默认的拖拽行为,确保我们自己的逻辑生效。通过比较鼠标当前的 e.clientY 和初始点击时的 startY 来计算垂直方向上的拖动距离 distY。接下来的条件判断分别处理向上拖动和向下拖动的情况。如果鼠标向上拖动,就增加目标 div 的高度,如果鼠标向下拖动,就减小目标 div 的高度。这样可以实现调整高度的效果。最后的两个条件判断用于限制目标 div 的最小和最大高度,以避免高度超出一定范围document.onmouseup = function () { ... }: 这是鼠标释放事件处理程序,在鼠标左键释放时触发。它用于清除鼠标移动事件处理程序,即当鼠标释放后停止拖动。在这之中 if (parseInt(targetDiv.style.height) >= 800) { targetDiv.style.height = 800 + 'px'; } if (parseInt(targetDiv.style.height) <= 320) { targetDiv.style.height = 320 + 'px'; }这一段是限制div拖动的最大高度和最小高度 vue3无关系组件之间的方法调用 https://blog.iletter.top/archives/146.html 2023-07-30T23:48:00+08:00 要实现在第一个页面中的方法 onClick 调用第二个页面中的 updateBankToUser 方法,我们可以使用事件总线来实现跨组件通信。在Vue 3中,我们可以使用 mitt 库来创建事件总线。首先,安装 mitt 库:npm install mitt然后,我们可以创建一个全局的事件总线,并在第二个页面中将 updateBankToUser 方法注册为一个事件处理程序,然后在第一个页面的 onClick 方法中触发这个事件,从而调用第二个页面中的方法。以下是示例代码:创建全局事件总线 bus.ts:// bus.ts import mitt from 'mitt'; export const bus = mitt();第二个页面 user.vue:<template> <div> <!-- ...其他代码... --> <el-button @click="updateBankToUser">更新银行的用户</el-button> </div> </template> <script setup> import { bus } from './bus'; const updateBankToUser = async () => { console.log('更新银行的用户'); // 在这里执行您的业务逻辑 } bus.on('callUpdateBankToUser', updateBankToUser); </script>第一个页面 parent.vue:<template> <div> <el-button @click="onClick">点击调用第二个页面的方法</el-button> </div> </template> <script setup> import { bus } from './bus'; const onClick = () => { console.log('调用第二个页面的方法'); // 在这里执行您的业务逻辑 bus.emit('callUpdateBankToUser'); } </script>在这个示例中,我们在 bus.ts 中创建了一个全局事件总线 bus。然后在第二个页面 user.vue 中,我们将 updateBankToUser 方法注册为名为 'callUpdateBankToUser' 的事件处理程序。在第一个页面 parent.vue 中的 onClick 方法中,我们使用 bus.emit 触发了 'callUpdateBankToUser' 事件,从而调用了第二个页面中的 updateBankToUser 方法。通过这种方式,您可以在不直接引用第二个页面的情况下,实现跨组件调用其中的方法。什么是全局事件总线bus.ts import mitt from 'mitt'; export const bus = mitt(); 两行的作用是啥在这里,bus.ts 文件中的两行代码用来创建全局事件总线。全局事件总线是一个简单的事件管理器,它可以用于在应用程序中实现组件之间的通信,特别是当组件之间没有直接父子关系时。具体来说,这两行代码的作用是:import mitt from 'mitt';: 这里我们导入了 mitt 库,它是一个简单、小巧的 JavaScript 事件库。mitt 库允许我们创建事件总线,以便在应用程序的任何地方都可以访问该事件总线。export const bus = mitt();: 在这里,我们创建了一个全局的事件总线 bus 并将其导出。通过 mitt() 调用,我们创建了一个新的事件总线实例,可以用来管理事件的注册、触发和取消。总结起来,bus.ts 文件中的这两行代码的目的是创建一个全局的事件总线,以便在应用程序的任何地方都可以使用它进行组件之间的通信。通过这个全局事件总线,我们可以在不直接引用其他组件的情况下,实现组件之间的方法调用和数据传递。 关于vue,ts,echart整合显示地图的实现方式 https://blog.iletter.top/archives/135.html 2023-05-24T23:48:00+08:00 这两天需要做一个大屏的东西,看着网上那个地图的效果很酷很炫,所以想整一个这样的东西,话不多说,开始整。因为项目本体是人人开源项目,我是在这个基础上做的一个二次开发吧。后端代码就省略了。其实很简单。在此就不会做太多的赘述了。后端不过有一个点是可以关注的,就是当你的数据库过于庞大的话,我们可以建一个缓存。也就是一下的示例代码。2023年5月25日:针对这个数据,因为map里面是放到缓存中了,一直没有释放,所以改用redis,又因为这个是二次开发的项目,所以人家对redis进行了一系列的改造,我在自己新建redis配置类的时候一直报错有冲突,所以在看了一下结构之后,对后端的代码进行了一系列的重构。redis工具类用于从 Redis 中获取缓存数据或者将数据存入 Redis public List<Object> getListFromRedis(String key) { Object cachedData = get(key); if (cachedData != null) { // 如果缓存中存在数据,则将其转换为 List 对象并返回 return (List) cachedData; } else { // 如果缓存中不存在数据,则返回空列表 return new ArrayList<>(); } } public void setListToRedis(String key, List<Object> data, long expire) { set(key, data, expire); } 控制层修改 getEnterpriseNums 方法,以先从 Redis 中获取数据,如果缓存中存在数据,则直接返回;如果缓存中不存在数据,则从数据库中获取数据,并将其存入 Redis 缓存中 @Autowired private RedisUtils redisUtils; @GetMapping("/enterpriseNums") @ApiOperation("返回地图上的区域数据") public Result getEnterpriseNums(){ // List<ShowEnterpriseDetailsNumsDTO> enterpriseDetailsList = enterpriseDetailsService.getEnterpriseNums(); // return new Result().ok(enterpriseDetailsList); String redisKey = "enterpriseNums"; // 定义 Redis 缓存的键名 List enterpriseDetailsList = redisUtils.getListFromRedis(redisKey); if (enterpriseDetailsList.isEmpty()) { // 从数据库中获取数据 enterpriseDetailsList = enterpriseDetailsService.getEnterpriseNums(); // 将数据存入 Redis 缓存,设置过期时间为一小时 redisUtils.setListToRedis(redisKey, enterpriseDetailsList, RedisUtils.HOUR_ONE_EXPIRE); } return new Result().ok(enterpriseDetailsList); }首先从 showEnterpriseDetailsServicesCache 缓存中获取工厂信息列表数据。如果列表数据为空(即缓存中没有数据),则调用 enterpriseDetailsService.getEnterpriseNums() 方法获取工厂信息数据,并将其存入缓存中。在下次调用 getEnterpriseNums() 方法时,将直接从缓存中获取数据,避免了重复查询的开销。通过以上优化,当第一次请求 getEnterpriseNums 接口时,数据将从数据库中获取,并存入 Redis 缓存中。之后的请求将直接从 Redis 缓存中获取数据,从而提高响应速度和性能。请确保 Redis 配置正确,并已经启动了 Redis 服务。前端echarts示例:https://echarts.apache.org/examples/zh/editor.html?c=map-HK他给的示例我们可以分析出来,我觉得是通过ajax请求来把地图搞到,然后绑定map最后,把数据显示出来。$.get(ROOT_PATH + '/data/asset/geo/HK.json', function (geoJson)用于异步加载地图数据。这段代码的作用是通过 AJAX 请求获取名为 'HK.json' 的地图数据文件,然后在成功获取数据后,使用echarts.registerMap('HK', geoJson)将地图数据注册到 ECharts 中,并命名为 'HK'。这样,之后在配置地图的 series 中就可以通过指定 map: 'HK'` 来使用该地图数据。再者我们需要提前的把地图数据导入到项目当中来。可以通过这个平台下载地图的数据http://datav.aliyun.com/portal/school/atlas/area_selector#&lat=30.332329214580188&lng=106.72278672066881&zoom=3.5然后的话导入方式,因为项目的原因我们需要这样做,先是importimport jiNanMapJson from "src/views/enterprise/assets/json/济南市.json"; import * as echarts from "echarts";再然后注册地图数据到 ECharts:echarts.registerMap('jiNan', jiNanMapJson);更新 mapOption 中的 series 配置,将 map: 'HK' 添加到相关的地图系列中,以便使用注册的地图数据:const mapOption = ref({ // ...其他配置项 series: [ { // ...其他系列配置 map: 'jiNan', // 使用注册的地图数据 // ...其他系列配置 } ] });通过这种方式,我们可以注册到其中,完整代码如下:<template> <div class="box"> <el-form-item> <el-button type="warning" @click="importHandle()">{{ $t("excel.import") }}</el-button> </el-form-item> <div style="width: 100%;margin-left: 6em;margin-bottom: 2em;"> <div class="topShow"> <div style="color:brown; font-size: 1.875rem /* 30/16 */;text-align: center;padding-top: 1.4375rem /* 23/16 */;"> 信用金桥大数据屏幕展示平台</div> <div style="color:brown; font-size: 1.25rem /* 20/16 */;text-align: right;padding-right: 2.1875rem /* 35/16 */;;"> {{ dataForm.currentTime }}</div> </div> </div> <div class="line_01"> <figure class="figure_left"> <div v-for="(item, index) in dataForm.enterpriseNums" :key="index" style="width: 23em;text-align: center; "> <div class="card"> <span style="color: red;">{{ item.name }}区</span> 在营企业总数 : <span style="color: sienna; font-size: 2em;">{{ item.value }} 家</span> </div> <br /> </div> </figure> <figure class="figure_center"> </figure> <figure class="figure_right"> <v-chart :option="mapOption" :autoresize="true" /> </figure> </div> <div class="line_02"> <figure class="figure_left"> <v-chart :option="pie" :autoresize="true" /> </figure> </div> <import ref="importRef"></import> </div> </template> <script lang="ts" setup> import { ref, provide, reactive } from "vue"; import { use } from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { BarChart, LineChart, PieChart, MapChart, RadarChart, ScatterChart, EffectScatterChart, LinesChart } from "echarts/charts"; import { GridComponent, PolarComponent, GeoComponent, TooltipComponent, LegendComponent, TitleComponent, VisualMapComponent, DatasetComponent, ToolboxComponent, DataZoomComponent } from "echarts/components"; import VChart, { THEME_KEY } from "vue-echarts"; import app from "@/constants/app"; import { getToken } from "@/utils/cache"; import Import from '@/views/enterprise/enterprise-library-import.vue'; import baseService from "@/service/baseService"; import jiNanMapJson from "src/views/enterprise/assets/json/济南市.json"; import zhangQiuMapJson from "src/views/enterprise/assets/json/章丘区.json"; import * as echarts from "echarts"; use([BarChart, LineChart, PieChart, MapChart, RadarChart, ScatterChart, EffectScatterChart, LinesChart, GridComponent, PolarComponent, GeoComponent, TooltipComponent, LegendComponent, TitleComponent, VisualMapComponent, DatasetComponent, CanvasRenderer, ToolboxComponent, DataZoomComponent]); provide(THEME_KEY, "westeros"); const dataForm = reactive({ currentTime: '', enterpriseNums: [], }); const importRef = ref(); const importHandle = () => { importRef.value.init(`${app.api}/enterprise/details/import?token=${getToken()}`); }; const list = ref([]); const industryNames = ref([]); const getIndustryInfo = async () => { let result = await (await baseService.get("/enterprise/details/industryInfo")).data; list.value = result; industryNames.value = result.map((item: any) => item.name); }; getIndustryInfo(); const updateTime = () => { const currentDate = new Date(); const year = String(currentDate.getFullYear()); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const date = String(currentDate.getDate()).padStart(2, '0'); const hours = String(currentDate.getHours()).padStart(2, '0'); const minutes = String(currentDate.getMinutes()).padStart(2, '0'); const seconds = String(currentDate.getSeconds()).padStart(2, '0'); dataForm.currentTime = `${year}年${month}月${date}日 ${hours}:${minutes}:${seconds}`; }; setInterval(() => { updateTime(); }, 1000); const mapMsg = ref([]); const getEnterpriseNums = async () => { let result = await (await baseService.get("/enterprise/details/enterpriseNums")).data; dataForm.enterpriseNums = result; mapMsg.value = result; //console.log(mapMsg) }; getEnterpriseNums(); echarts.registerMap('jiNan', jiNanMapJson); const mapOption = ref({ title: { text: '济南市各区在营企业统计', subtext: '数据来源XXXX', }, tooltip: { trigger: 'item', formatter: '{b}<br/>{c} (家)' }, toolbox: { show: false, orient: 'vertical', left: 'right', top: 'center', feature: { dataView: { readOnly: false }, restore: {}, saveAsImage: {} } }, visualMap: { min: 800, max: 500000, text: ['High', 'Low'], realtime: false, calculable: true, inRange: { color: ['lightskyblue', 'yellow', 'orangered'] } }, series: [ { name: '济南市各区注册企业数量', type: 'map', map: 'jiNan', label: { show: true }, data: mapMsg, } ] }); const pie = ref({ title: { text: "行业占比", left: "center" }, tooltip: { trigger: "item", formatter: "{a} <br/>{b} : {c} ({d}%)" }, legend: { type: 'scroll', orient: "vertical", right: 10, top: 20, bottom: 20, left: "left", data: industryNames }, series: [ { name: "行业占比", type: "pie", label: { show: false }, radius: "55%", center: ["50%", "60%"], data: list, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: "rgba(0, 0, 0, 0.5)" } } } ] }); </script> <style lang="less" scoped> .topShow { width: 90%; height: 5rem /* 80/16 */ ; font-family: "得意黑"; border-radius: 3.125rem /* 50/16 */ ; border-radius: 3.4375rem /* 55/16 */ ; border-radius: 2.5rem /* 40/16 */ ; background: #f0f0ff; box-shadow: .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } @font-face { font-family: '得意黑'; src: url('./assets/font/SmileySans-Oblique-2.ttf'); font-weight: normal; font-style: normal; } .card { border-radius: .5rem /* 8/16 */ ; font-family: "得意黑"; font-size: large; border-radius: 1.125rem /* 18/16 */ ; background: #f0f0ff; box-shadow: inset .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, inset -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } .box { position: relative; display: flex; flex-direction: column; justify-content: center; figure { display: inline-block; // position: relative; //margin: 2em auto; border: .0625rem /* 1/16 */ solid rgba(0, 0, 0, 0.1); border-radius: .5rem /* 8/16 */ ; box-shadow: 0 0 2.8125rem /* 45/16 */ rgba(0, 0, 0, 0.2); padding: 1.875rem /* 30/16 */ ; .echarts { width: 40vw; min-width: 25rem /* 400/16 */ ; height: 18.75rem /* 300/16 */ ; } } } .line_01 { width: 100%; height: auto; // 左对齐 .figure_left { float: left; width: 27em; border-radius: 1.125rem /* 18/16 */ ; background: #f0f0ff; box-shadow: .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } //右对齐 .figure_right { float: right; width: 50em; .echarts { width: auto; height: 43.75rem /* 700/16 */ ; } border-radius: 1.125rem /* 18/16 */ ; background: #f0f0ff; box-shadow: .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } .figure_center { //float: right; //position: absolute; width: 21em; border-radius: 1.125rem /* 18/16 */ ; background: #f0f0ff; box-shadow: .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } } .line_02 { width: 100%; height: auto; // 左对齐 .figure_left { float: left; width: 50em; border-radius: 1.125rem /* 18/16 */ ; background: #f0f0ff; box-shadow: .4375rem /* 7/16 */ .4375rem /* 7/16 */ .875rem /* 14/16 */ #ccccd9, -.4375rem /* 7/16 */ -.4375rem /* 7/16 */ .875rem /* 14/16 */ #ffffff; } }</style> 最终实现效果可参考echarts的案例。 前端实现时间刷新 https://blog.iletter.top/archives/133.html 2023-05-24T23:48:00+08:00 前端实现个时间刷新的案例:标签<div style="color:brown; font-size: 1.25rem /* 20/16 */;text-align: right;padding-right: 2.1875rem /* 35/16 */;;"> {{ dataForm.currentTime }}</div>scriptconst updateTime = () => { const currentDate = new Date(); const year = String(currentDate.getFullYear()); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const date = String(currentDate.getDate()).padStart(2, '0'); const hours = String(currentDate.getHours()).padStart(2, '0'); const minutes = String(currentDate.getMinutes()).padStart(2, '0'); const seconds = String(currentDate.getSeconds()).padStart(2, '0'); dataForm.currentTime = `${year}年${month}月${date}日 ${hours}:${minutes}:${seconds}`; };这里主要调用的是 Date();的前端的方法。然后我们设置定时器一秒刷新一次。setInterval(() => { updateTime(); }, 1000);这是根据人人开源项目做二次开发写的,其实二次开发真的有些难受。如果要根据vue的方式来写就是这样的<script> export default { data() { return { currentTime: '' }; }, mounted() { // 更新时间 this.updateTime(); // 每秒钟更新一次时间 setInterval(() => { this.updateTime(); }, 1000); }, methods: { updateTime() { const currentDate = new Date(); const year = currentDate.getFullYear(); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const day = String(currentDate.getDate()).padStart(2, '0'); const hours = String(currentDate.getHours()).padStart(2, '0'); const minutes = String(currentDate.getMinutes()).padStart(2, '0'); const seconds = String(currentDate.getSeconds()).padStart(2, '0'); this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } } }; </script> 在mounted生命周期钩子中,我们通过调用updateTime方法来初始化时间,并使用setInterval函数每秒钟更新一次时间。updateTime方法会获取当前时间并更新currentTime的值,然后将其显示在页面中。getYear方法返回的是四位数的年份,getMonth方法返回的是从0开始的月份(需要加1),getDate方法返回的是月份中的日期。我们将它们转换为字符串并使用padStart方法进行补零操作,以保证两位数的格式。最后,我们将年、月、日、时、分和秒拼接为一个完整的日期时间字符串,并将其赋值给currentTime,用于在页面中显示。其中有个点就是在获取日期时,getDay()方法返回的是星期几(0-6),而不是日期。今天就犯糊涂了,用了getday()另外推荐一个很好的拟态风格的css生成网站,简直不要太好用!!!!https://neumorphism.io/#e0e0e0