安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
320 lines
14 KiB
HTML
320 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>{$article.title} - 文章CMS</title>
|
||
<meta name="keywords" content="{$article.category_name|default=''}">
|
||
<meta name="description" content="{$article.summary|default=''|substr=0,150}">
|
||
|
||
<link rel="canonical" href="{:url('articles/read/' . $article.id)}">
|
||
{notempty name='$jsonLd'}
|
||
<script type="application/ld+json">{$jsonLd|raw}</script>
|
||
<script type="application/ld+json">{$breadcrumbLd|raw}</script>
|
||
{/notempty}
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
||
<link rel="stylesheet" type="text/css" href="/assets/layui/css/layui.css">
|
||
<link rel="stylesheet" type="text/css" href="/static/articles/css/main.css">
|
||
</head>
|
||
<body class="micronews">
|
||
<!-- 阅读进度条 + 继续上次阅读 -->
|
||
<div id="readProgressBar" style="position:fixed;top:0;left:0;height:3px;background:#5FB878;width:0;z-index:9999;"></div>
|
||
<div id="readResume" style="display:none;position:fixed;right:16px;bottom:16px;background:#fff;border:1px solid #eee;padding:8px 12px;box-shadow:0 2px 8px rgba(0,0,0,.12);z-index:9999;font-size:13px;">
|
||
上次读到 <b id="readResumePct">0</b>% · <a href="javascript:;" id="readResumeGo" style="color:#5FB878;">继续阅读</a>
|
||
<a href="javascript:;" id="readResumeClose" style="margin-left:8px;color:#999;">×</a>
|
||
</div>
|
||
|
||
<div class="micronews-header-wrap">
|
||
<div class="micronews-header w1000 layui-clear">
|
||
<h1 class="logo">
|
||
<a href="{:url('articles/index/index')}">
|
||
<img src="/static/articles/img/LOGO.png" alt="logo">
|
||
<span class="layui-hide">LOGO</span>
|
||
</a>
|
||
</h1>
|
||
<p class="nav">
|
||
<a href="{:url('articles/index/index')}" {eq name='nav_active' value='index'}class="active"{/eq}>最新</a>
|
||
{volist name='categories' id='c'}
|
||
<a href="{:url('articles/category/index', ['id'=>$c.id])}" {eq name='nav_active' value='$c.id'}class="active"{/eq}>{$c.name}</a>
|
||
{/volist}
|
||
</p>
|
||
<div class="search-bar">
|
||
<form class="layui-form" action="{:url('articles/search/index')}">
|
||
<div class="layui-form-item">
|
||
<div class="layui-input-block">
|
||
<input type="text" name="keyword" placeholder="搜索你要的内容" autocomplete="off" class="layui-input">
|
||
<button class="layui-btn search-btn" formnovalidate><i class="layui-icon layui-icon-search"></i></button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="login">
|
||
{if $is_login}
|
||
<a href="{:url('member/index/index')}"><img src="{:isset($member['avatar']) && $member['avatar'] ? $member['avatar'] : '/static/articles/img/header.png'}" style="width: 36px; height: 36px;"></a>
|
||
{else/}
|
||
<a href="{:url('member/login/index')}">登录</a>
|
||
{/if}
|
||
</div>
|
||
<div class="menu-icon">
|
||
<i class="layui-icon layui-icon-more-vertical"></i>
|
||
</div>
|
||
<div class="mobile-nav">
|
||
<ul class="layui-nav" lay-filter="">
|
||
<li class="layui-nav-item {eq name='nav_active' value='index'}layui-this{/eq}"><a href="{:url('articles/index/index')}">最新</a></li>
|
||
{volist name='categories' id='c'}
|
||
<li class="layui-nav-item {eq name='nav_active' value='$c.id'}layui-this{/eq}"><a href="{:url('articles/category/index', ['id'=>$c.id])}">{$c.name}</a></li>
|
||
{/volist}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="micronews-container micronews-details-container w1000">
|
||
<div class="layui-fluid">
|
||
<div class="layui-row">
|
||
<div class="layui-col-xs12 layui-col-sm12 layui-col-md8">
|
||
<div class="main">
|
||
<div class="title">
|
||
<h3>{$article.title}</h3>
|
||
<div class="b-txt">
|
||
<span class="label">{$article.category_name|default='未分类'}</span>
|
||
<span class="icon">
|
||
<i class="layui-icon layui-icon-radio"></i>
|
||
<b>{$article.view_count}</b>人
|
||
</span>
|
||
<a href="#message">
|
||
<span class="icon message">
|
||
<i class="layui-icon layui-icon-dialogue"></i>
|
||
<b>{$article.comment_count}</b>条
|
||
</span>
|
||
</a>
|
||
<a href="javascript:;" id="btnLike" class="icon like {if $liked}liked{/if}" title="点赞">
|
||
<i class="layui-icon layui-icon-praise"></i>
|
||
<b id="likeCount">{$article.like_count|default='0'}</b>
|
||
</a>
|
||
<a href="javascript:;" id="btnFav" class="icon fav {if $favorited}on{/if}" title="收藏">
|
||
<i class="layui-icon layui-icon-star"></i>
|
||
<span id="favText">{if $favorited}已收藏{else/}收藏{/if}</span>
|
||
</a>
|
||
<span class="icon time">
|
||
<i class="layui-icon layui-icon-log"></i>
|
||
{$article.create_at}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="article">
|
||
{if isset($contentHtml) && $contentHtml}{$contentHtml|raw}{else/}{$article.content|raw}{/if}
|
||
</div>
|
||
<div class="leave-message" id="message">
|
||
<div class="tit-box">
|
||
<span class="tit">网友跟帖</span>
|
||
<span class="num"><b>{$article.comment_count}</b>条</span>
|
||
</div>
|
||
<div class="content-box">
|
||
<div class="tear-box">
|
||
<a href="#"><img src="/static/articles/img/header_img1.png"></a>
|
||
<form class="layui-form">
|
||
<div class="layui-form-item layui-form-text">
|
||
<div class="layui-input-block">
|
||
<textarea id="onInput" placeholder="请输入内容" class="layui-textarea"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<div class="layui-input-block" style="text-align: right;">
|
||
<div class="message-text">
|
||
<div class="txt"></div>
|
||
</div>
|
||
<button type="button" class="layui-btn micronews-details-Publish">发表</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<!-- 楼层回复框:点击某条评论的「回复」后展开 -->
|
||
<div class="reply-box" id="replyBox" style="display:none;margin:10px 0;padding:10px;border:1px solid #eee;">
|
||
<div class="reply-tip" id="replyTip" style="margin-bottom:6px;color:#888;"></div>
|
||
<textarea id="replyContent" class="layui-textarea" placeholder="回复内容"></textarea>
|
||
<div style="text-align:right;margin-top:8px;">
|
||
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" id="replyCancel">取消</button>
|
||
<button type="button" class="layui-btn layui-btn-sm" id="replySubmit">提交回复</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ulCommentList">
|
||
{volist name='comments.data' id='cm'}
|
||
<div class="liCont">
|
||
<a href="#"><img src="/static/articles/img/header_img1.png"></a>
|
||
<div class="item-cont">
|
||
<div class="cont">
|
||
<p><span class="name">{$cm.nickname|default='游客'}</span><span class="time">{$cm.create_at}</span></p>
|
||
<p class="text">{$cm.content}</p>
|
||
<p><a href="javascript:;" class="btn-reply" data-id="{$cm.id}" data-name="{$cm.nickname|default='游客'}">回复</a></p>
|
||
</div>
|
||
{notempty name='$cm.replies'}
|
||
<div class="reply-list" style="margin-left:52px;border-left:2px solid #f0f0f0;padding-left:10px;">
|
||
{volist name='$cm.replies' id='rp'}
|
||
<div class="reply-item">
|
||
<p><span class="name">{$rp.nickname|default='游客'}</span><span class="time">{$rp.create_at}</span></p>
|
||
<p class="text">{$rp.content}</p>
|
||
</div>
|
||
{/volist}
|
||
</div>
|
||
{/notempty}
|
||
</div>
|
||
</div>
|
||
{/volist}
|
||
</div>
|
||
<div class="page-wrap">{$comments.render|raw}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layui-col-xs12 layui-col-sm12 layui-col-md4">
|
||
<div class="popular-info popular-info-tog">
|
||
<div class="layui-card">
|
||
<div class="layui-card-header">
|
||
<h3>资讯推荐</h3>
|
||
</div>
|
||
<div class="layui-card-body">
|
||
<ul class="list-box">
|
||
{volist name='related' id='r'}
|
||
<li class="list">
|
||
<a href="{:url('articles/Article/read', ['id'=>$r.id])}">{$r.title}</a>
|
||
</li>
|
||
{/volist}
|
||
{volist name='hot' id='h'}
|
||
<li class="list">
|
||
<a href="{:url('articles/Article/read', ['id'=>$h.id])}">{$h.title}</a>
|
||
</li>
|
||
{/volist}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="micronews-footer-wrap">
|
||
<div class="micronews-footer w1000">
|
||
<div class="ft-nav">
|
||
<a href="#">关于我们</a>
|
||
<a href="#">合作伙伴</a>
|
||
<a href="#">广告服务</a>
|
||
<a href="#">常见问题</a>
|
||
</div>
|
||
<div class="Copyright">
|
||
<span>Copyright </span> ©<span>文章CMS </span><span>Powered by YwxApp</span>
|
||
</div>
|
||
<div class="f-icon">
|
||
<a href="#" class="w-icon">
|
||
<img src="/static/articles/img/wechat_ic.png">
|
||
</a>
|
||
<a href="#" class="wb-icon">
|
||
<img src="/static/articles/img/qq_ic.png">
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="text/javascript" src="/assets/layui/layui.js"></script>
|
||
<script type="text/javascript" src="/assets/ywxapp/ywxapp.js"></script>
|
||
<script>
|
||
layui.config({
|
||
base: '/assets/layui/js/'
|
||
}).use('index', function () {
|
||
var index = layui.index;
|
||
index.EnterMessage();
|
||
index.seachBtn();
|
||
index.onInput();
|
||
index.arrowutil();
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
// 点赞 / 楼层回复(独立于模板自带 index 模块,避免冲突)
|
||
layui.use(['jquery', 'layer'], function () {
|
||
var $ = layui.jquery, layer = layui.layer;
|
||
var aid = "{$article.id}";
|
||
var likeUrl = "{:url('articles/article/like')}";
|
||
var commentUrl = "{:url('articles/article/comment')}";
|
||
|
||
$('#btnLike').on('click', function () {
|
||
$.post(likeUrl, { id: aid, target: 1 }, function (r) {
|
||
if (!r || !r.code) { layer.msg((r && r.message) || '操作失败'); return; }
|
||
var d = r.data || {};
|
||
$('#likeCount').text(d.count || 0);
|
||
$('#btnLike').toggleClass('liked', !!d.liked);
|
||
layer.msg(r.message || (d.liked ? '已点赞' : '已取消点赞'));
|
||
});
|
||
return false;
|
||
});
|
||
|
||
// 收藏
|
||
$('#btnFav').on('click', function () {
|
||
$.post("{:url('articles/article/favorite')}", { id: aid }, function (r) {
|
||
if (!r || !r.code) { layer.msg((r && r.message) || '请先登录'); return; }
|
||
var on = !!(r.data && r.data.favorited);
|
||
$('#btnFav').toggleClass('on', on);
|
||
$('#favText').text(on ? '已收藏' : '收藏');
|
||
layer.msg(r.message || '');
|
||
});
|
||
return false;
|
||
});
|
||
|
||
// 阅读进度(本地记录,无需登录)
|
||
var bar = document.getElementById('readProgressBar');
|
||
var pkey = 'articles_progress_' + aid;
|
||
function progress() {
|
||
var h = document.documentElement;
|
||
var top = h.scrollTop || document.body.scrollTop || 0;
|
||
var max = (h.scrollHeight - h.clientHeight) || 0;
|
||
return max > 0 ? Math.min(100, Math.round(top / max * 100)) : 0;
|
||
}
|
||
window.addEventListener('scroll', function () {
|
||
var p = progress();
|
||
if (bar) { bar.style.width = p + '%'; }
|
||
try { localStorage.setItem(pkey, p); } catch (e) {}
|
||
});
|
||
|
||
var saved = 0;
|
||
try { saved = parseInt(localStorage.getItem(pkey) || '0', 10); } catch (e) {}
|
||
if (saved >= 5 && saved <= 90) {
|
||
$('#readResumePct').text(saved);
|
||
$('#readResume').show();
|
||
$('#readResumeGo').on('click', function () {
|
||
var h = document.documentElement;
|
||
var max = (h.scrollHeight - h.clientHeight) || 0;
|
||
window.scrollTo({ top: Math.round(max * saved / 100), behavior: 'smooth' });
|
||
$('#readResume').hide();
|
||
});
|
||
$('#readResumeClose').on('click', function () { $('#readResume').hide(); });
|
||
}
|
||
|
||
var replyPid = 0;
|
||
$(document).on('click', '.btn-reply', function () {
|
||
replyPid = $(this).data('id');
|
||
$('#replyTip').text('回复 @' + ($(this).data('name') || ''));
|
||
$('#replyBox').show();
|
||
$('#replyContent').focus();
|
||
});
|
||
|
||
$('#replyCancel').on('click', function () {
|
||
replyPid = 0;
|
||
$('#replyBox').hide();
|
||
$('#replyContent').val('');
|
||
});
|
||
|
||
$('#replySubmit').on('click', function () {
|
||
var content = $.trim($('#replyContent').val());
|
||
if (!content) { layer.msg('请输入回复内容'); return; }
|
||
$.post(commentUrl, { aid: aid, pid: replyPid, content: content }, function (r) {
|
||
layer.msg((r && r.message) || '已提交');
|
||
if (r && r.code) { setTimeout(function () { location.reload(); }, 600); }
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|