diff --git a/assets/admin/src/schema/sections/post-list/comment.js b/assets/admin/src/schema/sections/post-list/comment.js index bf672280..aac10c7d 100644 --- a/assets/admin/src/schema/sections/post-list/comment.js +++ b/assets/admin/src/schema/sections/post-list/comment.js @@ -23,6 +23,10 @@ export const commentSchema = { comment_hot_threshold: 10, comment_quick_s: true, comment_city_s: true, + comment_city_type: 'province_city', + comment_quick_often: '不错,学习了 +感谢分享 +期待更新', comment_author_tag: true, comment_corderby: true, comment_empty_text: '暂无评论,来做第一个评论的人吧~', @@ -83,6 +87,14 @@ export const commentSchema = { { key: 'comment_like_s', label: '评论点赞', type: 'switch', default: true }, { key: 'comment_quick_s', label: '快捷回复', type: 'switch', default: true }, { key: 'comment_city_s', label: '评论城市', type: 'switch', default: true }, + { key: 'comment_city_type', label: '城市显示格式', type: 'radio', default: 'province_city', options: [ + { label: '省份+城市', value: 'province_city' }, + { label: '仅省份', value: 'province' }, + { label: '国家+省份', value: 'country_province' }, + ], dependsOn: { key: 'comment_city_s', value: true } }, + { key: 'comment_quick_often', label: '快捷回复短语', type: 'textarea', default: '不错,学习了 +感谢分享 +期待更新', help: '每行一条', dependsOn: { key: 'comment_quick_s', value: true } }, { key: 'comment_author_tag', label: '作者标签', type: 'switch', default: true }, { key: 'comment_corderby', label: '评论排序切换', type: 'switch', default: true, help: '前台显示最新/最早/最热切换按钮' }, ], diff --git a/assets/css/frontend/comments.css b/assets/css/frontend/comments.css index 37527bc7..aa95ba3b 100644 --- a/assets/css/frontend/comments.css +++ b/assets/css/frontend/comments.css @@ -1,784 +1,4 @@ -/* ============================================================ - * AKSRT Comments — v4.0 Zibll-Inspired - * - * 功能: - * - 评论表单(头像 + 输入框 + 操作栏) - * - 评论列表(嵌套回复 + 头像 + 时间) - * - 排序/过滤栏 - * - 空状态 - * - 移动端固定底部输入框(可选) - * ============================================================ */ - -/* ============================================================ - * 评论区容器 - * ============================================================ */ -.comments-area { - margin: 0; - padding: 0; - /* 对齐 zibll: 45px */ - --aksrt-comment-avatar: 45px; - --aksrt-comment-avatar-form: 45px; - --aksrt-comment-indent: 48px; -} - -/* 评论标题 */ -.comments-header { - margin-bottom: 16px; -} - -.comments-title { - font-size: 1rem; - font-weight: 700; - margin: 0; - display: flex; - align-items: center; - gap: 8px; - color: var(--color-ink); -} - -.comments-title i { - color: var(--color-primary); - font-size: 16px; -} - -.comments-count { - font-size: 0.8125rem; - font-weight: 400; - color: var(--color-ink-4); -} - -/* ============================================================ - * 登录提示 - * ============================================================ */ -.comment-login-prompt { - margin-bottom: 20px; -} - -.comment-login-prompt__inner { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 18px; - border-radius: var(--main-radius, var(--radius, 8px)); - background: var(--surface-1); - border: none; - box-shadow: var(--shadow-sm); - font-size: 14px; - color: var(--color-ink-4); -} - -.comment-login-prompt__inner i { - font-size: 16px; - color: var(--color-ink-4); -} - -.comment-login-prompt__btns { - margin-left: auto; - display: flex; - gap: 8px; -} - -/* ============================================================ - * 评论表单 - * ============================================================ */ -.comment-respond { - margin-bottom: 20px; -} - -.comment-form__inner { - display: flex; - gap: 10px; - align-items: flex-start; -} - -.comment-form__avatar { - flex-shrink: 0; -} - -.comment-form__avatar img { - width: var(--aksrt-comment-avatar-form); - height: var(--aksrt-comment-avatar-form); - border-radius: 50%; - object-fit: cover; -} - -.comment-form__avatar-placeholder { - width: var(--aksrt-comment-avatar-form); - height: var(--aksrt-comment-avatar-form); - border-radius: 50%; - background: var(--surface-3); - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ink-4); -} - -.comment-form__body { - flex: 1; - min-width: 0; -} - -.comment-form__reply-info { - font-size: 0.8125rem; - color: var(--color-ink-4); - margin-bottom: 8px; - display: flex; - align-items: center; - gap: 8px; -} - -.comment-form__cancel-reply { - color: var(--color-error, #ef4444); - font-size: 12px; -} - -.comment-form__textarea { - width: 100%; - min-height: 96px; - resize: vertical; - border-radius: 10px; - border: 1px solid var(--color-line); - padding: 12px 14px; - font-size: 14px; - background: var(--surface-4, var(--main-bg-color, var(--surface-1))); - color: var(--color-ink); - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.comment-form__textarea:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-primary-soft); -} - -.comment-form__actions { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 10px; - gap: 8px; -} - -.comment-form__tools { - display: flex; - gap: 8px; -} - -.comment-form__submit-area { - display: flex; - align-items: center; - gap: 8px; - margin-left: auto; - flex-wrap: wrap; - justify-content: flex-end; -} - -.comment-form__guest { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 6px; -} - -.comment-form__guest-toggle { - background: none; - border: 0; - padding: 0; - cursor: pointer; - color: var(--muted-color, #8b9198); - white-space: nowrap; -} - -.comment-form__guest-toggle:hover, -.comment-form__guest-toggle[aria-expanded="true"] { - color: var(--focus-color, #f04494); -} - -.comment-form__guest-fields { - display: flex; - gap: 6px; - align-items: center; - max-height: 0; - overflow: hidden; - opacity: 0; - transition: max-height 0.2s ease, opacity 0.15s ease; -} - -.comment-form__guest-fields.is-open, -.comment-form__guest-fields:not([hidden]) { - max-height: 48px; - opacity: 1; -} - -.comment-form__guest-field { - width: 110px; - height: 30px; - font-size: 12px; - padding: 0 8px; -} - -.comt-box .comt-textarea, -.comt-box #comment { - min-height: 72px; - padding: 10px 12px; - border-radius: 8px; - font-size: 13px; - line-height: 1.5; - margin-bottom: 6px; -} - -.comt-ctrl { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.comment-form__submit { - height: 30px; - padding: 0 12px; - font-size: 13px; -} - -.comment-form__submit { - display: inline-flex; - align-items: center; - gap: 6px; -} - -/* ============================================================ - * 排序/过滤栏 - * ============================================================ */ -.comment-filter { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 0; - margin-bottom: 12px; - border-bottom: 1px solid var(--color-line); -} - -.comment-filter__tabs { - display: flex; - list-style: none; - margin: 0; - padding: 0; - gap: 4px; -} - -.comment-filter__tabs li a { - display: inline-block; - padding: 4px 12px; - border-radius: 4px; - font-size: 13px; - font-weight: 500; - color: var(--color-ink-4); - text-decoration: none; - transition: background 0.15s ease, color 0.15s ease; -} - -.comment-filter__tabs li a:hover { - background: var(--surface-3); - color: var(--color-ink); -} - -.comment-filter__tabs li.active a { - background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); - color: var(--color-primary); - font-weight: 600; -} - -/* Zibll-compatible class names used by aksrt-comments.php */ -.comment-order-box { - display: flex; - list-style: none; - margin: 0; - padding: 0; - gap: 4px; -} -.comment-order-box li a, -.comment-order-box .comment-orderby { - display: inline-block; - padding: 4px 12px; - border-radius: 4px; - font-size: 13px; - font-weight: 500; - color: var(--color-ink-4); - text-decoration: none; - transition: background 0.15s ease, color 0.15s ease; -} -.comment-order-box li a:hover, -.comment-order-box .comment-orderby:hover { - background: var(--surface-3); - color: var(--color-ink); -} -.comment-order-box li.active a, -.comment-order-box li.active .comment-orderby { - background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); - color: var(--color-primary); - font-weight: 600; -} -.btn-only-author, -.comment-author-filter { - display: inline-flex; - align-items: center; - padding: 4px 12px; - border-radius: 999px; - border: 1px solid var(--color-line); - background: var(--surface-1); - color: var(--color-ink-3); - font-size: 12px; - font-weight: 600; - text-decoration: none; - transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; -} -.btn-only-author:hover, -.comment-author-filter:hover { - border-color: var(--color-primary); - color: var(--color-primary); -} -.btn-only-author.active, -.comment-author-filter.active { - border-color: var(--color-primary); - background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); - color: var(--color-primary); -} - -/* ============================================================ - * 评论列表 - * ============================================================ */ -.comment-list { - list-style: none; - margin: 0; - padding: 0; -} - -.comment-list .comment { - padding: 16px 0; - border-bottom: 1px solid var(--color-line); -} - -.comment-list .comment:last-child { - border-bottom: none; -} - -/* 嵌套回复 — 对齐 zibll: 86px 缩进 + 灰色背景 */ -.comment-list .children { - list-style: none; - margin: 0; - margin-left: 86px; -} - -.comment-list .children .comment { - padding: 14px 16px; - background: var(--surface-4, rgba(0, 0, 0, 0.03)); - border: 1px solid var(--color-line-soft); - border-radius: var(--radius-lg); -} - -/* 深层嵌套缩进递减 */ -.comment-list .children .children { - margin-left: 13px; -} - -/* 评论项内部结构(由 aksrt_comment_callback 渲染) */ -.aksrt-comment__body { - display: flex; - gap: 12px; -} - -.aksrt-comment__avatar { - flex-shrink: 0; -} - -.aksrt-comment__avatar img { - width: var(--aksrt-comment-avatar); - height: var(--aksrt-comment-avatar); - border-radius: 50%; - object-fit: cover; - margin-right: 20px; -} - -.aksrt-comment__content { - flex: 1; - min-width: 0; -} - -.aksrt-comment__header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 6px; - flex-wrap: wrap; -} - -.aksrt-comment__author { - font-weight: 600; - font-size: 14px; - color: var(--color-ink); -} - -.aksrt-comment__author a { - color: inherit; -} - -.aksrt-comment__author a:hover { - color: var(--color-primary); -} - -.aksrt-comment__badge { - font-size: 10px; - padding: 1px 6px; - border-radius: 3px; - background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); - color: var(--color-primary); - font-weight: 500; -} - -.aksrt-comment__time { - font-size: 12px; - color: var(--color-ink-4); -} - -.aksrt-comment__text { - font-size: 14px; - line-height: 1.6; - color: var(--color-ink); - margin: 0 0 8px; - word-wrap: break-word; - white-space: pre-wrap; -} - -/* 对齐 zibll: 评论内图片限制 */ -.aksrt-comment__text img:not(.smilie-icon) { - max-width: 260px; - max-height: 350px; -} - -.aksrt-comment__text p { - margin: 0 0 8px; -} - -.aksrt-comment__text p:last-child { - margin-bottom: 0; -} - -.aksrt-comment__actions { - display: flex; - gap: 12px; - font-size: 12px; -} - -.aksrt-comment__actions a, -.aksrt-comment__actions button { - color: var(--color-ink-4); - background: none; - border: none; - padding: 0; - cursor: pointer; - font-size: 12px; - display: inline-flex; - align-items: center; - gap: 4px; - transition: color 0.15s ease; -} - -.aksrt-comment__actions a:hover, -.aksrt-comment__actions button:hover { - color: var(--color-primary); -} - -/* 嵌套回复中的头像更小 */ -/* 对齐 zibll: 嵌套头像 26px */ -.children .aksrt-comment__avatar img { - width: 26px; - height: 26px; - margin-right: 6px; - vertical-align: 2px; -} - -/* ============================================================ - * 空状态 - * ============================================================ */ -.comments-empty { - text-align: center; - padding: 40px 20px; -} - -.comments-empty__icon { - font-size: 48px; - color: var(--color-ink-4); - opacity: 0.4; - margin-bottom: 12px; -} - -.comments-empty__text { - font-size: 1rem; - font-weight: 600; - color: var(--color-ink-4); - margin: 0 0 4px; -} - -.comments-empty__hint { - font-size: 0.8125rem; - color: var(--color-ink-4); - margin: 0; -} - -.comments-closed { - text-align: center; - font-size: 14px; - color: var(--color-ink-4); - padding: 20px; -} - -/* ============================================================ - * 评论分页 - * ============================================================ */ -.comment-pagination { - text-align: center; - padding: 16px 0; -} - -/* ============================================================ - * 移动端适配 - * ============================================================ */ -@media (max-width: 767px) { - .comment-form__inner { - flex-direction: column; - gap: 8px; - } - - .comment-form__avatar { - display: none; - } - - .comment-form__submit-area { - flex-wrap: wrap; - } - - .comment-form__guest-field { - width: 100%; - } - - .comment-list .children { - margin-left: 0; - } - - .children .aksrt-comment__avatar img { - width: 36px; - height: 36px; - } - - /* 移动端评论输入吸底 — 模仿 zibll mobile-fixed */ - .comment-respond.comment-form--mobile-fixed { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: var(--z-sticky, 200); - background: var(--surface-1); - border-top: 1px solid var(--color-line); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); - padding: 12px 16px; - padding-bottom: max(12px, env(safe-area-inset-bottom)); - } - - .comment-respond.comment-form--mobile-fixed .comment-form__textarea { - min-height: 44px; - max-height: 120px; - } - - .comment-respond.comment-form--mobile-fixed .comment-form__submit-area { - margin-top: 8px; - } - - /* 当评论区可见时隐藏吸底输入框 */ - .comment-respond.comment-form--mobile-fixed.is-visible { - display: none; - } -} - -/* ============================================================ - * 暗色模式 - * ============================================================ */ -html.dark .comment-form__textarea, -body.dark-theme .comment-form__textarea { - background: var(--surface-2); - border-color: var(--color-line); -} - -.theme-dark .comment-login-prompt__inner, -html.dark .comment-login-prompt__inner, -body.dark-theme .comment-login-prompt__inner { - background: var(--surface-1); -} - -/* ============================================================ - * 评论操作按钮(编辑/删除/审核) - * ============================================================ */ -.aksrt-comment-actions { - display: flex; - align-items: center; - gap: 8px; - margin-top: 8px; -} - -.aksrt-comment-action-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 10px; - border: 1px solid var(--color-line, rgba(50, 50, 50, 0.1)); - border-radius: var(--radius, 8px); - background: var(--surface-1, #fff); - color: var(--color-ink-3, #777); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: all 0.15s ease; -} - -.aksrt-comment-action-btn:hover { - border-color: var(--color-primary); - color: var(--color-primary); - background: var(--color-primary-soft, rgba(240, 68, 148, 0.08)); -} - -.aksrt-comment-action-btn--danger:hover { - border-color: var(--color-error); - color: var(--color-error); - background: rgba(239, 68, 68, 0.08); -} - -.aksrt-comment-action-btn--success:hover { - border-color: var(--color-success); - color: var(--color-success); - background: rgba(16, 185, 129, 0.08); -} - -.aksrt-comment-edit-form { - margin: 8px 0; -} - -.aksrt-comment-edit-textarea { - width: 100%; - min-height: 80px; - padding: 8px 12px; - border: 1px solid var(--color-line, #ddd); - border-radius: var(--radius, 8px); - font-size: 14px; - font-family: var(--font-body); - color: var(--color-ink); - background: var(--surface-1, #fff); - resize: vertical; - transition: border-color 0.15s; -} - -.aksrt-comment-edit-textarea:focus { - outline: none; - border-color: var(--color-primary); -} - -/* ============================================================ - * 评论互动:点赞 / 置顶 / 内联管理动作(统一合同) - * 这些动作均渲染在 .aksrt-comment__actions 内,复用其文字链按钮样式, - * 仅补充点赞激活态、置顶徽章与危险/成功色提示。 - * ============================================================ */ -.aksrt-comment__like.is-active, -.aksrt-comment__like.is-active i { - color: var(--color-primary); -} - -.aksrt-comment__like-count { - font-variant-numeric: tabular-nums; -} - -.aksrt-comment__action--danger:hover { - color: var(--color-error, #ef4444); -} - -.aksrt-comment__action--success:hover { - color: var(--color-success, #10b981); -} - -/* 置顶徽章 */ -.aksrt-comment__badge--pinned { - background: var(--color-warning-soft, rgba(245, 158, 11, 0.14)); - color: var(--color-warning, #d97706); - display: inline-flex; - align-items: center; - gap: 3px; -} - -/* 置顶评论容器高亮 */ -.aksrt-comment--pinned > .aksrt-comment__body { - position: relative; -} - -.comment-list .comment.aksrt-comment--pinned { - background: var(--color-warning-soft, rgba(245, 158, 11, 0.06)); - border-radius: var(--radius, 8px); - padding-left: 12px; - padding-right: 12px; -} - -/* zibll compat: emoji icon forced 30px */ -.comment-content .smilie-icon, -.comment-form .smilie-icon { - width: 30px !important; -} - -/* zibll compat: code block in comments */ -.comment-content .enlighter-default { - margin-bottom: 0; - opacity: 0.9; -} - - -/* Identity chips + 神评 */ -.aksrt-identity-chip { - display: inline-flex; - align-items: center; - gap: 3px; - vertical-align: middle; - max-width: 9em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.aksrt-identity-chip--comment { - font-size: 11px; - padding: 0 6px; -} -.medal-icon.img-icon, -.aksrt-identity-medal { - display: inline-block; - width: 1.35em; - height: 1.35em; - object-fit: contain; - vertical-align: middle; - margin-left: 4px; -} -.aksrt-identity-medal--comment { - width: 1.15em; - height: 1.15em; -} -.aksrt-identity-medal.is-wear { - outline: 1px solid rgba(245, 158, 11, 0.45); - border-radius: 3px; -} -.aksrt-comment__badge--hot { - color: #ea580c; - background: rgba(234, 88, 12, 0.1); - border-radius: 999px; - padding: 0 6px; - margin-left: 4px; - font-size: 11px; -} -.aksrt-comment__header .author-box { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 4px; -} +/* AKSRT comments aggregator */ +@import url("./comments/shell.css"); +@import url("./comments/mobile-item.css"); +@import url("./comment-tools.css"); diff --git a/assets/css/frontend/comments/mobile-item.css b/assets/css/frontend/comments/mobile-item.css new file mode 100644 index 00000000..705ed394 --- /dev/null +++ b/assets/css/frontend/comments/mobile-item.css @@ -0,0 +1,89 @@ +/* Zibll 1:1 FixedInput / virtual-input / item chrome */ +.virtual-input { display: none; } +@media (max-width: 767.98px) { + .virtual-input { + display: flex; align-items: center; gap: 10px; + padding: 10px 12px; margin-bottom: 12px; border-radius: 999px; + background: var(--surface-1, #fff); + box-shadow: var(--shadow-sm, 0 1px 4px rgba(0,0,0,.06)); + cursor: pointer; + } + .virtual-input .avatar-img { width: 32px; height: 32px; border-radius: 50%; } + .virtual-input .simulation { flex: 1; color: var(--color-ink-4, #999); font-size: 13px; } + #respond.mobile-fixed { + position: fixed; left: 0; right: 0; bottom: 0; z-index: 1050; + background: var(--surface-1, #fff); + border-radius: 12px 12px 0 0; + box-shadow: 0 -8px 28px rgba(0,0,0,.12); + padding: 12px 14px max(12px, env(safe-area-inset-bottom)); + transform: translateY(110%); transition: transform .25s ease; + max-height: 85vh; overflow: auto; + } + #respond.mobile-fixed.show { transform: translateY(0); } + #respond.mobile-fixed .fixed-body { + position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: -1; + opacity: 0; pointer-events: none; transition: opacity .2s ease; + } + #respond.mobile-fixed.show .fixed-body { opacity: 1; pointer-events: auto; } + #respond.mobile-fixed .comt-title { display: none; } +} +.comment-header .author-box { display: flex; align-items: center; gap: 8px; width: 100%; } +.comment-header .author-box name, +.comment-header .author-box .flex1 { flex: 1; min-width: 0; } +.comment-header .aksrt-comment__like, +.comment-header .action-comment-like { + margin-left: auto; display: inline-flex; align-items: center; gap: 4px; + color: var(--color-ink-3, #888); text-decoration: none; font-size: 13px; +} +.comment-header .aksrt-comment__like.is-active { color: var(--color-primary, #2060ef); } +.comt-meta { + display: flex; flex-wrap: wrap; align-items: center; gap: 8px; + font-size: 12px; margin-top: 6px; +} +.comt-meta .reply-link a, .comt-meta .comment-reply-link { color: var(--color-ink-3, #777); } +.comment-action-dropdown .dropdown-menu { min-width: 140px; padding: 6px 0; } +.comment-action-dropdown .dropdown-menu a { + display: block; padding: 6px 14px; color: var(--color-ink, #333); + text-decoration: none; font-size: 13px; +} +.comment-action-dropdown .dropdown-menu a:hover { background: var(--surface-2, #f5f5f5); } +.smilie-icon { width: 30px; height: 30px; vertical-align: -8px; display: inline-block; } +.comment-inline-img, .comment-content .box-img { + max-width: 260px; max-height: 350px; border-radius: 6px; display: block; margin: 6px 0; +} +.comment-code { + background: var(--surface-2, #f6f8fa); padding: 10px 12px; border-radius: 6px; + overflow: auto; font-size: 12px; +} +.commentlist .children { margin-left: 86px; padding-left: 0; list-style: none; } +.commentlist .children .children { margin-left: 23px; } +.commentlist .children .comt-avatar, +.commentlist .children .avatar-img.comt-avatar { width: 26px !important; height: 26px !important; } +.input-expand-wrap { display: inline-block; position: relative; } +.input-expand-wrap .dropdown-menu, +.input-expand-wrap .dropdown-smilie { + display: none; position: absolute; bottom: 100%; left: 0; z-index: 20; + background: var(--surface-1, #fff); box-shadow: 0 6px 24px rgba(0,0,0,.12); + border-radius: 8px; padding: 8px; +} +.input-expand-wrap.open > .dropdown-menu, +.input-expand-wrap.open .dropdown-smilie, +.dropup.open > .dropdown-menu { display: block; } +.dropdown-smilie { + display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; + max-width: 280px; max-height: 200px; overflow: auto; +} +.dropdown-smilie a img { width: 28px; height: 28px; } +.quick-reply-item { + display: block; padding: 6px 10px; color: var(--color-ink, #333); + text-decoration: none; font-size: 13px; +} +.quick-reply-item:hover { background: var(--surface-2, #f5f5f5); } +.comt-ctrl { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; } +.comt-tips-left { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; flex: 1; } +.comt-tips-right { margin-left: auto; display: flex; align-items: center; gap: 8px; } +#comments .comt-title { width: 90px; } +#comments .comt-title .comt-avatar img, +#comments .comt-avatar .avatar-img { width: 60px; height: 60px; border-radius: 50%; } +#comments .comt-box #comment { min-height: 88px; resize: vertical; } +@media (min-width: 768px) { .hide-pc { display: none !important; } } diff --git a/assets/css/frontend/comments/shell.css b/assets/css/frontend/comments/shell.css new file mode 100644 index 00000000..37527bc7 --- /dev/null +++ b/assets/css/frontend/comments/shell.css @@ -0,0 +1,784 @@ +/* ============================================================ + * AKSRT Comments — v4.0 Zibll-Inspired + * + * 功能: + * - 评论表单(头像 + 输入框 + 操作栏) + * - 评论列表(嵌套回复 + 头像 + 时间) + * - 排序/过滤栏 + * - 空状态 + * - 移动端固定底部输入框(可选) + * ============================================================ */ + +/* ============================================================ + * 评论区容器 + * ============================================================ */ +.comments-area { + margin: 0; + padding: 0; + /* 对齐 zibll: 45px */ + --aksrt-comment-avatar: 45px; + --aksrt-comment-avatar-form: 45px; + --aksrt-comment-indent: 48px; +} + +/* 评论标题 */ +.comments-header { + margin-bottom: 16px; +} + +.comments-title { + font-size: 1rem; + font-weight: 700; + margin: 0; + display: flex; + align-items: center; + gap: 8px; + color: var(--color-ink); +} + +.comments-title i { + color: var(--color-primary); + font-size: 16px; +} + +.comments-count { + font-size: 0.8125rem; + font-weight: 400; + color: var(--color-ink-4); +} + +/* ============================================================ + * 登录提示 + * ============================================================ */ +.comment-login-prompt { + margin-bottom: 20px; +} + +.comment-login-prompt__inner { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 18px; + border-radius: var(--main-radius, var(--radius, 8px)); + background: var(--surface-1); + border: none; + box-shadow: var(--shadow-sm); + font-size: 14px; + color: var(--color-ink-4); +} + +.comment-login-prompt__inner i { + font-size: 16px; + color: var(--color-ink-4); +} + +.comment-login-prompt__btns { + margin-left: auto; + display: flex; + gap: 8px; +} + +/* ============================================================ + * 评论表单 + * ============================================================ */ +.comment-respond { + margin-bottom: 20px; +} + +.comment-form__inner { + display: flex; + gap: 10px; + align-items: flex-start; +} + +.comment-form__avatar { + flex-shrink: 0; +} + +.comment-form__avatar img { + width: var(--aksrt-comment-avatar-form); + height: var(--aksrt-comment-avatar-form); + border-radius: 50%; + object-fit: cover; +} + +.comment-form__avatar-placeholder { + width: var(--aksrt-comment-avatar-form); + height: var(--aksrt-comment-avatar-form); + border-radius: 50%; + background: var(--surface-3); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-ink-4); +} + +.comment-form__body { + flex: 1; + min-width: 0; +} + +.comment-form__reply-info { + font-size: 0.8125rem; + color: var(--color-ink-4); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.comment-form__cancel-reply { + color: var(--color-error, #ef4444); + font-size: 12px; +} + +.comment-form__textarea { + width: 100%; + min-height: 96px; + resize: vertical; + border-radius: 10px; + border: 1px solid var(--color-line); + padding: 12px 14px; + font-size: 14px; + background: var(--surface-4, var(--main-bg-color, var(--surface-1))); + color: var(--color-ink); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.comment-form__textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-soft); +} + +.comment-form__actions { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; + gap: 8px; +} + +.comment-form__tools { + display: flex; + gap: 8px; +} + +.comment-form__submit-area { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + flex-wrap: wrap; + justify-content: flex-end; +} + +.comment-form__guest { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.comment-form__guest-toggle { + background: none; + border: 0; + padding: 0; + cursor: pointer; + color: var(--muted-color, #8b9198); + white-space: nowrap; +} + +.comment-form__guest-toggle:hover, +.comment-form__guest-toggle[aria-expanded="true"] { + color: var(--focus-color, #f04494); +} + +.comment-form__guest-fields { + display: flex; + gap: 6px; + align-items: center; + max-height: 0; + overflow: hidden; + opacity: 0; + transition: max-height 0.2s ease, opacity 0.15s ease; +} + +.comment-form__guest-fields.is-open, +.comment-form__guest-fields:not([hidden]) { + max-height: 48px; + opacity: 1; +} + +.comment-form__guest-field { + width: 110px; + height: 30px; + font-size: 12px; + padding: 0 8px; +} + +.comt-box .comt-textarea, +.comt-box #comment { + min-height: 72px; + padding: 10px 12px; + border-radius: 8px; + font-size: 13px; + line-height: 1.5; + margin-bottom: 6px; +} + +.comt-ctrl { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.comment-form__submit { + height: 30px; + padding: 0 12px; + font-size: 13px; +} + +.comment-form__submit { + display: inline-flex; + align-items: center; + gap: 6px; +} + +/* ============================================================ + * 排序/过滤栏 + * ============================================================ */ +.comment-filter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0; + margin-bottom: 12px; + border-bottom: 1px solid var(--color-line); +} + +.comment-filter__tabs { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 4px; +} + +.comment-filter__tabs li a { + display: inline-block; + padding: 4px 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + color: var(--color-ink-4); + text-decoration: none; + transition: background 0.15s ease, color 0.15s ease; +} + +.comment-filter__tabs li a:hover { + background: var(--surface-3); + color: var(--color-ink); +} + +.comment-filter__tabs li.active a { + background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); + color: var(--color-primary); + font-weight: 600; +} + +/* Zibll-compatible class names used by aksrt-comments.php */ +.comment-order-box { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 4px; +} +.comment-order-box li a, +.comment-order-box .comment-orderby { + display: inline-block; + padding: 4px 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + color: var(--color-ink-4); + text-decoration: none; + transition: background 0.15s ease, color 0.15s ease; +} +.comment-order-box li a:hover, +.comment-order-box .comment-orderby:hover { + background: var(--surface-3); + color: var(--color-ink); +} +.comment-order-box li.active a, +.comment-order-box li.active .comment-orderby { + background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); + color: var(--color-primary); + font-weight: 600; +} +.btn-only-author, +.comment-author-filter { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 999px; + border: 1px solid var(--color-line); + background: var(--surface-1); + color: var(--color-ink-3); + font-size: 12px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} +.btn-only-author:hover, +.comment-author-filter:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} +.btn-only-author.active, +.comment-author-filter.active { + border-color: var(--color-primary); + background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); + color: var(--color-primary); +} + +/* ============================================================ + * 评论列表 + * ============================================================ */ +.comment-list { + list-style: none; + margin: 0; + padding: 0; +} + +.comment-list .comment { + padding: 16px 0; + border-bottom: 1px solid var(--color-line); +} + +.comment-list .comment:last-child { + border-bottom: none; +} + +/* 嵌套回复 — 对齐 zibll: 86px 缩进 + 灰色背景 */ +.comment-list .children { + list-style: none; + margin: 0; + margin-left: 86px; +} + +.comment-list .children .comment { + padding: 14px 16px; + background: var(--surface-4, rgba(0, 0, 0, 0.03)); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-lg); +} + +/* 深层嵌套缩进递减 */ +.comment-list .children .children { + margin-left: 13px; +} + +/* 评论项内部结构(由 aksrt_comment_callback 渲染) */ +.aksrt-comment__body { + display: flex; + gap: 12px; +} + +.aksrt-comment__avatar { + flex-shrink: 0; +} + +.aksrt-comment__avatar img { + width: var(--aksrt-comment-avatar); + height: var(--aksrt-comment-avatar); + border-radius: 50%; + object-fit: cover; + margin-right: 20px; +} + +.aksrt-comment__content { + flex: 1; + min-width: 0; +} + +.aksrt-comment__header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + flex-wrap: wrap; +} + +.aksrt-comment__author { + font-weight: 600; + font-size: 14px; + color: var(--color-ink); +} + +.aksrt-comment__author a { + color: inherit; +} + +.aksrt-comment__author a:hover { + color: var(--color-primary); +} + +.aksrt-comment__badge { + font-size: 10px; + padding: 1px 6px; + border-radius: 3px; + background: var(--color-primary-soft, rgba(240, 68, 148, 0.12)); + color: var(--color-primary); + font-weight: 500; +} + +.aksrt-comment__time { + font-size: 12px; + color: var(--color-ink-4); +} + +.aksrt-comment__text { + font-size: 14px; + line-height: 1.6; + color: var(--color-ink); + margin: 0 0 8px; + word-wrap: break-word; + white-space: pre-wrap; +} + +/* 对齐 zibll: 评论内图片限制 */ +.aksrt-comment__text img:not(.smilie-icon) { + max-width: 260px; + max-height: 350px; +} + +.aksrt-comment__text p { + margin: 0 0 8px; +} + +.aksrt-comment__text p:last-child { + margin-bottom: 0; +} + +.aksrt-comment__actions { + display: flex; + gap: 12px; + font-size: 12px; +} + +.aksrt-comment__actions a, +.aksrt-comment__actions button { + color: var(--color-ink-4); + background: none; + border: none; + padding: 0; + cursor: pointer; + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 4px; + transition: color 0.15s ease; +} + +.aksrt-comment__actions a:hover, +.aksrt-comment__actions button:hover { + color: var(--color-primary); +} + +/* 嵌套回复中的头像更小 */ +/* 对齐 zibll: 嵌套头像 26px */ +.children .aksrt-comment__avatar img { + width: 26px; + height: 26px; + margin-right: 6px; + vertical-align: 2px; +} + +/* ============================================================ + * 空状态 + * ============================================================ */ +.comments-empty { + text-align: center; + padding: 40px 20px; +} + +.comments-empty__icon { + font-size: 48px; + color: var(--color-ink-4); + opacity: 0.4; + margin-bottom: 12px; +} + +.comments-empty__text { + font-size: 1rem; + font-weight: 600; + color: var(--color-ink-4); + margin: 0 0 4px; +} + +.comments-empty__hint { + font-size: 0.8125rem; + color: var(--color-ink-4); + margin: 0; +} + +.comments-closed { + text-align: center; + font-size: 14px; + color: var(--color-ink-4); + padding: 20px; +} + +/* ============================================================ + * 评论分页 + * ============================================================ */ +.comment-pagination { + text-align: center; + padding: 16px 0; +} + +/* ============================================================ + * 移动端适配 + * ============================================================ */ +@media (max-width: 767px) { + .comment-form__inner { + flex-direction: column; + gap: 8px; + } + + .comment-form__avatar { + display: none; + } + + .comment-form__submit-area { + flex-wrap: wrap; + } + + .comment-form__guest-field { + width: 100%; + } + + .comment-list .children { + margin-left: 0; + } + + .children .aksrt-comment__avatar img { + width: 36px; + height: 36px; + } + + /* 移动端评论输入吸底 — 模仿 zibll mobile-fixed */ + .comment-respond.comment-form--mobile-fixed { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: var(--z-sticky, 200); + background: var(--surface-1); + border-top: 1px solid var(--color-line); + box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); + padding: 12px 16px; + padding-bottom: max(12px, env(safe-area-inset-bottom)); + } + + .comment-respond.comment-form--mobile-fixed .comment-form__textarea { + min-height: 44px; + max-height: 120px; + } + + .comment-respond.comment-form--mobile-fixed .comment-form__submit-area { + margin-top: 8px; + } + + /* 当评论区可见时隐藏吸底输入框 */ + .comment-respond.comment-form--mobile-fixed.is-visible { + display: none; + } +} + +/* ============================================================ + * 暗色模式 + * ============================================================ */ +html.dark .comment-form__textarea, +body.dark-theme .comment-form__textarea { + background: var(--surface-2); + border-color: var(--color-line); +} + +.theme-dark .comment-login-prompt__inner, +html.dark .comment-login-prompt__inner, +body.dark-theme .comment-login-prompt__inner { + background: var(--surface-1); +} + +/* ============================================================ + * 评论操作按钮(编辑/删除/审核) + * ============================================================ */ +.aksrt-comment-actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} + +.aksrt-comment-action-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border: 1px solid var(--color-line, rgba(50, 50, 50, 0.1)); + border-radius: var(--radius, 8px); + background: var(--surface-1, #fff); + color: var(--color-ink-3, #777); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.aksrt-comment-action-btn:hover { + border-color: var(--color-primary); + color: var(--color-primary); + background: var(--color-primary-soft, rgba(240, 68, 148, 0.08)); +} + +.aksrt-comment-action-btn--danger:hover { + border-color: var(--color-error); + color: var(--color-error); + background: rgba(239, 68, 68, 0.08); +} + +.aksrt-comment-action-btn--success:hover { + border-color: var(--color-success); + color: var(--color-success); + background: rgba(16, 185, 129, 0.08); +} + +.aksrt-comment-edit-form { + margin: 8px 0; +} + +.aksrt-comment-edit-textarea { + width: 100%; + min-height: 80px; + padding: 8px 12px; + border: 1px solid var(--color-line, #ddd); + border-radius: var(--radius, 8px); + font-size: 14px; + font-family: var(--font-body); + color: var(--color-ink); + background: var(--surface-1, #fff); + resize: vertical; + transition: border-color 0.15s; +} + +.aksrt-comment-edit-textarea:focus { + outline: none; + border-color: var(--color-primary); +} + +/* ============================================================ + * 评论互动:点赞 / 置顶 / 内联管理动作(统一合同) + * 这些动作均渲染在 .aksrt-comment__actions 内,复用其文字链按钮样式, + * 仅补充点赞激活态、置顶徽章与危险/成功色提示。 + * ============================================================ */ +.aksrt-comment__like.is-active, +.aksrt-comment__like.is-active i { + color: var(--color-primary); +} + +.aksrt-comment__like-count { + font-variant-numeric: tabular-nums; +} + +.aksrt-comment__action--danger:hover { + color: var(--color-error, #ef4444); +} + +.aksrt-comment__action--success:hover { + color: var(--color-success, #10b981); +} + +/* 置顶徽章 */ +.aksrt-comment__badge--pinned { + background: var(--color-warning-soft, rgba(245, 158, 11, 0.14)); + color: var(--color-warning, #d97706); + display: inline-flex; + align-items: center; + gap: 3px; +} + +/* 置顶评论容器高亮 */ +.aksrt-comment--pinned > .aksrt-comment__body { + position: relative; +} + +.comment-list .comment.aksrt-comment--pinned { + background: var(--color-warning-soft, rgba(245, 158, 11, 0.06)); + border-radius: var(--radius, 8px); + padding-left: 12px; + padding-right: 12px; +} + +/* zibll compat: emoji icon forced 30px */ +.comment-content .smilie-icon, +.comment-form .smilie-icon { + width: 30px !important; +} + +/* zibll compat: code block in comments */ +.comment-content .enlighter-default { + margin-bottom: 0; + opacity: 0.9; +} + + +/* Identity chips + 神评 */ +.aksrt-identity-chip { + display: inline-flex; + align-items: center; + gap: 3px; + vertical-align: middle; + max-width: 9em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.aksrt-identity-chip--comment { + font-size: 11px; + padding: 0 6px; +} +.medal-icon.img-icon, +.aksrt-identity-medal { + display: inline-block; + width: 1.35em; + height: 1.35em; + object-fit: contain; + vertical-align: middle; + margin-left: 4px; +} +.aksrt-identity-medal--comment { + width: 1.15em; + height: 1.15em; +} +.aksrt-identity-medal.is-wear { + outline: 1px solid rgba(245, 158, 11, 0.45); + border-radius: 3px; +} +.aksrt-comment__badge--hot { + color: #ea580c; + background: rgba(234, 88, 12, 0.1); + border-radius: 999px; + padding: 0 6px; + margin-left: 4px; + font-size: 11px; +} +.aksrt-comment__header .author-box { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; +} diff --git a/assets/img/smilies/OK.gif b/assets/img/smilies/OK.gif new file mode 100644 index 00000000..89eac0e5 Binary files /dev/null and b/assets/img/smilies/OK.gif differ diff --git a/assets/img/smilies/aini.gif b/assets/img/smilies/aini.gif new file mode 100644 index 00000000..67dd43d7 Binary files /dev/null and b/assets/img/smilies/aini.gif differ diff --git a/assets/img/smilies/aixin.gif b/assets/img/smilies/aixin.gif new file mode 100644 index 00000000..13600b90 Binary files /dev/null and b/assets/img/smilies/aixin.gif differ diff --git a/assets/img/smilies/aoman.gif b/assets/img/smilies/aoman.gif new file mode 100644 index 00000000..0088ed6f Binary files /dev/null and b/assets/img/smilies/aoman.gif differ diff --git a/assets/img/smilies/baiyan.gif b/assets/img/smilies/baiyan.gif new file mode 100644 index 00000000..3aba83f9 Binary files /dev/null and b/assets/img/smilies/baiyan.gif differ diff --git a/assets/img/smilies/bangbangtang.gif b/assets/img/smilies/bangbangtang.gif new file mode 100644 index 00000000..c236e5fc Binary files /dev/null and b/assets/img/smilies/bangbangtang.gif differ diff --git a/assets/img/smilies/baojin.gif b/assets/img/smilies/baojin.gif new file mode 100644 index 00000000..3203d387 Binary files /dev/null and b/assets/img/smilies/baojin.gif differ diff --git a/assets/img/smilies/baoquan.gif b/assets/img/smilies/baoquan.gif new file mode 100644 index 00000000..a4aa25f6 Binary files /dev/null and b/assets/img/smilies/baoquan.gif differ diff --git a/assets/img/smilies/bishi.gif b/assets/img/smilies/bishi.gif new file mode 100644 index 00000000..493cae9f Binary files /dev/null and b/assets/img/smilies/bishi.gif differ diff --git a/assets/img/smilies/bizui.gif b/assets/img/smilies/bizui.gif new file mode 100644 index 00000000..f3dc4dab Binary files /dev/null and b/assets/img/smilies/bizui.gif differ diff --git a/assets/img/smilies/cahan.gif b/assets/img/smilies/cahan.gif new file mode 100644 index 00000000..06450cdb Binary files /dev/null and b/assets/img/smilies/cahan.gif differ diff --git a/assets/img/smilies/caidao.gif b/assets/img/smilies/caidao.gif new file mode 100644 index 00000000..3f94a155 Binary files /dev/null and b/assets/img/smilies/caidao.gif differ diff --git a/assets/img/smilies/chi.gif b/assets/img/smilies/chi.gif new file mode 100644 index 00000000..dfdda693 Binary files /dev/null and b/assets/img/smilies/chi.gif differ diff --git a/assets/img/smilies/ciya.gif b/assets/img/smilies/ciya.gif new file mode 100644 index 00000000..f84ea240 Binary files /dev/null and b/assets/img/smilies/ciya.gif differ diff --git a/assets/img/smilies/dabing.gif b/assets/img/smilies/dabing.gif new file mode 100644 index 00000000..e2f17106 Binary files /dev/null and b/assets/img/smilies/dabing.gif differ diff --git a/assets/img/smilies/daku.gif b/assets/img/smilies/daku.gif new file mode 100644 index 00000000..56427e20 Binary files /dev/null and b/assets/img/smilies/daku.gif differ diff --git a/assets/img/smilies/dan.gif b/assets/img/smilies/dan.gif new file mode 100644 index 00000000..48626b07 Binary files /dev/null and b/assets/img/smilies/dan.gif differ diff --git a/assets/img/smilies/deyi.gif b/assets/img/smilies/deyi.gif new file mode 100644 index 00000000..e9b49980 Binary files /dev/null and b/assets/img/smilies/deyi.gif differ diff --git a/assets/img/smilies/doge.gif b/assets/img/smilies/doge.gif new file mode 100644 index 00000000..3f61e751 Binary files /dev/null and b/assets/img/smilies/doge.gif differ diff --git a/assets/img/smilies/fadai.gif b/assets/img/smilies/fadai.gif new file mode 100644 index 00000000..8fe5a4f5 Binary files /dev/null and b/assets/img/smilies/fadai.gif differ diff --git a/assets/img/smilies/fanu.gif b/assets/img/smilies/fanu.gif new file mode 100644 index 00000000..fc20d67d Binary files /dev/null and b/assets/img/smilies/fanu.gif differ diff --git a/assets/img/smilies/fendou.gif b/assets/img/smilies/fendou.gif new file mode 100644 index 00000000..453c2d2c Binary files /dev/null and b/assets/img/smilies/fendou.gif differ diff --git a/assets/img/smilies/ganga.gif b/assets/img/smilies/ganga.gif new file mode 100644 index 00000000..ed2dac28 Binary files /dev/null and b/assets/img/smilies/ganga.gif differ diff --git a/assets/img/smilies/gouyin.gif b/assets/img/smilies/gouyin.gif new file mode 100644 index 00000000..5beb0989 Binary files /dev/null and b/assets/img/smilies/gouyin.gif differ diff --git a/assets/img/smilies/guzhang.gif b/assets/img/smilies/guzhang.gif new file mode 100644 index 00000000..b955b1c6 Binary files /dev/null and b/assets/img/smilies/guzhang.gif differ diff --git a/assets/img/smilies/haixiu.gif b/assets/img/smilies/haixiu.gif new file mode 100644 index 00000000..18025fc0 Binary files /dev/null and b/assets/img/smilies/haixiu.gif differ diff --git a/assets/img/smilies/hanxiao.gif b/assets/img/smilies/hanxiao.gif new file mode 100644 index 00000000..3cce2295 Binary files /dev/null and b/assets/img/smilies/hanxiao.gif differ diff --git a/assets/img/smilies/haobang.gif b/assets/img/smilies/haobang.gif new file mode 100644 index 00000000..d5c704af Binary files /dev/null and b/assets/img/smilies/haobang.gif differ diff --git a/assets/img/smilies/haqian.gif b/assets/img/smilies/haqian.gif new file mode 100644 index 00000000..5ee910db Binary files /dev/null and b/assets/img/smilies/haqian.gif differ diff --git a/assets/img/smilies/hecai.gif b/assets/img/smilies/hecai.gif new file mode 100644 index 00000000..6e842452 Binary files /dev/null and b/assets/img/smilies/hecai.gif differ diff --git a/assets/img/smilies/hexie.gif b/assets/img/smilies/hexie.gif new file mode 100644 index 00000000..9326d1ac Binary files /dev/null and b/assets/img/smilies/hexie.gif differ diff --git a/assets/img/smilies/huaixiao.gif b/assets/img/smilies/huaixiao.gif new file mode 100644 index 00000000..63e37b76 Binary files /dev/null and b/assets/img/smilies/huaixiao.gif differ diff --git a/assets/img/smilies/jie.gif b/assets/img/smilies/jie.gif new file mode 100644 index 00000000..8177e333 Binary files /dev/null and b/assets/img/smilies/jie.gif differ diff --git a/assets/img/smilies/jingkong.gif b/assets/img/smilies/jingkong.gif new file mode 100644 index 00000000..25a0e2ee Binary files /dev/null and b/assets/img/smilies/jingkong.gif differ diff --git a/assets/img/smilies/jingxi.gif b/assets/img/smilies/jingxi.gif new file mode 100644 index 00000000..500abe85 Binary files /dev/null and b/assets/img/smilies/jingxi.gif differ diff --git a/assets/img/smilies/jingya.gif b/assets/img/smilies/jingya.gif new file mode 100644 index 00000000..280d8488 Binary files /dev/null and b/assets/img/smilies/jingya.gif differ diff --git a/assets/img/smilies/juhua.gif b/assets/img/smilies/juhua.gif new file mode 100644 index 00000000..7dfd66e9 Binary files /dev/null and b/assets/img/smilies/juhua.gif differ diff --git a/assets/img/smilies/keai.gif b/assets/img/smilies/keai.gif new file mode 100644 index 00000000..44441d0a Binary files /dev/null and b/assets/img/smilies/keai.gif differ diff --git a/assets/img/smilies/kelian.gif b/assets/img/smilies/kelian.gif new file mode 100644 index 00000000..107647af Binary files /dev/null and b/assets/img/smilies/kelian.gif differ diff --git a/assets/img/smilies/koubi.gif b/assets/img/smilies/koubi.gif new file mode 100644 index 00000000..e3b4eda8 Binary files /dev/null and b/assets/img/smilies/koubi.gif differ diff --git a/assets/img/smilies/ku.gif b/assets/img/smilies/ku.gif new file mode 100644 index 00000000..05b2d290 Binary files /dev/null and b/assets/img/smilies/ku.gif differ diff --git a/assets/img/smilies/kuaikule.gif b/assets/img/smilies/kuaikule.gif new file mode 100644 index 00000000..627ce215 Binary files /dev/null and b/assets/img/smilies/kuaikule.gif differ diff --git a/assets/img/smilies/kulou.gif b/assets/img/smilies/kulou.gif new file mode 100644 index 00000000..842e8a70 Binary files /dev/null and b/assets/img/smilies/kulou.gif differ diff --git a/assets/img/smilies/kun.gif b/assets/img/smilies/kun.gif new file mode 100644 index 00000000..68c70dfd Binary files /dev/null and b/assets/img/smilies/kun.gif differ diff --git a/assets/img/smilies/lanqiu.gif b/assets/img/smilies/lanqiu.gif new file mode 100644 index 00000000..63268ee0 Binary files /dev/null and b/assets/img/smilies/lanqiu.gif differ diff --git a/assets/img/smilies/leiben.gif b/assets/img/smilies/leiben.gif new file mode 100644 index 00000000..ecd47162 Binary files /dev/null and b/assets/img/smilies/leiben.gif differ diff --git a/assets/img/smilies/lenghan.gif b/assets/img/smilies/lenghan.gif new file mode 100644 index 00000000..f4b7abab Binary files /dev/null and b/assets/img/smilies/lenghan.gif differ diff --git a/assets/img/smilies/liuhan.gif b/assets/img/smilies/liuhan.gif new file mode 100644 index 00000000..da474f1e Binary files /dev/null and b/assets/img/smilies/liuhan.gif differ diff --git a/assets/img/smilies/liulei.gif b/assets/img/smilies/liulei.gif new file mode 100644 index 00000000..b50865b7 Binary files /dev/null and b/assets/img/smilies/liulei.gif differ diff --git a/assets/img/smilies/nanguo.gif b/assets/img/smilies/nanguo.gif new file mode 100644 index 00000000..79e8983b Binary files /dev/null and b/assets/img/smilies/nanguo.gif differ diff --git a/assets/img/smilies/penxue.gif b/assets/img/smilies/penxue.gif new file mode 100644 index 00000000..6f568786 Binary files /dev/null and b/assets/img/smilies/penxue.gif differ diff --git a/assets/img/smilies/piezui.gif b/assets/img/smilies/piezui.gif new file mode 100644 index 00000000..fed13970 Binary files /dev/null and b/assets/img/smilies/piezui.gif differ diff --git a/assets/img/smilies/pijiu.gif b/assets/img/smilies/pijiu.gif new file mode 100644 index 00000000..f8e5a2aa Binary files /dev/null and b/assets/img/smilies/pijiu.gif differ diff --git a/assets/img/smilies/qiang.gif b/assets/img/smilies/qiang.gif new file mode 100644 index 00000000..11471071 Binary files /dev/null and b/assets/img/smilies/qiang.gif differ diff --git a/assets/img/smilies/qiaoda.gif b/assets/img/smilies/qiaoda.gif new file mode 100644 index 00000000..3f52e3c9 Binary files /dev/null and b/assets/img/smilies/qiaoda.gif differ diff --git a/assets/img/smilies/qinqin.gif b/assets/img/smilies/qinqin.gif new file mode 100644 index 00000000..5ee1c516 Binary files /dev/null and b/assets/img/smilies/qinqin.gif differ diff --git a/assets/img/smilies/qiudale.gif b/assets/img/smilies/qiudale.gif new file mode 100644 index 00000000..d809261a Binary files /dev/null and b/assets/img/smilies/qiudale.gif differ diff --git a/assets/img/smilies/quantou.gif b/assets/img/smilies/quantou.gif new file mode 100644 index 00000000..a5ffd4ab Binary files /dev/null and b/assets/img/smilies/quantou.gif differ diff --git a/assets/img/smilies/saorao.gif b/assets/img/smilies/saorao.gif new file mode 100644 index 00000000..a35f8fc5 Binary files /dev/null and b/assets/img/smilies/saorao.gif differ diff --git a/assets/img/smilies/se.gif b/assets/img/smilies/se.gif new file mode 100644 index 00000000..74567e8e Binary files /dev/null and b/assets/img/smilies/se.gif differ diff --git a/assets/img/smilies/shengli.gif b/assets/img/smilies/shengli.gif new file mode 100644 index 00000000..5c9c26ed Binary files /dev/null and b/assets/img/smilies/shengli.gif differ diff --git a/assets/img/smilies/shouqiang.gif b/assets/img/smilies/shouqiang.gif new file mode 100644 index 00000000..db0495ce Binary files /dev/null and b/assets/img/smilies/shouqiang.gif differ diff --git a/assets/img/smilies/shuai.gif b/assets/img/smilies/shuai.gif new file mode 100644 index 00000000..541575dc Binary files /dev/null and b/assets/img/smilies/shuai.gif differ diff --git a/assets/img/smilies/shui.gif b/assets/img/smilies/shui.gif new file mode 100644 index 00000000..452d39ac Binary files /dev/null and b/assets/img/smilies/shui.gif differ diff --git a/assets/img/smilies/tiaopi.gif b/assets/img/smilies/tiaopi.gif new file mode 100644 index 00000000..f57d3f0e Binary files /dev/null and b/assets/img/smilies/tiaopi.gif differ diff --git a/assets/img/smilies/touxiao.gif b/assets/img/smilies/touxiao.gif new file mode 100644 index 00000000..17297802 Binary files /dev/null and b/assets/img/smilies/touxiao.gif differ diff --git a/assets/img/smilies/tu.gif b/assets/img/smilies/tu.gif new file mode 100644 index 00000000..c7f9315b Binary files /dev/null and b/assets/img/smilies/tu.gif differ diff --git a/assets/img/smilies/tuosai.gif b/assets/img/smilies/tuosai.gif new file mode 100644 index 00000000..6a8d2648 Binary files /dev/null and b/assets/img/smilies/tuosai.gif differ diff --git a/assets/img/smilies/weiqu.gif b/assets/img/smilies/weiqu.gif new file mode 100644 index 00000000..c73c5c18 Binary files /dev/null and b/assets/img/smilies/weiqu.gif differ diff --git a/assets/img/smilies/weixiao.gif b/assets/img/smilies/weixiao.gif new file mode 100644 index 00000000..771e0917 Binary files /dev/null and b/assets/img/smilies/weixiao.gif differ diff --git a/assets/img/smilies/woshou.gif b/assets/img/smilies/woshou.gif new file mode 100644 index 00000000..88b8dc95 Binary files /dev/null and b/assets/img/smilies/woshou.gif differ diff --git a/assets/img/smilies/wozuimei.gif b/assets/img/smilies/wozuimei.gif new file mode 100644 index 00000000..0e5b7e5e Binary files /dev/null and b/assets/img/smilies/wozuimei.gif differ diff --git a/assets/img/smilies/wunai.gif b/assets/img/smilies/wunai.gif new file mode 100644 index 00000000..a4be9ed7 Binary files /dev/null and b/assets/img/smilies/wunai.gif differ diff --git a/assets/img/smilies/xia.gif b/assets/img/smilies/xia.gif new file mode 100644 index 00000000..5ececcdb Binary files /dev/null and b/assets/img/smilies/xia.gif differ diff --git a/assets/img/smilies/xiaojiujie.gif b/assets/img/smilies/xiaojiujie.gif new file mode 100644 index 00000000..3d9411cd Binary files /dev/null and b/assets/img/smilies/xiaojiujie.gif differ diff --git a/assets/img/smilies/xiaoku.gif b/assets/img/smilies/xiaoku.gif new file mode 100644 index 00000000..2746fb66 Binary files /dev/null and b/assets/img/smilies/xiaoku.gif differ diff --git a/assets/img/smilies/xiaoyanger.gif b/assets/img/smilies/xiaoyanger.gif new file mode 100644 index 00000000..a8c66406 Binary files /dev/null and b/assets/img/smilies/xiaoyanger.gif differ diff --git a/assets/img/smilies/xieyanxiao.gif b/assets/img/smilies/xieyanxiao.gif new file mode 100644 index 00000000..6512cb5c Binary files /dev/null and b/assets/img/smilies/xieyanxiao.gif differ diff --git a/assets/img/smilies/xigua.gif b/assets/img/smilies/xigua.gif new file mode 100644 index 00000000..f7a2426d Binary files /dev/null and b/assets/img/smilies/xigua.gif differ diff --git a/assets/img/smilies/xu.gif b/assets/img/smilies/xu.gif new file mode 100644 index 00000000..ff5f9166 Binary files /dev/null and b/assets/img/smilies/xu.gif differ diff --git a/assets/img/smilies/yangtuo.gif b/assets/img/smilies/yangtuo.gif new file mode 100644 index 00000000..df47ca29 Binary files /dev/null and b/assets/img/smilies/yangtuo.gif differ diff --git a/assets/img/smilies/yinxian.gif b/assets/img/smilies/yinxian.gif new file mode 100644 index 00000000..4a94f636 Binary files /dev/null and b/assets/img/smilies/yinxian.gif differ diff --git a/assets/img/smilies/yiwen.gif b/assets/img/smilies/yiwen.gif new file mode 100644 index 00000000..6b084202 Binary files /dev/null and b/assets/img/smilies/yiwen.gif differ diff --git a/assets/img/smilies/youhengheng.gif b/assets/img/smilies/youhengheng.gif new file mode 100644 index 00000000..f65ad8a4 Binary files /dev/null and b/assets/img/smilies/youhengheng.gif differ diff --git a/assets/img/smilies/youling.gif b/assets/img/smilies/youling.gif new file mode 100644 index 00000000..3719a11f Binary files /dev/null and b/assets/img/smilies/youling.gif differ diff --git a/assets/img/smilies/yun.gif b/assets/img/smilies/yun.gif new file mode 100644 index 00000000..eb3cb995 Binary files /dev/null and b/assets/img/smilies/yun.gif differ diff --git a/assets/img/smilies/zaijian.gif b/assets/img/smilies/zaijian.gif new file mode 100644 index 00000000..7daa1e40 Binary files /dev/null and b/assets/img/smilies/zaijian.gif differ diff --git a/assets/img/smilies/zhayanjian.gif b/assets/img/smilies/zhayanjian.gif new file mode 100644 index 00000000..771e0917 Binary files /dev/null and b/assets/img/smilies/zhayanjian.gif differ diff --git a/assets/img/smilies/zhemo.gif b/assets/img/smilies/zhemo.gif new file mode 100644 index 00000000..c4d0bf14 Binary files /dev/null and b/assets/img/smilies/zhemo.gif differ diff --git a/assets/img/smilies/zhouma.gif b/assets/img/smilies/zhouma.gif new file mode 100644 index 00000000..47d225b0 Binary files /dev/null and b/assets/img/smilies/zhouma.gif differ diff --git a/assets/img/smilies/zhuakuang.gif b/assets/img/smilies/zhuakuang.gif new file mode 100644 index 00000000..45530613 Binary files /dev/null and b/assets/img/smilies/zhuakuang.gif differ diff --git a/assets/img/smilies/zuohengheng.gif b/assets/img/smilies/zuohengheng.gif new file mode 100644 index 00000000..59d08147 Binary files /dev/null and b/assets/img/smilies/zuohengheng.gif differ diff --git a/assets/js/comment-ajax.js b/assets/js/comment-ajax.js index 7a4a8ac4..9e3d3951 100644 --- a/assets/js/comment-ajax.js +++ b/assets/js/comment-ajax.js @@ -1,23 +1,15 @@ /** * AKSRT Comment Actions client * - * Single client for per-comment actions via REST aksrt/v1: - * like / edit / delete / moderate (approve|hold|spam) / pin + * REST aksrt/v1: like / delete / moderate / pin. + * Edit uses moveForm (#respond) via comment-move-form.js — not inline textarea. + * Submit owned by assets/js/core/comments.js (single submit contract). * - * Comment SUBMISSION is owned by main.js initCommentSystem() (the native - * wp-comments-post.php flow, which runs captcha verification, rate limiting and - * the moderation pipeline, then re-renders the list through the canonical - * server callback). This file deliberately does NOT bind the submit form, so - * there is exactly one comment-submit contract. - * - * Markup contract (rendered by aksrt_comment_callback, identical on first - * paint, load-more and post-submit reload): + * Markup (aksrt_comment_callback): * li.aksrt-comment[data-comment-id] - * .aksrt-comment__text (content) - * .aksrt-comment__actions - * [data-comment-action="like|edit|delete|approve|hold|spam|pin"] - * - * Depends on: aksrtData.restNonce (wp_rest) + aksrtData.siteUrl. + * .comment-header .aksrt-comment__like[data-comment-action=like] + * .comment-content#comment-content-{id} + * .comt-meta [data-comment-action=pin|approve|hold|spam|delete] * * @package AKSRT */ @@ -66,11 +58,10 @@ var commentId = getCommentId(commentEl); if (!commentId) return; + // edit is handled by comment-move-form.js (moveForm into #respond). switch (action) { case 'like': handleLike(actionBtn, commentId); break; - case 'edit': handleEdit(commentEl); break; - case 'save-edit': saveEdit(commentEl, commentId, actionBtn); break; - case 'cancel-edit': cancelEdit(commentEl); break; + case 'edit': break; case 'delete': handleDelete(commentEl, commentId); break; case 'pin': handlePin(commentEl, actionBtn, commentId); break; case 'approve': @@ -104,63 +95,6 @@ .finally(function () { btn.dataset.busy = ''; }); } - function handleEdit(commentEl) { - var contentEl = commentEl.querySelector(CONTENT_SELECTOR); - if (!contentEl || commentEl.querySelector('.aksrt-comment-edit-form')) return; - - var temp = document.createElement('div'); - temp.innerHTML = contentEl.innerHTML; - var originalText = (temp.textContent || temp.innerText || '').trim(); - - var editForm = document.createElement('div'); - editForm.className = 'aksrt-comment-edit-form'; - editForm.innerHTML = - '' + - '
' + - '' + - '' + - '
'; - editForm.querySelector('textarea').value = originalText; - - contentEl.style.display = 'none'; - contentEl.parentNode.insertBefore(editForm, contentEl.nextSibling); - editForm.querySelector('textarea').focus(); - } - - function cancelEdit(commentEl) { - var editForm = commentEl.querySelector('.aksrt-comment-edit-form'); - if (!editForm) return; - var contentEl = commentEl.querySelector(CONTENT_SELECTOR); - if (contentEl) contentEl.style.display = ''; - editForm.remove(); - } - - function saveEdit(commentEl, commentId, saveBtn) { - var editForm = commentEl.querySelector('.aksrt-comment-edit-form'); - if (!editForm) return; - var textarea = editForm.querySelector('.aksrt-comment-edit-textarea'); - var newContent = textarea ? textarea.value.trim() : ''; - if (!newContent) return; - - if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '保存中...'; } - - restPost('/comment/edit', { comment_id: commentId, content: newContent }) - .then(function (result) { - if (result.success) { - var contentEl = commentEl.querySelector(CONTENT_SELECTOR); - if (contentEl) { contentEl.innerHTML = result.data.content; contentEl.style.display = ''; } - editForm.remove(); - } else { - notify((result.data && result.data.message) || result.message || '编辑失败', 'error'); - if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '保存'; } - } - }) - .catch(function () { - notify('网络错误', 'error'); - if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = '保存'; } - }); - } - function handleDelete(commentEl, commentId) { var msg = (window.aksrt && aksrt.__) ? aksrt.__('comment_confirm_delete', '确定要删除这条评论吗?') : '确定要删除这条评论吗?'; var doDelete = function () { diff --git a/assets/js/comment-tools.js b/assets/js/comment-tools.js index 81d774eb..b720efbc 100644 --- a/assets/js/comment-tools.js +++ b/assets/js/comment-tools.js @@ -1,422 +1,141 @@ /** - * AKSRT Comment Tools - * - * Handles comment toolbar actions (emoji, code, bold, italic, quote, link), - * Ctrl+Enter submit, and quick quote from selection. - * - * @package AKSRT + * Comment expand tools — zibll-style shortcode inserts ([g=] [code] [img=] quick). */ - (function () { 'use strict'; - var COMMENT_TOOLS = { - emojiPicker: null, - quickReplyPanel: null, - quickQuoteEnabled: false, - quickReplyTemplates: [ - '感谢分享,这篇内容对我很有帮助!', - '已收藏,后面会按这个思路继续研究。', - '这个细节讲得很清楚,学习了。', - '我这边也复现成功了,补充一点经验:', - '先留个脚印,等我测试后再回来反馈结果。' - ], - - init: function () { - this.bindToolbar(); - this.bindCtrlEnter(); - this.bindQuickQuote(); - }, - - /** - * Insert text at cursor position in textarea - */ - insertAtCursor: function (textarea, before, after) { - if (!textarea) return; - var start = textarea.selectionStart; - var end = textarea.selectionEnd; - var text = textarea.value; - var selected = text.substring(start, end); - var replacement = before + (selected || '') + (after || ''); - textarea.value = text.substring(0, start) + replacement + text.substring(end); - textarea.focus(); - var newPos = start + before.length + selected.length; - textarea.setSelectionRange(newPos, newPos); - }, - - /** - * Wrap selected text with markers - */ - wrapSelection: function (textarea, marker) { - this.insertAtCursor(textarea, marker, marker); - }, - - uploadImage: function (btn, textarea) { - if (!window.aksrtData || !aksrtData.commentUploadEnabled || !aksrtData.commentUploadUrl) { - return; - } - - var input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/jpeg,image/png,image/gif,image/webp'; - input.style.display = 'none'; - document.body.appendChild(input); - - var self = this; - input.addEventListener('change', function () { - var file = input.files && input.files[0]; - if (!file) { - input.remove(); - return; - } - - var maxSize = parseInt(aksrtData.commentUploadMaxSize || 0, 10) || (2 * 1024 * 1024); - if (file.size > maxSize) { - window.alert('图片大小不能超过 2MB'); - input.remove(); - return; - } - - btn.disabled = true; - var originalHtml = btn.innerHTML; - btn.innerHTML = ''; - - var formData = new FormData(); - formData.append('comment_image', file); - - fetch(aksrtData.commentUploadUrl, { - method: 'POST', - body: formData, - credentials: 'same-origin', - headers: { - 'X-WP-Nonce': aksrtData.restNonce || '' - } - }) - .then(function (response) { return response.json(); }) - .then(function (payload) { - if (!payload || payload.success !== true || !payload.url) { - throw new Error((payload && payload.message) || '上传失败'); - } - self.insertAtCursor(textarea, '[img]' + payload.url + '[/img]', ''); - }) - .catch(function (error) { - window.alert(error && error.message ? error.message : '上传失败'); - }) - .finally(function () { - btn.disabled = false; - btn.innerHTML = originalHtml; - input.remove(); - }); - }); - - input.click(); - }, - - /** - * Bind toolbar button actions - */ - bindToolbar: function () { - var self = this; - document.addEventListener('click', function (e) { - var btn = e.target.closest('[data-toolbar-action]'); - if (!btn) return; - - var action = btn.getAttribute('data-toolbar-action'); - var form = btn.closest('.comment-form'); - var textarea = form ? form.querySelector('textarea') : document.getElementById('comment'); - if (!textarea) return; - - switch (action) { - case 'bold': - self.wrapSelection(textarea, '**'); - break; - case 'italic': - self.wrapSelection(textarea, '*'); - break; - case 'code': - var selected = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); - if (selected.indexOf('\n') >= 0) { - self.insertAtCursor(textarea, '```\n', '\n```'); - } else { - self.wrapSelection(textarea, '`'); - } - break; - case 'quote': - self.insertAtCursor(textarea, '> ', '\n'); - break; - case 'link': - self.askUrl(btn, '链接地址', function (url) { - if (url) { - var linkText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd) || '链接文字'; - self.insertAtCursor(textarea, '[' + linkText + '](' + url + ')'); - } - }); - break; - case 'image': - self.askUrl(btn, '图片地址', function (url) { - if (url) { - self.insertAtCursor(textarea, '[img]' + url + '[/img]'); - } - }); - break; - case 'upload-image': - self.uploadImage(btn, textarea); - break; - case 'emoji': - self.toggleEmojiPicker(btn, textarea); - break; - case 'quick-reply': - self.toggleQuickReply(btn, textarea); - break; - } - }); - }, - - /** - * Inline URL input — replaces native prompt() with a small input below the button. - */ - askUrl: function (anchor, placeholder, callback) { - var existing = document.querySelector('.aksrt-inline-url-input'); - if (existing) existing.remove(); - - var input = document.createElement('input'); - input.type = 'url'; - input.className = 'aksrt-inline-url-input'; - input.placeholder = placeholder || 'https://'; - input.value = 'https://'; - input.style.cssText = 'display:block;width:100%;max-width:320px;margin:4px 0;padding:6px 10px;border:1px solid #dce3ec;border-radius:8px;font-size:13px;'; - - var parent = anchor.parentElement; - parent.insertBefore(input, anchor.nextSibling); - input.focus(); - input.select(); - - function done() { - var val = input.value.trim(); - input.remove(); - callback(val); - } - - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { e.preventDefault(); done(); } - if (e.key === 'Escape') { input.remove(); callback(''); } - }); - input.addEventListener('blur', function () { done(); }); - }, - - /** - * Simple emoji picker - */ - toggleEmojiPicker: function (btn, textarea) { - var self = this; - this.removeQuickReplyPanel(); - if (this.emojiPicker) { - this.emojiPicker.remove(); - this.emojiPicker = null; - return; - } - - var emojis = [ - { key: 'grinning', emoji: '😀' }, - { key: 'joy', emoji: '😂' }, - { key: 'rofl', emoji: '🤣' }, - { key: 'blush', emoji: '😊' }, - { key: 'heart-eyes', emoji: '😍' }, - { key: 'smiling-hearts', emoji: '🥰' }, - { key: 'sunglasses', emoji: '😎' }, - { key: 'thinking', emoji: '🤔' }, - { key: 'cry', emoji: '😢' }, - { key: 'sob', emoji: '😭' }, - { key: 'angry', emoji: '😡' }, - { key: 'pleading', emoji: '🥺' }, - { key: 'scream', emoji: '😱' }, - { key: 'rolling-eyes', emoji: '🙄' }, - { key: 'sleeping', emoji: '😴' }, - { key: 'nauseated', emoji: '🤮' }, - { key: 'thumbs-up', emoji: '👍' }, - { key: 'thumbs-down', emoji: '👎' }, - { key: 'clap', emoji: '👏' }, - { key: 'pray', emoji: '🙏' }, - { key: 'muscle', emoji: '💪' }, - { key: 'heart', emoji: '❤️' }, - { key: 'fire', emoji: '🔥' }, - { key: 'star', emoji: '⭐' }, - { key: 'tada', emoji: '🎉' }, - { key: 'confetti', emoji: '🎊' }, - { key: 'check', emoji: '✅' }, - { key: 'cross', emoji: '❌' }, - { key: 'warning', emoji: '⚠️' }, - { key: 'hundred', emoji: '💯' }, - { key: 'ok', emoji: '🆗' }, - { key: 'new', emoji: '🆕' } - ]; - - var picker = document.createElement('div'); - picker.className = 'aksrt-emoji-picker'; - picker.style.cssText = 'position:absolute;bottom:100%;left:0;background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:8px;display:grid;grid-template-columns:repeat(8,1fr);gap:4px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,0.1);'; - - emojis.forEach(function (entry) { - var item = document.createElement('button'); - item.type = 'button'; - item.style.cssText = 'border:none;background:none;cursor:pointer;font-size:18px;padding:4px;border-radius:4px;transition:background 0.15s;'; - item.textContent = entry.emoji; - item.setAttribute('title', entry.key); - item.addEventListener('mouseenter', function () { item.style.background = '#f5f5f5'; }); - item.addEventListener('mouseleave', function () { item.style.background = 'none'; }); - item.addEventListener('click', function (e) { - e.preventDefault(); - self.insertAtCursor(textarea, '[g=' + entry.key + ']', ''); - picker.remove(); - self.emojiPicker = null; - }); - picker.appendChild(item); - }); - - btn.style.position = 'relative'; - btn.appendChild(picker); - this.emojiPicker = picker; - - // Close on outside click - var closeHandler = function (e) { - if (!picker.contains(e.target) && e.target !== btn) { - picker.remove(); - self.emojiPicker = null; - document.removeEventListener('click', closeHandler); - } - }; - setTimeout(function () { document.addEventListener('click', closeHandler); }, 0); - }, - - removeQuickReplyPanel: function () { - if (this.quickReplyPanel) { - this.quickReplyPanel.remove(); - this.quickReplyPanel = null; - } - }, - - toggleQuickReply: function (btn, textarea) { - var self = this; - if (this.quickReplyPanel) { - this.removeQuickReplyPanel(); - return; - } - - if (this.emojiPicker) { - this.emojiPicker.remove(); - this.emojiPicker = null; - } - - var panel = document.createElement('div'); - panel.className = 'comment-quick-reply'; - - var title = document.createElement('div'); - title.className = 'comment-quick-reply__title'; - title.textContent = '快捷回复'; - panel.appendChild(title); - - var templates = document.createElement('div'); - templates.className = 'comment-quick-reply__templates'; - - this.quickReplyTemplates.forEach(function (text) { - var item = document.createElement('button'); - item.type = 'button'; - item.className = 'comment-quick-reply__template'; - item.textContent = text; - item.addEventListener('click', function (e) { - e.preventDefault(); - var needsLineBreak = textarea.value && !/\n\s*$/.test(textarea.value); - var prefix = needsLineBreak ? '\n' : ''; - self.insertAtCursor(textarea, prefix + text, ''); - self.removeQuickReplyPanel(); - }); - templates.appendChild(item); - }); - - panel.appendChild(templates); - - var toolbar = btn.closest('.comment-toolbar'); - if (toolbar && toolbar.parentNode) { - toolbar.parentNode.insertBefore(panel, toolbar.nextSibling); - } else { - btn.parentNode.appendChild(panel); - } - - this.quickReplyPanel = panel; - - var closeHandler = function (e) { - if (!panel.contains(e.target) && e.target !== btn) { - self.removeQuickReplyPanel(); - document.removeEventListener('click', closeHandler); - } - }; - setTimeout(function () { document.addEventListener('click', closeHandler); }, 0); - }, - - /** - * Ctrl+Enter to submit comment - */ - bindCtrlEnter: function () { - document.addEventListener('keydown', function (e) { - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - var textarea = e.target; - if (textarea.tagName !== 'TEXTAREA') return; - var form = textarea.closest('form'); - if (!form) return; - var submitBtn = form.querySelector('[type="submit"]'); - if (submitBtn) { - submitBtn.click(); - } - } - }); - }, - - /** - * Quick quote: select text on page, show "quote in comment" button - */ - bindQuickQuote: function () { - if (!this.quickQuoteEnabled) return; - var self = this; - var quoteBtn = null; - - document.addEventListener('mouseup', function () { - setTimeout(function () { - if (quoteBtn) { - quoteBtn.remove(); - quoteBtn = null; - } - - var selection = window.getSelection(); - var text = selection.toString().trim(); - if (text.length < 5 || text.length > 500) return; - - var range = selection.getRangeAt(0); - var rect = range.getBoundingClientRect(); - - quoteBtn = document.createElement('button'); - quoteBtn.type = 'button'; - quoteBtn.textContent = '引用评论'; - quoteBtn.style.cssText = 'position:fixed;top:' + (rect.top - 36) + 'px;left:' + (rect.left + rect.width / 2) + 'px;transform:translateX(-50%);padding:4px 12px;background:#1890ff;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.15);'; - quoteBtn.addEventListener('click', function () { - var textarea = document.getElementById('comment'); - if (textarea) { - var quoted = text.split('\n').map(function (line) { return '> ' + line; }).join('\n') + '\n\n'; - self.insertAtCursor(textarea, quoted, ''); - textarea.scrollIntoView({ behavior: 'smooth', block: 'center' }); - textarea.focus(); - } - quoteBtn.remove(); - quoteBtn = null; - }); - document.body.appendChild(quoteBtn); - }, 10); - }); - }, - }; - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function () { COMMENT_TOOLS.init(); }); - } else { - COMMENT_TOOLS.init(); + function notify(msg) { + if (window.aksrt && typeof window.aksrt.toast === 'function') { + window.aksrt.toast(msg, 'info'); + } } + + function findTextarea(from) { + var form = from && from.closest ? from.closest('form') : null; + if (form) { + return form.querySelector('textarea.grin, textarea#comment, textarea[name="comment"]'); + } + return document.querySelector('#comment, textarea.grin'); + } + + function insertAtCursor(textarea, value) { + if (!textarea) return; + var start = textarea.selectionStart || 0; + var end = textarea.selectionEnd || 0; + var val = textarea.value || ''; + textarea.value = val.slice(0, start) + value + val.slice(end); + var pos = start + value.length; + textarea.selectionStart = textarea.selectionEnd = pos; + textarea.focus(); + textarea.dispatchEvent(new Event('input')); + } + + function restUpload(file) { + var d = window.aksrtData || {}; + var url = d.commentUploadUrl || ((d.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '') + '/comment/upload-image'); + var fd = new FormData(); + fd.append('image', file); + fd.append('file', file); + return fetch(url, { + method: 'POST', + headers: { 'X-WP-Nonce': d.restNonce || d.nonce || '' }, + credentials: 'same-origin', + body: fd + }).then(function (r) { return r.json(); }); + } + + document.addEventListener('click', function (e) { + var sm = e.target && e.target.closest ? e.target.closest('[data-smilie], a.smilie-icon') : null; + if (sm) { + e.preventDefault(); + var key = sm.getAttribute('data-smilie'); + if (!key && sm.querySelector) { + var img = sm.querySelector('[data-smilie]'); + if (img) key = img.getAttribute('data-smilie'); + } + if (!key) return; + insertAtCursor(findTextarea(sm), '[g=' + key + ']'); + var wrap = sm.closest('.dropup, .input-expand-wrap'); + if (wrap) wrap.classList.remove('open'); + return; + } + + var codeBtn = e.target && e.target.closest ? e.target.closest('.input-code-submit') : null; + if (codeBtn) { + e.preventDefault(); + var box = codeBtn.closest('.dropdown-code, .input-expand-code'); + var ta = box ? box.querySelector('textarea') : null; + var code = ta ? ta.value : ''; + if (!code || code.length < 2) { + notify('请输入代码'); + return; + } + insertAtCursor(findTextarea(codeBtn), '[code]\n' + code + '\n[/code]\n'); + if (ta) ta.value = ''; + var w = codeBtn.closest('.dropup, .input-expand-wrap'); + if (w) w.classList.remove('open'); + return; + } + + var imgBtn = e.target && e.target.closest ? e.target.closest('.input-image-submit') : null; + if (imgBtn) { + e.preventDefault(); + var ibox = imgBtn.closest('.dropdown-image, .input-expand-image'); + var input = ibox ? ibox.querySelector('.input-image-url') : null; + var url = input ? (input.value || '').trim() : ''; + if (!/^https?:\/\//i.test(url)) { + notify('请输入有效图片地址'); + return; + } + insertAtCursor(findTextarea(imgBtn), '[img=' + url + ']'); + if (input) input.value = ''; + var iw = imgBtn.closest('.dropup, .input-expand-wrap'); + if (iw) iw.classList.remove('open'); + return; + } + + var upBtn = e.target && e.target.closest ? e.target.closest('.input-image-upload') : null; + if (upBtn) { + e.preventDefault(); + var fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/jpeg,image/png,image/gif,image/webp'; + fileInput.addEventListener('change', function () { + var file = fileInput.files && fileInput.files[0]; + if (!file) return; + restUpload(file) + .then(function (json) { + var u = (json && (json.url || json.source_url || (json.data && (json.data.url || json.data.source_url)))) || ''; + if (!u) { + notify((json && json.message) || '上传失败'); + return; + } + insertAtCursor(findTextarea(upBtn), '[img=' + u + ']'); + }) + .catch(function () { notify('上传失败'); }); + }); + fileInput.click(); + return; + } + + var q = e.target && e.target.closest ? e.target.closest('.quick-reply-item') : null; + if (q) { + e.preventDefault(); + var phrase = q.getAttribute('data-text') || q.textContent || ''; + insertAtCursor(findTextarea(q), phrase); + var qw = q.closest('.dropup, .input-expand-wrap'); + if (qw) qw.classList.remove('open'); + } + }); + + document.addEventListener('keydown', function (e) { + if (!(e.ctrlKey || e.metaKey) || e.key !== 'Enter') return; + var ta = e.target; + if (!ta || ta.id !== 'comment') return; + var form = ta.closest('#commentform'); + if (!form) return; + e.preventDefault(); + var submit = form.querySelector('#submit, [type="submit"]'); + if (submit) submit.click(); + }); })(); diff --git a/assets/js/core/comment-fixed-input.js b/assets/js/core/comment-fixed-input.js new file mode 100644 index 00000000..28d2238b --- /dev/null +++ b/assets/js/core/comment-fixed-input.js @@ -0,0 +1,90 @@ +/** + * Mobile FixedInput + autoheight for comment respond (zibll parity). + */ +(function () { + 'use strict'; + + function $(sel, root) { + return (root || document).querySelector(sel); + } + + function FixedInput(el, action) { + if (!el) return; + if (!el.dataset.fixedBound) { + el.addEventListener('click', function (e) { + if (e.target && e.target.closest && e.target.closest('.fixed-body')) { + FixedInput(el, 'hide'); + } + }); + el.dataset.fixedBound = '1'; + } + if (action === 'show') { + el.classList.add('show'); + window.setTimeout(function () { + var field = el.querySelector('textarea, input'); + if (field) field.focus(); + }, 100); + return; + } + if (action === 'hide') { + var cancel = el.querySelector('#cancel-comment-reply-link'); + if (cancel) cancel.click(); + el.querySelectorAll('input,textarea').forEach(function (n) { + try { n.blur(); } catch (e) {} + }); + el.querySelectorAll('.dropup.open, .open').forEach(function (n) { + n.classList.remove('open'); + }); + el.classList.remove('show'); + } + } + + document.addEventListener('click', function (e) { + var trigger = e.target && e.target.closest ? e.target.closest('[fixed-input]') : null; + if (!trigger) return; + e.preventDefault(); + var targetSel = trigger.getAttribute('fixed-input') || '#respond'; + var panel = document.querySelector(targetSel); + var comments = document.getElementById('comments'); + if (comments && comments.scrollIntoView) { + comments.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + FixedInput(panel, 'show'); + }); + + // Autoheight textareas. + function bindAutoheight(ta) { + if (!ta || ta.dataset.autoheightBound) return; + ta.dataset.autoheightBound = '1'; + var max = parseInt(ta.getAttribute('maxheight') || '188', 10) || 188; + var resize = function () { + ta.style.height = 'auto'; + var h = Math.min(ta.scrollHeight, max); + ta.style.height = h + 'px'; + ta.style.overflowY = ta.scrollHeight > max ? 'auto' : 'hidden'; + }; + ta.addEventListener('input', resize); + resize(); + } + + function scan() { + document.querySelectorAll('textarea[autoheight], textarea.grin').forEach(bindAutoheight); + } + + document.addEventListener('DOMContentLoaded', scan); + if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') { + window.AksrtFrontend.register('aksrt-comment-fixed-input', function (root) { + scan(); + // Expose for submit hide. + root = root || document; + var respond = root.querySelector ? root.querySelector('#respond') : document.getElementById('respond'); + if (respond) { + respond.aksrtFixedInput = function (action) { + FixedInput(respond, action); + }; + } + }); + } + + window.AksrtFixedInput = FixedInput; +})(); diff --git a/assets/js/core/comment-move-form.js b/assets/js/core/comment-move-form.js new file mode 100644 index 00000000..5293b613 --- /dev/null +++ b/assets/js/core/comment-move-form.js @@ -0,0 +1,243 @@ +/** + * Comment moveForm for reply/edit (zibll parity) — REST-backed edit fetch. + */ +(function () { + 'use strict'; + + var respondId = 'respond'; + var tempId = 'aksrt-wp-temp-form-div'; + var editingId = 0; + + function getRespond() { + return document.getElementById(respondId); + } + + function restRoot() { + var d = window.aksrtData || {}; + return (d.restUrl || d.rest_url || '/wp-json/aksrt/v1').replace(/\/$/, ''); + } + + function nonce() { + var d = window.aksrtData || {}; + return d.restNonce || d.nonce || ''; + } + + function notify(msg, type) { + if (window.aksrt && typeof window.aksrt.toast === 'function') { + window.aksrt.toast(msg, type || 'info'); + return; + } + if (window.notyf) { + try { window.notyf[type === 'error' ? 'error' : 'success'](msg); } catch (e) {} + } + } + + function setActionText(text) { + var el = document.querySelector('#' + respondId + ' .action-text'); + if (!el) return; + if (text) { + el.style.display = ''; + el.textContent = text; + } else { + el.style.display = 'none'; + el.textContent = ''; + } + } + + function ensureParentField(respond) { + var form = respond.querySelector('#commentform') || respond; + var parent = form.querySelector('[name="comment_parent"]'); + if (!parent) { + parent = document.createElement('input'); + parent.type = 'hidden'; + parent.name = 'comment_parent'; + parent.id = 'comment_parent'; + parent.value = '0'; + form.appendChild(parent); + } + return parent; + } + + function ensureEditField(respond, commentId) { + var form = respond.querySelector('#commentform') || respond; + var existing = form.querySelector('[name="edit_comment_ID"]'); + if (commentId) { + if (!existing) { + existing = document.createElement('input'); + existing.type = 'hidden'; + existing.name = 'edit_comment_ID'; + existing.id = 'edit_comment_ID'; + form.appendChild(existing); + } + existing.value = String(commentId); + // Hide parent while editing. + var parent = form.querySelector('[name="comment_parent"]'); + if (parent) parent.name = 'comment_parent_bak'; + } else if (existing) { + existing.remove(); + var bak = form.querySelector('[name="comment_parent_bak"]'); + if (bak) bak.name = 'comment_parent'; + } + } + + function moveForm(commId, parentId, isEdit) { + var respond = getRespond(); + if (!respond) return false; + var comm = document.getElementById(commId); + var parent = ensureParentField(respond); + var cancel = document.getElementById('cancel-comment-reply-link'); + var submit = respond.querySelector('#submit'); + var textarea = respond.querySelector('#comment'); + + if (!document.getElementById(tempId)) { + var temp = document.createElement('div'); + temp.id = tempId; + temp.style.display = 'none'; + respond.parentNode.insertBefore(temp, respond); + } + + if (comm && comm.parentNode) { + comm.parentNode.insertBefore(respond, comm.nextSibling); + } + + parent.value = isEdit ? '0' : String(parentId || '0'); + editingId = isEdit ? parseInt(parentId, 10) || 0 : 0; + + if (cancel) { + cancel.style.display = ''; + cancel.onclick = function () { + var t = document.getElementById(tempId); + var r = getRespond(); + if (t && r && t.parentNode) { + t.parentNode.insertBefore(r, t); + t.parentNode.removeChild(t); + } + ensureEditField(r, 0); + if (parent) parent.value = '0'; + setActionText(''); + if (textarea) textarea.value = ''; + editingId = 0; + cancel.style.display = 'none'; + if (window.AksrtFixedInput) window.AksrtFixedInput(r, 'hide'); + else if (r) r.classList.remove('show'); + return false; + }; + } + + if (isEdit && editingId) { + setActionText('编辑评论'); + ensureEditField(respond, editingId); + if (textarea) { + textarea.disabled = true; + textarea.value = '加载中…'; + } + if (submit) submit.disabled = true; + fetch(restRoot() + '/comment/get?comment_id=' + encodeURIComponent(String(editingId)), { + headers: { 'X-WP-Nonce': nonce() }, + credentials: 'same-origin', + }) + .then(function (r) { return r.json(); }) + .then(function (json) { + var content = (json && (json.content || (json.data && json.data.content))) || ''; + if (textarea) { + textarea.disabled = false; + textarea.value = content; + textarea.dispatchEvent(new Event('input')); + } + if (submit) submit.disabled = false; + }) + .catch(function () { + notify('加载评论失败', 'error'); + if (textarea) { + textarea.disabled = false; + textarea.value = ''; + } + if (submit) submit.disabled = false; + }); + } else { + setActionText('回复评论'); + ensureEditField(respond, 0); + if (textarea) { + textarea.focus(); + } + } + + if (window.AksrtFixedInput) { + window.AksrtFixedInput(respond, 'show'); + } else { + respond.classList.add('show'); + } + + if (comm && comm.scrollIntoView) { + comm.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + return false; + } + + document.addEventListener('click', function (e) { + var reply = e.target && e.target.closest ? e.target.closest('.comment-reply-link') : null; + var edit = e.target && e.target.closest ? e.target.closest('.comment-edit-link, [data-comment-action="edit"]') : null; + if (reply) { + e.preventDefault(); + var cid = reply.getAttribute('data-commentid') || reply.getAttribute('data-comment-id') || ''; + var postid = reply.getAttribute('data-postid') || ''; + if (!cid) { + var li = reply.closest('[data-comment-id], #comment-' + (cid || '')); + if (li && li.getAttribute('data-comment-id')) cid = li.getAttribute('data-comment-id'); + } + if (!cid) { + var m = (reply.closest('li[id^="comment-"]') || {}).id || ''; + var mm = /comment-(\d+)/.exec(m); + if (mm) cid = mm[1]; + } + moveForm('div-comment-' + cid, cid, false); + return; + } + if (edit) { + e.preventDefault(); + var eid = edit.getAttribute('data-commentid') || edit.getAttribute('data-comment-id') || ''; + if (!eid) { + var row = edit.closest('[data-comment-id]'); + if (row) eid = row.getAttribute('data-comment-id'); + } + if (!eid) return; + moveForm('div-comment-' + eid, eid, true); + } + }); + + // Dropup toggles (guest / expand). + document.addEventListener('click', function (e) { + var t = e.target && e.target.closest ? e.target.closest('[data-toggle-class="open"]') : null; + if (!t) return; + var targetSel = t.getAttribute('data-target'); + if (!targetSel) return; + e.preventDefault(); + var wrap = t.closest('.dropup, .input-expand-wrap, #comment-user-info') || t.parentElement; + var panel = targetSel.charAt(0) === '#' || targetSel.charAt(0) === '.' + ? (wrap && wrap.querySelector(targetSel)) || document.querySelector(targetSel) + : null; + if (wrap) wrap.classList.toggle('open'); + if (panel && panel !== wrap) panel.classList.toggle('open'); + }); + + // Close dropups on outside click. + document.addEventListener('click', function (e) { + if (e.target.closest && e.target.closest('.dropup, .input-expand-wrap, [data-toggle-class="open"]')) return; + document.querySelectorAll('.dropup.open, .input-expand-wrap.open, #comment-user-info.open').forEach(function (n) { + n.classList.remove('open'); + }); + }); + + // Scroll helper for @parent. + document.addEventListener('click', function (e) { + var a = e.target && e.target.closest ? e.target.closest('[data-scroll-to]') : null; + if (!a) return; + e.preventDefault(); + var sel = a.getAttribute('data-scroll-to'); + var el = sel ? document.querySelector(sel) : null; + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + + window.aksrtCommentMoveForm = moveForm; + window.aksrtGetEditingCommentId = function () { return editingId; }; +})(); diff --git a/assets/js/core/comments.js b/assets/js/core/comments.js index 79987458..da9f2c20 100644 --- a/assets/js/core/comments.js +++ b/assets/js/core/comments.js @@ -206,11 +206,17 @@ var formData = collectFormData(); var restRoot = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, '') : ''; - var restBase = (restRoot || '') + '/wp-json/aksrt/v1/comment/submit'; + var editId = form.querySelector('[name="edit_comment_ID"]'); + var isEdit = editId && parseInt(editId.value, 10) > 0; + var restBase = (restRoot || '') + '/wp-json/aksrt/v1/comment/' + (isEdit ? 'edit' : 'submit'); // Try REST endpoint for local-insert HTML response var restBody = {}; formData.forEach(function (v, k) { restBody[k] = v; }); + if (isEdit) { + restBody.comment_id = parseInt(editId.value, 10); + restBody.content = restBody.comment || restBody.content || ''; + } fetch(restBase, { method: 'POST', @@ -223,6 +229,21 @@ }) .then(function (r) { return r.json(); }) .then(function (json) { + if (isEdit && json && json.success) { + var cid = restBody.comment_id; + var contentEl = document.querySelector('#comment-content-' + cid + ', [data-comment-id="' + cid + '"] .comment-content'); + var rendered = (json.data && json.data.content) || json.content || ''; + if (contentEl && rendered) contentEl.innerHTML = rendered; + var textarea = form.querySelector('#comment'); + if (textarea) textarea.value = ''; + if (editId) editId.remove(); + restoreRespondPosition(); + setLoading(false); + showToast(json.message || '评论已更新', 'success'); + var respondEl = document.getElementById('respond'); + if (respondEl && window.AksrtFixedInput) window.AksrtFixedInput(respondEl, 'hide'); + return; + } if (json && json.success && json.html) { // Local insert: prepend new comment HTML localInsertComment(json.html, json.is_reply, json.parent_id, json.needs_moderation); @@ -252,6 +273,18 @@ // Move respond back to original position restoreRespondPosition(); + // Hide mobile FixedInput after successful submit. + var respondEl = document.getElementById('respond'); + if (respondEl && window.AksrtFixedInput) { + window.AksrtFixedInput(respondEl, 'hide'); + } else if (respondEl) { + respondEl.classList.remove('show'); + } + + // If editing via moveForm, clear edit field. + var editField = form.querySelector('[name="edit_comment_ID"]'); + if (editField) editField.remove(); + return; } diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 226939ae..42f31107 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -16,6 +16,12 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-icons.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-domain-model.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-template-tags.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-frontend-tags.php'; +// Comment kernel (shared by post / forum / shop surfaces). +require_once AKSRT_DIR . '/inc/helpers/comments/content.php'; +require_once AKSRT_DIR . '/inc/helpers/comments/actions.php'; +require_once AKSRT_DIR . '/inc/helpers/comments/geo.php'; +require_once AKSRT_DIR . '/inc/helpers/comments/form-parts.php'; +require_once AKSRT_DIR . '/inc/helpers/comments/item.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-flow.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-homepage.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-single-components.php'; diff --git a/inc/core-hooks/part-1.php b/inc/core-hooks/part-1.php index 9f848ff1..70d3a44d 100644 --- a/inc/core-hooks/part-1.php +++ b/inc/core-hooks/part-1.php @@ -20,6 +20,12 @@ defined('ABSPATH') || exit; // ============================================================ add_filter('comments_open', function ($open, $post_id) { + if (class_exists('\AKSRT\Settings\Settings')) { + $display = \AKSRT\Settings\Settings::get_comment_display_settings(); + if (!empty($display['close_comments'])) { + return false; + } + } if (!get_option('aksrt_comment_enabled', true)) { return false; } diff --git a/inc/helpers/aksrt-comments.php b/inc/helpers/aksrt-comments.php index 14293e38..2f9bdf1a 100644 --- a/inc/helpers/aksrt-comments.php +++ b/inc/helpers/aksrt-comments.php @@ -94,31 +94,36 @@ if (!function_exists('aksrt_comments_build_context')) : ? $settings['toolbar'] : ['smilie' => true, 'code' => true, 'image' => true]; - return [ - 'settings' => $settings, - 'comment_count' => (int) get_comments_number(), - 'is_logged_in' => is_user_logged_in(), - 'current_user' => wp_get_current_user(), - 'require_login' => !empty($settings['require_login']), - 'order' => $order, - 'reverse_comments' => $order === 'newest', - 'is_hottest' => $order === 'hottest', - 'author_only' => isset($_GET['aksrt_author_only']) && (int) $_GET['aksrt_author_only'] === 1, - 'post_author_id' => (int) get_the_author_meta('ID'), - 'avatar_size' => $avatar_size, - 'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)), - 'allow_reply' => !empty($settings['allow_reply']), - 'empty_text' => (string) ($settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt')), - 'toolbar' => $toolbar, - 'upload_enabled' => !empty($toolbar['upload_img']), - 'box_placeholder' => (string) ($settings['box_placeholder'] ?? __('说点什么吧...', 'aksrt')), - 'submit_text' => (string) ($settings['submit_text'] ?? __('提交评论', 'aksrt')), - 'title_text' => (string) ($settings['title'] ?? __('评论', 'aksrt')), - 'corderby_enabled' => !empty($settings['corderby']), - 'oauth_providers' => class_exists('\AKSRT\Auth\OAuth') - ? \AKSRT\Auth\OAuth::get_instance()->get_auth_providers_data() - : [], - ]; + $phrases = is_array($settings['quick_phrases'] ?? null) ? $settings['quick_phrases'] : []; + + return [ + 'settings' => $settings, + 'comment_count' => (int) get_comments_number(), + 'is_logged_in' => is_user_logged_in(), + 'current_user' => wp_get_current_user(), + 'require_login' => !empty($settings['require_login']), + 'order' => $order, + 'reverse_comments' => $order === 'newest', + 'is_hottest' => $order === 'hottest', + 'author_only' => isset($_GET['aksrt_author_only']) && (int) $_GET['aksrt_author_only'] === 1, + 'post_author_id' => (int) get_the_author_meta('ID'), + 'avatar_size' => $avatar_size, + 'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)), + 'allow_reply' => !empty($settings['allow_reply']), + 'empty_text' => (string) ($settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt')), + 'toolbar' => $toolbar, + 'upload_enabled' => !empty($toolbar['upload_img']), + 'box_placeholder' => (string) ($settings['box_placeholder'] ?? __('说点什么吧...', 'aksrt')), + 'submit_text' => (string) ($settings['submit_text'] ?? __('提交评论', 'aksrt')), + 'title_text' => (string) ($settings['title'] ?? __('评论', 'aksrt')), + 'corderby_enabled' => !empty($settings['corderby']), + 'close_comments' => !empty($settings['close_comments']), + 'quick_s' => !isset($settings['quick_s']) || !empty($settings['quick_s']), + 'quick_phrases' => $phrases, + 'oauth_providers' => class_exists('\AKSRT\Auth\OAuth') + ? \AKSRT\Auth\OAuth::get_instance()->get_auth_providers_data() + : [], + ]; } endif; @@ -440,73 +445,105 @@ if (!function_exists('aksrt_comments_render_form')) : * @param array $ctx Comments context. */ function aksrt_comments_render_form(array $ctx): void { - $is_logged_in = !empty($ctx['is_logged_in']); - $current_user = $ctx['current_user'] ?? null; - $avatar_size = (int) $ctx['avatar_size']; - $toolbar = is_array($ctx['toolbar'] ?? null) ? $ctx['toolbar'] : []; - $upload_enabled = !empty($ctx['upload_enabled']); - ?> -
-
-
-
-
+ $is_logged_in = !empty($ctx['is_logged_in']); + $current_user = $ctx['current_user'] ?? null; + $avatar_size = max(40, (int) $ctx['avatar_size']); + $require_name_email = (bool) get_option('require_name_email'); + $comment_author = isset($_COOKIE['comment_author_' . COOKIEHASH]) + ? sanitize_text_field(wp_unslash((string) $_COOKIE['comment_author_' . COOKIEHASH])) + : ''; + $comment_email = isset($_COOKIE['comment_author_email_' . COOKIEHASH]) + ? sanitize_text_field(wp_unslash((string) $_COOKIE['comment_author_email_' . COOKIEHASH])) + : ''; + $placeholder = (string) $ctx['box_placeholder']; + + if (function_exists('aksrt_get_comment_virtual_input_html')) { + echo aksrt_get_comment_virtual_input_html($placeholder, '#respond'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + ?> +
+
+ +
+
- +
+ ID, $avatar_size, '', '', ['class' => 'avatar-img comt-avatar']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
+

display_name); ?>

-
+
+ 'avatar-img comt-avatar']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
+

+ +

- -
- - -
- -
- -
- - -
- - -
-
+
+ + -
- -
+
+
+
+ + + + +
+
+ + +
+
+ + > + + + + + +
+
-
+ +
+ - - - -
- -
-
-
- - + set_query_var('aksrt_comment_order', (string) $ctx['order']); + set_query_var('aksrt_comment_settings', $ctx['settings']); + + $avatar_size = max(45, (int) $ctx['avatar_size']); + $count = (int) $ctx['comment_count']; + $count_label = $count > 0 + ? sprintf(__('共%s条', 'aksrt'), (string) $count) + : __('抢沙发', 'aksrt'); + ?> +
+
+
+ + +
+
+ +
+ + + + + + + + + 0 || !empty($ctx['author_only']) || have_comments()); + if ($show_filter) { + aksrt_comments_render_filter($ctx); + } + ?> + + + + + + + + + +

+
- - - - - - - - - 0 || !empty($ctx['author_only']) || have_comments()); - if ($show_filter) { - aksrt_comments_render_filter($ctx); - } - ?> + } + endif; - - - - - - - -

- -
- - */ - function aksrt_comment_smilie_map(): array { - return [ - 'grinning' => '😀', - 'joy' => '😂', - 'rofl' => '🤣', - 'blush' => '😊', - 'heart-eyes' => '😍', - 'smiling-hearts' => '🥰', - 'sunglasses' => '😎', - 'thinking' => '🤔', - 'cry' => '😢', - 'sob' => '😭', - 'angry' => '😡', - 'pleading' => '🥺', - 'scream' => '😱', - 'rolling-eyes' => '🙄', - 'sleeping' => '😴', - 'nauseated' => '🤮', - 'thumbs-up' => '👍', - 'thumbs-down' => '👎', - 'clap' => '👏', - 'pray' => '🙏', - 'muscle' => '💪', - 'heart' => '❤️', - 'fire' => '🔥', - 'star' => '⭐', - 'tada' => '🎉', - 'confetti' => '🎊', - 'check' => '✅', - 'cross' => '❌', - 'warning' => '⚠️', - 'hundred' => '💯', - 'ok' => '🆗', - 'new' => '🆕', - ]; - } -endif; -if (!function_exists('aksrt_render_comment_smilie')) : - /** - * 渲染单个 grin 表情为 smilie-icon 图片。 - * - * @param string $key grin key. - * @return string - */ - function aksrt_render_comment_smilie(string $key): string { - $map = aksrt_comment_smilie_map(); - if (!isset($map[$key])) { - return '[g=' . $key . ']'; - } - $emoji = $map[$key]; - $svg = '' . esc_html($emoji) . ''; - $src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg); - return sprintf( - '%2$s', - esc_url($src), - esc_attr($emoji), - esc_attr($key) - ); - } -endif; -if (!function_exists('aksrt_render_comment_content')) : - /** - * 将评论中的 grin token 渲染为 smilie-icon,并按 post 白名单输出。 - * - * @param string $content Comment HTML/text. - * @return string - */ - function aksrt_render_comment_content(string $content): string { - $rendered = preg_replace_callback('/\[g=([a-z0-9-]+)\]/i', static function (array $matches): string { - return aksrt_render_comment_smilie(sanitize_key($matches[1])); - }, $content); - $allowed = wp_kses_allowed_html('post'); - if (!isset($allowed['img'])) { - $allowed['img'] = []; - } - $allowed['img'] = array_merge($allowed['img'], [ - 'class' => true, - 'src' => true, - 'alt' => true, - 'title' => true, - 'width' => true, - 'height' => true, - 'loading' => true, - 'decoding' => true, - 'data-smilie-key' => true, - ]); - return wp_kses((string) $rendered, $allowed); - } -endif; if (!function_exists('aksrt_reading_minutes')) : /** * 估算文章阅读分钟数 @@ -822,329 +727,3 @@ add_action('wp_ajax_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); add_action('wp_ajax_nopriv_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); add_action('wp_ajax_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); add_action('wp_ajax_nopriv_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); -// Float moderator-pinned top-level comments to the front of the first-page list. -// Runs during comments_template() before comments.php is loaded. -add_filter('comments_array', 'aksrt_reorder_pinned_comments', 20, 2); -if (!function_exists('aksrt_comment_action_permissions')) : - /** - * Single source of truth for per-comment action permissions. - * - * Both the renderer (button visibility) and the REST endpoints - * (server-side enforcement) must read from this contract so the UI never - * advertises an action the backend would reject. - * - * @param WP_Comment|int $comment Comment object or ID. - * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool} - */ - function aksrt_comment_action_permissions($comment): array { - $comment = $comment instanceof WP_Comment ? $comment : get_comment($comment); - $none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; - if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { - return $none; - } - $uid = get_current_user_id(); - $is_wp_mod = current_user_can('moderate_comments') || current_user_can('manage_options'); - $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === $uid; - $post_id = (int) $comment->comment_post_ID; - $post = $post_id > 0 ? get_post($post_id) : null; - $is_post_author = $post instanceof WP_Post && (int) $post->post_author === $uid; - $ctx = $post_id > 0 ? $post_id : null; - - $can = static function (string $cap) use ($uid, $ctx): bool { - if (function_exists('aksrt_user_can')) { - return aksrt_user_can($cap, $uid, $ctx); - } - if (class_exists('\AKSRT\Settings\Capabilities')) { - return \AKSRT\Settings\Capabilities::user_can($cap, $uid, $ctx); - } - return false; - }; - - $edit_window = 10; - if (class_exists('\AKSRT\Settings\Settings')) { - $display = \AKSRT\Settings\Settings::get_comment_display_settings(); - $edit_window = (int) ($display['edit_window'] ?? 10); - } - $within_window = $edit_window > 0 - && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); - - $can_edit_own = $is_owner && $within_window && $can('comment_edit'); - $can_edit_others = (!$is_owner) && ( - ($is_post_author && $can('comment_edit_my_post')) - || $can('comment_edit_other') - ); - $can_delete_own = $is_owner && $can('comment_delete'); - $can_delete_others = (!$is_owner) && ( - ($is_post_author && $can('comment_delete_my_post')) - || $can('comment_delete_other') - ); - $can_pin = (int) $comment->comment_parent === 0 && ( - ($is_post_author && $can('comment_set_topping_my_post')) - || $can('comment_set_topping_other') - ); - - return [ - 'can_edit' => $is_wp_mod || $can_edit_own || $can_edit_others, - 'can_delete' => $is_wp_mod || $can_delete_own || $can_delete_others, - 'can_moderate' => $is_wp_mod || $can('comment_audit_other') || ($is_post_author && $can('comment_audit_my_post')), - 'can_pin' => $is_wp_mod || $can_pin, - ]; - } -endif; -if (!function_exists('aksrt_is_comment_pinned')) : - /** - * Whether a comment is pinned ("置顶") by a moderator. - * - * @param int|WP_Comment $comment Comment object or ID. - * @return bool - */ - function aksrt_is_comment_pinned($comment): bool { - $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; - if ($comment_id <= 0) { - return false; - } - return '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true); - } -endif; -if (!function_exists('aksrt_update_comment_counter')) : - /** - * Atomically bump a numeric comment-meta counter, floored at zero. - * - * @param int $comment_id Comment ID. - * @param string $meta_key Meta key. - * @param int $delta Signed delta. - * @return int Latest value (>= 0). - */ - function aksrt_update_comment_counter($comment_id, $meta_key, $delta) { - global $wpdb; - $comment_id = (int) $comment_id; - $delta = (int) $delta; - $exists = (int) $wpdb->get_var($wpdb->prepare( - "SELECT 1 FROM {$wpdb->commentmeta} WHERE comment_id = %d AND meta_key = %s LIMIT 1", - $comment_id, - $meta_key - )); - if ($exists) { - $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->commentmeta} - SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) - WHERE comment_id = %d AND meta_key = %s", - $delta, - $comment_id, - $meta_key - )); - } else { - add_comment_meta($comment_id, $meta_key, max(0, $delta), true); - } - wp_cache_delete($comment_id, 'comment_meta'); - return (int) get_comment_meta($comment_id, $meta_key, true); - } -endif; -if (!function_exists('aksrt_get_comment_likes')) : - /** - * Comment like count (shared by renderer, "hottest" sort and REST). - * - * @param int|WP_Comment $comment Comment object or ID. - * @return int - */ - function aksrt_get_comment_likes($comment): int { - $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; - if ($comment_id <= 0) { - return 0; - } - return max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); - } -endif; -if (!function_exists('aksrt_is_comment_liked')) : - /** - * Whether the current visitor already liked a comment. - * - * @param int $comment_id Comment ID. - * @return bool - */ - function aksrt_is_comment_liked($comment_id): bool { - $comment_id = (int) $comment_id; - if ($comment_id <= 0) { - return false; - } - if (is_user_logged_in()) { - $liked = get_user_meta(get_current_user_id(), '_aksrt_liked_comments', true); - $liked = is_array($liked) ? array_map('intval', $liked) : []; - return in_array($comment_id, $liked, true); - } - $cookie = 'aksrt_clike_' . $comment_id; - return !empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; - } -endif; -if (!function_exists('aksrt_set_comment_like_state')) : - /** - * Persist a visitor's like state for a comment. - * - * @param int $comment_id Comment ID. - * @param bool $active Target state. - */ - function aksrt_set_comment_like_state($comment_id, $active): void { - $comment_id = (int) $comment_id; - if ($comment_id <= 0) { - return; - } - if (is_user_logged_in()) { - $user_id = get_current_user_id(); - $liked = get_user_meta($user_id, '_aksrt_liked_comments', true); - $liked = is_array($liked) ? array_map('intval', $liked) : []; - if ($active) { - $liked[] = $comment_id; - } else { - $liked = array_filter($liked, static function ($id) use ($comment_id) { - return (int) $id !== $comment_id; - }); - } - update_user_meta($user_id, '_aksrt_liked_comments', array_values(array_unique($liked))); - return; - } - $cookie = 'aksrt_clike_' . $comment_id; - $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; - $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; - setcookie($cookie, $active ? '1' : '', $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS, $path, $domain, is_ssl(), true); - if ($active) { - $_COOKIE[$cookie] = '1'; - } else { - unset($_COOKIE[$cookie]); - } - } -endif; -if (!function_exists('aksrt_reorder_pinned_comments')) : - /** - * Float pinned top-level comments to the front of the queried list. - * - * Attached to `comments_array` on singular views so the first-page render - * and the canonical renderer agree on pin ordering. Nested replies keep - * their parent ordering; only top-level pins are reordered. - * - * @param array $comments Comment objects. - * @param int $post_id Post ID. - * @return array - */ - function aksrt_reorder_pinned_comments($comments, $post_id) { - if (!is_array($comments) || count($comments) < 2) { - return $comments; - } - $pinned = []; - $normal = []; - foreach ($comments as $comment) { - if ($comment instanceof WP_Comment && (int) $comment->comment_parent === 0 && aksrt_is_comment_pinned($comment)) { - $pinned[] = $comment; - } else { - $normal[] = $comment; - } - } - if (empty($pinned)) { - return $comments; - } - return array_merge($pinned, $normal); - } -endif; -if (!function_exists('aksrt_comment_callback')) : - /** - * Canonical comment list item renderer. - * - * This is the single contract used by first-page rendering (comments.php), - * AJAX pagination (\AKSRT\Admin\Admin::ajax_load_more_comments) and the post-submit - * list reload (main.js reloadCommentList(), which refetches the rendered - * page). Emits a stable `data-comment-id` plus reply, like, management - * (edit/delete/moderate) and pin controls gated through - * aksrt_comment_action_permissions(). - * - * @param WP_Comment $comment 评论对象 - * @param array $args 参数 - * @param int $depth 嵌套深度 - */ - function aksrt_comment_callback($comment, $args, $depth) { - $GLOBALS['comment'] = $comment; - $settings = get_query_var('aksrt_comment_settings', []); - $settings = is_array($settings) ? $settings : []; - $comment_id = (int) $comment->comment_ID; - $depth_class = 'aksrt-depth-' . min((int) ($settings['nested_depth'] ?? 3), (int) $depth); - $author_name = get_comment_author($comment); - $comment_date = get_comment_date('c', $comment); - $comment_label = get_comment_date('Y-m-d', $comment); - $avatar_size = max(24, (int) ($settings['avatar_size'] ?? 48)); - $allow_reply = !empty($settings['allow_reply']); - $show_author_tag = !isset($settings['author_tag']) || !empty($settings['author_tag']); - $like_enabled = !isset($settings['like_enabled']) || !empty($settings['like_enabled']); - $is_post_author = (int) $comment->user_id > 0 && (int) $comment->user_id === (int) get_post_field('post_author', $comment->comment_post_ID); - $is_pinned = aksrt_is_comment_pinned($comment); - $perms = aksrt_comment_action_permissions($comment); - $is_pending = '0' === $comment->comment_approved; - $is_spam = 'spam' === $comment->comment_approved; - $like_count = aksrt_get_comment_likes($comment); - $is_liked = aksrt_is_comment_liked($comment_id); - $li_classes = trim($depth_class . ($is_pinned ? ' aksrt-comment--pinned' : '')); - ?> -
  • id="comment-" data-comment-id="" data-comment-status="comment_approved); ?>"> -
      -
    • -
      -
      - 'avatar-img comt-avatar']); ?> - - user_id; - if ($comment_user_id > 0 && function_exists('aksrt_render_user_identity_chips')) { - echo aksrt_render_user_identity_chips($comment_user_id, 'comment'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - ?> - - - - - - - - - -
      - -
      -
      - -

      - - - -
      - -
    • -
    -
  • - false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; + if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { + return $none; + } + $uid = get_current_user_id(); + $is_wp_mod = current_user_can('moderate_comments') || current_user_can('manage_options'); + $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === $uid; + $post_id = (int) $comment->comment_post_ID; + $post = $post_id > 0 ? get_post($post_id) : null; + $is_post_author = $post instanceof WP_Post && (int) $post->post_author === $uid; + $ctx = $post_id > 0 ? $post_id : null; + + $can = static function (string $cap) use ($uid, $ctx): bool { + if (function_exists('aksrt_user_can')) { + return aksrt_user_can($cap, $uid, $ctx); + } + if (class_exists('\AKSRT\Settings\Capabilities')) { + return \AKSRT\Settings\Capabilities::user_can($cap, $uid, $ctx); + } + return false; + }; + + $edit_window = 10; + if (class_exists('\AKSRT\Settings\Settings')) { + $display = \AKSRT\Settings\Settings::get_comment_display_settings(); + $edit_window = (int) ($display['edit_window'] ?? 10); + } + $within_window = $edit_window > 0 + && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); + + $can_edit_own = $is_owner && $within_window && $can('comment_edit'); + $can_edit_others = (!$is_owner) && ( + ($is_post_author && $can('comment_edit_my_post')) + || $can('comment_edit_other') + ); + $can_delete_own = $is_owner && $can('comment_delete'); + $can_delete_others = (!$is_owner) && ( + ($is_post_author && $can('comment_delete_my_post')) + || $can('comment_delete_other') + ); + $can_pin = (int) $comment->comment_parent === 0 && ( + ($is_post_author && $can('comment_set_topping_my_post')) + || $can('comment_set_topping_other') + ); + + return [ + 'can_edit' => $is_wp_mod || $can_edit_own || $can_edit_others, + 'can_delete' => $is_wp_mod || $can_delete_own || $can_delete_others, + 'can_moderate' => $is_wp_mod || $can('comment_audit_other') || ($is_post_author && $can('comment_audit_my_post')), + 'can_pin' => $is_wp_mod || $can_pin, + ]; + } +endif; + +if (!function_exists('aksrt_is_comment_pinned')) : + /** + * @param int|WP_Comment $comment Comment. + */ + function aksrt_is_comment_pinned($comment): bool { + $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; + if ($comment_id <= 0) { + return false; + } + return '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true); + } +endif; + +if (!function_exists('aksrt_update_comment_counter')) : + /** + * Atomically bump a numeric comment-meta counter, floored at zero. + * + * @param int $comment_id Comment ID. + * @param string $meta_key Meta key. + * @param int $delta Signed delta. + * @return int Latest value (>= 0). + */ + function aksrt_update_comment_counter($comment_id, $meta_key, $delta) { + global $wpdb; + $comment_id = (int) $comment_id; + $delta = (int) $delta; + $exists = (int) $wpdb->get_var($wpdb->prepare( + "SELECT 1 FROM {$wpdb->commentmeta} WHERE comment_id = %d AND meta_key = %s LIMIT 1", + $comment_id, + $meta_key + )); + if ($exists) { + $wpdb->query($wpdb->prepare( + "UPDATE {$wpdb->commentmeta} + SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) + WHERE comment_id = %d AND meta_key = %s", + $delta, + $comment_id, + $meta_key + )); + } else { + add_comment_meta($comment_id, $meta_key, max(0, $delta), true); + } + wp_cache_delete($comment_id, 'comment_meta'); + return (int) get_comment_meta($comment_id, $meta_key, true); + } +endif; + +if (!function_exists('aksrt_get_comment_likes')) : + /** + * @param int|WP_Comment $comment Comment. + */ + function aksrt_get_comment_likes($comment): int { + $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; + if ($comment_id <= 0) { + return 0; + } + return max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); + } +endif; + +if (!function_exists('aksrt_is_comment_liked')) : + /** + * @param int $comment_id Comment ID. + */ + function aksrt_is_comment_liked($comment_id): bool { + $comment_id = (int) $comment_id; + if ($comment_id <= 0) { + return false; + } + if (is_user_logged_in()) { + $liked = get_user_meta(get_current_user_id(), '_aksrt_liked_comments', true); + $liked = is_array($liked) ? array_map('intval', $liked) : []; + return in_array($comment_id, $liked, true); + } + $cookie = 'aksrt_clike_' . $comment_id; + return !empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; + } +endif; + +if (!function_exists('aksrt_set_comment_like_state')) : + /** + * @param int $comment_id Comment ID. + * @param bool $active Target state. + */ + function aksrt_set_comment_like_state($comment_id, $active): void { + $comment_id = (int) $comment_id; + if ($comment_id <= 0) { + return; + } + if (is_user_logged_in()) { + $user_id = get_current_user_id(); + $liked = get_user_meta($user_id, '_aksrt_liked_comments', true); + $liked = is_array($liked) ? array_map('intval', $liked) : []; + if ($active) { + $liked[] = $comment_id; + } else { + $liked = array_filter($liked, static function ($id) use ($comment_id) { + return (int) $id !== $comment_id; + }); + } + update_user_meta($user_id, '_aksrt_liked_comments', array_values(array_unique($liked))); + return; + } + $cookie = 'aksrt_clike_' . $comment_id; + $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + setcookie($cookie, $active ? '1' : '', $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS, $path, $domain, is_ssl(), true); + if ($active) { + $_COOKIE[$cookie] = '1'; + } else { + unset($_COOKIE[$cookie]); + } + } +endif; + +if (!function_exists('aksrt_reorder_pinned_comments')) : + /** + * Float pinned top-level comments to the front of the queried list. + * + * @param array $comments Comment objects. + * @param int $post_id Post ID. + * @return array + */ + function aksrt_reorder_pinned_comments($comments, $post_id) { + if (!is_array($comments) || count($comments) < 2) { + return $comments; + } + $pinned = []; + $normal = []; + foreach ($comments as $comment) { + if ($comment instanceof WP_Comment && (int) $comment->comment_parent === 0 && aksrt_is_comment_pinned($comment)) { + $pinned[] = $comment; + } else { + $normal[] = $comment; + } + } + if (empty($pinned)) { + return $comments; + } + return array_merge($pinned, $normal); + } +endif; + +add_filter('comments_array', 'aksrt_reorder_pinned_comments', 20, 2); diff --git a/inc/helpers/comments/content.php b/inc/helpers/comments/content.php new file mode 100644 index 00000000..e54a7aae --- /dev/null +++ b/inc/helpers/comments/content.php @@ -0,0 +1,147 @@ + + */ + function aksrt_comment_smilie_keys(): array { + return [ + 'aoman', 'baiyan', 'bishi', 'bizui', 'cahan', 'ciya', 'dabing', 'daku', 'deyi', 'doge', + 'fadai', 'fanu', 'fendou', 'ganga', 'guzhang', 'haixiu', 'hanxiao', 'zuohengheng', 'zhuakuang', + 'zhouma', 'zhemo', 'zhayanjian', 'zaijian', 'yun', 'youhengheng', 'yiwen', 'yinxian', 'xu', + 'xieyanxiao', 'xiaoku', 'xiaojiujie', 'xia', 'wunai', 'wozuimei', 'weixiao', 'weiqu', 'tuosai', + 'tu', 'touxiao', 'tiaopi', 'shui', 'se', 'saorao', 'qiudale', 'qinqin', 'qiaoda', 'piezui', + 'penxue', 'nanguo', 'liulei', 'liuhan', 'lenghan', 'leiben', 'kun', 'kuaikule', 'ku', 'koubi', + 'kelian', 'keai', 'jingya', 'jingxi', 'jingkong', 'jie', 'huaixiao', 'haqian', 'aini', 'OK', + 'qiang', 'quantou', 'shengli', 'woshou', 'gouyin', 'baoquan', 'aixin', 'bangbangtang', + 'xiaoyanger', 'xigua', 'hexie', 'pijiu', 'lanqiu', 'juhua', 'hecai', 'haobang', 'caidao', + 'baojin', 'chi', 'dan', 'kulou', 'shuai', 'shouqiang', 'yangtuo', 'youling', + ]; + } +endif; + +if (!function_exists('aksrt_comment_smilie_map')) : + /** + * Smilie key => URL map (GIF under assets/img/smilies). + * + * @return array + */ + function aksrt_comment_smilie_map(): array { + static $map = null; + if (is_array($map)) { + return $map; + } + $base = trailingslashit(get_template_directory_uri()) . 'assets/img/smilies/'; + $map = []; + foreach (aksrt_comment_smilie_keys() as $key) { + $map[$key] = $base . rawurlencode($key) . '.gif'; + } + return $map; + } +endif; + +if (!function_exists('aksrt_comment_smilies_base_uri')) : + /** + * Base URI for smilie GIFs (localized to JS). + */ + function aksrt_comment_smilies_base_uri(): string { + return trailingslashit(get_template_directory_uri()) . 'assets/img/smilies/'; + } +endif; + +if (!function_exists('aksrt_render_comment_smilie')) : + /** + * Render a single [g=key] smilie as . + */ + function aksrt_render_comment_smilie(string $key): string { + // Preserve case for OK.gif; sanitize other keys. + $raw = trim($key); + if ($raw === '') { + return ''; + } + $lookup = $raw === 'OK' ? 'OK' : sanitize_key($raw); + // Allow original casing match for files like OK.gif. + $map = aksrt_comment_smilie_map(); + if (!isset($map[$lookup]) && isset($map[$raw])) { + $lookup = $raw; + } + if (!isset($map[$lookup])) { + // Fallback: try case-insensitive. + foreach ($map as $k => $url) { + if (strcasecmp((string) $k, $raw) === 0) { + $lookup = (string) $k; + break; + } + } + } + if (!isset($map[$lookup])) { + return '[g=' . esc_html($raw) . ']'; + } + return sprintf( + '%2$s', + esc_url($map[$lookup]), + esc_attr('[' . $lookup . ']'), + esc_attr($lookup) + ); + } +endif; + +if (!function_exists('aksrt_render_comment_content')) : + /** + * Filter comment body: smilies, images, code blocks (zibll parity). + */ + function aksrt_render_comment_content(string $content): string { + // [img=url] + $rendered = preg_replace( + '/\[img=(https?:\/\/[^\s\[\]]+)\]/i', + '' . esc_attr__('评论图片', 'aksrt') . '', + $content + ); + // [code]...[/code] + $rendered = str_replace( + ['[code]', '[/code]'], + ['
    ', '
    '], + (string) $rendered + ); + // [g=key] + $rendered = preg_replace_callback( + '/\[g=([^\]]+)\]/i', + static function (array $matches): string { + return aksrt_render_comment_smilie(trim((string) $matches[1])); + }, + (string) $rendered + ); + + $allowed = wp_kses_allowed_html('post'); + if (!isset($allowed['img'])) { + $allowed['img'] = []; + } + $allowed['img'] = array_merge($allowed['img'], [ + 'class' => true, + 'src' => true, + 'alt' => true, + 'title' => true, + 'width' => true, + 'height' => true, + 'loading' => true, + 'decoding' => true, + 'data-smilie' => true, + 'data-src' => true, + ]); + $allowed['pre'] = array_merge($allowed['pre'] ?? [], ['class' => true]); + $allowed['code'] = $allowed['code'] ?? []; + + return wp_kses((string) $rendered, $allowed); + } +endif; diff --git a/inc/helpers/comments/form-parts.php b/inc/helpers/comments/form-parts.php new file mode 100644 index 00000000..eab7c895 --- /dev/null +++ b/inc/helpers/comments/form-parts.php @@ -0,0 +1,130 @@ + 'avatar-img']); + if ($placeholder === '') { + $placeholder = __('说点什么吧...', 'aksrt'); + } + $html = '
    '; + $html .= '
    '; + $html .= $avatar; + $html .= '
    ' . esc_html($placeholder) . '
    '; + $html .= '
    '; + $html .= '' . esc_html__('提交', 'aksrt') . ''; + $html .= '
    '; + return $html; + } +endif; + +if (!function_exists('aksrt_get_input_expand_but')) : + /** + * zibll-style expand toolbar button + dropdown. + * + * @param string $type smilie|code|image|quick|upload. + * @param mixed $arg1 Type-specific. + * @param array $ctx Optional context (upload enabled, phrases). + */ + function aksrt_get_input_expand_but(string $type = 'smilie', $arg1 = null, array $ctx = []): string { + switch ($type) { + case 'smilie': + $map = function_exists('aksrt_comment_smilie_map') ? aksrt_comment_smilie_map() : []; + $icons = ''; + foreach ($map as $key => $url) { + $icons .= '[' . esc_attr((string) $key) . ']'; + } + $but = '' . esc_html__('表情', 'aksrt') . ''; + $drop = ''; + return '' . $but . $drop . ''; + + case 'code': + $but = '' . esc_html__('代码', 'aksrt') . ''; + $drop = ''; + return '' . $but . $drop . ''; + + case 'image': + $upload = !empty($arg1) || !empty($ctx['upload']); + $but = '' . esc_html__('图片', 'aksrt') . ''; + $upload_btn = $upload + ? '' + : ''; + $drop = ''; + return '' . $but . $drop . ''; + + case 'quick': + $phrases = []; + if (is_string($arg1) && $arg1 !== '') { + $phrases = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $arg1) ?: []))); + } elseif (!empty($ctx['quick_phrases']) && is_array($ctx['quick_phrases'])) { + $phrases = $ctx['quick_phrases']; + } + if (empty($phrases)) { + $phrases = [ + __('不错,学习了', 'aksrt'), + __('感谢分享', 'aksrt'), + __('期待更新', 'aksrt'), + ]; + } + $items = ''; + foreach ($phrases as $p) { + $items .= '' . esc_html((string) $p) . ''; + } + $but = '' . esc_html__('快捷', 'aksrt') . ''; + $drop = ''; + return '' . $but . $drop . ''; + + default: + return ''; + } + } +endif; + +if (!function_exists('aksrt_render_comment_expand_toolbar')) : + /** + * Render left-side expand tools based on settings. + * + * @param array $ctx Comments context. + */ + function aksrt_render_comment_expand_toolbar(array $ctx): void { + $toolbar = is_array($ctx['toolbar'] ?? null) ? $ctx['toolbar'] : []; + $upload = !empty($ctx['upload_enabled']); + $phrases = is_array($ctx['quick_phrases'] ?? null) ? $ctx['quick_phrases'] : []; + $quick_s = !isset($ctx['quick_s']) || !empty($ctx['quick_s']); + + if (!empty($toolbar['smilie'])) { + echo aksrt_get_input_expand_but('smilie'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + if (!empty($toolbar['code'])) { + echo aksrt_get_input_expand_but('code'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + if (!empty($toolbar['image'])) { + echo aksrt_get_input_expand_but('image', $upload, ['upload' => $upload]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + if ($quick_s) { + echo aksrt_get_input_expand_but('quick', null, ['quick_phrases' => $phrases]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } +endif; diff --git a/inc/helpers/comments/geo.php b/inc/helpers/comments/geo.php new file mode 100644 index 00000000..fbc5c12e --- /dev/null +++ b/inc/helpers/comments/geo.php @@ -0,0 +1,142 @@ +comment_author_IP : ''; + } + $ip = trim((string) $ip); + if ($ip === '' || !class_exists('\AKSRT\Extensions\IPGeo')) { + return; + } + $geo = \AKSRT\Extensions\IPGeo::get_instance()->lookup($ip); + if (!is_array($geo)) { + return; + } + // Store compact array. + $addr = [ + 'country' => (string) ($geo['country'] ?? ''), + 'province' => (string) ($geo['province'] ?? ''), + 'city' => (string) ($geo['city'] ?? ''), + ]; + if ($addr['country'] === '' && $addr['province'] === '' && $addr['city'] === '') { + return; + } + update_comment_meta($comment_id, 'aksrt_comment_addr', $addr); + } +endif; + +if (!function_exists('aksrt_get_comment_addr')) : + /** + * @param int|WP_Comment $comment Comment. + * @return array{country?:string,province?:string,city?:string} + */ + function aksrt_get_comment_addr($comment): array { + $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; + if ($comment_id <= 0) { + return []; + } + $raw = get_comment_meta($comment_id, 'aksrt_comment_addr', true); + return is_array($raw) ? $raw : []; + } +endif; + +if (!function_exists('aksrt_format_comment_city_label')) : + /** + * Format addr array into display string. + * + * @param array $addr Addr. + * @param string $type Format. + */ + function aksrt_format_comment_city_label(array $addr, string $type = 'province_city'): string { + $country = trim((string) ($addr['country'] ?? '')); + $province = trim((string) ($addr['province'] ?? '')); + $city = trim((string) ($addr['city'] ?? '')); + switch ($type) { + case 'province': + return $province !== '' ? $province : $city; + case 'country_province': + $parts = array_filter([$country, $province]); + return implode(' ', $parts); + case 'province_city': + default: + $parts = array_filter([$province, $city]); + if (empty($parts) && $country !== '') { + return $country; + } + return implode(' ', $parts); + } + } +endif; + +if (!function_exists('aksrt_get_comment_city_badge_html')) : + /** + * HTML badge for comment city (empty string if disabled/missing). + */ + function aksrt_get_comment_city_badge_html($comment): string { + if (!aksrt_comment_city_enabled()) { + return ''; + } + $addr = aksrt_get_comment_addr($comment); + $label = aksrt_format_comment_city_label($addr, aksrt_comment_city_type()); + if ($label === '') { + return ''; + } + return '' . esc_html($label) . ''; + } +endif; + +// Persist geo after any successful insert (covers REST + core). +add_action('comment_post', static function ($comment_id): void { + if (function_exists('aksrt_store_comment_addr')) { + aksrt_store_comment_addr((int) $comment_id); + } +}, 20, 1); diff --git a/inc/helpers/comments/item.php b/inc/helpers/comments/item.php new file mode 100644 index 00000000..4c547233 --- /dev/null +++ b/inc/helpers/comments/item.php @@ -0,0 +1,267 @@ +comment_ID : (int) $comment; + if ($comment_id <= 0) { + return ''; + } + $like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0; + $is_liked = function_exists('aksrt_is_comment_liked') && aksrt_is_comment_liked($comment_id); + $classes = 'action action-comment-like muted-2-color flex0 ml10 aksrt-comment__like' . ($is_liked ? ' is-active' : ''); + $count_attr = $like_count > 0 ? (string) $like_count : ''; + return sprintf( + '', + esc_attr($classes), + $is_liked ? '1' : '0', + $comment_id, + $is_liked ? 'true' : 'false', + $is_liked ? 'fas' : 'far', + $like_count > 0 ? '' : ' hidden', + esc_html($count_attr) + ); + } +endif; + +if (!function_exists('aksrt_get_comments_user_name_html')) : + /** + * Name row: topping + author link + identity + author/hot badges. + * + * @param WP_Comment $comment Comment. + * @param array $settings Display settings. + */ + function aksrt_get_comments_user_name_html(WP_Comment $comment, array $settings = []): string { + $user_id = (int) $comment->user_id; + $show_author_tag = !isset($settings['author_tag']) || !empty($settings['author_tag']); + $is_pinned = function_exists('aksrt_is_comment_pinned') && aksrt_is_comment_pinned($comment); + $is_post_author = $user_id > 0 && $user_id === (int) get_post_field('post_author', $comment->comment_post_ID); + + $topping = ''; + if ($is_pinned) { + $topping = ''; + } + + if ($user_id > 0) { + $user = get_userdata($user_id); + $display = $user instanceof WP_User ? $user->display_name : get_comment_author($comment); + $url = get_author_posts_url($user_id); + $name = '' . esc_html($display) . ''; + if (function_exists('aksrt_render_user_identity_chips')) { + $name .= aksrt_render_user_identity_chips($user_id, 'comment'); + } + } else { + $name = '' . esc_html(get_comment_author($comment)) . ''; + } + + $badges = ''; + if ($show_author_tag && $is_post_author) { + $badges .= '' . esc_html__('作者', 'aksrt') . ''; + } + if (function_exists('aksrt_is_blog_comment_hot') && aksrt_is_blog_comment_hot($comment)) { + $badges .= ' ' . esc_html__('神评', 'aksrt') . ''; + } + + return '' . $topping . $name . $badges . ''; + } +endif; + +if (!function_exists('aksrt_get_comment_header_html')) : + /** + * @param WP_Comment $comment Comment. + * @param array $settings Settings. + */ + function aksrt_get_comment_header_html(WP_Comment $comment, array $settings = []): string { + $avatar_size = max(24, (int) ($settings['avatar_size'] ?? 45)); + $like_enabled = !isset($settings['like_enabled']) || !empty($settings['like_enabled']); + $avatar = get_avatar($comment, $avatar_size, '', '', ['class' => 'avatar-img comt-avatar']); + $name = aksrt_get_comments_user_name_html($comment, $settings); + $like = aksrt_get_comment_like_html($comment, $like_enabled); + $inner = '
    ' . $avatar . $name . $like . '
    '; + return '
    ' . $inner . '
    '; + } +endif; + +if (!function_exists('aksrt_get_comments_action_dropdown_html')) : + /** + * More menu: pin / edit / approve / delete (REST-driven via data-comment-action). + * + * @param WP_Comment $comment Comment. + * @param int $depth Depth. + */ + function aksrt_get_comments_action_dropdown_html(WP_Comment $comment, int $depth = 0): string { + $perms = function_exists('aksrt_comment_action_permissions') + ? aksrt_comment_action_permissions($comment) + : ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; + $is_pinned = function_exists('aksrt_is_comment_pinned') && aksrt_is_comment_pinned($comment); + $is_pending = '0' === (string) $comment->comment_approved; + $is_spam = 'spam' === (string) $comment->comment_approved; + $lists = ''; + + if (!empty($perms['can_pin']) && (int) $comment->comment_parent === 0) { + $label = $is_pinned ? __('取消置顶', 'aksrt') : __('设置置顶', 'aksrt'); + $lists .= '
  • ' . esc_html($label) . '
  • '; + } + if (!empty($perms['can_edit'])) { + $lists .= '
  • ' . esc_html__('编辑', 'aksrt') . '
  • '; + } + if (!empty($perms['can_moderate'])) { + if ($is_pending) { + $lists .= '
  • ' . esc_html__('批准', 'aksrt') . '
  • '; + } elseif (!$is_spam) { + $lists .= '
  • ' . esc_html__('驳回', 'aksrt') . '
  • '; + } + $lists .= '
  • ' . esc_html__('垃圾', 'aksrt') . '
  • '; + } + if (!empty($perms['can_delete'])) { + $lists .= '
  • ' . esc_html__('删除', 'aksrt') . '
  • '; + } + + if ($lists === '') { + return ''; + } + + return '' + . '' + . '' + . ''; + } +endif; + +if (!function_exists('aksrt_get_comment_footer_html')) : + /** + * comt-meta: time-ago · city · @parent · approve · reply · dropdown. + * + * @param WP_Comment $comment Comment. + * @param array $args wp_list_comments args. + * @param int $depth Depth. + * @param array $settings Settings. + */ + function aksrt_get_comment_footer_html(WP_Comment $comment, array $args, int $depth, array $settings = []): string { + $is_pending = '0' === (string) $comment->comment_approved; + $allow_reply = !empty($settings['allow_reply']); + + // Time ago. + $absolute = date_i18n('Y-m-d H:i:s', strtotime((string) $comment->comment_date)); + $ago = function_exists('aksrt_time_ago') + ? aksrt_time_ago((string) $comment->comment_date) + : human_time_diff(strtotime((string) $comment->comment_date), current_time('timestamp')) . __('前', 'aksrt'); + $time_html = '' . esc_html($ago) . ''; + + // City. + $city_html = function_exists('aksrt_get_comment_city_badge_html') + ? aksrt_get_comment_city_badge_html($comment) + : ''; + + // @ parent. + $parent_html = ''; + if ((int) $comment->comment_parent > 0) { + $parent_author = get_comment_author((int) $comment->comment_parent); + $parent_html = '@' . esc_html($parent_author) . ''; + } + + // Pending badge. + $approve_html = ''; + if ($is_pending) { + $approve_html .= '' . esc_html__('待审核', 'aksrt') . ''; + } + $approve_html .= ''; + + // Reply. + $reply_html = ''; + if ($allow_reply && !$is_pending && $depth > 0) { + $max_depth = (int) ($args['max_depth'] ?? get_option('thread_comments_depth', 5)); + $link = get_comment_reply_link( + array_merge( + $args, + [ + 'add_below' => 'div-comment', + 'reply_text' => __('回复', 'aksrt'), + 'depth' => $depth, + 'max_depth' => $max_depth, + ] + ), + $comment + ); + if ($link) { + // Ensure data-commentid for moveForm path. + if (strpos($link, 'data-commentid') === false) { + $link = str_replace( + 'comment-reply-link', + 'comment-reply-link" data-commentid="' . (int) $comment->comment_ID . '" data-postid="' . (int) $comment->comment_post_ID, + $link + ); + } + $reply_html = '' . $link . ''; + } + } + + $actions = aksrt_get_comments_action_dropdown_html($comment, $depth); + $info = $time_html . $city_html . $parent_html . $approve_html . $reply_html; + + return '
    ' . $info . $actions . '
    '; + } +endif; + +if (!function_exists('aksrt_comment_callback')) : + /** + * Canonical comment list item renderer (zibll structure). + * + * @param WP_Comment $comment Comment. + * @param array $args Args. + * @param int $depth Depth. + */ + function aksrt_comment_callback($comment, $args, $depth) { + $GLOBALS['comment'] = $comment; + $settings = get_query_var('aksrt_comment_settings', []); + $settings = is_array($settings) ? $settings : []; + if (empty($settings) && class_exists('\AKSRT\Settings\Settings')) { + $settings = \AKSRT\Settings\Settings::get_comment_display_settings(); + } + + $comment_id = (int) $comment->comment_ID; + $depth_class = 'aksrt-depth-' . min((int) ($settings['nested_depth'] ?? 3), (int) $depth); + $is_pinned = function_exists('aksrt_is_comment_pinned') && aksrt_is_comment_pinned($comment); + $is_pending = '0' === (string) $comment->comment_approved; + $li_classes = trim($depth_class . ($is_pinned ? ' aksrt-comment--pinned' : '')); + + $header = aksrt_get_comment_header_html($comment, $settings); + $footer = aksrt_get_comment_footer_html($comment, $args, (int) $depth, $settings); + + if ($is_pending) { + $body = '

    ' . esc_html__('您的评论正在等待审核。', 'aksrt') . '

    '; + } else { + $body = function_exists('aksrt_render_comment_content') + ? aksrt_render_comment_content((string) get_comment_text($comment)) + : wp_kses_post((string) get_comment_text($comment)); + } + + echo '
  • '; + echo '
      '; + echo '
    • '; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- composed helpers escape. + echo $header; + echo ''; + echo '
    • '; + echo '
    '; + // Closing
  • is emitted by Walker_Comment after children. + } +endif; diff --git a/src/AKSRT/Admin/Schema/SchemaPostList.php b/src/AKSRT/Admin/Schema/SchemaPostList.php index c9fe0cda..4248283b 100644 --- a/src/AKSRT/Admin/Schema/SchemaPostList.php +++ b/src/AKSRT/Admin/Schema/SchemaPostList.php @@ -265,6 +265,14 @@ class SchemaPostList { ['key' => 'comment_like_s', 'label' => '评论点赞', 'type' => 'switch', 'default' => true], ['key' => 'comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true], ['key' => 'comment_city_s', 'label' => '评论城市', 'type' => 'switch', 'default' => true], + ['key' => 'comment_city_type', 'label' => '城市显示格式', 'type' => 'radio', 'default' => 'province_city', 'options' => [ + ['label' => '省份+城市', 'value' => 'province_city'], + ['label' => '仅省份', 'value' => 'province'], + ['label' => '国家+省份', 'value' => 'country_province'], + ]], + ['key' => 'comment_quick_often', 'label' => '快捷回复短语', 'type' => 'textarea', 'default' => "不错,学习了 +感谢分享 +期待更新"], ['key' => 'comment_author_tag', 'label' => '作者标签', 'type' => 'switch', 'default' => true], ['key' => 'comment_corderby', 'label' => '评论排序切换', 'type' => 'switch', 'default' => true, 'description' => '前台显示最新/最早/最热切换按钮'], ]], diff --git a/src/AKSRT/Core/CommentActions.php b/src/AKSRT/Core/CommentActions.php index f20bb720..649bed2b 100644 --- a/src/AKSRT/Core/CommentActions.php +++ b/src/AKSRT/Core/CommentActions.php @@ -92,6 +92,51 @@ class CommentActions { 'active' => ['required' => false, 'type' => 'boolean'], ], ]); + + register_rest_route('aksrt/v1', '/comment/get', [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'get_comment'], + 'permission_callback' => [$this, 'check_get_permission'], + 'args' => [ + 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'], + ], + ]); + } + + /** + * Permission: can load raw content for edit? + */ + public function check_get_permission(\WP_REST_Request $request): bool { + $comment_id = (int) $request->get_param('comment_id'); + if ($comment_id <= 0 || !is_user_logged_in()) { + return false; + } + $perms = $this->permissions($comment_id); + return !empty($perms['can_edit']); + } + + /** + * Return raw comment content for moveForm edit. + */ + public function get_comment(\WP_REST_Request $request): \WP_REST_Response { + $comment_id = (int) $request->get_param('comment_id'); + $comment = get_comment($comment_id); + if (!$comment) { + return new \WP_REST_Response([ + 'success' => false, + 'code' => 'comment_not_found', + 'message' => __('评论不存在。', 'aksrt'), + ], 404); + } + return new \WP_REST_Response([ + 'success' => true, + 'content' => (string) $comment->comment_content, + 'data' => [ + 'content' => (string) $comment->comment_content, + 'comment_id' => $comment_id, + 'parent' => (int) $comment->comment_parent, + ], + ]); } /** @@ -193,6 +238,13 @@ class CommentActions { $params = []; } + $edit_id = absint($params['edit_comment_ID'] ?? $params['edit_comment_id'] ?? 0); + if ($edit_id > 0) { + $request->set_param('comment_id', $edit_id); + $request->set_param('content', (string) ($params['comment'] ?? $params['content'] ?? '')); + return $this->edit_comment($request); + } + $post_id = absint($params['comment_post_ID'] ?? $params['post_id'] ?? 0); $content = isset($params['comment']) ? (string) $params['comment'] : ''; $parent = absint($params['comment_parent'] ?? 0); diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index 4db9bd0c..ba075792 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -226,7 +226,22 @@ trait ScriptsPart1b { // Keep aggregator handle for any third-party deps that target it. $enqueue_module('single-post'); $enqueue_module('post-card'); // 相关推荐使用 post-card 局部样式 - $enqueue_module('comments'); + // Comments pack (zibll 1:1 discrete files). + $comment_parts = ['comments/shell', 'comments/mobile-item']; + $comment_dep = $deps[0] ?? $core_dep; + foreach ($comment_parts as $part) { + $path = get_template_directory() . '/assets/css/frontend/' . $part . '.css'; + if (!file_exists($path)) { + continue; + } + wp_enqueue_style( + 'aksrt-' . str_replace('/', '-', $part), + $base_uri . '/assets/css/frontend/' . $part . '.css', + is_string($comment_dep) ? [$comment_dep] : (array) $deps, + filemtime($path) ?: $version + ); + } + $enqueue_module('comments'); // aggregator handle for third-party deps $enqueue_module('widgets'); $enqueue_module('sidebar'); $enqueue_module('breadcrumb'); diff --git a/src/AKSRT/Core/ScriptsPart1c.php b/src/AKSRT/Core/ScriptsPart1c.php index 1946c97b..9aa874be 100644 --- a/src/AKSRT/Core/ScriptsPart1c.php +++ b/src/AKSRT/Core/ScriptsPart1c.php @@ -116,6 +116,8 @@ trait ScriptsPart1c { 'aksrt-core-slider' => 'slider.js', 'aksrt-core-article' => 'article.js', 'aksrt-core-comments' => 'comments.js', + 'aksrt-core-comment-fixed' => 'comment-fixed-input.js', + 'aksrt-core-comment-move' => 'comment-move-form.js', 'aksrt-core-pagination' => 'pagination.js', 'aksrt-core-archive' => 'archive.js', 'aksrt-core-footer-tabbar' => 'footer-tabbar.js', @@ -424,28 +426,38 @@ trait ScriptsPart1c { } - // Comment toolbar (emoji, code, bold, Ctrl+Enter) — only when the form is open. - if (is_singular() && comments_open()) { + // Comment toolbar (smilie/code/image/quick shortcodes) — post form or forum topic reply. + $load_comment_tools = (is_singular() && comments_open()) + || (is_singular('aksrt_forum_topic') || is_singular('forum_topic')); + if ($load_comment_tools) { $comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js'; wp_enqueue_script( 'aksrt-comment-tools', $base_uri . '/assets/js/comment-tools.js', - ['aksrt-script'], + ['aksrt-script', 'aksrt-core-comment-fixed', 'aksrt-core-comment-move'], file_exists($comment_tools_path) ? (string) filemtime($comment_tools_path) : $version, true ); + $comment_tools_css = get_template_directory() . '/assets/css/frontend/comment-tools.css'; + if (file_exists($comment_tools_css)) { + wp_enqueue_style( + 'aksrt-comment-tools', + $base_uri . '/assets/css/frontend/comment-tools.css', + ['aksrt-comments'], + (string) filemtime($comment_tools_css) + ); + } } - // Comment actions client (like / edit / delete / moderate / pin) — load - // wherever comments can exist, including closed-comment posts. Submission - // itself is handled by main.js initCommentSystem(). + // Comment actions client (like / edit / delete / moderate / pin). + // Submit path lives in assets/js/core/comments.js (REST). if (is_singular() && (comments_open() || (int) get_comments_number() > 0)) { $comment_ajax_path = get_template_directory() . '/assets/js/comment-ajax.js'; if (file_exists($comment_ajax_path)) { wp_enqueue_script( 'aksrt-comment-ajax', $base_uri . '/assets/js/comment-ajax.js', - ['aksrt-script'], + ['aksrt-script', 'aksrt-core-comments'], (string) filemtime($comment_ajax_path), true ); @@ -534,6 +546,9 @@ trait ScriptsPart1c { 'commentUploadUrl' => esc_url_raw(rest_url('aksrt/v1/comment/upload-image')), 'commentUploadMaxSize' => 2 * 1024 * 1024, 'commentIasMax' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings()['paging_ajax_ias_max'] : 3, + 'commentQuickPhrases' => class_exists('\AKSRT\Settings\Settings') ? (\AKSRT\Settings\Settings::get_comment_display_settings()['quick_phrases'] ?? []) : [], + 'commentSmiliesBase' => function_exists('aksrt_comment_smilies_base_uri') ? aksrt_comment_smilies_base_uri() : (trailingslashit(get_template_directory_uri()) . 'assets/img/smilies/'), + 'restUrl' => esc_url_raw(rest_url('aksrt/v1')), 'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_enabled'] : true, 'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_pages'] : ['home', 'cat'], 'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['badge_show'] : true, diff --git a/src/AKSRT/Settings/PostDisplay.php b/src/AKSRT/Settings/PostDisplay.php index c0bb6662..beff49b4 100644 --- a/src/AKSRT/Settings/PostDisplay.php +++ b/src/AKSRT/Settings/PostDisplay.php @@ -465,9 +465,48 @@ class PostDisplay { 'submit_text' => $submit_text !== '' ? $submit_text : __('提交评论', 'aksrt'), 'box_placeholder' => $box_placeholder !== '' ? $box_placeholder : __('说点什么吧...', 'aksrt'), 'title' => $title !== '' ? $title : __('评论', 'aksrt'), + // Extended zibll-parity settings. + 'close_comments' => !empty($settings['comment_close_comments']), + 'hot_threshold' => max(1, min(999, (int) ($settings['comment_hot_threshold'] ?? 10))), + 'quick_s' => !isset($settings['comment_quick_s']) || !empty($settings['comment_quick_s']), + 'quick_phrases' => self::parse_comment_quick_phrases((string) ($settings['comment_quick_often'] ?? '')), + 'city_s' => !isset($settings['comment_city_s']) || !empty($settings['comment_city_s']), + 'city_type' => self::normalize_comment_city_type((string) ($settings['comment_city_type'] ?? 'province_city')), ]; } + /** + * @return array + */ + private static function parse_comment_quick_phrases(string $raw): array { + if (trim($raw) === '') { + $raw = "不错,学习了\n感谢分享\n期待更新"; + } + $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; + $out = []; + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line !== '') { + $out[] = $line; + } + } + return array_values(array_unique($out)); + } + + private static function normalize_comment_city_type(string $type): string { + $type = sanitize_key($type); + // Map legacy zibll pro/city labels. + if ($type === 'pro') { + return 'province'; + } + if ($type === 'city') { + return 'province_city'; + } + return in_array($type, ['province_city', 'province', 'country_province'], true) + ? $type + : 'province_city'; + } + public static function apply_list_orderby($query): void { if (is_admin() || !($query instanceof \WP_Query) || !$query->is_main_query()) { diff --git a/templates/pages/page-product.php b/templates/pages/page-product.php index 04e1f1ab..c9d70490 100644 --- a/templates/pages/page-product.php +++ b/templates/pages/page-product.php @@ -275,35 +275,49 @@ $author = $author_id > 0 ? get_userdata($author_id) : null;
    -
    - - -
    - - - +
    +
    + + +
    +
    + + + +
    -
    +
    -
    -
    - +
    +
    + - + - +
    -

    +
    + +
    -
    +
    - +