chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
<?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),此处不再重复定义。
*/
}