feat(share): align social sharing runtime

This commit is contained in:
2026-09-27 13:49:32 +08:00
parent 26ac8167f3
commit cf6c11852e
33 changed files with 802 additions and 1919 deletions
@@ -82,6 +82,12 @@
</n-tab-pane>
</n-tabs>
</div>
<AksSorterField
v-else-if="field.type === 'custom' && field.renderer === 'sorter'"
:model-value="getFieldValue(field.key) || {}"
:options="field.options || []"
@update:model-value="setFieldValue(field.key, $event)"
/>
<slot
v-else-if="field.type === 'custom'"
:name="`field-${field.key}`"
@@ -0,0 +1,112 @@
<template>
<div class="aksrt-sorter">
<div class="aksrt-sorter__column">
<div class="aksrt-sorter__title">
<span>已启用</span>
<n-tag size="small" type="success" :bordered="false">{{ columns.enabled.length }}</n-tag>
</div>
<draggable
v-model="columns.enabled"
item-key="value"
group="aksrt-sorter"
handle=".aksrt-sorter__handle"
ghost-class="aksrt-sorter__ghost"
animation="180"
class="aksrt-sorter__list"
@change="commit"
>
<template #item="{ element }">
<div class="aksrt-sorter__item">
<i class="fa fas fa-grip-vertical aksrt-sorter__handle" aria-hidden="true"></i>
<i v-if="element.icon" :class="element.icon" aria-hidden="true"></i>
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
<n-empty v-if="columns.enabled.length === 0" size="small" description="暂无启用项" />
</div>
<div class="aksrt-sorter__column aksrt-sorter__column--disabled">
<div class="aksrt-sorter__title">
<span>未启用</span>
<n-tag size="small" :bordered="false">{{ columns.disabled.length }}</n-tag>
</div>
<draggable
v-model="columns.disabled"
item-key="value"
group="aksrt-sorter"
handle=".aksrt-sorter__handle"
ghost-class="aksrt-sorter__ghost"
animation="180"
class="aksrt-sorter__list"
@change="commit"
>
<template #item="{ element }">
<div class="aksrt-sorter__item">
<i class="fa fas fa-grip-vertical aksrt-sorter__handle" aria-hidden="true"></i>
<i v-if="element.icon" :class="element.icon" aria-hidden="true"></i>
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
<n-empty v-if="columns.disabled.length === 0" size="small" description="暂无未启用项" />
</div>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
import draggable from 'vuedraggable'
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
options: { type: Array, default: () => [] },
})
const emit = defineEmits(['update:modelValue'])
const columns = reactive({ enabled: [], disabled: [] })
let syncing = false
function keysOf(value) {
if (Array.isArray(value)) return value.map(String)
return value && typeof value === 'object' ? Object.keys(value) : []
}
function syncColumns() {
syncing = true
const byKey = new Map(props.options.map(option => [String(option.value), option]))
const used = new Set()
const normalize = keys => keys.filter(key => byKey.has(key) && !used.has(key)).map(key => {
used.add(key)
return byKey.get(key)
})
columns.enabled = normalize(keysOf(props.modelValue?.enabled))
columns.disabled = normalize(keysOf(props.modelValue?.disabled))
for (const option of props.options) {
if (!used.has(String(option.value))) columns.disabled.push(option)
}
syncing = false
}
function commit() {
if (syncing) return
const toMap = items => Object.fromEntries(items.map(item => [String(item.value), String(item.label)]))
emit('update:modelValue', {
enabled: toMap(columns.enabled),
disabled: toMap(columns.disabled),
})
}
watch(() => [props.modelValue, props.options], syncColumns, { immediate: true, deep: true })
</script>
<style scoped>
.aksrt-sorter { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; }
.aksrt-sorter__column { min-height:210px; padding:12px; border:1px solid var(--aksrt-color-border); border-radius:6px; background:var(--aksrt-color-surface); }
.aksrt-sorter__column--disabled { background:var(--aksrt-color-canvas); }
.aksrt-sorter__title { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; font-weight:600; }
.aksrt-sorter__list { display:grid; gap:7px; min-height:44px; }
.aksrt-sorter__item { display:flex; align-items:center; gap:9px; min-height:38px; padding:7px 10px; border:1px solid var(--aksrt-color-border); border-radius:4px; background:var(--aksrt-color-surface); }
.aksrt-sorter__handle { color:var(--aksrt-color-text-tertiary); cursor:grab; }
.aksrt-sorter__ghost { opacity:.45; border-color:var(--aksrt-color-primary); }
@media (max-width:720px) { .aksrt-sorter { grid-template-columns:1fr; } }
</style>
@@ -84,6 +84,16 @@ export const globalSection = {
canonicalDomain: 'global',
section: 'i18n',
}),
schemaPage({
key: 'global-share',
name: 'GlobalShare',
label: '社交分享',
route: '/global/share',
title: '社交分享',
desc: '配置分享平台、微信 JSAPI 与海报分享。',
canonicalDomain: 'global',
section: 'share',
}),
schemaPage({
key: 'global-email',
name: 'GlobalEmail',
@@ -57,16 +57,6 @@ export const postListSection = {
canonicalDomain: 'post_list',
section: 'feature',
}),
schemaPage({
key: 'post-list-share',
name: 'PostListShare',
label: '分享设置',
route: '/post-list/share',
title: '分享设置',
desc: '配置微信分享凭据、海报分享和社交分享选项。',
canonicalDomain: 'post_list',
section: 'share',
}),
schemaPage({
key: 'post-list-comment',
name: 'PostListComment',
@@ -14,6 +14,7 @@ import { customCodeSchema } from './custom-code.js'
import { seoSchema } from './seo.js'
import { codeHighlightSchema } from './code-highlight.js'
import { systemNoticeSchema } from './system-notice.js'
import { shareSchema } from './share.js'
export const globalSchemas = {
logo: logoSchema,
@@ -0,0 +1,98 @@
const themeRoot = typeof window !== 'undefined' && window.aksrtAdminData?.themeUri
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
: ''
const shareOptions = [
{ label: 'QQ空间', value: 'qzone', icon: 'fa fab fa-qq' },
{ label: '微博', value: 'weibo', icon: 'fa fab fa-weibo' },
{ label: 'QQ好友', value: 'qq', icon: 'fa fab fa-qq' },
{ label: 'Facebook', value: 'facebook', icon: 'fa fab fa-facebook-f' },
{ label: 'X', value: 'twitter', icon: 'fa fab fa-twitter' },
{ label: 'Telegram', value: 'telegram', icon: 'fa fab fa-telegram-plane' },
{ label: 'WhatsApp', value: 'whatsapp', icon: 'fa fab fa-whatsapp' },
{ label: 'LinkedIn', value: 'linkedin', icon: 'fa fab fa-linkedin-in' },
{ label: 'Reddit', value: 'reddit', icon: 'fa fab fa-reddit-alien' },
{ label: 'LINE', value: 'line', icon: 'fa fab fa-line' },
{ label: 'VK', value: 'vk', icon: 'fa fab fa-vk' },
{ label: '邮件', value: 'email', icon: 'fa fas fa-envelope' },
{ label: '海报分享', value: 'poster', icon: 'fa fas fa-image' },
{ label: '复制链接', value: 'copy', icon: 'fa fas fa-link' },
]
const defaultShareItems = {
enabled: { qzone: 'QQ空间', weibo: '微博', qq: 'QQ好友', poster: '海报分享', copy: '复制链接' },
disabled: { facebook: 'Facebook', twitter: 'X', telegram: 'Telegram', whatsapp: 'WhatsApp', linkedin: 'LinkedIn', reddit: 'Reddit', line: 'LINE', vk: 'VK', email: '邮件' },
}
export const shareSchema = {
key: 'global.share',
title: '社交分享',
icon: 'fa fas fa-share-alt',
description: '配置文章分享平台、微信 JSAPI 分享和海报分享。',
defaults: {
share_s: true,
share_items: defaultShareItems,
wechat_share_s: false,
wechat_share_option: { appid: '', app_secret: '', only_logo: false },
share_img: true,
share_img_compatible_s: false,
share_img_byimg: themeRoot ? `${themeRoot}/assets/img/slider-bg.jpg` : '',
share_logo: themeRoot ? `${themeRoot}/assets/img/logo.png` : '',
share_desc: '扫一扫 立即查看',
},
groups: [
{
key: 'share',
title: '社交分享',
icon: 'fa fas fa-share-alt',
fields: [
{ key: 'share_s', label: '文章分享功能', type: 'switcher', default: true, help: '关闭后,在文章页面将不显示分享按钮。' },
{
key: 'share_items',
label: '分享平台',
type: 'custom',
renderer: 'sorter',
sanitize_type: 'array',
default: defaultShareItems,
options: shareOptions,
description: '左侧启用并拖拽排序,右侧为未启用项(同时启用的数量不建议超过 6 个)。若开启海外平台,用户网络必须能正常访问对应服务。',
},
{ key: 'wechat_share_s', label: '微信分享有图', type: 'switcher', default: false, help: '使用微信 JSAPI 在微信 App 内分享。' },
{
key: 'wechat_share_hint',
label: '',
type: 'content',
ui_only: true,
dependsOn: { key: 'wechat_share_s', value: true },
content: '<p><strong>微信分享有图配置</strong></p><p>此功能用于用户从公众号进入页面后调用微信默认分享能力,并不会额外显示分享按钮。请在公众号后台将当前站点域名配置为 JS 接口安全域名。</p><p><a href="https://mp.weixin.qq.com/" target="_blank" rel="noopener noreferrer">前往微信公众平台</a></p>',
},
{
key: 'wechat_share_option',
label: '微信分享有图配置',
type: 'fieldset',
default: { appid: '', app_secret: '', only_logo: false },
dependsOn: { key: 'wechat_share_s', value: true },
fields: [
{ key: 'appid', label: 'AppID', type: 'input', default: '' },
{ key: 'app_secret', label: 'AppSecret', type: 'input', default: '' },
{ key: 'only_logo', label: '全部使用网站图标', type: 'switcher', default: false, help: '默认自动使用页面封面图;开启后所有页面均使用网站图标。图片启用防盗链时建议开启。' },
],
},
{ key: 'share_img', label: '生成海报分享', type: 'switcher', default: true, help: '网站图片使用 OSS 等云存储时,请先配置允许本站 Canvas 读取图片的跨域规则。' },
{
key: 'share_img_warning',
label: '',
type: 'notice',
ui_only: true,
alertType: 'warning',
content: '海报图片加载失败时,请检查图片 CDN/OSS 的跨域响应头。',
dependsOn: { key: 'share_img', value: true },
},
{ key: 'share_img_compatible_s', label: '海报分享兼容模式', type: 'switcher', default: false, dependsOn: { key: 'share_img', value: true }, help: '通过服务端代理读取海报图片,改善跨域兼容性;会增加服务器图片处理开销。' },
{ key: 'share_img_byimg', label: '海报分享默认图片', type: 'media', default: themeRoot ? `${themeRoot}/assets/img/slider-bg.jpg` : '', dependsOn: { key: 'share_img', value: true }, help: '文章没有图片时使用,建议尺寸 800×500。' },
{ key: 'share_logo', label: '海报分享 LOGO', type: 'media', default: themeRoot ? `${themeRoot}/assets/img/logo.png` : '', dependsOn: { key: 'share_img', value: true }, help: '显示在海报底部,建议尺寸 300×100、大小 50KB 以内。' },
{ key: 'share_desc', label: '海报分享底部文案', type: 'input', default: '扫一扫 立即查看', dependsOn: { key: 'share_img', value: true } },
],
},
],
}
@@ -8,7 +8,6 @@ import { pageSchema } from './page.js'
import { filterSchema } from './filter.js'
import { featureSchema } from './feature.js'
import { paySchema } from './pay.js'
import { shareSchema } from './share.js'
import { commentSchema } from './comment.js'
import { submitSchema } from './submit.js'
@@ -19,7 +18,6 @@ export const postListSchemas = {
filter: filterSchema,
feature: featureSchema,
pay: paySchema,
share: shareSchema,
comment: commentSchema,
submit: submitSchema,
}
@@ -1,51 +0,0 @@
export const shareSchema = {
key: 'post_list.share',
title: '分享设置',
icon: 'fa fas fa-share-alt',
description: '配置微信分享凭据、海报分享和社交分享选项。',
defaults: {
share_s: true,
share_wechat_s: false,
share_wechat_appid: '',
share_wechat_app_secret: '',
share_only_logo: false,
share_img_compatible_s: false,
share_img_byimg: '',
share_logo: '',
share_img: '',
share_desc: '扫一扫 立即查看',
},
groups: [
{
key: 'basic',
title: '分享按钮',
icon: 'fa fas fa-share-alt',
fields: [
{ key: 'share_s', label: '分享按钮', type: 'switch', default: true, help: '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响' },
],
},
{
key: 'wechat',
title: '微信分享',
icon: 'fa fab fa-weixin',
fields: [
{ key: 'share_wechat_s', label: '启用微信分享', type: 'switch', default: false, help: '启用后文章页将支持微信 JSAPI 分享' },
{ key: 'share_wechat_appid', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'share_wechat_s', value: true }, help: '微信公众号 AppID' },
{ key: 'share_wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'share_wechat_s', value: true }, help: '微信公众号 AppSecret(请确保 JS 安全域名已配置)' },
{ key: 'share_only_logo', label: '仅显示 Logo', type: 'switch', default: false, help: '分享时仅显示站点 Logo' },
],
},
{
key: 'poster',
title: '海报分享',
icon: 'fa fas fa-image',
fields: [
{ key: 'share_img_compatible_s', label: '兼容模式', type: 'switch', default: false, help: '启用后使用兼容模式生成分享海报' },
{ key: 'share_img_byimg', label: '海报默认图', type: 'media', default: '', help: '海报分享使用的默认背景图' },
{ key: 'share_logo', label: '海报 LOGO', type: 'media', default: '', help: '海报上叠加的站点 LOGO' },
{ key: 'share_img', label: '分享图片', type: 'media', default: '', help: '社交平台分享预览图' },
{ key: 'share_desc', label: '分享描述', type: 'textarea', default: '扫一扫 立即查看', help: '海报或分享卡片的描述文案' },
],
},
],
}
-179
View File
@@ -837,185 +837,6 @@ article.article.box-body,
.action-btn i { font-size: 1.2em; }
/* ===== 分享下拉面板 ===== */
/* 确保父容器作为定位参考 */
.share-dropdown {
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
z-index: 100;
/* 默认隐藏 — 最高优先级确保不被覆盖 */
display: none !important;
}
.share-dropdown[aria-hidden="false"] {
display: block !important;
animation: shareDropdownIn 0.2s ease-out forwards;
}
@keyframes shareDropdownIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.share-dropdown__grid {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--surface-page);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.1));
}
.share-item {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
border: none;
border-radius: var(--radius-btn, 6px);
background: transparent;
color: var(--color-ink-2, var(--color-ink-3));
cursor: pointer;
font-size: 11px;
transition: background 0.15s, color 0.15s, transform 0.15s;
white-space: nowrap;
}
.share-item:hover {
transform: none;
}
.share-item i {
font-size: 18px;
}
.share-item[data-platform="qq"]:hover { background: #e8f4fd; color: #12b7f5; }
.share-item[data-platform="weibo"]:hover { background: #fceae9; color: #ff6a00; }
.share-item[data-platform="twitter"]:hover { background: #f0f0f0; color: #000; }
.share-item[data-platform="wechat"]:hover { background: #e6f7ec; color: #07c160; }
.share-item[data-platform="copy"]:hover { background: #f0f2f5; color: #555; }
.share-item[data-platform="poster"]:hover { background: #fff4e8; color: #f59e0b; }
/* 微信二维码浮层 */
#share-qrcode-box {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
padding: 14px;
background: var(--surface-page);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.12));
text-align: center;
z-index: 101;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
}
#share-qrcode-box.is-visible {
pointer-events: auto;
opacity: 1;
}
#share-qrcode-image {
width: 140px;
height: 140px;
display: block;
}
/* 暗色模式 */
.theme-dark .share-dropdown__grid,
html.dark .share-dropdown__grid {
background: var(--surface-1);
border-color: var(--color-line);
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
}
.theme-dark #share-qrcode-box,
html.dark #share-qrcode-box {
background: var(--surface-1);
border-color: var(--color-line);
}
.theme-dark .share-item,
html.dark .share-item {
color: var(--color-ink-2, #aaa);
}
.theme-dark .share-item[data-platform="qq"]:hover { background: rgba(18,183,245,0.12); color: #12b7f5; }
.theme-dark .share-item[data-platform="weibo"]:hover { background: rgba(255,106,0,0.12); color: #ff6a00; }
.theme-dark .share-item[data-platform="twitter"]:hover { background: rgba(255,255,255,0.08); color: #fff; }
.theme-dark .share-item[data-platform="wechat"]:hover { background: rgba(7,193,96,0.12); color: #07c160; }
.theme-dark .share-item[data-platform="copy"]:hover { background: rgba(255,255,255,0.06); color: #ccc; }
.theme-dark .share-item[data-platform="poster"]:hover { background: rgba(245,158,11,0.12); color: #f59e0b; }
/* Narrow screens: bottom sheet so dropdown never clips off-screen */
@media (max-width: 767px) {
.share-dropdown {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
transform: none;
width: 100%;
max-width: none;
z-index: 1200;
padding: 0 0 calc(12px + env(safe-area-inset-bottom, 0px));
background: transparent;
}
.share-dropdown[aria-hidden="false"] {
animation: shareSheetIn 0.22s ease-out forwards;
}
@keyframes shareSheetIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.share-dropdown__grid {
justify-content: space-around;
flex-wrap: wrap;
gap: 6px;
margin: 0 10px;
padding: 14px 12px 16px;
border-radius: 14px 14px 10px 10px;
box-shadow: 0 -6px 28px rgba(0, 0, 0, 0.16);
}
.share-item {
min-width: 64px;
flex: 1 1 28%;
max-width: 33%;
}
#share-qrcode-box {
position: static;
margin: 10px auto 0;
transform: none;
}
body.aksrt-share-sheet-open {
overflow: hidden;
}
body.aksrt-share-sheet-open::before {
content: '';
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 1190;
}
}
.theme-dark .post-drop-meta__menu,
html.dark .post-drop-meta__menu {
background: var(--surface-1, #323335);
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -157,185 +157,6 @@
.action-btn i { font-size: 1.2em; }
/* ===== 分享下拉面板 ===== */
/* 确保父容器作为定位参考 */
.share-dropdown {
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
z-index: 100;
/* 默认隐藏 — 最高优先级确保不被覆盖 */
display: none !important;
}
.share-dropdown[aria-hidden="false"] {
display: block !important;
animation: shareDropdownIn 0.2s ease-out forwards;
}
@keyframes shareDropdownIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.share-dropdown__grid {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--surface-page);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.1));
}
.share-item {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
border: none;
border-radius: var(--radius-btn, 6px);
background: transparent;
color: var(--color-ink-2, var(--color-ink-3));
cursor: pointer;
font-size: 11px;
transition: background 0.15s, color 0.15s, transform 0.15s;
white-space: nowrap;
}
.share-item:hover {
transform: none;
}
.share-item i {
font-size: 18px;
}
.share-item[data-platform="qq"]:hover { background: #e8f4fd; color: #12b7f5; }
.share-item[data-platform="weibo"]:hover { background: #fceae9; color: #ff6a00; }
.share-item[data-platform="twitter"]:hover { background: #f0f0f0; color: #000; }
.share-item[data-platform="wechat"]:hover { background: #e6f7ec; color: #07c160; }
.share-item[data-platform="copy"]:hover { background: #f0f2f5; color: #555; }
.share-item[data-platform="poster"]:hover { background: #fff4e8; color: #f59e0b; }
/* 微信二维码浮层 */
#share-qrcode-box {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
padding: 14px;
background: var(--surface-page);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.12));
text-align: center;
z-index: 101;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
}
#share-qrcode-box.is-visible {
pointer-events: auto;
opacity: 1;
}
#share-qrcode-image {
width: 140px;
height: 140px;
display: block;
}
/* 暗色模式 */
.theme-dark .share-dropdown__grid,
html.dark .share-dropdown__grid {
background: var(--surface-1);
border-color: var(--color-line);
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
}
.theme-dark #share-qrcode-box,
html.dark #share-qrcode-box {
background: var(--surface-1);
border-color: var(--color-line);
}
.theme-dark .share-item,
html.dark .share-item {
color: var(--color-ink-2, #aaa);
}
.theme-dark .share-item[data-platform="qq"]:hover { background: rgba(18,183,245,0.12); color: #12b7f5; }
.theme-dark .share-item[data-platform="weibo"]:hover { background: rgba(255,106,0,0.12); color: #ff6a00; }
.theme-dark .share-item[data-platform="twitter"]:hover { background: rgba(255,255,255,0.08); color: #fff; }
.theme-dark .share-item[data-platform="wechat"]:hover { background: rgba(7,193,96,0.12); color: #07c160; }
.theme-dark .share-item[data-platform="copy"]:hover { background: rgba(255,255,255,0.06); color: #ccc; }
.theme-dark .share-item[data-platform="poster"]:hover { background: rgba(245,158,11,0.12); color: #f59e0b; }
/* Narrow screens: bottom sheet so dropdown never clips off-screen */
@media (max-width: 767px) {
.share-dropdown {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
transform: none;
width: 100%;
max-width: none;
z-index: 1200;
padding: 0 0 calc(12px + env(safe-area-inset-bottom, 0px));
background: transparent;
}
.share-dropdown[aria-hidden="false"] {
animation: shareSheetIn 0.22s ease-out forwards;
}
@keyframes shareSheetIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.share-dropdown__grid {
justify-content: space-around;
flex-wrap: wrap;
gap: 6px;
margin: 0 10px;
padding: 14px 12px 16px;
border-radius: 14px 14px 10px 10px;
box-shadow: 0 -6px 28px rgba(0, 0, 0, 0.16);
}
.share-item {
min-width: 64px;
flex: 1 1 28%;
max-width: 33%;
}
#share-qrcode-box {
position: static;
margin: 10px auto 0;
transform: none;
}
body.aksrt-share-sheet-open {
overflow: hidden;
}
body.aksrt-share-sheet-open::before {
content: '';
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 1190;
}
}
.theme-dark .post-drop-meta__menu,
html.dark .post-drop-meta__menu {
background: var(--surface-1, #323335);
+28
View File
@@ -8887,6 +8887,26 @@ img.alignleft {
margin-top: 10px;
}
.share-btn icon {
width: 36px;
height: 36px;
display: inline-flex;
border-radius: 100%;
background: var(--main-border-color);
justify-content: center;
align-items: center;
font-size: 22px;
flex-shrink: 0;
}
.share-btn icon .icon {
margin: auto !important;
}
.share-button .share-btn icon {
margin-right: 10px;
}
.share-button .share-btn .icon {
width: 1em;
font-size: inherit;
@@ -8907,6 +8927,14 @@ img.alignleft {
align-items: center;
}
.horizontal .share-btn icon {
display: flex;
margin-bottom: 6px;
width: 50px;
height: 50px;
font-size: 28px;
}
.drop-newadd {
border-radius: var(--main-radius);
}
+1 -1
View File
File diff suppressed because one or more lines are too long
-305
View File
@@ -1,305 +0,0 @@
/**
* AKSRT Post Share
*
* Handles share dropdowns, social share targets, copy link,
* and WeChat bridge handoff with QR fallback.
*
* @package AKSRT
*/
(function () {
'use strict';
function getMetaContent(selectors) {
for (var i = 0; i < selectors.length; i += 1) {
var node = document.querySelector(selectors[i]);
var content = node ? node.getAttribute('content') : '';
if (content) return content;
}
return '';
}
function getPlatform(node) {
if (!node) return '';
return node.getAttribute('data-share-platform') || node.getAttribute('data-platform') || '';
}
function resolvePayload(node) {
// Unified payload via the shared builder; fall back to local resolution
// only if the builder script is unavailable.
if (window.AksrtShareData && typeof window.AksrtShareData.resolve === 'function') {
return window.AksrtShareData.resolve(node);
}
var dropdown = document.getElementById('share-dropdown');
var source = node && typeof node.closest === 'function'
? node.closest('[data-share-url], [data-share-title], [data-share-desc], [data-share-image]')
: null;
var context = source || dropdown;
function readAttr(name) {
return (node && node.getAttribute(name)) || (context && context.getAttribute(name)) || '';
}
return {
title: readAttr('data-share-title') || document.title,
url: readAttr('data-share-url') || window.location.href,
desc: readAttr('data-share-desc') || getMetaContent(['meta[property="og:description"]', 'meta[name="description"]']),
text: readAttr('data-share-text') || '',
image: readAttr('data-share-image') || getMetaContent(['meta[property="og:image"]']),
qrcode: readAttr('data-share-qrcode') || ''
};
}
function openWindow(url) {
if (!url) return;
window.open(url, '_blank', 'width=640,height=560,scrollbars=yes,resizable=yes');
}
function buildShareUrl(platform, payload) {
switch (platform) {
case 'weibo':
return 'https://service.weibo.com/share/share.php?url=' + encodeURIComponent(payload.url) + '&title=' + encodeURIComponent(payload.title + (payload.text ? ' ' + payload.text : ''));
case 'qq':
return 'https://connect.qq.com/widget/shareqq/index.html?url=' + encodeURIComponent(payload.url) + '&title=' + encodeURIComponent(payload.title) + '&desc=' + encodeURIComponent(payload.desc || payload.text || '');
case 'twitter':
return 'https://x.com/intent/tweet?url=' + encodeURIComponent(payload.url) + '&text=' + encodeURIComponent(payload.title);
case 'telegram':
return 'https://t.me/share/url?url=' + encodeURIComponent(payload.url) + '&text=' + encodeURIComponent(payload.title);
case 'linkedin':
return 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(payload.url);
default:
return '';
}
}
function fallbackCopy(text) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
try {
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
if (!document.execCommand('copy')) {
throw new Error('copy command failed');
}
} finally {
document.body.removeChild(textarea);
}
}
function copyLink(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).catch(function () {
fallbackCopy(text);
});
}
try {
fallbackCopy(text);
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
function initShareDropdown() {
var dropdown = document.getElementById('share-dropdown');
var triggers = Array.prototype.slice.call(document.querySelectorAll('#action-share-trigger, .action-share[data-type="share"], .footer-tabbar .action-share')).filter(function (el, idx, arr) { return arr.indexOf(el) === idx; });
var trigger = triggers[0] || null;
if (!trigger || !dropdown || dropdown.dataset.aksrtShareBound === '1') return;
dropdown.dataset.aksrtShareBound = '1';
var qrBox = document.getElementById('share-qrcode-box');
var qrImage = document.getElementById('share-qrcode-image');
function closeDropdown() {
dropdown.setAttribute('aria-hidden', 'true');
triggers.forEach(function (t) { t.setAttribute('aria-expanded', 'false'); });
document.body.classList.remove('aksrt-share-sheet-open');
if (window.AksrtShare && typeof window.AksrtShare.hideQrFallback === 'function') {
window.AksrtShare.hideQrFallback(qrBox);
} else if (qrBox) {
qrBox.classList.remove('is-visible');
qrBox.setAttribute('aria-hidden', 'true');
}
}
function toggleDropdown() {
var isOpen = dropdown.getAttribute('aria-hidden') === 'false';
if (isOpen) {
closeDropdown();
return;
}
dropdown.setAttribute('aria-hidden', 'false');
triggers.forEach(function (t) { t.setAttribute('aria-expanded', 'true'); });
if (window.matchMedia && window.matchMedia('(max-width: 767px)').matches) {
document.body.classList.add('aksrt-share-sheet-open');
}
}
trigger.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
toggleDropdown();
});
triggers.slice(1).forEach(function (extraTrigger) {
extraTrigger.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
// Reuse primary trigger click semantics by synthetic toggle
trigger.click();
});
});
document.addEventListener('click', function (event) {
if (!trigger.contains(event.target) && !dropdown.contains(event.target)) {
closeDropdown();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
closeDropdown();
}
});
dropdown.addEventListener('click', function (event) {
var item = event.target.closest('.share-item');
if (!item) return;
event.preventDefault();
event.stopPropagation();
handleShareAction(item, {
qrBox: qrBox,
qrImage: qrImage,
closeDropdown: closeDropdown,
withinDropdown: true
});
});
}
function handleWeixinShare(node, payload, options) {
options = options || {};
var share = window.AksrtShare;
if (!share || typeof share.openWeixinShare !== 'function') {
notyf('微信分享模块未加载', 'danger');
return Promise.resolve({ mode: 'error' });
}
if (
options.qrBox &&
options.qrBox.classList.contains('is-visible') &&
(!share.isWeixinEnv || !share.isWeixinEnv())
) {
share.hideQrFallback(options.qrBox);
return Promise.resolve({ mode: 'closed' });
}
return share.openWeixinShare({
title: payload.title,
url: payload.url,
desc: payload.desc || payload.text || '',
image: payload.image,
qrBox: options.qrBox || null,
qrImage: options.qrImage || null
}).then(function (result) {
if (result && result.message) {
notyf(result.message, result.mode === 'error' ? 'danger' : (result.mode === 'bridge' ? 'success' : 'info'));
}
return result;
});
}
function handleShareAction(node, options) {
var platform = getPlatform(node);
if (!platform) return Promise.resolve({ mode: 'noop' });
var payload = resolvePayload(node);
if (!payload.url) return Promise.resolve({ mode: 'noop' });
if (platform === 'copy') {
return copyLink(payload.url).then(function () {
notyf('链接已复制', 'success');
if (options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return { mode: 'copy' };
}).catch(function () {
notyf('复制失败,请手动复制链接', 'danger');
return { mode: 'error' };
});
}
if (platform === 'wechat') {
return handleWeixinShare(node, payload, options).then(function (result) {
if (result && result.mode !== 'qr' && result.mode !== 'error' && options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return result;
});
}
if (platform === 'poster') {
if (window.AksrtPosterShare && typeof window.AksrtPosterShare.open === 'function') {
window.AksrtPosterShare.open(payload);
if (options && typeof options.closeDropdown === 'function') options.closeDropdown();
return Promise.resolve({ mode: 'poster' });
}
notyf('海报模块未加载', 'danger');
return Promise.resolve({ mode: 'error' });
}
var shareUrl = buildShareUrl(platform, payload);
if (!shareUrl) {
return Promise.resolve({ mode: 'noop' });
}
openWindow(shareUrl);
if (options && typeof options.closeDropdown === 'function') {
options.closeDropdown();
}
return Promise.resolve({ mode: 'window' });
}
function bindGenericShareButtons() {
document.addEventListener('click', function (event) {
var node = event.target.closest('[data-share-platform]');
if (!node || node.closest('#share-dropdown')) return;
event.preventDefault();
handleShareAction(node, {});
});
document.addEventListener('click', function (event) {
var node = event.target.closest('[data-share-copy]');
if (!node) return;
event.preventDefault();
var url = node.getAttribute('data-share-url') || window.location.href;
copyLink(url).then(function () {
notyf('链接已复制', 'success');
}).catch(function () {
notyf('复制失败,请手动复制链接', 'danger');
});
});
}
function init() {
if (document.documentElement.dataset.aksrtPostShareInitialized === '1') return;
document.documentElement.dataset.aksrtPostShareInitialized = '1';
initShareDropdown();
bindGenericShareButtons();
}
jQuery(document).off('auto_fun.aksrt-post-share').on('auto_fun.aksrt-post-share', function () { init(document); });
})();
+169 -376
View File
@@ -1,406 +1,199 @@
/**
* AKSRT Poster Share — 海报分享
*
* 生成文章分享海报图片,支持下载和分享。
* 使用 Canvas API 绘制,无需外部依赖。
* Reference-theme poster sharing runtime.
*
* @package AKSRT
*/
(function () {
(function (window, document, $) {
'use strict';
/**
* 初始化海报分享
*
* @param {Object} options 配置项
*/
function PosterShare(options) {
this.options = Object.assign({
triggerSelector: '[data-aksrt-poster]',
title: '',
excerpt: '',
author: '',
date: '',
siteName: '',
qrCodeUrl: '',
width: 750,
height: 1334,
backgroundColor: '#ffffff',
textColor: '#333333',
accentColor: '#1890ff',
}, options || {});
var WIDTH = 768;
var HEIGHT = 1168;
var FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",Arial,sans-serif';
this.init();
function notice(message, type) {
if (typeof window.notyf === 'function') window.notyf(message, type || '');
}
PosterShare.prototype.init = function () {
var self = this;
function ajaxUrl() {
return (window.aksrtData && window.aksrtData.ajaxUrl) || (window._win && window._win.ajax_url) || '';
}
document.querySelectorAll(this.options.triggerSelector).forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
function nonce() {
return (window.aksrtData && window.aksrtData.nonce) || '';
}
// Unified payload first (title/excerpt/author/date/qr), then allow an
// explicit data-aksrt-poster JSON blob to override individual fields.
var resolved = (window.AksrtShareData && typeof window.AksrtShareData.resolve === 'function')
? window.AksrtShareData.resolve(btn)
: {};
function loadImage(sources) {
sources = sources.filter(Boolean);
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= sources.length) { reject(new Error('image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = sources[index];
}
next();
});
}
var config = {};
function qrCodeDataUrl(text) {
return new Promise(function (resolve) {
if (!text || typeof window.tbquire !== 'function') { resolve(''); return; }
window.tbquire(['qrcode'], function () {
var host = $('<div>').css({ position: 'fixed', left: '-9999px', top: '-9999px' }).appendTo('body');
try {
config = JSON.parse(btn.dataset.aksrtPoster || '{}');
} catch (err) {}
var merged = Object.assign({}, self.options, {
title: resolved.title || self.options.title,
excerpt: resolved.desc || self.options.excerpt,
author: resolved.author || self.options.author,
date: resolved.date || self.options.date,
siteName: resolved.siteName || self.options.siteName,
qrCodeUrl: resolved.qrcode || self.options.qrCodeUrl
}, config);
self.generate(merged);
host.qrcode({ width: 160, height: 160, correctLevel: 0, text: text, background: '#fff', foreground: '#333' });
var canvas = host.find('canvas').get(0);
resolve(canvas ? canvas.toDataURL('image/png') : '');
} catch (error) {
resolve('');
} finally {
host.remove();
}
});
});
};
}
var WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
var FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
function coverRect(boxWidth, boxHeight, sourceWidth, sourceHeight) {
var sx = 0, sy = 0, sw = sourceWidth, sh = sourceHeight;
if (sourceWidth / sourceHeight > boxWidth / boxHeight) {
sw = boxWidth * sourceHeight / boxHeight;
sx = (sourceWidth - sw) / 2;
} else {
sh = boxHeight * sourceWidth / boxWidth;
sy = (sourceHeight - sh) / 2;
}
return { sx: sx, sy: sy, sw: sw, sh: sh };
}
PosterShare.prototype.generate = function (config) {
var self = this;
config.width = config.width || 768;
config.height = config.height || 1168;
function containRect(x, y, boxWidth, boxHeight, sourceWidth, sourceHeight) {
var ratio = Math.min(boxWidth / sourceWidth, boxHeight / sourceHeight);
var width = sourceWidth * ratio;
var height = sourceHeight * ratio;
return { x: x + (boxWidth - width) / 2, y: y + (boxHeight - height) / 2, width: width, height: height };
}
var overlay = this.createOverlay();
document.body.appendChild(overlay);
function drawLines(ctx, text, maxWidth, maxRows, x, y, lineHeight) {
var chars = String(text || '').replace(/[\r\n]+/g, ' ').split('');
var rows = [], line = '';
chars.forEach(function (char) {
var next = line + char;
if (line && ctx.measureText(next).width > maxWidth) { rows.push(line); line = char; }
else line = next;
});
if (line) rows.push(line);
if (rows.length > maxRows) {
rows = rows.slice(0, maxRows);
while (rows[maxRows - 1] && ctx.measureText(rows[maxRows - 1] + '...').width > maxWidth) {
rows[maxRows - 1] = rows[maxRows - 1].slice(0, -1);
}
rows[maxRows - 1] += '...';
}
rows.forEach(function (row, index) { ctx.fillText(row, x, y + index * lineHeight); });
}
function renderPoster(data) {
return Promise.all([
loadImage([data.banner, data.banner_default, data.banner_spare]),
data.logo ? loadImage([data.logo]).catch(function () { return null; }) : Promise.resolve(null),
qrCodeDataUrl(data.url).then(function (src) { return src ? loadImage([src]).catch(function () { return null; }) : null; })
]).then(function (assets) {
var banner = assets[0], logo = assets[1], qr = assets[2];
var canvas = document.createElement('canvas');
canvas.width = config.width;
canvas.height = config.height;
canvas.width = WIDTH; canvas.height = HEIGHT;
var ctx = canvas.getContext('2d');
var imageHeight = HEIGHT / 2;
// Background
ctx.fillStyle = config.backgroundColor;
ctx.fillRect(0, 0, config.width, config.height);
ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, WIDTH, HEIGHT);
var crop = coverRect(WIDTH, imageHeight, banner.width, banner.height);
ctx.drawImage(banner, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, WIDTH, imageHeight);
var coverSources = [config.coverImg, config.banner, config.banner_default, config.banner_spare].filter(Boolean);
var now = new Date();
ctx.fillStyle = '#fff'; ctx.font = '35px ' + FONT; ctx.fillText('周' + '日一二三四五六'.charAt(now.getDay()), 30, 70);
ctx.font = '28px ' + FONT; ctx.fillText((now.getMonth() + 1) + ' / ' + now.getDate(), 30, 115);
if (coverSources.length) {
self.loadImageWithRetry(coverSources, 0, function (coverImg) {
self.drawPosterContent(ctx, canvas, config, overlay, coverImg);
}, function () {
self.drawPosterContent(ctx, canvas, config, overlay, null);
});
} else {
self.drawPosterContent(ctx, canvas, config, overlay, null);
ctx.strokeStyle = '#00a1ff'; ctx.lineWidth = 7; ctx.lineCap = 'round'; ctx.beginPath();
ctx.moveTo(40, imageHeight + 38); ctx.lineTo(40, imageHeight + 74); ctx.stroke();
ctx.fillStyle = '#424242'; ctx.font = 'bold 35px ' + FONT;
drawLines(ctx, data.title, WIDTH - 120, 2, 58, imageHeight + 68, 52);
ctx.fillStyle = '#fc4e1f'; ctx.font = '24px ' + FONT;
drawLines(ctx, data.tags, WIDTH - 80, 1, 40, imageHeight + 172, 36);
ctx.fillStyle = '#585858'; ctx.font = '25px ' + FONT;
drawLines(ctx, data.content, WIDTH - 80, 4, 40, imageHeight + 232, 39);
ctx.strokeStyle = 'rgba(122,122,122,.2)'; ctx.lineWidth = 2; ctx.strokeRect(30, HEIGHT - 230, WIDTH - 60, 200);
if (logo) {
var logoRect = containRect(80, HEIGHT - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
};
PosterShare.prototype.loadImageWithRetry = function (sources, idx, onLoad, onFail) {
if (idx >= sources.length) { onFail(); return; }
var self = this;
var retries = 0;
var src = sources[idx];
function tryLoad() {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () { onLoad(img); };
img.onerror = function () {
retries++;
if (retries < 3) { tryLoad(); }
else { self.loadImageWithRetry(sources, idx + 1, onLoad, onFail); }
};
img.src = src;
}
tryLoad();
};
PosterShare.prototype.drawPosterContent = function (ctx, canvas, config, overlay, coverImg) {
var self = this;
var W = config.width;
var isProduct = config.isProduct;
var y = 0;
// Cover image area (top)
var coverH = isProduct ? W : Math.round(W * 0.52);
if (coverImg) {
var imgRatio = coverImg.width / coverImg.height;
var drawW, drawH, drawX, drawY;
if (imgRatio > W / coverH) {
drawH = coverH; drawW = coverH * imgRatio;
drawX = -(drawW - W) / 2; drawY = 0;
} else {
drawW = W; drawH = W / imgRatio;
drawX = 0; drawY = -(drawH - coverH) / 2;
}
ctx.drawImage(coverImg, drawX, drawY, drawW, drawH);
} else {
ctx.fillStyle = config.accentColor;
ctx.fillRect(0, 0, W, coverH);
}
// Date/weekday badge (top-left)
if (config.date) {
var d = new Date(config.date);
if (!isNaN(d.getTime())) {
var weekday = WEEKDAYS[d.getDay()];
var dateStr = (d.getMonth() + 1) + '/' + d.getDate();
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.beginPath();
ctx.roundRect ? ctx.roundRect(20, 20, 80, 65, 8) : ctx.rect(20, 20, 80, 65);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 20px ' + FONT_FAMILY;
ctx.textAlign = 'center';
ctx.fillText(weekday, 60, 47);
ctx.font = '16px ' + FONT_FAMILY;
ctx.fillText(dateStr, 60, 72);
ctx.textAlign = 'left';
}
}
y = coverH + 36;
// Title with vertical accent line
ctx.fillStyle = config.accentColor;
ctx.fillRect(50, y - 6, 4, 44);
ctx.fillStyle = config.textColor;
ctx.font = 'bold 32px ' + FONT_FAMILY;
var titleLines = this.wrapText(ctx, config.title, W - 140);
titleLines.slice(0, 3).forEach(function (line) {
ctx.fillText(line, 66, y + 28);
y += 44;
});
y += 16;
// Product price section
if (isProduct && config.price !== undefined) {
ctx.font = 'bold 36px ' + FONT_FAMILY;
ctx.fillStyle = '#e74c3c';
var priceLabel = config.priceUnit === 'points' ? (config.price + ' 积分') : ('¥' + config.price);
ctx.fillText(priceLabel, 60, y + 10);
if (config.originalPrice && config.originalPrice > config.price) {
var priceW = ctx.measureText(priceLabel).width;
ctx.font = '22px ' + FONT_FAMILY;
ctx.fillStyle = '#999';
var origText = '¥' + config.originalPrice;
ctx.fillText(origText, 60 + priceW + 16, y + 10);
var origW = ctx.measureText(origText).width;
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(60 + priceW + 16, y + 5);
ctx.lineTo(60 + priceW + 16 + origW, y + 5);
ctx.stroke();
}
if (config.priceTag) {
ctx.font = '18px ' + FONT_FAMILY;
ctx.fillStyle = '#fff';
var tagW = ctx.measureText(config.priceTag).width + 16;
ctx.fillStyle = config.accentColor;
ctx.beginPath();
ctx.roundRect ? ctx.roundRect(W - 60 - tagW, y - 10, tagW, 30, 4) : ctx.rect(W - 60 - tagW, y - 10, tagW, 30);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(config.priceTag, W - 60 - tagW / 2, y + 10);
ctx.textAlign = 'left';
}
y += 48;
}
// Excerpt
if (config.excerpt) {
ctx.fillStyle = '#666';
ctx.font = '22px ' + FONT_FAMILY;
var excerptLines = this.wrapText(ctx, config.excerpt, W - 120);
excerptLines.slice(0, 5).forEach(function (line) {
ctx.fillText(line, 60, y + 4);
y += 34;
});
if (excerptLines.length > 5) {
ctx.fillText('...', 60, y + 4);
y += 34;
}
}
y += 20;
// Author & date
ctx.fillStyle = '#999';
ctx.font = '18px ' + FONT_FAMILY;
if (config.author) { ctx.fillText(config.author, 60, y); y += 26; }
// Bottom: site name + QR code
var bottomY = config.height - 110;
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(50, bottomY);
ctx.lineTo(W - 50, bottomY);
ctx.stroke();
if (config.siteName) {
ctx.fillStyle = '#999';
ctx.font = '18px ' + FONT_FAMILY;
ctx.fillText(config.siteName, 60, bottomY + 36);
}
if (config.qrCodeUrl) {
var qrImg = new Image();
qrImg.crossOrigin = 'anonymous';
qrImg.onload = function () {
var qrSize = 90;
var qrX = W - 50 - qrSize;
var qrY = bottomY + 6;
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
ctx.fillStyle = '#999';
ctx.font = '13px ' + FONT_FAMILY;
ctx.textAlign = 'center';
ctx.fillText('扫码阅读', qrX + qrSize / 2, qrY + qrSize + 16);
ctx.textAlign = 'left';
self.showPoster(canvas, overlay);
};
qrImg.onerror = function () { self.showPoster(canvas, overlay); };
qrImg.src = config.qrCodeUrl;
} else {
self.showPoster(canvas, overlay);
}
};
PosterShare.prototype.showPoster = function (canvas, overlay) {
// 创建海报容器
var posterWrap = document.createElement('div');
posterWrap.className = 'aksrt-poster-share__wrap';
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.className = 'aksrt-poster-share__image';
img.alt = '分享海报';
posterWrap.appendChild(img);
// 操作按钮
var actions = document.createElement('div');
actions.className = 'aksrt-poster-share__actions';
var downloadBtn = document.createElement('button');
downloadBtn.type = 'button';
downloadBtn.className = 'aksrt-poster-share__btn';
downloadBtn.innerHTML = '<i class="fa fas fa-download"></i> 保存图片';
downloadBtn.addEventListener('click', function () {
var link = document.createElement('a');
var postId = (window.aksrtData && aksrtData.postId) || Date.now();
link.download = 'poster_' + postId + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
actions.appendChild(downloadBtn);
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'aksrt-poster-share__btn aksrt-poster-share__btn--secondary';
closeBtn.textContent = '关闭';
closeBtn.addEventListener('click', function () {
overlay.remove();
});
actions.appendChild(closeBtn);
posterWrap.appendChild(actions);
// 替换加载状态
overlay.innerHTML = '';
overlay.appendChild(posterWrap);
};
PosterShare.prototype.createOverlay = function () {
var overlay = document.createElement('div');
overlay.className = 'aksrt-poster-share__overlay';
overlay.innerHTML = '<div class="aksrt-poster-share__loading"><i class="fa fas fa-spinner fa-spin"></i> 生成海报中...</div>';
overlay.addEventListener('click', function (e) {
if (e.target === overlay) overlay.remove();
});
return overlay;
};
PosterShare.prototype.wrapText = function (ctx, text, maxWidth) {
var lines = [];
var currentLine = '';
for (var i = 0; i < text.length; i++) {
var char = text[i];
var testLine = currentLine + char;
var metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine !== '') {
lines.push(currentLine);
currentLine = char;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
};
// 自动初始化
function autoInit() {
document.querySelectorAll('[data-aksrt-poster]').forEach(function (el) {
if (el.dataset.posterInit) return;
el.dataset.posterInit = '1';
var config = {};
try {
config = JSON.parse(el.dataset.aksrtPoster || '{}');
} catch (e) {}
new PosterShare({ triggerSelector: '[data-aksrt-poster]' });
if (qr) ctx.drawImage(qr, WIDTH - 200, HEIGHT - 200, 120, 120);
ctx.fillStyle = '#b4b4b4'; ctx.font = '22px ' + FONT; ctx.textAlign = 'center';
ctx.fillText(data.description || '扫一扫 立即查看', WIDTH / 2, HEIGHT - 52);
return canvas.toDataURL('image/png');
});
}
jQuery(document).off('auto_fun.aksrt-poster-share').on('auto_fun.aksrt-poster-share', autoInit);
function modalId(postId) { return 'modal_poster_' + postId; }
/**
* Generate a poster directly from a unified share payload, without binding
* to DOM triggers. Used by post-share.js for the "生成海报" share item.
*
* @param {Object} payload Canonical share payload (AksrtShareData shape).
*/
PosterShare.open = function (payload) {
payload = payload || {};
var inst = Object.create(PosterShare.prototype);
inst.options = {
width: 750,
height: 1334,
backgroundColor: '#ffffff',
textColor: '#333333',
accentColor: '#1890ff'
};
inst.generate(Object.assign({}, inst.options, {
title: payload.title || document.title,
excerpt: payload.excerpt || payload.desc || '',
author: payload.author || '',
date: payload.date || '',
siteName: payload.siteName || '',
qrCodeUrl: payload.qrCodeUrl || payload.qrcode || ''
}));
};
function createModal(postId) {
var id = modalId(postId);
var html = '<div class="modal fade flex jc" id="' + id + '" tabindex="-1" role="dialog" aria-hidden="true">' +
'<div class="modal-dialog poster-share hover-show" role="document"><div class="modal-content" style="height:220px;overflow:hidden">' +
'<div class="poster-imgbox hover-show"></div><div class="flex jc loading-mask absolute main-bg radius8">' +
'<div class="loading zts fa-2x"></div><div class="muted-2-color box-body">正在生成图片,请稍候...</div></div></div>' +
'<div class="padding-6 abs-center hover-show-con text-center poster-share-actions" style="top:auto">' +
'<a href="#" class="but cir jb-blue poster-download hide"><i class="fa fas fa-download" aria-hidden="true"></i></a>' +
'<button type="button" data-dismiss="modal" class="but cir jb-yellow ml10"><svg aria-hidden="true" class="icon em12"><use xlink:href="#icon-close"></use></svg></button>' +
'</div></div></div>';
$('body').append(html);
return $('#' + id);
}
window.AksrtPosterShare = PosterShare;
})();
function showPoster($modal, postId, src) {
var $box = $modal.find('.poster-imgbox').empty().append($('<img>', { src: src, alt: '分享海报', class: 'loaded-img' })).addClass('loaded');
$modal.find('.poster-download').attr({ href: src, download: 'poster_' + postId + '.png' }).removeClass('hide');
$modal.find('.loading-mask').fadeOut(160);
$modal.find('.modal-content').css({ height: $box.outerHeight(), transition: '.2s' });
window.setTimeout(function () { $modal.find('.modal-content').css({ height: '', overflow: '', transition: '' }); }, 220);
}
function requestPoster($trigger, postId, $modal) {
$.ajax({
type: 'POST', url: ajaxUrl(), dataType: 'json',
data: { action: 'aksrt_poster_share_data', id: postId, nonce: nonce() }
}).done(function (response) {
if (!response || !response.success || !response.data) {
notice((response && response.data && response.data.message) || '海报数据加载失败', 'danger');
$modal.modal('hide');
$modal.remove();
return;
}
renderPoster(response.data).then(function (src) { showPoster($modal, postId, src); }).catch(function () {
notice('海报加载失败,请检查图片跨域设置', 'danger');
$modal.modal('hide');
$modal.remove();
});
}).fail(function (xhr) {
var message = xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message;
notice(message || '海报数据加载失败', 'danger');
$modal.modal('hide');
}).always(function () { $trigger.removeAttr('disabled'); });
}
$('body').off('click.aksrtPosterShare', '[poster-share]').on('click.aksrtPosterShare', '[poster-share]', function (event) {
event.preventDefault();
var $trigger = $(this);
var postId = parseInt($trigger.attr('poster-share'), 10) || 0;
if (!postId || $trigger.attr('disabled')) return;
var $modal = $('#' + modalId(postId));
if ($modal.length) { $modal.modal('show'); return; }
$trigger.attr('disabled', 'disabled');
$('.modal').modal('hide');
$modal = createModal(postId);
$modal.modal('show');
requestPoster($trigger, postId, $modal);
});
})(window, document, window.jQuery);
-108
View File
@@ -1,108 +0,0 @@
/**
* AKSRT Share Data Builder
*
* Single source of truth for share payloads. Every share surface
* (dropdown links, copy, WeChat bridge, poster) resolves its data through
* window.AksrtShareData so no script assembles title/url/cover/qrcode on its
* own.
*
* Canonical payload shape:
* { title, url, desc, text, image, qrcode, author, date, siteName }
*
* Data is read from the nearest [data-share-*] context (falling back to
* #share-dropdown), then to OpenGraph/meta tags and document defaults.
*
* @package AKSRT
*/
(function () {
'use strict';
function metaContent(selectors) {
for (var i = 0; i < selectors.length; i += 1) {
var node = document.querySelector(selectors[i]);
var content = node ? node.getAttribute('content') : '';
if (content) return content;
}
return '';
}
function siteName() {
return (
metaContent(['meta[property="og:site_name"]']) ||
(document.querySelector('.site-title, .logo-text, .navbar-logo') || {}).textContent ||
document.title ||
''
).trim();
}
/**
* Find the element that carries the share data attributes for a node.
*/
function context(node) {
var source = node && typeof node.closest === 'function'
? node.closest('[data-share-url], [data-share-title], [data-share-desc], [data-share-image], [data-aksrt-poster]')
: null;
return source || document.getElementById('share-dropdown');
}
/**
* Generate a QR data URL for a link, reusing the WeChat share encoder
* when available. Returns '' if no encoder is loaded.
*/
function qrcode(url) {
if (window.AksrtShare && typeof window.AksrtShare.buildQrDataUrl === 'function' && url) {
return window.AksrtShare.buildQrDataUrl(url, 200) || '';
}
return '';
}
/**
* Resolve a canonical share payload for a triggering node.
*
* @param {Element} node Triggering element (button/link).
* @param {Object} [overrides] Optional explicit overrides.
* @returns {Object}
*/
function resolve(node, overrides) {
var ctx = context(node);
function attr(name) {
return (node && node.getAttribute && node.getAttribute(name)) ||
(ctx && ctx.getAttribute && ctx.getAttribute(name)) || '';
}
var url = attr('data-share-url') || window.location.href;
var payload = {
title: attr('data-share-title') || document.title,
url: url,
desc: attr('data-share-desc') || metaContent(['meta[property="og:description"]', 'meta[name="description"]']),
text: attr('data-share-text') || '',
image: attr('data-share-image') || metaContent(['meta[property="og:image"]']),
qrcode: attr('data-share-qrcode') || '',
author: attr('data-share-author') || '',
date: attr('data-share-date') || '',
siteName: attr('data-share-site') || siteName()
};
if (!payload.qrcode) {
payload.qrcode = qrcode(payload.url);
}
if (overrides && typeof overrides === 'object') {
Object.keys(overrides).forEach(function (key) {
if (overrides[key] !== undefined && overrides[key] !== null && overrides[key] !== '') {
payload[key] = overrides[key];
}
});
}
return payload;
}
window.AksrtShareData = {
resolve: resolve,
qrcode: qrcode,
siteName: siteName
};
})();
-481
View File
@@ -1,481 +0,0 @@
/**
* AKSRT Weixin Share
*
* Bridge-first WeChat share helper with QR fallback.
*
* @package AKSRT
*/
(function () {
'use strict';
/**
* Local QR code data URL generator — no third-party services.
* Uses canvas rendering with a minimal QR matrix encoder.
*/
function buildQRCodeDataUrl(text, size) {
size = size || 160;
try {
var matrix = generateQRMatrix(text || ' ');
if (!matrix) return '';
var canvas = document.createElement('canvas');
var moduleCount = matrix.length;
var scale = Math.floor(size / (moduleCount + 8));
var margin = Math.floor((size - scale * moduleCount) / 2);
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#000000';
for (var r = 0; r < moduleCount; r++) {
for (var c = 0; c < moduleCount; c++) {
if (matrix[r][c]) {
ctx.fillRect(margin + c * scale, margin + r * scale, scale, scale);
}
}
}
return canvas.toDataURL('image/png');
} catch (e) {
return '';
}
}
/**
* Minimal QR Code matrix generator (Version 1-6, Byte mode, Error Correction L).
* Returns a 2D boolean array or null on failure.
*/
function generateQRMatrix(text) {
var data = encodeUTF8(text);
// Choose version based on data length (EC level L, Byte mode)
var version = 1;
var capacities = [0, 17, 32, 53, 78, 106, 134];
for (var v = 1; v <= 6; v++) {
if (data.length <= capacities[v]) { version = v; break; }
if (v === 6) return null; // data too long for small versions
}
var moduleCount = version * 4 + 17;
var matrix = createMatrix(moduleCount);
var reserved = createMatrix(moduleCount);
// Place finder patterns
placeFinder(matrix, reserved, 0, 0);
placeFinder(matrix, reserved, moduleCount - 7, 0);
placeFinder(matrix, reserved, 0, moduleCount - 7);
// Place alignment patterns (version >= 2)
if (version >= 2) {
var alignPos = getAlignmentPositions(version);
for (var i = 0; i < alignPos.length; i++) {
for (var j = 0; j < alignPos.length; j++) {
if (isInFinder(alignPos[i], alignPos[j], moduleCount)) continue;
placeAlignment(matrix, reserved, alignPos[i], alignPos[j]);
}
}
}
// Place timing patterns
for (var i = 8; i < moduleCount - 8; i++) {
if (!reserved[6][i]) { matrix[6][i] = (i % 2 === 0); reserved[6][i] = true; }
if (!reserved[i][6]) { matrix[i][6] = (i % 2 === 0); reserved[i][6] = true; }
}
// Reserve format info areas
for (var i = 0; i < 8; i++) {
reserved[8][i] = true; reserved[i][8] = true;
reserved[8][moduleCount - 1 - i] = true; reserved[moduleCount - 1 - i][8] = true;
}
reserved[8][8] = true;
if (version >= 2) {
for (var i = moduleCount - 11; i < moduleCount - 8; i++) {
for (var j = 0; j < 6; j++) { reserved[j][i] = true; reserved[i][j] = true; }
}
}
// Dark module
matrix[moduleCount - 8][8] = true;
reserved[moduleCount - 8][8] = true;
// Encode data
var dataBits = encodeData(data, version);
placeData(matrix, reserved, dataBits, moduleCount);
// Apply best mask
var bestMask = 0;
var bestPenalty = Infinity;
for (var m = 0; m < 8; m++) {
var masked = applyMask(matrix, reserved, m, moduleCount);
var penalty = calcPenalty(masked, moduleCount);
if (penalty < bestPenalty) { bestPenalty = penalty; bestMask = m; }
}
matrix = applyMask(matrix, reserved, bestMask, moduleCount);
// Place format info
placeFormatInfo(matrix, bestMask, moduleCount);
return matrix;
}
function createMatrix(n) {
var m = [];
for (var i = 0; i < n; i++) { m[i] = []; for (var j = 0; j < n; j++) m[i][j] = false; }
return m;
}
function encodeUTF8(str) {
var bytes = [];
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
if (c < 0x80) bytes.push(c);
else if (c < 0x800) { bytes.push(0xc0 | (c >> 6)); bytes.push(0x80 | (c & 0x3f)); }
else { bytes.push(0xe0 | (c >> 12)); bytes.push(0x80 | ((c >> 6) & 0x3f)); bytes.push(0x80 | (c & 0x3f)); }
}
return bytes;
}
function placeFinder(m, r, row, col) {
for (var dr = -1; dr <= 7; dr++) {
for (var dc = -1; dc <= 7; dc++) {
var rr = row + dr, cc = col + dc;
if (rr < 0 || rr >= m.length || cc < 0 || cc >= m.length) continue;
var val = (dr >= 0 && dr <= 6 && (dc === 0 || dc === 6)) || (dc >= 0 && dc <= 6 && (dr === 0 || dr === 6)) || (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4);
m[rr][cc] = val;
r[rr][cc] = true;
}
}
}
function placeAlignment(m, r, row, col) {
for (var dr = -2; dr <= 2; dr++) {
for (var dc = -2; dc <= 2; dc++) {
var val = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0);
m[row + dr][col + dc] = val;
r[row + dr][col + dc] = true;
}
}
}
function getAlignmentPositions(v) {
if (v <= 1) return [];
if (v === 2) return [6, 18];
if (v === 3) return [6, 22];
if (v === 4) return [6, 26];
if (v === 5) return [6, 30];
return [6, 34];
}
function isInFinder(row, col, n) {
return (row < 9 && col < 9) || (row < 9 && col >= n - 8) || (row >= n - 8 && col < 9);
}
function encodeData(data, version) {
var bits = [];
// Mode indicator: Byte = 0100
bits.push(0, 1, 0, 0);
// Character count
var ccBits = version <= 9 ? 8 : 16;
for (var i = ccBits - 1; i >= 0; i--) bits.push((data.length >> i) & 1);
// Data
for (var i = 0; i < data.length; i++) {
for (var j = 7; j >= 0; j--) bits.push((data[i] >> j) & 1);
}
// Terminator
var capacities = [0, 152, 272, 440, 640, 864, 1088];
var cap = capacities[version] || 152;
for (var i = 0; i < 4 && bits.length < cap; i++) bits.push(0);
// Pad to byte boundary
while (bits.length % 8 !== 0) bits.push(0);
// Pad bytes
var padBytes = [0xEC, 0x11];
var pi = 0;
while (bits.length < cap) {
var pb = padBytes[pi % 2];
for (var j = 7; j >= 0; j--) bits.push((pb >> j) & 1);
pi++;
}
return bits;
}
function placeData(matrix, reserved, bits, n) {
var bitIndex = 0;
for (var right = n - 1; right >= 1; right -= 2) {
if (right === 6) right = 5;
for (var vert = 0; vert < n; vert++) {
for (var j = 0; j < 2; j++) {
var col = right - j;
var upward = ((right + 1) & 2) === 0;
var row = upward ? n - 1 - vert : vert;
if (!reserved[row][col] && bitIndex < bits.length) {
matrix[row][col] = bits[bitIndex] === 1;
bitIndex++;
}
}
}
}
}
function applyMask(matrix, reserved, maskNum, n) {
var result = createMatrix(n);
for (var r = 0; r < n; r++) {
for (var c = 0; c < n; c++) {
result[r][c] = matrix[r][c];
if (!reserved[r][c]) {
var invert = false;
switch (maskNum) {
case 0: invert = (r + c) % 2 === 0; break;
case 1: invert = r % 2 === 0; break;
case 2: invert = c % 3 === 0; break;
case 3: invert = (r + c) % 3 === 0; break;
case 4: invert = (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0; break;
case 5: invert = (r * c) % 2 + (r * c) % 3 === 0; break;
case 6: invert = ((r * c) % 2 + (r * c) % 3) % 2 === 0; break;
case 7: invert = ((r + c) % 2 + (r * c) % 3) % 2 === 0; break;
}
if (invert) result[r][c] = !result[r][c];
}
}
}
return result;
}
function calcPenalty(matrix, n) {
var penalty = 0;
// Rule 1: consecutive same-color modules in row/column
for (var r = 0; r < n; r++) {
var run = 1;
for (var c = 1; c < n; c++) {
if (matrix[r][c] === matrix[r][c - 1]) { run++; } else { run = 1; }
if (run === 5) penalty += 3;
else if (run > 5) penalty += 1;
}
}
for (var c = 0; c < n; c++) {
var run = 1;
for (var r = 1; r < n; r++) {
if (matrix[r][c] === matrix[r - 1][c]) { run++; } else { run = 1; }
if (run === 5) penalty += 3;
else if (run > 5) penalty += 1;
}
}
return penalty;
}
function placeFormatInfo(matrix, maskNum, n) {
var formatInfo = getFormatInfo(maskNum);
// Place around top-left finder
var positions = [
[8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8],
[7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8]
];
for (var i = 0; i < 15; i++) {
var r = positions[i][0], c = positions[i][1];
matrix[r][c] = formatInfo[i] === 1;
}
// Place around bottom-left and top-right finders
var positions2 = [
[n - 1, 8], [n - 2, 8], [n - 3, 8], [n - 4, 8], [n - 5, 8], [n - 6, 8], [n - 7, 8],
[8, n - 8], [8, n - 7], [8, n - 6], [8, n - 5], [8, n - 4], [8, n - 3], [8, n - 2], [8, n - 1]
];
for (var i = 0; i < 15; i++) {
var r = positions2[i][0], c = positions2[i][1];
matrix[r][c] = formatInfo[i] === 1;
}
}
function getFormatInfo(maskNum) {
// EC level L = 01, mask number = maskNum
var data = (0x01 << 3) | maskNum; // 5 bits
var bits = data << 10;
// BCH(15,5) error correction
var gen = 0x537; // generator polynomial
for (var i = 14; i >= 10; i--) {
if (bits & (1 << i)) bits ^= gen << (i - 10);
}
var formatBits = (data << 10) | bits;
// XOR with mask pattern
formatBits ^= 0x5412;
var result = [];
for (var i = 14; i >= 0; i--) result.push((formatBits >> i) & 1);
return result;
}
function isWeixinEnv() {
return /micromessenger/i.test(window.navigator.userAgent || '') || !!window.WeixinJSBridge;
}
function normalizePayload(payload) {
payload = payload || {};
return {
title: payload.title || document.title,
url: payload.url || window.location.href,
desc: payload.desc || '',
image: payload.image || ''
};
}
function hideQrFallback(qrBox) {
if (!qrBox) return;
qrBox.classList.remove('is-visible');
qrBox.setAttribute('aria-hidden', 'true');
}
function showQrFallback(payload) {
var qrBox = payload.qrBox;
var qrImage = payload.qrImage;
if (!qrBox || !qrImage || !payload.url) {
return Promise.resolve({
mode: 'error',
message: '当前页面缺少微信二维码容器'
});
}
if (qrImage.dataset.qrValue !== payload.url || !qrImage.getAttribute('src')) {
qrImage.dataset.qrValue = payload.url;
var dataUrl = buildQRCodeDataUrl(payload.url, 160);
if (dataUrl) {
qrImage.src = dataUrl;
} else {
qrImage.alt = '二维码生成失败,请直接复制链接分享';
}
}
qrBox.classList.add('is-visible');
qrBox.setAttribute('aria-hidden', 'false');
return Promise.resolve({
mode: 'qr',
message: '请使用微信扫一扫分享'
});
}
function bridgeWithWxSdk(payload) {
return new Promise(function (resolve, reject) {
if (!window.wx || typeof window.wx.ready !== 'function') {
reject(new Error('wx sdk unavailable'));
return;
}
var done = false;
var timer = window.setTimeout(function () {
if (done) return;
done = true;
reject(new Error('微信分享桥接未就绪'));
}, 1200);
function finishSuccess() {
if (done) return;
done = true;
window.clearTimeout(timer);
resolve({
mode: 'bridge',
message: '已同步微信分享信息,请使用右上角菜单继续分享'
});
}
function finishError(error) {
if (done) return;
done = true;
window.clearTimeout(timer);
reject(error instanceof Error ? error : new Error('微信分享桥接失败'));
}
try {
if (typeof window.wx.error === 'function') {
window.wx.error(function (error) {
finishError(new Error((error && error.errMsg) || '微信签名校验失败'));
});
}
window.wx.ready(function () {
try {
if (typeof window.wx.updateAppMessageShareData === 'function') {
window.wx.updateAppMessageShareData({
title: payload.title,
desc: payload.desc,
link: payload.url,
imgUrl: payload.image
});
}
if (typeof window.wx.updateTimelineShareData === 'function') {
window.wx.updateTimelineShareData({
title: payload.title,
link: payload.url,
imgUrl: payload.image
});
}
finishSuccess();
} catch (error) {
finishError(error);
}
});
} catch (error) {
finishError(error);
}
});
}
function bridgeWithWeixinJsBridge(payload) {
return new Promise(function (resolve, reject) {
var bridge = window.WeixinJSBridge;
if (!bridge || typeof bridge.invoke !== 'function') {
reject(new Error('WeixinJSBridge unavailable'));
return;
}
try {
bridge.invoke('sendAppMessage', {
title: payload.title,
desc: payload.desc,
link: payload.url,
img_url: payload.image
}, function (response) {
var errMsg = response && response.err_msg ? response.err_msg : '';
if (!errMsg || /:ok$/i.test(errMsg)) {
resolve({
mode: 'bridge',
message: '已调起微信分享面板'
});
return;
}
reject(new Error(errMsg));
});
} catch (error) {
reject(error);
}
});
}
function openWeixinShare(payload) {
var data = normalizePayload(payload);
data.qrBox = payload && payload.qrBox ? payload.qrBox : null;
data.qrImage = payload && payload.qrImage ? payload.qrImage : null;
if (!isWeixinEnv()) {
return showQrFallback(data);
}
return bridgeWithWxSdk(data)
.catch(function () {
return bridgeWithWeixinJsBridge(data);
})
.catch(function (error) {
return showQrFallback(data).then(function (result) {
result.message = (error && error.message ? error.message + ',' : '') + '已切换为二维码分享';
return result;
});
});
}
window.AksrtShare = Object.assign({}, window.AksrtShare || {}, {
isWeixinEnv: isWeixinEnv,
hideQrFallback: hideQrFallback,
showQrFallback: showQrFallback,
openWeixinShare: openWeixinShare,
// Exposed so the unified share-data-builder can render poster QR codes
// without a second encoder.
buildQrDataUrl: buildQRCodeDataUrl
});
})();
+1
View File
@@ -33,6 +33,7 @@ require_once AKSRT_DIR . '/inc/helpers/homepage/content.php';
require_once AKSRT_DIR . '/inc/helpers/homepage/header-components.php';
require_once AKSRT_DIR . '/inc/helpers/homepage/layout.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-single-components.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-share.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-single.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-yiyan.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-single-render.php';
-4
View File
@@ -61,10 +61,6 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
'yiyan_footer' => false,
'yiyan_box' => false,
];
// Normalize legacy aliases → canonical render keys (idempotent).
if (!array_key_exists('share_enabled', $settings) && array_key_exists('share_s', $settings)) {
$settings['share_enabled'] = !empty($settings['share_s']);
}
if (!array_key_exists('hide_time', $settings) && array_key_exists('single_hide_time_s', $settings)) {
$settings['hide_time'] = !empty($settings['single_hide_time_s']);
}
+262
View File
@@ -0,0 +1,262 @@
<?php
/**
* Social sharing and poster runtime.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_share_platform_labels')) {
/** @return array<string,string> */
function aksrt_share_platform_labels(): array {
return [
'qzone' => __('QQ空间', 'aksrt'), 'weibo' => __('微博', 'aksrt'), 'qq' => __('QQ好友', 'aksrt'),
'facebook' => 'Facebook', 'twitter' => 'X', 'telegram' => 'Telegram', 'whatsapp' => 'WhatsApp',
'linkedin' => 'LinkedIn', 'reddit' => 'Reddit', 'line' => 'LINE', 'vk' => 'VK',
'email' => __('邮件', 'aksrt'), 'poster' => __('海报分享', 'aksrt'), 'copy' => __('复制链接', 'aksrt'),
];
}
}
if (!function_exists('aksrt_active_share_items')) {
/** @return string[] */
function aksrt_active_share_items(): array {
$config = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_share_settings() : [];
return !empty($config['enabled']) && is_array($config['items'] ?? null) ? $config['items'] : [];
}
}
if (!function_exists('aksrt_build_share_url')) {
/** @param array<string,string> $params */
function aksrt_build_share_url(string $base, array $params): string {
$query = [];
foreach ($params as $key => $value) {
if ($value !== '') {
$query[] = rawurlencode($key) . '=' . rawurlencode($value);
}
}
return $query ? $base . (str_contains($base, '?') ? '&' : '?') . implode('&', $query) : $base;
}
}
if (!function_exists('aksrt_share_context')) {
/** @return array{id:int,title:string,link_title:string,desc:string,image:string,url:string,tags:string} */
function aksrt_share_context($post = null): array {
$post = get_post($post);
if (!$post instanceof WP_Post) {
return ['id' => 0, 'title' => '', 'link_title' => '', 'desc' => '', 'image' => '', 'url' => '', 'tags' => ''];
}
$subtitle = class_exists('\\AKSRT\\Core\\PostMetaBox')
? trim((string) \AKSRT\Core\PostMetaBox::get_meta_value((int) $post->ID, 'subtitle', ''))
: '';
$title = trim(wp_strip_all_tags(get_the_title($post))) . $subtitle;
$desc = trim(wp_strip_all_tags((string) get_the_excerpt($post)));
if ($desc === '') {
$desc = wp_trim_words(wp_strip_all_tags(strip_shortcodes($post->post_content)), 70, '...');
}
$url = (string) (get_permalink($post) ?: '');
if (is_user_logged_in() && class_exists('\\AKSRT\\Settings\\Settings') && \AKSRT\Settings\Settings::affiliate_enabled()
&& class_exists('\\AKSRT\\Payment\\ReferralAttribution')) {
$link = \AKSRT\Payment\ReferralAttribution::get_instance()->create_link(get_current_user_id(), $url);
if (!empty($link['url'])) {
$url = (string) $link['url'];
}
}
$tags = '';
if ($post->post_type === 'post') {
$author = get_the_author_meta('display_name', (int) $post->post_author);
$cats = get_the_category((int) $post->ID);
$tags = sprintf(__('作者: %s', 'aksrt'), $author);
if (!empty($cats[0])) {
$tags .= sprintf(__(' · 分类: %s', 'aksrt'), $cats[0]->name);
}
}
$site = (string) get_bloginfo('name');
return [
'id' => (int) $post->ID,
'title' => $title,
'link_title' => $title . ($site !== '' ? ' - ' . $site : ''),
'desc' => $desc,
'image' => (string) (get_the_post_thumbnail_url($post, 'full') ?: ''),
'url' => $url,
'tags' => $tags,
];
}
}
if (!function_exists('aksrt_share_platform_href')) {
/** @param array<string,mixed> $ctx */
function aksrt_share_platform_href(string $key, array $ctx): string {
$url = (string) ($ctx['url'] ?? '');
$title = (string) ($ctx['link_title'] ?? '');
$desc = (string) ($ctx['desc'] ?? '');
$image = (string) ($ctx['image'] ?? '');
switch ($key) {
case 'qzone': return aksrt_build_share_url('https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey', ['url' => $url, 'title' => $title, 'pics' => $image, 'summary' => $desc]);
case 'weibo': return aksrt_build_share_url('https://service.weibo.com/share/share.php', ['url' => $url, 'title' => $title, 'pic' => $image, 'searchPic' => 'false']);
case 'qq': return aksrt_build_share_url('https://connect.qq.com/widget/shareqq/index.html', ['url' => $url, 'title' => $title, 'pics' => $image, 'desc' => $desc]);
case 'facebook': return aksrt_build_share_url('https://www.facebook.com/sharer/sharer.php', ['u' => $url]);
case 'twitter': return aksrt_build_share_url('https://twitter.com/intent/tweet', ['url' => $url, 'text' => $title]);
case 'telegram': return aksrt_build_share_url('https://t.me/share/url', ['url' => $url, 'text' => $title]);
case 'whatsapp': return aksrt_build_share_url('https://api.whatsapp.com/send', ['text' => trim($title . ' ' . $url)]);
case 'linkedin': return aksrt_build_share_url('https://www.linkedin.com/sharing/share-offsite/', ['url' => $url]);
case 'reddit': return aksrt_build_share_url('https://www.reddit.com/submit', ['url' => $url, 'title' => $title]);
case 'line': return aksrt_build_share_url('https://social-plugins.line.me/lineit/share', ['url' => $url]);
case 'vk': return aksrt_build_share_url('https://vk.com/share.php', ['url' => $url, 'title' => $title, 'image' => $image]);
case 'email': return aksrt_build_share_url('mailto:', ['subject' => $title, 'body' => trim($title . "\n" . $url . ($desc !== '' ? "\n\n" . $desc : ''))]);
default: return '#';
}
}
}
if (!function_exists('aksrt_get_share_buttons')) {
/** @param string[] $items */
function aksrt_get_share_buttons(array $items, $post = null): string {
$ctx = aksrt_share_context($post);
if ($ctx['id'] <= 0 || $ctx['url'] === '') {
return '';
}
$labels = aksrt_share_platform_labels();
$html = '';
foreach ($items as $key) {
$key = sanitize_key((string) $key);
if (!isset($labels[$key])) {
continue;
}
$icon_key = $key === 'email' ? 'd-email' : $key . '-color';
$icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-' . $icon_key) : '';
$attr = '';
$target = ' target="_blank"';
$href = aksrt_share_platform_href($key, $ctx);
if ($key === 'poster') {
$target = '';
$attr = ' poster-share="' . esc_attr((string) $ctx['id']) . '"';
} elseif ($key === 'copy') {
$target = '';
$attr = ' data-clipboard-text="' . esc_attr($ctx['url']) . '" data-clipboard-tag="' . esc_attr__('链接', 'aksrt') . '"';
}
$safe_href = $href === '#' ? '#' : esc_url($href);
$html .= '<a rel="nofollow noopener" class="share-btn ' . esc_attr($key) . '"' . $target . $attr
. ' title="' . esc_attr($labels[$key]) . '" href="' . $safe_href . '"><icon>' . $icon
. '</icon><text>' . esc_html($labels[$key]) . '</text></a>';
}
return $html;
}
}
if (!function_exists('aksrt_get_share_html')) {
function aksrt_get_share_html(string $class = '', $post = null): string {
$buttons = aksrt_get_share_buttons(aksrt_active_share_items(), $post);
return $buttons === '' ? '' : '<div' . ($class !== '' ? ' class="' . esc_attr($class) . '"' : '') . '>' . $buttons . '</div>';
}
}
if (!function_exists('aksrt_render_share_buttons')) {
/** @param array<string,mixed> $args */
function aksrt_render_share_buttons(array $args = []): void {
echo aksrt_get_share_html(!empty($args['horizontal']) ? 'horizontal' : '', $args['post'] ?? null); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
if (!function_exists('aksrt_get_post_share_btn')) {
function aksrt_get_post_share_btn($post = null, string $class = '', bool $modal = false, bool $show_text = true): string {
$post = get_post($post);
if (!$post instanceof WP_Post || aksrt_active_share_items() === []) {
return '';
}
$icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-share') : '<i class="fa fas fa-share-alt"></i>';
$text = $show_text ? '<text>' . esc_html__('分享', 'aksrt') . '</text>' : '';
if ($modal || wp_is_mobile()) {
$count = max(1, count(aksrt_active_share_items()));
$url = add_query_arg(['action' => 'aksrt_share_modal', 'id' => (int) $post->ID, 'nonce' => wp_create_nonce('aksrt_public_nonce')], admin_url('admin-ajax.php'));
return '<a class="' . esc_attr(trim($class . ' aksrt-modal-link')) . '" href="' . esc_url($url) . '" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($url)
. '" data-aksrt-modal-class="modal-mini" data-aksrt-modal-height="' . esc_attr((string) min(520, 180 + (int) ceil($count / 5) * 72))
. '" data-aksrt-modal-mobile-bottom="true" data-aksrt-modal-cache="false">' . $icon . $text . '</a>';
}
return '<span class="hover-show dropup ' . esc_attr($class) . '">' . $icon . $text
. '<div class="zib-widget hover-show-con share-button dropdown-menu">' . aksrt_get_share_html('', $post) . '</div></span>';
}
}
if (!function_exists('aksrt_share_modal_ajax')) {
function aksrt_share_modal_ajax(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$post = get_post(absint($_REQUEST['id'] ?? 0));
if (!$post instanceof WP_Post || get_post_status($post) !== 'publish') {
status_header(404);
echo '<div class="box-body text-center">' . esc_html__('内容不存在或参数错误', 'aksrt') . '</div>';
exit;
}
$icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-share', 'em14') : '';
echo '<div class="modal-colorful-header colorful-bg jb-vip1"><button type="button" class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">'
. (function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-close', 'ic-close') : '&times;')
. '</button><div class="colorful-make"></div><div class="text-center"><div class="em2x">' . $icon
. '</div><div class="mt10 em12 padding-w10"><span class="em09">' . esc_html__('分享内容', 'aksrt') . '</span></div></div></div>'
. aksrt_get_share_html('horizontal', $post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit;
}
add_action('wp_ajax_aksrt_share_modal', 'aksrt_share_modal_ajax');
add_action('wp_ajax_nopriv_aksrt_share_modal', 'aksrt_share_modal_ajax');
}
if (!function_exists('aksrt_share_image_data_url')) {
function aksrt_share_image_data_url(string $url): string {
if ($url === '' || !wp_http_validate_url($url)) {
return $url;
}
$key = 'aksrt_share_img_' . md5($url);
$cached = wp_cache_get($key, 'aksrt_share');
if (is_string($cached)) {
return $cached;
}
$response = wp_safe_remote_get($url, ['timeout' => 8, 'redirection' => 3, 'limit_response_size' => 5 * MB_IN_BYTES]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return $url;
}
$mime = strtolower(trim((string) wp_remote_retrieve_header($response, 'content-type')));
$mime = trim(explode(';', $mime)[0]);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
return $url;
}
$body = wp_remote_retrieve_body($response);
if ($body === '') {
return $url;
}
$data = 'data:' . $mime . ';base64,' . base64_encode($body);
wp_cache_set($key, $data, 'aksrt_share', HOUR_IN_SECONDS);
return $data;
}
}
if (!function_exists('aksrt_poster_share_data_ajax')) {
function aksrt_poster_share_data_ajax(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$post = get_post(absint($_POST['id'] ?? 0));
if (!$post instanceof WP_Post || get_post_status($post) !== 'publish') {
wp_send_json_error(['message' => __('参数错误,或内容已删除', 'aksrt')], 404);
}
$config = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_share_settings() : [];
if (empty($config['poster_enabled'])) {
wp_send_json_error(['message' => __('海报分享未启用', 'aksrt')], 403);
}
$ctx = aksrt_share_context($post);
$data = [
'url' => esc_url_raw($ctx['url']), 'banner' => esc_url_raw($ctx['image']),
'banner_default' => esc_url_raw((string) ($config['poster_img'] ?? '')),
'banner_spare' => get_template_directory_uri() . '/assets/img/share_img.jpg',
'logo' => esc_url_raw((string) ($config['poster_logo'] ?? '')),
'title' => function_exists('aksrt_str_cut') ? aksrt_str_cut($ctx['title'], 0, 32) : $ctx['title'],
'content' => function_exists('aksrt_str_cut') ? aksrt_str_cut($ctx['desc'], 0, 70) : $ctx['desc'],
'tags' => $ctx['tags'], 'description' => (string) ($config['share_desc'] ?? __('扫一扫 立即查看', 'aksrt')),
];
if (!empty($config['compatible_s'])) {
foreach (['banner', 'banner_default', 'logo'] as $key) {
$data[$key] = aksrt_share_image_data_url((string) $data[$key]);
}
}
wp_send_json_success(apply_filters('aksrt_poster_share_data', $data, $post));
}
add_action('wp_ajax_aksrt_poster_share_data', 'aksrt_poster_share_data_ajax');
add_action('wp_ajax_nopriv_aksrt_poster_share_data', 'aksrt_poster_share_data_ajax');
}
+1 -1
View File
@@ -141,7 +141,7 @@ if (!function_exists('aksrt_get_single_meta_box')) :
if ($edit_link) {
$more .= '<a href="' . esc_url($edit_link) . '" class="post-drop-meta__item"><i class="fa fas fa-edit" aria-hidden="true"></i><span>' . esc_html__('编辑文章', 'aksrt') . '</span></a>';
}
$more .= '<button type="button" class="post-drop-meta__item" data-share-copy data-share-url="' . esc_url((string) $permalink) . '"><i class="fa fas fa-link" aria-hidden="true"></i><span>' . esc_html__('复制链接', 'aksrt') . '</span></button>';
$more .= '<button type="button" class="post-drop-meta__item" data-clipboard-text="' . esc_attr((string) $permalink) . '" data-clipboard-tag="' . esc_attr__('链接', 'aksrt') . '"><i class="fa fas fa-link" aria-hidden="true"></i><span>' . esc_html__('复制链接', 'aksrt') . '</span></button>';
$post_author_id = (int) get_post_field('post_author', $post_id);
if ($ban_item === '' && is_user_logged_in() && !(function_exists('aksrt_is_user_ban_restricted') && $post_author_id > 0 && aksrt_is_user_ban_restricted($post_author_id))) {
$report_payload = wp_json_encode([
+2 -44
View File
@@ -178,40 +178,6 @@ if (!function_exists('aksrt_single_render_box_header')) :
}
endif;
if (!function_exists('aksrt_render_share_dropdown')) :
/**
* Shared #share-dropdown markup (actions row + tabbar fallback).
*
* @param array<string,mixed> $attrs Optional data-share-* overrides.
*/
function aksrt_render_share_dropdown(array $attrs = []): void {
static $printed = false;
if ($printed) {
return;
}
$printed = true;
$title = (string) ($attrs['title'] ?? get_the_title());
$url = (string) ($attrs['url'] ?? (get_permalink() ?: ''));
$image = (string) ($attrs['image'] ?? (get_the_post_thumbnail_url(null, 'large') ?: ''));
?>
<div class="share-dropdown" id="share-dropdown" aria-hidden="true"
data-share-title="<?php echo esc_attr($title); ?>"
data-share-url="<?php echo esc_url($url); ?>"
<?php if ($image !== '') : ?>data-share-image="<?php echo esc_url($image); ?>"<?php endif; ?>>
<div class="share-dropdown__grid">
<button type="button" class="share-item" data-platform="qq"><i class="fa fab fa-qq"></i><span>QQ</span></button>
<button type="button" class="share-item" data-platform="weibo"><i class="fa fab fa-weibo"></i><span>微博</span></button>
<button type="button" class="share-item" data-platform="twitter"><i class="fa fab fa-twitter"></i><span>X</span></button>
<button type="button" class="share-item" data-platform="wechat"><i class="fa fab fa-weixin"></i><span>微信</span></button>
<button type="button" class="share-item" data-platform="copy"><i class="fa fas fa-link"></i><span><?php esc_html_e('复制链接', 'aksrt'); ?></span></button>
<button type="button" class="share-item" data-platform="poster"><i class="fa fas fa-image"></i><span><?php esc_html_e('海报分享', 'aksrt'); ?></span></button>
</div>
<div id="share-qrcode-box" aria-hidden="true"><img id="share-qrcode-image" src="" alt="QR Code"></div>
</div>
<?php
}
endif;
if (!function_exists('aksrt_single_render_actions')) :
/**
* Like / reward / share / favorite row.
@@ -247,11 +213,8 @@ if (!function_exists('aksrt_single_render_actions')) :
}
?>
<?php if (!empty($settings['share_enabled'])) : ?>
<button type="button" class="action action-share" id="action-share-trigger" aria-haspopup="true" aria-expanded="false">
<i class="fa fas fa-share-alt" aria-hidden="true"></i>
<span class="action-btn__label"><?php esc_html_e('分享', 'aksrt'); ?></span>
</button>
<?php if (!empty($settings['share_enabled']) && function_exists('aksrt_get_post_share_btn')) : ?>
<?php echo aksrt_get_post_share_btn($post_id, 'action action-share'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?>
<?php if (!empty($settings['favorite_enabled'])) : ?>
@@ -261,11 +224,6 @@ if (!function_exists('aksrt_single_render_actions')) :
</button>
<?php endif; ?>
</div>
<?php if (!empty($settings['share_enabled']) && function_exists('aksrt_render_share_dropdown')) : ?>
<?php aksrt_render_share_dropdown();
?>
<?php endif; ?>
</div>
<?php
@@ -448,43 +448,8 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
<?php
endif;
if ($share_on) :
$share_title = get_the_title($post_id);
$share_url = get_permalink($post_id) ?: '';
$share_image = get_the_post_thumbnail_url($post_id, 'large') ?: '';
?>
<button type="button"
class="tabbar-item single-action-tabbar action-share"
data-type="share"
data-share-title="<?php echo esc_attr((string) $share_title); ?>"
data-share-url="<?php echo esc_url((string) $share_url); ?>"
<?php if ($share_image !== '') : ?>data-share-image="<?php echo esc_url((string) $share_image); ?>"<?php endif; ?>
aria-haspopup="true"
aria-expanded="false"
aria-label="<?php esc_attr_e('分享', 'aksrt'); ?>">
<i class="fa fas fa-share-alt" aria-hidden="true"></i>
<?php if ($show_text) : ?><span><?php esc_html_e('分享', 'aksrt'); ?></span><?php endif; ?>
</button>
<button type="button"
class="tabbar-item single-action-tabbar action-poster"
data-type="poster"
data-share-platform="poster"
data-share-title="<?php echo esc_attr((string) $share_title); ?>"
data-share-url="<?php echo esc_url((string) $share_url); ?>"
<?php if ($share_image !== '') : ?>data-share-image="<?php echo esc_url((string) $share_image); ?>"<?php endif; ?>
aria-label="<?php esc_attr_e('海报', 'aksrt'); ?>">
<i class="fa fas fa-image" aria-hidden="true"></i>
<?php if ($show_text) : ?><span><?php esc_html_e('海报', 'aksrt'); ?></span><?php endif; ?>
</button>
<?php
endif;
if ($share_on && function_exists('aksrt_render_share_dropdown')) {
aksrt_render_share_dropdown([
'title' => get_the_title($post_id),
'url' => get_permalink($post_id) ?: '',
'image' => get_the_post_thumbnail_url($post_id, 'large') ?: '',
]);
if ($share_on && function_exists('aksrt_get_post_share_btn')) {
echo aksrt_get_post_share_btn($post_id, 'tabbar-item single-action-tabbar', true, $show_text); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ($comments_open) :
+36
View File
@@ -371,6 +371,42 @@ class GlobalSchema {
]],
],
],
'share' => [
'key' => 'share',
'title' => '社交分享',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [[
'key' => 'share',
'title' => '社交分享',
'fields' => [
['key' => 'share_s', 'label' => '文章分享功能', 'type' => 'switcher', 'default' => true, 'help' => '关闭后,在文章页面将不显示分享按钮。'],
['key' => 'share_items', 'label' => '分享平台', 'type' => 'custom', 'renderer' => 'sorter', 'sanitize_type' => 'array', 'default' => [
'enabled' => ['qzone' => 'QQ空间', 'weibo' => '微博', 'qq' => 'QQ好友', 'poster' => '海报分享', 'copy' => '复制链接'],
'disabled' => ['facebook' => 'Facebook', 'twitter' => 'X', 'telegram' => 'Telegram', 'whatsapp' => 'WhatsApp', 'linkedin' => 'LinkedIn', 'reddit' => 'Reddit', 'line' => 'LINE', 'vk' => 'VK', 'email' => '邮件'],
], 'options' => [
['label' => 'QQ空间', 'value' => 'qzone'], ['label' => '微博', 'value' => 'weibo'], ['label' => 'QQ好友', 'value' => 'qq'],
['label' => 'Facebook', 'value' => 'facebook'], ['label' => 'X', 'value' => 'twitter'], ['label' => 'Telegram', 'value' => 'telegram'],
['label' => 'WhatsApp', 'value' => 'whatsapp'], ['label' => 'LinkedIn', 'value' => 'linkedin'], ['label' => 'Reddit', 'value' => 'reddit'],
['label' => 'LINE', 'value' => 'line'], ['label' => 'VK', 'value' => 'vk'], ['label' => '邮件', 'value' => 'email'],
['label' => '海报分享', 'value' => 'poster'], ['label' => '复制链接', 'value' => 'copy'],
], 'description' => '左侧启用并拖拽排序,右侧为未启用项(同时启用的数量不建议超过 6 个)。若开启海外平台,用户网络必须能正常访问对应服务。'],
['key' => 'wechat_share_s', 'label' => '微信分享有图', 'type' => 'switcher', 'default' => false, 'help' => '使用微信 JSAPI 在微信 App 内分享。'],
['key' => 'wechat_share_hint', 'label' => '', 'type' => 'content', 'ui_only' => true, 'dependsOn' => ['key' => 'wechat_share_s', 'value' => true], 'content' => '<p><strong>微信分享有图配置</strong></p><p>此功能用于用户从公众号进入页面后调用微信默认分享能力,并不会额外显示分享按钮。请在公众号后台将当前站点域名配置为 JS 接口安全域名。</p><p><a href="https://mp.weixin.qq.com/" target="_blank" rel="noopener noreferrer">前往微信公众平台</a></p>'],
['key' => 'wechat_share_option', 'label' => '微信分享有图配置', 'type' => 'fieldset', 'default' => ['appid' => '', 'app_secret' => '', 'only_logo' => false], 'dependsOn' => ['key' => 'wechat_share_s', 'value' => true], 'fields' => [
['key' => 'appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
['key' => 'app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
['key' => 'only_logo', 'label' => '全部使用网站图标', 'type' => 'switcher', 'default' => false, 'help' => '默认自动使用页面封面图;开启后所有页面均使用网站图标。图片启用防盗链时建议开启。'],
]],
['key' => 'share_img', 'label' => '生成海报分享', 'type' => 'switcher', 'default' => true, 'help' => '网站图片使用 OSS 等云存储时,请先配置允许本站 Canvas 读取图片的跨域规则。'],
['key' => 'share_img_warning', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '海报图片加载失败时,请检查图片 CDN/OSS 的跨域响应头。', 'dependsOn' => ['key' => 'share_img', 'value' => true]],
['key' => 'share_img_compatible_s', 'label' => '海报分享兼容模式', 'type' => 'switcher', 'default' => false, 'dependsOn' => ['key' => 'share_img', 'value' => true], 'help' => '通过服务端代理读取海报图片,改善跨域兼容性;会增加服务器图片处理开销。'],
['key' => 'share_img_byimg', 'label' => '海报分享默认图片', 'type' => 'media', 'default' => get_template_directory_uri() . '/assets/img/slider-bg.jpg', 'dependsOn' => ['key' => 'share_img', 'value' => true], 'help' => '文章没有图片时使用,建议尺寸 800×500。'],
['key' => 'share_logo', 'label' => '海报分享 LOGO', 'type' => 'media', 'default' => get_template_directory_uri() . '/assets/img/logo.png', 'dependsOn' => ['key' => 'share_img', 'value' => true], 'help' => '显示在海报底部,建议尺寸 300×100、大小 50KB 以内。'],
['key' => 'share_desc', 'label' => '海报分享底部文案', 'type' => 'input', 'default' => '扫一扫 立即查看', 'dependsOn' => ['key' => 'share_img', 'value' => true]],
],
]],
],
'email' => \AKSRT\Settings\MailTemplates::build_schema(),
'custom-code' => [
'key' => 'custom-code',
-22
View File
@@ -280,28 +280,6 @@ class SchemaPostList {
]],
],
],
'share' => [
'key' => 'share',
'title' => '分享设置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '分享按钮', 'fields' => [
['key' => 'share_s', 'label' => '分享按钮', 'type' => 'switcher', 'default' => true, 'help' => '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响'],
]],
['key' => 'wechat', 'title' => '微信分享', 'fields' => [
['key' => 'share_wechat_s', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
['key' => 'share_wechat_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
['key' => 'share_wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
['key' => 'share_only_logo', 'label' => '仅显示Logo', 'type' => 'switch', 'default' => false],
['key' => 'share_img_compatible_s', 'label' => '海报兼容模式', 'type' => 'switch', 'default' => false, 'description' => '启用后使用兼容模式生成分享海报'],
['key' => 'share_img_byimg', 'label' => '海报默认图', 'type' => 'media', 'default' => ''],
['key' => 'share_logo', 'label' => '海报LOGO', 'type' => 'media', 'default' => ''],
['key' => 'share_img', 'label' => '分享图片', 'type' => 'media', 'default' => ''],
['key' => 'share_desc', 'label' => '分享描述', 'type' => 'textarea', 'default' => '扫一扫 立即查看'],
]],
],
],
'submit' => [
'key' => 'submit',
'title' => '前台投稿',
-28
View File
@@ -551,34 +551,6 @@ trait ScriptsPart1b {
}
}
// Unified share-data builder — only when the article renders share controls.
if (!empty($singular_assets['needs_share'])) {
$share_data_js = get_template_directory() . '/assets/js/share-data-builder.js';
if (file_exists($share_data_js)) {
wp_enqueue_script(
'aksrt-share-data',
$base_uri . '/assets/js/share-data-builder.js',
['loader_js'],
(string) filemtime($share_data_js),
true
);
}
}
// Poster generation is part of the article share dropdown.
if (!empty($singular_assets['needs_share'])) {
$poster_js = get_template_directory() . '/assets/js/poster-share.js';
if (file_exists($poster_js)) {
wp_enqueue_script(
'aksrt-poster-share',
$base_uri . '/assets/js/poster-share.js',
['loader_js', 'aksrt-share-data'],
(string) filemtime($poster_js),
true
);
}
}
// Unified authentication page shell.
if ($is_auth_page) {
$enqueue_module('pages');
+8 -18
View File
@@ -424,26 +424,16 @@ trait ScriptsPart1c {
}
}
// Post/resource share functionality
if (!empty($singular_assets['needs_share'])) {
$weixin_share_path = get_template_directory() . '/assets/js/weixin-share.js';
if (file_exists($weixin_share_path)) {
// Reference-theme share runtime: direct platform links need no JS;
// only poster generation is loaded when a share surface can render.
if (!empty($singular_assets['needs_share']) || is_page_template('templates/pages/page-posts-navs.php')) {
$poster_share_path = get_template_directory() . '/assets/js/poster-share.js';
if (file_exists($poster_share_path)) {
wp_enqueue_script(
'aksrt-weixin-share',
$base_uri . '/assets/js/weixin-share.js',
'aksrt-poster-share',
$base_uri . '/assets/js/poster-share.js',
['loader_js'],
(string) filemtime($weixin_share_path),
true
);
}
$post_share_path = get_template_directory() . '/assets/js/post-share.js';
if (file_exists($post_share_path)) {
wp_enqueue_script(
'aksrt-post-share',
$base_uri . '/assets/js/post-share.js',
['loader_js', 'aksrt-weixin-share', 'aksrt-share-data'],
(string) filemtime($post_share_path),
(string) filemtime($poster_share_path),
true
);
}
+3 -10
View File
@@ -48,10 +48,6 @@ class WeChatShare {
'1.6.0',
true
);
// Ensure our bridge runs after jweixin.
if (wp_script_is('aksrt-weixin-share', 'registered') || wp_script_is('aksrt-weixin-share', 'enqueued')) {
wp_script_add_data('aksrt-weixin-share', 'group', 1);
}
}
public function print_share_date(): void {
@@ -154,12 +150,9 @@ window.WeChatShareDate = <?php echo wp_json_encode($payload, JSON_UNESCAPED_UNIC
private function resolve_image(): string {
$only_logo = !empty($this->config['only_logo']);
$default = '';
if (!empty($this->config['share_img'])) {
$default = (string) $this->config['share_img'];
} elseif (class_exists('\AKSRT\Settings\Settings')) {
$default = (string) (\AKSRT\Settings\Settings::get_value('favicon', '') ?: \AKSRT\Settings\Settings::get_value('iconpng', ''));
}
$default = class_exists('\\AKSRT\\Settings\\Settings')
? (string) \AKSRT\Settings\Settings::favicon_url()
: (string) get_site_icon_url(512);
if ($only_logo) {
return $default;
}
+23 -13
View File
@@ -130,24 +130,34 @@ trait FacadeBulk {
'dropdown' => !empty($option['dropdown']),
];
}
/**
* Share and poster settings.
*
* @return array{wechat_enabled:bool,wechat_appid:string,wechat_app_secret:string,only_logo:bool,compatible_s:bool,poster_img:string,poster_logo:string,share_img:string,share_desc:string}
*/
/** Share and poster settings using the reference theme's canonical keys. */
public static function get_share_settings(): array {
$settings = self::get_all();
$wechat = is_array($settings['wechat_share_option'] ?? null) ? $settings['wechat_share_option'] : [];
$defaults = [
'qzone' => 'QQ空间', 'weibo' => '微博', 'qq' => 'QQ好友', 'poster' => '海报分享', 'copy' => '复制链接',
];
$configured = is_array($settings['share_items'] ?? null) ? $settings['share_items'] : [];
$enabled = is_array($configured['enabled'] ?? null) ? $configured['enabled'] : $defaults;
$allowed = ['qzone', 'weibo', 'qq', 'facebook', 'twitter', 'telegram', 'whatsapp', 'linkedin', 'reddit', 'line', 'vk', 'email', 'poster', 'copy'];
$items = array_values(array_intersect(array_map('sanitize_key', array_keys($enabled)), $allowed));
$poster_enabled = !isset($settings['share_img']) || !empty($settings['share_img']);
if (!$poster_enabled) {
$items = array_values(array_diff($items, ['poster']));
}
return [
'wechat_enabled' => !empty($settings['share_wechat_s']),
'wechat_appid' => sanitize_text_field((string) ($settings['share_wechat_appid'] ?? '')),
'wechat_app_secret' => sanitize_text_field((string) ($settings['share_wechat_app_secret'] ?? '')),
'only_logo' => !empty($settings['share_only_logo']),
'enabled' => !isset($settings['share_s']) || !empty($settings['share_s']),
'items' => $items,
'wechat_enabled' => !empty($settings['wechat_share_s']),
'wechat_appid' => sanitize_text_field((string) ($wechat['appid'] ?? '')),
'wechat_app_secret' => sanitize_text_field((string) ($wechat['app_secret'] ?? '')),
'only_logo' => !empty($wechat['only_logo']),
'poster_enabled' => $poster_enabled,
'compatible_s' => !empty($settings['share_img_compatible_s']),
'poster_img' => esc_url_raw((string) ($settings['share_img_byimg'] ?? '')),
'poster_logo' => esc_url_raw((string) ($settings['share_logo'] ?? '')),
'share_img' => esc_url_raw((string) ($settings['share_img'] ?? '')),
'share_desc' => sanitize_textarea_field((string) ($settings['share_desc'] ?? '')) ?: __('扫一扫 立即查看', 'aksrt'),
'poster_img' => esc_url_raw((string) ($settings['share_img_byimg'] ?? get_template_directory_uri() . '/assets/img/slider-bg.jpg')),
'poster_logo' => esc_url_raw((string) ($settings['share_logo'] ?? get_template_directory_uri() . '/assets/img/logo.png')),
'share_desc' => sanitize_text_field((string) ($settings['share_desc'] ?? '')) ?: __('扫一扫 立即查看', 'aksrt'),
];
}
-18
View File
@@ -281,9 +281,7 @@ class PostDisplay {
'like_enabled' => !isset($settings['post_like_s']) || !empty($settings['post_like_s']),
// 参考主题无收藏开关:收藏按钮常显(字段已随 1:1 复刻移除)。
'favorite_enabled' => true,
// Canonical + legacy share.
'share_enabled' => $share_enabled,
'share_s' => $share_enabled,
// Author card posts(参考主题无独立配置:保持默认)。
'author_card_limit' => 6,
'author_card_orderby' => 'views',
@@ -346,22 +344,6 @@ class PostDisplay {
];
}
public static function get_share_settings(): array {
$settings = Settings::get_all();
return [
'enabled' => !isset($settings['share_s']) || !empty($settings['share_s']),
'wechat_appid' => sanitize_text_field((string) ($settings['wechat_share_appid'] ?? '')),
'wechat_timestamp' => sanitize_text_field((string) ($settings['wechat_share_timestamp'] ?? '')),
'wechat_nonce' => sanitize_text_field((string) ($settings['wechat_share_nonce'] ?? '')),
'wechat_signature' => sanitize_text_field((string) ($settings['wechat_share_signature'] ?? '')),
'poster_enabled' => !empty($settings['post_share_poster_s']),
'poster_qr_enabled' => !empty($settings['post_share_poster_qr_s']),
'poster_bg' => (string) ($settings['post_share_poster_bg'] ?? ''),
'social_items' => is_array($settings['social_share_items'] ?? null) ? $settings['social_share_items'] : [],
'copy_link_enabled' => !isset($settings['post_share_copylink_s']) || !empty($settings['post_share_copylink_s']),
];
}
public static function get_submit_settings(): array {
$settings = Settings::get_all();
+3 -7
View File
@@ -61,13 +61,9 @@ get_header();
<?php echo esc_html($page_desc !== '' ? $page_desc : __('暂无简介内容,请设置', 'aksrt')); ?>
</div>
<div class="pull-right page-share mt20">
<?php
if (function_exists('aksrt_render_share_buttons')) {
aksrt_render_share_buttons(['compact' => true]);
} elseif (function_exists('aksrt_get_share_html')) {
echo aksrt_get_share_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
<?php if (function_exists('aksrt_render_share_buttons')) {
aksrt_render_share_buttons(['post' => $post_id]);
} ?>
</div>
</div>
</div>
@@ -33,8 +33,8 @@ const metaBox = fs.readFileSync(path.join(root, 'src/Core/PostMetaBox.php'), 'ut
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-frontend-tags.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-single-render.php'), 'utf8');
const contentCss = fs.readFileSync(path.join(root, 'assets/css/frontend/single-post/content.css'), 'utf8');
const shareJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/post-list/share.js'), 'utf8');
const sharePhp = php;
const shareJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/share.js'), 'utf8');
const sharePhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
const featuresJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/features.js'), 'utf8');
const usersPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
@@ -233,11 +233,22 @@ test('JS schema 默认值与依赖对齐参考主题', () => {
assert.match(js, /key:\s*'interval',[\s\S]*?dependsOn:\s*\{\s*key:\s*'autoplay',\s*value:\s*true\s*\}/);
});
test('share_s 迁移至分享设置页 basic 组(键不变,无双路)', () => {
assert.match(sharePhp, /\['key' => 'basic', 'title' => '分享按钮', 'fields' => \[\s*\['key' => 'share_s', 'label' => '分享按钮', 'type' => 'switcher', 'default' => true, 'help' => '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响'\]/);
assert.match(shareJs, /key: 'basic',/);
test('社交分享位于全局功能并使用参考主题唯一键集', () => {
assert.match(sharePhp, /'share' => \[\s*'key'\s*=> 'share',\s*'title'\s*=> '社交分享'/);
assert.match(sharePhp, /'share_s', 'label' => '文章分享功能', 'type' => 'switcher', 'default' => true/);
assert.match(sharePhp, /'share_items', 'label' => '分享平台', 'type' => 'custom', 'renderer' => 'sorter', 'sanitize_type' => 'array'/);
assert.match(shareJs, /key: 'share_items',[\s\S]*?type: 'custom',[\s\S]*?renderer: 'sorter'/);
assert.match(sharePhp, /'wechat_share_s', 'label' => '微信分享有图'/);
assert.match(sharePhp, /'wechat_share_option', 'label' => '微信分享有图配置', 'type' => 'fieldset'/);
assert.match(sharePhp, /'share_img', 'label' => '生成海报分享', 'type' => 'switcher', 'default' => true/);
for (const key of ['share_img_compatible_s', 'share_img_byimg', 'share_logo', 'share_desc']) {
assert.ok(sharePhp.includes(`'${key}'`), `PHP 社交分享缺少 ${key}`);
assert.ok(shareJs.includes(`key: '${key}'`), `JS 社交分享缺少 ${key}`);
}
assert.match(shareJs, /key: 'global\.share'/);
assert.match(shareJs, /share_s: true/);
assert.match(shareJs, /key: 'share_s', label: '分享按钮', type: 'switch', default: true, help: '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响'/);
assert.doesNotMatch(sharePhp, /share_wechat_s|share_wechat_appid|share_wechat_app_secret|share_only_logo/);
assert.doesNotMatch(shareJs, /share_wechat_s|share_wechat_appid|share_wechat_app_secret|share_only_logo/);
});
test('post_rewards_* 迁移至用户功能页 rewards 组(对齐参考主题 8977 行)', () => {
+5 -5
View File
@@ -1,5 +1,5 @@
{
"generated_at": "2026-09-26T13:42:28+00:00",
"generated_at": "2026-09-27T05:39:56+00:00",
"errors": [],
"warnings": [
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
@@ -76,6 +76,7 @@
"logo",
"search",
"seo",
"share",
"system-notice"
],
"files": [
@@ -105,7 +106,6 @@
"list",
"page",
"pay",
"share",
"submit",
"thumb"
],
@@ -232,7 +232,7 @@
"entry": true,
"missing_chunks": [],
"chunks": [
"SchemaPage.CowEGY91.js",
"SchemaPage.CTIwAdRb.js",
"vendor-vue.BYzo-2I-.js",
"FaIcon.B3OOcphx.js",
"AksMediaField.DJPSTsh8.js",
@@ -248,8 +248,8 @@
"vendor-draggable.YNevSrwY.js",
"PageHeader.jmNZVzHG.js",
"FooterTabbarSettings.BFZnXJRS.js",
"PostPageSettings.DVS1TYjI.js",
"CommentSettings.CfVVYNWs.js",
"PostPageSettings.BoIGVd50.js",
"CommentSettings.ZGBaYxEI.js",
"PermissionsCapabilities.Dwo7ED9D.js",
"CmsCredits.BeYe9Q4u.js",
"ThemeInfoPage.BEUFF9-H.js",