Anyon e634118a22 refactor(plugin): 迁移 v8 插件化组件体系
将 v6 中直接放在本地 app 的后台与微信能力迁移为 v8 插件组件,并把运行时基础能力沉淀到独立插件包。

主要内容:

- 新增 think-library、system、worker、static、install 等基础插件包。

- 新增 account、payment、wechat-client、wechat-service、wemall、wuma 等业务插件包。

- 移除 v6 的 app/admin 与 app/wechat 本地应用实现,改由插件分发接管。

- 将 Helper 能力彻底并入 System,统一为 plugin\system\helper\* 命名空间。

- 同步插件迁移发布清单与根 route 占位,保证安装发布流程可复现。
2026-05-08 15:30:46 +08:00

354 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// +----------------------------------------------------------------------
// | Static Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-static
// | github 代码仓库https://github.com/zoujingli/think-plugs-static
// +----------------------------------------------------------------------
$(function () {
window.$body = $('body');
let loginI18n = window.taLoginI18n || {};
function t(key, fallback) {
return typeof loginI18n[key] === 'string' && loginI18n[key].length > 0 ? loginI18n[key] : fallback;
}
/*! 登录界面背景切换 */
$('[data-bg-transition]').each(function (i, el) {
el.idx = 0, el.imgs = [], el.SetBackImage = function (css) {
window.setTimeout(function () {
$(el).removeClass(el.imgs.join(' ')).addClass(css)
}, 1000) && $body.removeClass(el.imgs.join(' ')).addClass(css)
}, el.lazy = window.setInterval(function () {
el.imgs.length > 0 && el.SetBackImage(el.imgs[++el.idx] || el.imgs[el.idx = 0]);
}, 5000) && el.dataset.bgTransition.split(',').forEach(function (image) {
layui.img(image, function (img, cssid, style) {
style = document.createElement('style'), cssid = 'LoginBackImage' + (el.imgs.length + 1);
style.innerHTML = '.' + cssid + '{background-image:url("' + encodeURI(image) + '")!important}';
document.head.appendChild(style) && el.imgs.push(cssid);
});
});
});
let ambientFrame = 0, ambientPoint = {
x: Math.round(window.innerWidth * 0.78),
y: Math.round(window.innerHeight * 0.22)
};
function paintAmbient() {
ambientFrame = 0;
$('.login-container').css({
'--cursor-x': ambientPoint.x + 'px',
'--cursor-y': ambientPoint.y + 'px'
});
}
function queueAmbient(x, y) {
ambientPoint.x = x;
ambientPoint.y = y;
if (ambientFrame) return;
ambientFrame = (window.requestAnimationFrame || window.setTimeout)(paintAmbient, 16);
}
queueAmbient(ambientPoint.x, ambientPoint.y);
$(document).on('mousemove touchmove', function (event) {
let point = event.touches && event.touches[0] ? event.touches[0] : event;
queueAmbient(point.clientX, point.clientY);
});
$(window).on('resize', function () {
queueAmbient(Math.round(window.innerWidth * 0.78), Math.round(window.innerHeight * 0.22));
});
function decodeBase64ToArrayBuffer(value) {
let binary = atob(value), bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
function encodeArrayBufferToBase64(buffer) {
let bytes = new Uint8Array(buffer), binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
async function encryptPassword(form, password) {
let publicKey = form.dataset.loginPasswordKey || '';
if (!publicKey || !window.crypto || !window.crypto.subtle || typeof TextEncoder === 'undefined' || window.isSecureContext === false) {
return {mode: 'plain', value: password};
}
try {
// PHP openssl_private_decrypt with OAEP padding only interoperates with the SHA-1 variant here.
let key = await window.crypto.subtle.importKey(
'spki',
decodeBase64ToArrayBuffer(publicKey),
{name: 'RSA-OAEP', hash: 'SHA-1'},
false,
['encrypt']
);
let result = await window.crypto.subtle.encrypt({name: 'RSA-OAEP'}, key, new TextEncoder().encode(password));
return {mode: 'rsa', value: encodeArrayBufferToBase64(result)};
} catch (e) {
return {mode: 'plain', value: password};
}
}
function reloadLoginPage() {
try {
let url = new URL(location.href);
url.hash = '';
url.searchParams.set('_login_reload', String(Date.now()));
location.replace(url.toString());
} catch (e) {
location.href = location.pathname + '?_login_reload=' + Date.now();
}
}
function createLoginSlider(form) {
let $form = $(form), $container = $form.closest('.login-container');
let $popup = $container.find('[data-login-slider-popup]');
let $panel = $popup.find('[data-login-slider-panel]');
if ($panel.length < 1) {
return {
syncError: $.noop,
ensureVerified: function () {
return true;
}
};
}
let $bg = $panel.find('[data-slider-bg]');
let $piece = $panel.find('[data-slider-piece]');
let $stage = $panel.find('.slider-stage');
let $track = $panel.find('[data-slider-track]');
let $message = $panel.find('[data-slider-message]');
let $status = $panel.find('[data-slider-status]');
let $handle = $panel.find('[data-slider-handle]');
let $refresh = $panel.find('[data-slider-refresh]');
let $uniqid = $form.find('[name="uniqid"]');
let $verify = $form.find('[name="verify"]');
let $mode = $form.find('[name="password_mode"]');
let request = form.dataset.loginSlider || '';
let check = form.dataset.loginCheck || '';
let state = {
bgWidth: 0,
currentLeft: 0,
dragging: false,
loaded: false,
maxLeft: 0,
originX: 0,
pieceWidth: 100,
required: false,
sourceWidth: 600,
startLeft: 0,
verified: false,
working: false,
};
function setStatus(text, type) {
$panel.removeClass('is-error is-success');
type && $panel.addClass(type);
$message.text(text);
$status.text(text);
}
function setPosition(left) {
state.currentLeft = Math.max(0, Math.min(left, state.maxLeft));
$handle.css('left', state.currentLeft + 'px');
$track.css('width', (state.currentLeft + $handle.outerWidth()) + 'px');
$piece.css('left', state.currentLeft + 'px');
}
function recalculate() {
state.bgWidth = $stage.innerWidth();
state.maxLeft = Math.max(state.bgWidth - $handle.outerWidth(), 0);
$piece.css('width', (state.pieceWidth / state.sourceWidth * 100) + '%');
setPosition(state.currentLeft);
}
function resetChallenge() {
state.verified = false;
state.working = false;
$uniqid.val('');
$verify.val('');
$panel.removeClass('is-error is-success');
setPosition(0);
setStatus(t('dragToVerify', '请按住滑块,拖动完成验证'));
}
function hideChallenge() {
$popup.removeClass('is-visible');
$body.removeClass('login-verify-active');
window.setTimeout(function () {
if (!$popup.hasClass('is-visible')) {
$popup.addClass('layui-hide');
}
}, 220);
}
function loadChallenge() {
if (request.length < 5) return $.msg.tips(t('sliderApiMissing', '请设置滑块验证接口'));
resetChallenge();
let handleChallenge = function (ret) {
if (parseInt(ret.code, 10) !== 200) {
ret.data && ret.data.reload && reloadLoginPage();
return false;
}
state.sourceWidth = parseInt(ret.data.width || 600);
state.pieceWidth = parseInt(ret.data.piece_width || 100);
state.loaded = true;
$uniqid.val(ret.data.uniqid || '');
$bg.attr('src', ret.data.bgimg || '');
$piece.attr('src', ret.data.water || '');
(window.requestAnimationFrame || window.setTimeout)(recalculate, 0);
setStatus(t('dragToVerify', '请按住滑块,拖动完成验证'));
return false;
};
handleChallenge.allowHttpError = true;
$.form.load(request, {token: form.dataset.loginToken || ''}, 'post', handleChallenge, false);
}
function showChallenge(refresh) {
state.required = true;
$popup.removeClass('layui-hide');
$body.addClass('login-verify-active');
(window.requestAnimationFrame || window.setTimeout)(function () {
$popup.addClass('is-visible');
recalculate();
}, 0);
if (refresh || !$uniqid.val()) loadChallenge();
}
function verifyCurrentPosition() {
if (state.working || !$uniqid.val() || check.length < 5) return;
state.working = true;
setStatus(t('verifying', '正在校验...'));
$.form.load(check, {
uniqid: $uniqid.val(),
verify: Math.round(state.currentLeft * state.sourceWidth / Math.max(state.bgWidth, 1))
}, 'post', function (ret) {
state.working = false;
let value = Math.round(state.currentLeft * state.sourceWidth / Math.max(state.bgWidth, 1));
let result = parseInt(ret.data && ret.data.state || -1);
if (result === 1) {
state.verified = true;
state.required = false;
$verify.val(String(value));
$panel.removeClass('is-error').addClass('is-success');
$message.text(t('verifyPassedContinue', '验证通过,请继续登录'));
$status.text(t('sliderVerified', '滑块验证通过'));
window.setTimeout(hideChallenge, 260);
} else if (result === 0) {
state.verified = false;
state.required = true;
$verify.val('');
$panel.removeClass('is-success').addClass('is-error');
$message.text(t('wrongPositionRetry', '位置不正确,请重试'));
$status.text(t('wrongPositionRetry', '位置不正确,请重试'));
window.setTimeout(function () {
if (!state.verified) {
$panel.removeClass('is-error');
setPosition(0);
setStatus(t('dragToVerify', '请按住滑块,拖动完成验证'));
}
}, 500);
} else {
loadChallenge();
}
return false;
}, false);
}
function getPoint(event) {
return event.touches && event.touches[0] ? event.touches[0] : event;
}
function startDrag(event) {
if (state.working || state.verified || !$uniqid.val()) return;
let point = getPoint(event);
state.dragging = true;
state.originX = point.clientX;
state.startLeft = state.currentLeft;
$handle.addClass('is-active');
event.preventDefault();
}
function moveDrag(event) {
if (!state.dragging) return;
let point = getPoint(event);
setPosition(state.startLeft + point.clientX - state.originX);
event.preventDefault();
}
function endDrag() {
if (!state.dragging) return;
state.dragging = false;
$handle.removeClass('is-active');
verifyCurrentPosition();
}
$bg.on('load', recalculate);
$(window).on('resize', recalculate);
$handle.on('mousedown touchstart', startDrag);
$(document).on('mousemove touchmove', moveDrag);
$(document).on('mouseup touchend touchcancel', endDrag);
$refresh.on('click', function () {
showChallenge(true);
});
return {
syncError: function (data) {
if (data && data.need_verify) {
state.verified = false;
state.required = true;
showChallenge(!!data.refresh_verify);
}
},
ensureVerified: function () {
if (state.required && !state.verified) {
showChallenge(false);
$.msg.tips(t('needVerifyFirst', '请先完成滑块验证'));
return false;
}
$mode.val('plain');
return true;
},
setPasswordMode: function (mode) {
$mode.val(mode);
}
};
}
/*! 后台登录提交处理 */
$body.find('form[data-login-form]').each(function (idx, form) {
let slider = createLoginSlider(form);
$(form).vali(function (data) {
if (!slider.ensureVerified()) return false;
encryptPassword(form, data.password || '').then(function (cipher) {
let payload = $.extend({}, data, {password: cipher.value, password_mode: cipher.mode});
slider.setPasswordMode(cipher.mode);
let handleSubmit = function (ret) {
if (parseInt(ret.code, 10) !== 200) {
if (ret.data && ret.data.reload) {
reloadLoginPage();
return false;
}
slider.syncError(ret.data || {});
return false;
}
};
handleSubmit.allowHttpError = true;
$.form.load(location.href, payload, "post", handleSubmit, null, null, 'false');
});
});
});
});