Files

203 lines
7.8 KiB
PHP
Raw Permalink 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.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace ywxapp\controller;
use think\facade\View;
use ywxapp\model\Configure as ConfModel;
use ywxapp\model\Links;
use ywxapp\model\Notice;
use ywxapp\model\Ad;
use ywxapp\library\SkinOverlay;
use ywxapp\library\SkinVariables;
use ywxapp\library\TemplateManager;
/**
* 前台控制器基类(统一核心前台与插件前台).
*
* - 核心前台(app\frontend\...):afterAuth 不启用后台 layout,渲染视图走应用默认视图目录。
* - 插件前台(addon\*\...):视图根切到 addon/<name>/view/frontend/
* fetch 叠加皮肤覆盖层、配色变量与风格切换器。
*
* 公共逻辑(站点配置 / 友情链接 / user / success / error)统一落地于此,
* 原 Frontend、AddonFrontend 已并入并删除。
*/
abstract class FrontendBase extends BaseController
{
/**
* 视图类实例
* @var \think\View
*/
protected $view;
/**
* 前台默认放行登录:前台本质是「开放展示」,故默认不拦截(游客可访问)。
* 需要登录 + 用户组权限的控制器(如插件用户中心、开发者中心)把本属性
* 设为 [] 或具体 action 数组,即复用 BaseController 同一份 verifyAuth 守卫。
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* 控制器初始化骨架.
* @return void
*/
public function _initialize()
{
$this->view = $this->app->view;
if ($this->isAddonContext()) {
// 插件前台:视图根切到插件自身 view/frontend/
$this->switchAddonViewPath();
} else {
// 核心前台(如 app/wxapp):统一走全局 layout 外壳,
// 子模板写裸内容,由 common/layout.html 的 {__CONTENT__} 包裹。
$this->view->config([
'layout_on' => true,
'layout_name' => 'common/layout',
]);
// 默认页面标题(子模板可通过 view() 的变量覆盖)
$this->view->assign('title', 'YwxApp 框架展示');
}
// 登录与用户组权限校验:默认放行(noNeedLogin=['*']),需登录的子类自行收窄。
// verifyAuth 内部 tokenParse() 还原登录态并做权限校验,未登录整页 302 / AJAX 返 401
// 与后台、会员中心共用同一份 Auth::verifyAuth 逻辑。
$this->auth->verifyAuth($this->noNeedLogin, $this->noNeedVerify);
if ($this->auth && $this->auth->isLogin) {
$this->view->assign('member', $this->auth->info);
}
$siteConf = ConfModel::cache(true)->column('value', 'name');
$this->view->assign('siteConf', $siteConf);
$links = Links::cache(true)->where('status', 1)->order('sort desc')->select();
$this->view->assign('links', $links);
// 全站置顶公告条(方案 A):仅取当前生效 + 置顶 + 启用的公告
$typeList = Notice::typeList();
$topNotices = array_map(function ($n) use ($typeList) {
$n['type_text'] = $typeList[$n['type']] ?? '公告';
return $n;
}, Notice::getActiveTopNotices());
$this->view->assign('topNotices', $topNotices);
// 全站广告位:按 position 分组的启用广告,模板可随时调用 {notempty name="adSlots.home_top"}
$this->view->assign('adSlots', Ad::getSlots());
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
$this->assignSite();
// 前台主导航菜单(后台可配置,两级下拉),供 header.html 渲染
$this->view->assign('navMenus', \app\backend\model\NavMenu::getNavTree());
$this->initialize();
}
/**
* 控制器初始化 initialize(子类重写).
* @return void
*/
protected function initialize()
{
}
/**
* 赋值到模板.
*/
public function assign($name, $value = null)
{
View::assign($name, $value);
return $this;
}
/**
* 从控制器类名反推插件目录并切换视图根目录到 addon/<name>/view/frontend/.
* @return void
*/
protected function switchAddonViewPath()
{
// 重构版 MultiApp 已将 appPath 设为 addon/<插件>/,直接拼视图目录,无需正则
$this->view->config([
'view_path' => $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR
. 'frontend' . DIRECTORY_SEPARATOR,
]);
}
/**
* 当前插件名(从控制器命名空间反推 addon\<name>\....
* 由 appPathaddon/<插件>/)剥 rootPath 取首段,无需正则。
* @return string
*/
protected function currentAddon(): string
{
$rel = trim(substr($this->app->getAppPath(), strlen($this->app->getRootPath())), DIRECTORY_SEPARATOR);
$seg = explode(DIRECTORY_SEPARATOR, $rel);
return $seg[0] === 'addon' && isset($seg[1]) ? $seg[1] : '';
}
/**
* 判断当前控制器是否属于插件上下文(addon\ 命名空间).
* @return bool
*/
protected function isAddonContext(): bool
{
return strpos($this->app->getNamespace(), 'addon\\') === 0;
}
/**
* 获取当前登录用户信息(前台会员).
* @return mixed
*/
protected function user()
{
if ($this->auth && $this->auth->isLogin) {
return $this->auth->info;
}
$this->result->error('获取用户信息失败!');
}
/**
* 渲染模板:插件前台叠加皮肤覆盖层 / 配色变量 / 风格切换器.
*/
protected function fetch($template = '', $vars = [], $replace = [], $config = [])
{
if ($this->isAddonContext()) {
$addon = $this->currentAddon();
$restore = null;
if ($addon && $overlay = SkinOverlay::resolve($addon, 'frontend')) {
// 把视图根目录切到「插件视图 + 皮肤覆盖」合并层,使 extend 的 layout 也走皮肤
$restore = View::getConfig('view_path');
View::config(['view_path' => $overlay]);
}
$result = View::fetch($template, $vars, $replace, $config);
if ($restore !== null) {
View::config(['view_path' => $restore]);
}
// 注入皮肤变量 CSS(Discuz 式配色层,无需改 HTML
$style = SkinVariables::styleTag($addon, 'frontend');
if ($style !== '' && ($pos = stripos($result, '</head>')) !== false) {
$result = substr($result, 0, $pos) . $style . "\n" . substr($result, $pos);
} elseif ($style !== '') {
$result = $style . $result;
}
// 注入前台风格切换器(界面设置开启 allow_member_select 时自动出现)
$switcher = TemplateManager::switcherHtml($addon, 'frontend');
if ($switcher !== '') {
if (($pos = stripos($result, '</body>')) !== false) {
$result = substr($result, 0, $pos) . $switcher . "\n" . substr($result, $pos);
} else {
$result .= $switcher;
}
}
return $result;
}
return View::fetch($template, $vars, $replace, $config);
}
/**
* success/error 已统一提升到 BaseControllersuccess/error 代理 Result),此处不再重复定义。
*/
}