// +---------------------------------------------------------------------- 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//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//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\\...). * 由 appPath(addon/<插件>/)剥 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, '')) !== 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, '')) !== 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 已统一提升到 BaseController(success/error 代理 Result),此处不再重复定义。 */ }