初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
<?php
namespace core\plugin\traits;
/**
* 配置管理 Trait
* 实现配置的获取、合并和覆盖机制
*/
trait ConfigTrait
{
/**
* 获取应用配置
*/
protected function getAppConfig(): array
{
static $config = null;
if ($config === null) {
// Trait 中 __DIR__ 是 core/plugin/traits,需要两级目录才能到达 core/plugin
$configFile = dirname(dirname(__DIR__)) . '/plugin/config/app.php';
$config = file_exists($configFile) ? include $configFile : [];
}
return $config;
}
/**
* 获取配置项(合并 appConfig + info.php 配置)
* info.php 中的配置优先级更高
*/
protected function getConfig(string $key, mixed $default = null): mixed
{
// 先从 appConfig 获取
$appConfigValue = $this->getAppConfigValue($key);
// 再从插件 info.php 获取(覆盖 appConfig)
$pluginConfigValue = $this->getPluginConfigValue($key);
// 插件配置优先
if ($pluginConfigValue !== null) {
return $pluginConfigValue;
}
return $appConfigValue !== null ? $appConfigValue : $default;
}
/**
* 从 appConfig 获取配置值
*/
private function getAppConfigValue(string $key): mixed
{
$keys = explode('.', $key);
$value = $this->appConfig;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return null;
}
$value = $value[$k];
}
return $value;
}
/**
* 从插件 info.php 获取配置值
*/
private function getPluginConfigValue(string $key): mixed
{
static $pluginInfo = null;
if ($pluginInfo === null) {
$infoFile = $this->getPluginInfoFile();
$pluginInfo = file_exists($infoFile) ? include $infoFile : [];
}
if (empty($pluginInfo)) {
return null;
}
$keys = explode('.', $key);
$value = $pluginInfo;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return null;
}
$value = $value[$k];
}
return $value;
}
/**
* 获取插件信息配置文件路径
*/
protected function getPluginInfoFile(): string
{
return $this->pluginPath . '/config/info.php';
}
/**
* 获取插件信息(包含隐式默认配置)
* info.php 中的配置会覆盖默认配置
*/
protected function getPluginInfo(): array
{
// 先获取显式配置(info.php)
$infoFile = $this->getPluginInfoFile();
$explicitConfig = [];
if (file_exists($infoFile)) {
$explicitConfig = include $infoFile;
$explicitConfig = is_array($explicitConfig) ? $explicitConfig : [];
}
// 合并隐式默认配置
return array_merge($this->getImplicitConfig(), $explicitConfig);
}
/**
* 获取隐式默认配置
*/
protected function getImplicitConfig(): array
{
return [
'dependencies' => [],
'require' => [
'composer' => [],
],
'npm_require' => [
'admin' => [],
'web' => [],
],
'supports' => [
'backend' => true,
'frontend' => true,
'admin' => true,
'web' => true,
],
];
}
/**
* 获取插件配置文件路径
*/
protected function getPluginConfigFile(): string
{
return $this->pluginPath . '/config/installed.php';
}
/**
* 保存插件安装状态
*/
protected function savePluginConfig(string $version): void
{
$configFile = $this->getPluginConfigFile();
$configDir = dirname($configFile);
if (!is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
$existingConfig = [];
if (file_exists($configFile)) {
$existingConfig = include $configFile;
$existingConfig = is_array($existingConfig) ? $existingConfig : [];
}
$config = array_merge($existingConfig, [
'name' => $this->getPluginName(),
'version' => $version,
'installed_at' => date('Y-m-d H:i:s'),
'status' => 'installed',
]);
$content = "<?php\n\n/**\n * 插件安装状态配置\n * 此文件由系统自动生成,请勿手动修改\n */\n\nreturn " . var_export($config, true) . ";\n";
file_put_contents($configFile, $content);
$this->output("📝 Plugin config saved: {$configFile}");
}
/**
* 获取插件安装状态
*/
protected function getPluginConfig(): ?array
{
$configFile = $this->getPluginConfigFile();
if (!file_exists($configFile)) {
return null;
}
$config = include $configFile;
return is_array($config) ? $config : null;
}
/**
* 检查插件是否已安装
*/
public function isInstalled(): bool
{
$config = $this->getPluginConfig();
return $config !== null && ($config['status'] ?? '') === 'installed';
}
/**
* 获取插件版本
*/
public function getVersion(): ?string
{
$config = $this->getPluginConfig();
return $config['version'] ?? null;
}
/**
* 删除插件配置文件
*/
protected function removePluginConfig(): void
{
$configFile = $this->getPluginConfigFile();
if (file_exists($configFile)) {
unlink($configFile);
$this->output("📝 Plugin config removed: {$configFile}");
}
}
}
@@ -0,0 +1,291 @@
<?php
namespace core\plugin\traits;
/**
* 依赖安装 Trait
*/
trait DependencyTrait
{
/**
* 合并 Composer 依赖(后端)
*/
protected function installComposerDeps(): void
{
$pluginInfo = $this->getPluginInfo();
$require = $pluginInfo['require']['composer'] ?? [];
if (empty($require)) {
$this->output("📦 No Composer dependencies to merge");
return;
}
$config = $this->getConfig('dependencies.backend', []);
if (empty($config['enabled'])) {
$this->output("📦 Composer dependency merge disabled");
return;
}
$this->output("📦 Merging Composer dependencies...");
$composerFile = base_path('composer.json');
if (!file_exists($composerFile)) {
$this->output(" ⚠️ composer.json not found");
return;
}
$composerContent = file_get_contents($composerFile);
$composer = json_decode($composerContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->output(" ⚠️ Invalid composer.json format");
return;
}
$merged = false;
// 合并 require
if (!empty($config['merge_require'])) {
foreach ($require as $package => $version) {
if (!isset($composer['require'][$package])) {
$composer['require'][$package] = $version;
$merged = true;
$this->output(" ➕ Added to require: {$package}:{$version}");
}
}
}
// 保存
if ($merged) {
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$this->output(" ✅ Composer dependencies merged. Run 'composer install' manually.");
} else {
$this->output(" ℹ️ No new dependencies to merge");
}
}
/**
* 合并 NPM 依赖(前端)
*/
protected function installNpmDeps(): void
{
$pluginInfo = $this->getPluginInfo();
$npmRequire = $pluginInfo['npm_require'] ?? [];
if (empty($npmRequire)) {
$this->output("📦 No NPM dependencies to merge");
return;
}
$projectRoot = $this->getProjectRoot();
// 合并 Admin 端依赖(前端代码统一在 frontend 目录下)
$adminConfig = $this->getConfig('dependencies.admin', []);
if (!empty($adminConfig['enabled'])) {
$adminPath = $this->getFrontendProjectPath('admin');
$this->mergeNpmDeps($npmRequire['admin'] ?? [], $adminPath, 'Admin', $adminConfig);
$this->mergeNpmDeps($npmRequire['common'] ?? [], $adminPath, 'Admin (common)', $adminConfig);
}
// 合并 Web 端依赖
$webConfig = $this->getConfig('dependencies.web', []);
if (!empty($webConfig['enabled'])) {
$webPath = $this->getFrontendProjectPath('web');
$this->mergeNpmDeps($npmRequire['web'] ?? [], $webPath, 'Web', $webConfig);
$this->mergeNpmDeps($npmRequire['common'] ?? [], $webPath, 'Web (common)', $webConfig);
}
}
/**
* 合并 NPM 依赖到 package.json
*/
protected function mergeNpmDeps(array $packages, string $workDir, string $endpoint, array $config): void
{
if (empty($packages) || !is_dir($workDir)) {
return;
}
$packageFile = $workDir . '/package.json';
if (!file_exists($packageFile)) {
$this->output(" ⚠️ {$endpoint}: package.json not found");
return;
}
$packageContent = file_get_contents($packageFile);
$package = json_decode($packageContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->output(" ⚠️ {$endpoint}: Invalid package.json format");
return;
}
$merged = false;
// 合并 dependencies
if (!empty($config['merge_prod'])) {
foreach ($packages as $pkg => $version) {
if (!isset($package['dependencies'][$pkg])) {
$package['dependencies'][$pkg] = $version;
$merged = true;
$this->output(" ➕ {$endpoint}: Added {$pkg}@{$version}");
}
}
}
// 合并 devDependencies
if (!empty($config['merge_dev'])) {
foreach ($packages as $pkg => $version) {
if (!isset($package['devDependencies'][$pkg])) {
$package['devDependencies'][$pkg] = $version;
$merged = true;
$this->output(" ➕ {$endpoint}: Added dev {$pkg}@{$version}");
}
}
}
// 保存
if ($merged) {
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$this->output(" ✅ {$endpoint}: Dependencies merged. Run 'npm install' manually.");
}
}
/**
* 移除合并的依赖
*/
protected function removeMergedDependencies(): void
{
$this->output("🗑️ Removing merged dependencies...");
$pluginInfo = $this->getPluginInfo();
// 移除 Composer 依赖
$composerRequire = $pluginInfo['require']['composer'] ?? [];
if (!empty($composerRequire)) {
$this->removeComposerDeps($composerRequire);
}
// 移除 NPM 依赖
$npmRequire = $pluginInfo['npm_require'] ?? [];
if (!empty($npmRequire)) {
// 前端代码统一在 frontend 目录下
$this->removeNpmDeps($npmRequire['admin'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin');
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin (common)');
$this->removeNpmDeps($npmRequire['web'] ?? [], $this->getFrontendProjectPath('web'), 'Web');
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('web'), 'Web (common)');
}
$this->output(" ✅ Dependencies removed. Run 'composer install' or 'npm install' to update.");
}
/**
* 移除 Composer 依赖
*/
protected function removeComposerDeps(array $packages): void
{
$composerFile = base_path('composer.json');
if (!file_exists($composerFile)) {
return;
}
$composer = json_decode(file_get_contents($composerFile), true);
$removed = false;
foreach ($packages as $package => $version) {
if (isset($composer['require'][$package])) {
unset($composer['require'][$package]);
$removed = true;
$this->output(" ➖ Removed composer: {$package}");
}
}
if ($removed) {
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
}
}
/**
* 移除 NPM 依赖
*/
protected function removeNpmDeps(array $packages, string $workDir, string $endpoint): void
{
if (empty($packages) || !is_dir($workDir)) {
return;
}
$packageFile = $workDir . '/package.json';
if (!file_exists($packageFile)) {
return;
}
$package = json_decode(file_get_contents($packageFile), true);
$removed = false;
foreach ($packages as $pkg => $version) {
// 移除 dependencies
if (isset($package['dependencies'][$pkg])) {
unset($package['dependencies'][$pkg]);
$removed = true;
$this->output(" ➖ {$endpoint}: Removed {$pkg}");
}
// 移除 devDependencies
if (isset($package['devDependencies'][$pkg])) {
unset($package['devDependencies'][$pkg]);
$removed = true;
$this->output(" ➖ {$endpoint}: Removed dev {$pkg}");
}
}
if ($removed) {
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
}
}
/**
* 执行 Shell 命令
*/
protected function runShellCommand(string $command, string $workDir, string $label): void
{
if (!is_dir($workDir)) {
$this->output(" ⚠️ {$label}: Directory not found: {$workDir}");
return;
}
$this->output(" 🔧 {$label}: Running '{$command}'");
try {
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptorspec, $pipes, $workDir);
if (is_resource($process)) {
stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
foreach ($pipes as $pipe) {
fclose($pipe);
}
$exitCode = proc_close($process);
if ($exitCode === 0) {
$this->output(" ✅ {$label}: Completed successfully");
} else {
$this->output(" ❌ {$label}: Failed with exit code {$exitCode}");
if (!empty($error)) {
$this->output(" Error: {$error}");
}
}
}
} catch (\Throwable $e) {
$this->output(" ❌ {$label}: Error - {$e->getMessage()}");
}
}
}
+510
View File
@@ -0,0 +1,510 @@
<?php
namespace core\plugin\traits;
use core\uuid\Snowflake;
use support\Db;
/**
* 菜单操作 Trait
*/
trait MenuTrait
{
/**
* 预加载系统菜单的 code 映射(用于智能关联父级)
*
* @var array
*/
protected array $_admin_menu_codes = [];
protected array $_web_menu_codes = [];
/**
* 加载菜单文件
*/
protected function loadMenus(): void
{
$menuResourceDir = $this->getConfig('resource.menu', 'menu');
$menuDir = $this->pluginPath . '/resource/' . $menuResourceDir;
$this->output("📂 Loading menus from: {$menuDir}");
if (!is_dir($menuDir)) {
$this->output("📂 No menu directory found, skipping...");
return;
}
$files = scandir($menuDir);
$this->output("📂 Menu files found: " . implode(', ', array_diff($files, ['.', '..'])));
// 预加载系统菜单的 code 映射(用于智能关联父级)
$this->preloadSystemMenuCodes('admin');
$this->preloadSystemMenuCodes('web');
// 只加载 admin 和 web 两种类型的菜单
$this->loadMenuFile($menuDir . '/admin.php', 'admin');
$this->loadMenuFile($menuDir . '/web.php', 'web');
}
/**
* 预加载系统菜单的 code 映射
* 用于插件菜单关联到系统已有菜单
*
* @param string $type 菜单类型 (admin/web)
*/
protected function preloadSystemMenuCodes(string $type): void
{
$tableName = $type === 'admin' ? 'sys_menu' : 'web_menu';
$connection = $this->connection ?? null;
try {
// 获取系统中非插件来源的菜单 code -> id 映射
$menus = Db::connection($connection)
->table($tableName)
->where('source', 'system')
->whereNotNull('code')
->where('code', '!=', '')
->select(['id', 'code', 'pid', 'type'])
->get()
->toArray();
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$this->$key = [];
foreach ($menus as $menu) {
$code = $menu->code;
$this->$key[$code] = [
'id' => $menu->id,
'pid' => $menu->pid,
'type' => $menu->type,
];
}
$this->output(" 📋 Preloaded {$type} system menus: " . count($this->$key) . " codes");
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to preload {$type} system menus: {$e->getMessage()}");
}
}
/**
* 根据 pid_code 查找父级 ID
*
* @param string|null $pidCode 父级菜单的 code
* @param string $type 菜单类型
* @return int 父级 ID,0 表示顶级菜单
*/
protected function findParentIdByCode(?string $pidCode, string $type): int
{
if (empty($pidCode)) {
return 0;
}
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$codes = $this->$key ?? [];
if (!isset($codes[$pidCode])) {
return 0;
}
$parentInfo = $codes[$pidCode];
// 如果父级是目录类型(type=1),则返回其 ID
// 否则作为顶级菜单
if ($type === 'admin') {
// admin: 1=目录 2=菜单 3=按钮
return in_array($parentInfo['type'], [1]) ? $parentInfo['id'] : 0;
} else {
// web 菜单类型判断
return $parentInfo['id'];
}
}
/**
* 加载单个菜单文件
*/
protected function loadMenuFile(string $filePath, string $type): void
{
if (!file_exists($filePath)) {
$this->output(" ⚠️ {$type}: Menu file not found");
return;
}
$menu = include $filePath;
if (empty($menu)) {
$this->output(" ⚠️ {$type}: Empty menu file");
return;
}
if (!is_array($menu)) {
$this->output(" ❌ {$type}: Invalid menu format");
return;
}
$this->saveMenuToSystem($type, $menu);
$this->output(" ✅ {$type}: Menu loaded (" . count($menu) . " items)");
}
/**
* 保存菜单到系统
*/
protected function saveMenuToSystem(string $type, array $menu): void
{
if ($type === 'admin') {
$this->saveAdminMenu($menu);
} else {
$this->saveWebMenu($menu);
}
}
/**
* 保存后台菜单
*/
protected function saveAdminMenu(array $menu): void
{
$tableName = 'sys_menu';
$this->output(" 📝 Saving admin menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertAdminMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Admin menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save admin menu: {$e->getMessage()}");
}
}
/**
* 插入后台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertAdminMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'admin');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeMenuItem($item, $plugin, $level, $index);
$data = [
'id' => $menuId,
'pid' => $parentId,
'app' => $item['app'] ?? $plugin,
'source' => 'plugin',
'title' => $normalizedItem['title'],
'code' => $normalizedItem['code'],
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'path' => $normalizedItem['path'],
'component' => $normalizedItem['component'],
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'is_link' => $normalizedItem['is_link'],
'is_cache' => $normalizedItem['is_cache'],
'is_sync' => $normalizedItem['is_sync'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_admin_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertAdminMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$title = $item['name'] ?? $item['title'] ?? '';
// 获取路径
$path = $item['path'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和路径生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 path 生成 code: /test-plugin/index -> test-plugin:index
if (!empty($path)) {
$code = trim($path, '/');
$code = str_replace('/', ':', $code);
}
// 如果路径也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'title' => $title,
'code' => $code,
'type' => $item['type'] ?? ($level === 1 ? 1 : 2), // 1=目录 2=菜单
'sort' => $item['sort'] ?? 0,
'path' => $path,
'component' => $item['component'] ?? ($level === 1 ? '/layout' : ''),
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'is_link' => $item['is_link'] ?? 0,
'is_cache' => $item['is_cache'] ?? 0,
'is_sync' => $item['is_sync'] ?? 0,
];
}
/**
* 保存前台菜单
*/
protected function saveWebMenu(array $menu): void
{
$tableName = 'web_menu';
$this->output(" 📝 Saving web menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertWebMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Web menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save web menu: {$e->getMessage()}");
}
}
/**
* 插入前台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertWebMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'web');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeWebMenuItem($item, $plugin, $level, $index);
// target: 1=当前窗口 2=新窗口
$targetValue = 1; // 默认当前窗口
if (isset($item['target'])) {
$targetValue = $item['target'] === '_self' || $item['target'] === 1 || $item['target'] === '1' ? 1 : 2;
}
$data = [
'id' => $menuId,
'app' => $item['app'] ?? $plugin,
'category' => $item['category'] ?? '1',
'source' => 'plugin',
'code' => $normalizedItem['code'],
'name' => $normalizedItem['name'],
'url' => $normalizedItem['url'],
'pid' => $parentId,
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'target' => $targetValue,
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'enabled' => $normalizedItem['enabled'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_web_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertWebMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化前台菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeWebMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$name = $item['name'] ?? $item['title'] ?? '';
// 获取 URL
$url = $item['path'] ?? $item['url'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和 URL 生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 url 生成 code
if (!empty($url)) {
$code = trim($url, '/');
$code = str_replace('/', ':', $code);
}
// 如果 URL 也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'name' => $name,
'code' => $code,
'url' => $url,
'type' => $item['type'] ?? 1,
'sort' => $item['sort'] ?? 0,
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'enabled' => $item['enabled'] ?? 1,
];
}
/**
* 清除插件菜单
*/
protected function clearPluginMenus(string $plugin): void
{
$this->clearMenusByTable('sys_menu', $plugin);
$this->clearMenusByTable('web_menu', $plugin);
}
/**
* 根据表清除菜单
*/
protected function clearMenusByTable(string $tableName, string $plugin): void
{
try {
$connection = $this->connection ?? null;
if (!Db::connection($connection)->getSchemaBuilder()->hasTable($tableName)) {
return;
}
$query = Db::connection($connection)
->table($tableName)
->where('source', 'plugin')
->where(function ($query) use ($tableName, $plugin) {
$query->where('app', $plugin)
->orWhere('code', 'like', $plugin . ':%');
if ($tableName === 'sys_menu') {
$query->orWhere('path', 'like', '/' . $plugin . '%')
->orWhere('component', 'like', '/' . $plugin . '/%');
} else {
$query->orWhere('url', 'like', '/' . $plugin . '%');
}
});
$deleted = $query->delete();
if ($deleted > 0) {
$this->output(" 🗑️ Cleared {$deleted} menus from {$tableName}");
}
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to clear {$tableName}: {$e->getMessage()}");
}
}
/**
* 导入插件菜单(快捷方法)
*/
public function importPluginMenus(): void
{
$this->importMenus();
}
/**
* 卸载插件菜单(快捷方法)
*/
public function uninstallPluginMenus(): void
{
$this->clearMenus();
}
/**
* 导入菜单(子类可重写)
*/
protected function importMenus(): void
{
$this->loadMenus();
}
/**
* 清除菜单(子类可重写)
*/
protected function clearMenus(): void
{
$this->clearPluginMenus($this->pluginName);
}
}
@@ -0,0 +1,668 @@
<?php
namespace core\plugin\traits;
use Illuminate\Database\Schema\Builder;
use support\Db;
/**
* 迁移操作 Trait
*/
trait MigrationTrait
{
/**
* 插件临时目录路径
*/
const RUNTIME_PLUGIN_PATH = 'install/plugin';
/**
* 运行迁移
*/
public function runMigrations(): void
{
$migrationsDir = $this->findMigrationsDir();
if (!$migrationsDir) {
$this->output("⚠️ No migrations directory found");
return;
}
$this->output("📁 Migrations dir: {$migrationsDir}");
// 获取已执行的迁移
$executed = $this->getExecutedMigrations();
// 获取所有迁移文件
$files = glob($migrationsDir . '/*.php');
$pending = [];
foreach ($files as $file) {
$basename = basename($file, '.php');
if (!in_array($basename, $executed)) {
$pending[] = [$file, $basename];
}
}
sort($pending);
if (empty($pending)) {
$this->output("✅ No pending migrations");
return;
}
$this->output("📋 Pending migrations: " . count($pending));
$batch = $this->getNextBatch();
$schema = $this->getSchemaBuilder();
// 直接执行迁移
foreach ($pending as $migration) {
$this->runMigration($migration[0], $migration[1], $schema);
}
// 记录批次
$this->recordBatch($batch, $pending);
$this->output("✅ Migrations completed (batch: {$batch})");
}
/**
* 运行单个迁移
*/
protected function runMigration(string $file, string $name, Builder $schema): void
{
$this->output(" 📝 Migrating: {$name}");
try {
$migration = require $file;
$migration->up($schema);
$this->output(" ✅ Migrated: {$name}");
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
throw $e;
}
}
/**
* 回滚迁移
* @param bool $dropTables 是否删除表
*/
public function rollbackMigrations(bool $dropTables = false): void
{
if ($dropTables) {
$this->dropAllTables();
$this->saveMigrationRows([]);
$this->output("✅ All plugin tables dropped");
return;
}
// 不删除表,只清理迁移日志
$this->saveMigrationRows([]);
$this->output("📝 Migration records cleared (tables preserved)");
}
/**
* 删除插件所有表
*/
protected function dropAllTables(): void
{
$tables = $this->getPluginTables();
if (empty($tables)) {
$this->output(" ⚠️ No tables to drop");
return;
}
$schema = $this->getSchemaBuilder();
foreach ($tables as $table) {
$this->output(" 🗑️ Dropping table: {$table}");
$schema->dropIfExists($table);
}
}
/**
* 获取插件相关的所有表
*/
protected function getPluginTables(): array
{
$tables = [];
$migrationsDir = $this->findMigrationsDir();
// 从迁移文件解析表名
if (is_dir($migrationsDir)) {
$files = glob($migrationsDir . '/*.php');
foreach ($files as $file) {
$content = file_get_contents($file);
if (preg_match_all('/->create\([\'"](\w+)[\'"]/', $content, $matches)) {
$tables = array_merge($tables, $matches[1]);
}
}
}
// 从数据库查询以插件名前缀的表
$prefix = $this->pluginName . '_';
try {
$connection = $this->connection ?? null;
$db = Db::connection($connection)->getDoctrineConnection();
$tablesList = $db->getSchemaManager()->listTableNames();
foreach ($tablesList as $tableName) {
if (strpos($tableName, $prefix) === 0) {
$tables[] = $tableName;
}
}
} catch (\Throwable $e) {
// 忽略查询错误
}
return array_unique($tables);
}
/**
* 回滚单个迁移
*/
protected function rollbackMigration(string $name, Builder $schema): void
{
$this->output(" 📝 Rolling back: {$name}");
$migrationsDir = $this->findMigrationsDir();
$file = $migrationsDir . '/' . $name . '.php';
if (!file_exists($file)) {
$this->output(" ⚠️ Migration file not found: {$file}");
return;
}
try {
$migration = require $file;
$migration->down($schema);
$this->output(" ✅ Rolled back: {$name}");
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
}
}
/**
* 查找迁移目录
*/
protected function findMigrationsDir(): ?string
{
$resourceDir = $this->getConfig('resource.migration', 'database/migrations');
$dirs = [
$this->pluginPath . '/resource/database/migrations',
$this->pluginPath . '/' . $resourceDir,
$this->pluginPath . '/install/migrations',
base_path("resource/database/migrations/plugin/{$this->pluginName}"),
];
foreach ($dirs as $dir) {
if (is_dir($dir)) {
return $dir;
}
}
return null;
}
/**
* 获取 Schema Builder
*/
protected function getSchemaBuilder(): Builder
{
return Db::connection($this->connection ?? null)->getSchemaBuilder();
}
/**
* 获取迁移日志文件
*/
protected function getMigrationLogFile(): string
{
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-migrations.log');
}
/**
* 获取已执行的迁移
*/
protected function getExecutedMigrations(): array
{
$logFile = $this->getMigrationLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
return array_filter(explode(PHP_EOL, $content));
}
/**
* 获取迁移记录行
*/
protected function getMigrationRows(): array
{
$logFile = $this->getMigrationLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
$rows = [];
foreach (explode(PHP_EOL, $content) as $line) {
$parts = explode(',', $line, 2);
if (count($parts) === 2) {
$rows[] = $parts;
}
}
return $rows;
}
/**
* 保存迁移记录行
*/
protected function saveMigrationRows(array $rows): void
{
$logFile = $this->getMigrationLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
if (empty($rows)) {
@unlink($logFile);
return;
}
$lines = array_map(fn($r) => "{$r[0]},{$r[1]}", $rows);
file_put_contents($logFile, implode(PHP_EOL, $lines) . PHP_EOL, LOCK_EX);
}
/**
* 获取下一批次号
*/
protected function getNextBatch(): int
{
$rows = $this->getMigrationRows();
if (empty($rows)) {
return 1;
}
$batches = array_map(fn($row) => (int)$row[0], $rows);
return max($batches) + 1;
}
/**
* 记录批次
*/
protected function recordBatch(int $batch, array $migrations): void
{
$logFile = $this->getMigrationLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$lines = array_map(fn($m) => "{$batch},{$m[1]}", $migrations);
file_put_contents($logFile, PHP_EOL . implode(PHP_EOL, $lines), FILE_APPEND | LOCK_EX);
}
/**
* 清理日志
*/
protected function clearLogs(): void
{
@unlink($this->getMigrationLogFile());
@unlink($this->getSeedLogFile());
}
/**
* 导出 SQL(从迁移文件生成)
*
* @param bool $withData 是否包含数据
* @param string|null $outputFile 输出文件路径(null 则输出到插件目录)
* @return bool 是否成功
*/
public function exportSql(bool $withData = false, ?string $outputFile = null): bool
{
$migrationsDir = $this->findMigrationsDir();
if (!$migrationsDir || !is_dir($migrationsDir)) {
$this->output("⚠️ No migrations directory found");
return false;
}
$files = glob($migrationsDir . '/*.php');
if (empty($files)) {
$this->output("⚠️ No migration files found");
return false;
}
$this->output("📤 Exporting SQL from " . count($files) . " migration file(s)...");
// 从迁移文件生成 SQL
$sql = $this->generateSqlFromMigrations($files, $withData);
// 默认输出到插件的 resource/database 目录
if (!$outputFile) {
$outputDir = $this->pluginPath . '/resource/database';
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
}
$filename = $withData ? 'install.sql' : 'structure.sql';
$outputFile = $outputDir . '/' . $filename;
}
$dir = dirname($outputFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($outputFile, $sql);
$this->output("✅ SQL exported to: {$outputFile}");
return true;
}
/**
* 从迁移文件生成 SQL
*/
protected function generateSqlFromMigrations(array $files, bool $withData): string
{
$sql = "-- Plugin: {$this->pluginName}\n";
$sql .= "-- Exported at: " . date('Y-m-d H:i:s') . "\n";
$sql .= "-- Generated from migration files\n\n";
$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
// 按文件名排序
sort($files);
foreach ($files as $file) {
$basename = basename($file, '.php');
$this->output(" 📤 Processing: {$basename}");
$sql .= $this->generateSqlFromMigrationFile($file, $withData);
$sql .= "\n";
}
$sql .= "SET FOREIGN_KEY_CHECKS = 1;\n";
return $sql;
}
/**
* 从单个迁移文件生成 SQL
*/
protected function generateSqlFromMigrationFile(string $file, bool $withData): string
{
$sql = "";
$content = file_get_contents($file);
// 提取所有表名
preg_match_all('/->create\([\'"](\w+)[\'"]\s*,/', $content, $tableMatches);
if (empty($tableMatches[1])) {
return $sql;
}
foreach ($tableMatches[1] as $tableName) {
$sql .= "-- ----------------------------\n";
$sql .= "-- Table structure for {$tableName}\n";
$sql .= "-- ----------------------------\n";
$sql .= "DROP TABLE IF EXISTS `{$tableName}`;\n";
// 从迁移代码提取 CREATE TABLE 部分
// 匹配 ->create('table_name', function (Blueprint $table) { ... });
$pattern = '/->create\([\'"]' . preg_quote($tableName, '/') . '[\'"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)/';
if (preg_match($pattern, $content, $matches)) {
$body = $matches[1];
$sql .= $this->generateCreateTableFromBlueprint($tableName, $body);
}
$sql .= ";\n\n";
// 如果需要数据,尝试从数据库获取(如果表存在)
if ($withData) {
$sql .= $this->generateInsertDataSql($tableName);
$sql .= "\n";
}
}
return $sql;
}
/**
* 从 Blueprint 代码生成 CREATE TABLE
*/
protected function generateCreateTableFromBlueprint(string $tableName, string $body): string
{
$columns = [];
$indexes = [];
// 匹配 $table->列方法
// $table->id();
// $table->string('name', 100);
// $table->text('content')->nullable();
// $table->foreignId('user_id')->references('id')->on('users');
// 匹配各种列定义
$lines = explode("\n", $body);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line) || $line === '$table->') {
continue;
}
// 移除 $table-> 前缀和结尾分号
$line = ltrim($line, '$table->');
$line = rtrim($line, ';');
// 匹配方法调用
if (preg_match('/^(\w+)\s*\(([^)]*)\)(.*)/', $line, $matches)) {
$method = $matches[1];
$args = $matches[2];
$modifiers = $matches[3] ?? '';
$colDef = $this->parseBlueprintMethod($method, $args, $modifiers);
if ($colDef) {
$columns[] = $colDef;
}
}
}
if (empty($columns)) {
return "CREATE TABLE `{$tableName}` ()";
}
return "CREATE TABLE `{$tableName}` (\n " . implode(",\n ", $columns) . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
}
/**
* 解析 Blueprint 方法
*/
protected function parseBlueprintMethod(string $method, string $args, string $modifiers): ?string
{
// 解析参数
$argList = [];
if ($args) {
// 简单解析逗号分隔的参数
$depth = 0;
$current = '';
for ($i = 0; $i < strlen($args); $i++) {
$char = $args[$i];
if ($char === '(') {
$depth++;
$current .= $char;
} elseif ($char === ')') {
$depth--;
$current .= $char;
} elseif ($char === ',' && $depth === 0) {
$argList[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
if ($current) {
$argList[] = trim($current);
}
}
// 处理修饰符
$nullable = strpos($modifiers, '->nullable()') !== false;
$default = null;
if (preg_match('/->default\(([^)]+)\)/', $modifiers, $dm)) {
$default = $dm[1];
}
$autoIncrement = strpos($modifiers, '->autoIncrement()') !== false
|| strpos($modifiers, '->autoIncrements()') !== false;
$primary = strpos($modifiers, '->primary()') !== false;
// 根据方法名生成列定义
return match ($method) {
'id' => $this->formatColumn('id', 'bigint', $nullable, $default, $autoIncrement, $primary),
'bigIncrements' => $this->formatColumn(null, 'bigint', false, null, true, true),
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
'integer', 'increments', 'mediumIncrements', 'smallIncrements', 'tinyIncrements' =>
$this->formatColumn($argList[0] ?? null, 'int', $nullable, $default, $autoIncrement, $primary),
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
'string' => $this->formatColumn($argList[0] ?? null, 'varchar(' . ($argList[1] ?? 255) . ')', $nullable, $default, false, false),
'text' => $this->formatColumn($argList[0] ?? null, 'text', $nullable, $default, false, false),
'longText' => $this->formatColumn($argList[0] ?? null, 'longtext', $nullable, $default, false, false),
'mediumText' => $this->formatColumn($argList[0] ?? null, 'mediumtext', $nullable, $default, false, false),
'json' => $this->formatColumn($argList[0] ?? null, 'json', $nullable, $default, false, false),
'boolean' => $this->formatColumn($argList[0] ?? null, 'tinyint(1)', $nullable, $default, false, false),
'date' => $this->formatColumn($argList[0] ?? null, 'date', $nullable, $default, false, false),
'datetime' => $this->formatColumn($argList[0] ?? null, 'datetime', $nullable, $default, false, false),
'timestamp' => $this->formatColumn($argList[0] ?? null, 'timestamp', $nullable, $default, false, false),
'time' => $this->formatColumn($argList[0] ?? null, 'time', $nullable, $default, false, false),
'float' => $this->formatColumn($argList[0] ?? null, 'float', $nullable, $default, false, false),
'double' => $this->formatColumn($argList[0] ?? null, 'double', $nullable, $default, false, false),
'decimal' => $this->formatColumn($argList[0] ?? null, 'decimal(' . ($argList[1] ?? '10,2') . ')', $nullable, $default, false, false),
'enum' => $this->formatColumn($argList[0] ?? null, 'enum(' . ($argList[1] ?? '') . ')', $nullable, $default, false, false),
'uuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
'foreignId', 'foreignIdFor' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, false, false),
'foreignUuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
'rememberToken' => $this->formatColumn('remember_token', 'varchar(100)', true, null, false, false),
'timestamps' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
'timestampable' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
'softDeletes' => "`deleted_at` datetime NULL",
'softDeletesTz' => "`deleted_at` datetime NULL",
default => null,
};
}
/**
* 格式化列定义
*/
protected function formatColumn(?string $name, string $type, bool $nullable, ?string $default, bool $autoIncrement, bool $primary): ?string
{
if (!$name) {
return null;
}
$col = "`{$name}` {$type}";
if ($nullable) {
$col .= " NULL";
} else {
$col .= " NOT NULL";
}
if ($autoIncrement) {
$col .= " AUTO_INCREMENT";
}
if ($default !== null) {
$col .= " DEFAULT " . $default;
}
if ($primary) {
$col .= ", PRIMARY KEY (`{$name}`)";
}
return $col;
}
/**
* 生成 INSERT DATA SQL(从数据库)
*/
protected function generateInsertDataSql(string $table): string
{
$sql = "-- ----------------------------\n";
$sql .= "-- Data for {$table}\n";
$sql .= "-- ----------------------------\n";
try {
$connection = $this->connection ?? null;
$pdo = Db::connection($connection)->getPdo();
// 检查表是否存在
$stmt = $pdo->query("SHOW TABLES LIKE '{$table}'");
if ($stmt->rowCount() === 0) {
$sql .= "-- Table does not exist, skipping data\n";
return $sql;
}
$stmt = $pdo->query("SELECT * FROM `{$table}`");
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (empty($rows)) {
$sql .= "-- No data\n";
return $sql;
}
foreach ($rows as $row) {
$values = array_map([$this, 'quoteValue'], array_values($row));
$sql .= "INSERT INTO `{$table}` (`" . implode('`, `', array_keys($row)) . "`) VALUES (" . implode(', ', $values) . ");\n";
}
} catch (\Throwable $e) {
$sql .= "-- Error: {$e->getMessage()}\n";
}
return $sql;
}
/**
* 转义值
*/
protected function quoteValue(mixed $value): string
{
if ($value === null) {
return 'NULL';
}
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_int($value) || is_float($value)) {
return (string)$value;
}
// 使用 PDO 的 quote
$connection = $this->connection ?? null;
$pdo = Db::connection($connection)->getPdo();
return $pdo->quote($value);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace core\plugin\traits;
/**
* 种子操作 Trait
*/
trait SeedTrait
{
/**
* 插件临时目录路径
*/
const RUNTIME_PLUGIN_PATH = 'install/plugin';
/**
* 运行种子
*/
public function runSeeds(): void
{
$seedsDir = $this->findSeedsDir();
if (!$seedsDir) {
$this->output("⚠️ No seeds directory found");
return;
}
$this->output("📁 Seeds dir: {$seedsDir}");
$executed = $this->getExecutedSeeds();
$files = glob($seedsDir . '/*.php');
$pending = [];
foreach ($files as $file) {
$basename = basename($file, '.php');
$seedName = str_replace('Seeder', '', $basename);
if (!in_array($seedName, $executed)) {
$pending[] = [$file, $basename];
}
}
sort($pending);
if (empty($pending)) {
$this->output("✅ No pending seeds");
return;
}
$this->output("📋 Pending seeds: " . count($pending));
foreach ($pending as $seed) {
$this->runSeed($seed[0], $seed[1]);
}
$this->output("✅ Seeds completed");
}
/**
* 运行单个种子
*/
protected function runSeed(string $file, string $className): void
{
$this->output(" 📝 Seeding: {$className}");
require_once $file;
$seeder = null;
$classes = get_declared_classes();
foreach ($classes as $cls) {
if (str_ends_with($cls, '\\' . $className) || $cls === $className) {
$seeder = new $cls();
break;
}
}
if (!$seeder) {
$this->output(" ❌ Seeder class not found: {$className}");
return;
}
try {
if (method_exists($seeder, 'run')) {
$seeder->run();
}
$this->output(" ✅ Seeded: {$className}");
$this->recordSeed(str_replace('Seeder', '', $className));
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
}
}
/**
* 查找种子目录
*/
protected function findSeedsDir(): ?string
{
$resourceDir = $this->getConfig('resource.seed', 'database/seeds');
$dirs = [
$this->pluginPath . '/resource/database/seeds',
$this->pluginPath . '/' . $resourceDir,
$this->pluginPath . '/install/seeds',
base_path("resource/database/seeds/plugin/{$this->pluginName}"),
];
foreach ($dirs as $dir) {
if (is_dir($dir)) {
return $dir;
}
}
return null;
}
/**
* 获取种子日志文件
*/
protected function getSeedLogFile(): string
{
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-seeds.log');
}
/**
* 获取已执行的种子
*/
protected function getExecutedSeeds(): array
{
$logFile = $this->getSeedLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
return array_filter(explode(PHP_EOL, $content));
}
/**
* 记录种子
*/
protected function recordSeed(string $name): void
{
$logFile = $this->getSeedLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
file_put_contents($logFile, PHP_EOL . $name, FILE_APPEND | LOCK_EX);
}
}
@@ -0,0 +1,179 @@
<?php
namespace core\plugin\traits;
/**
* 模板资源操作 Trait
*/
trait TemplateTrait
{
/**
* 复制模板资源到目标目录
*/
protected function copyTemplates(): void
{
$templateDir = $this->pluginPath . '/resource/template';
if (!is_dir($templateDir)) {
$this->output("📂 No template directory found, skipping...");
return;
}
$this->output("📂 Copying templates from {$templateDir}...");
$this->copyTemplateDir('admin');
$this->copyTemplateDir('web');
}
/**
* 复制单个端的模板
*/
protected function copyTemplateDir(string $endpoint): void
{
$resourceDir = $this->getConfig('resource.template', 'template');
$targetBase = $this->getConfig("template.{$endpoint}");
$sourceDir = $this->pluginPath . '/resource/' . $resourceDir . '/' . $endpoint;
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
if (!is_dir($sourceDir)) {
$this->output(" ⚠️ {$endpoint}: Template source not found");
return;
}
if (!is_dir(dirname($targetDir))) {
mkdir(dirname($targetDir), 0755, true);
}
$this->recurseCopy($sourceDir, $targetDir);
$this->output(" ✅ {$endpoint}: Templates copied to {$targetDir}");
}
/**
* 递归复制目录
*/
protected function recurseCopy(string $source, string $target): void
{
if (!is_dir($source)) {
return;
}
if (!is_dir($target)) {
mkdir($target, 0755, true);
}
$files = scandir($source);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$sourcePath = $source . '/' . $file;
$targetPath = $target . '/' . $file;
if (is_dir($sourcePath)) {
$this->recurseCopy($sourcePath, $targetPath);
} else {
copy($sourcePath, $targetPath);
}
}
}
/**
* 删除插件模板资源
*/
protected function deleteTemplates(): void
{
if ($this->preserveTemplatesDuringUninstall) {
$this->output('📦 Template resources are preserved during uninstall.');
return;
}
$this->output("🗑️ Deleting plugin templates...");
$this->deleteTemplateDir('admin');
$this->deleteTemplateDir('web');
}
/**
* 删除单个端模板
*/
protected function deleteTemplateDir(string $endpoint): void
{
$targetBase = $this->getConfig("template.{$endpoint}");
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
if (!is_dir($targetDir)) {
$this->output(" ⚠️ {$endpoint}: Template directory not found");
return;
}
$this->recurseDelete($targetDir);
$this->output(" ✅ {$endpoint}: Templates deleted");
}
/**
* 递归删除目录
*/
protected function recurseDelete(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = scandir($dir);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->recurseDelete($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
/**
* 导入模板(子类可重写)
*/
protected function importTemplates(): void
{
$this->copyTemplates();
}
/**
* 获取项目根目录
*/
protected function getProjectRoot(): string
{
return dirname(base_path());
}
/**
* 获取前端基础目录
* 前端代码统一在 frontend 目录下
*/
protected function getFrontendPath(): string
{
return $this->getProjectRoot() . DIRECTORY_SEPARATOR . 'frontend';
}
/**
* 获取前端指定项目目录
*
* @param string $frontendType 前端类型(admin/web/uni-app)
* @return string 前端项目目录
*/
protected function getFrontendProjectPath(string $frontendType): string
{
return $this->getFrontendPath() . DIRECTORY_SEPARATOR . $frontendType;
}
}