#feature: add config page
This commit is contained in:
106
app/Http/Controllers/Admin/ConfigController.php
Normal file
106
app/Http/Controllers/Admin/ConfigController.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Config;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class ConfigController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$configs = Config::query()
|
||||||
|
->orderBy('key')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'configs' => $configs,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'key' => ['required', 'string', 'max:255', 'unique:configs,key'],
|
||||||
|
'value' => ['nullable', 'string'],
|
||||||
|
'description' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$config = Config::query()->create([
|
||||||
|
'key' => $data['key'],
|
||||||
|
'value' => $this->decodeValue($data['value'] ?? null),
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'config' => $config,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Config $config): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'key' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('configs', 'key')->ignore($config->id),
|
||||||
|
],
|
||||||
|
'value' => ['nullable', 'string'],
|
||||||
|
'description' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$config->update([
|
||||||
|
'key' => $data['key'],
|
||||||
|
'value' => $this->decodeValue($data['value'] ?? null),
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'config' => $config->refresh(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Config $config): JsonResponse
|
||||||
|
{
|
||||||
|
$config->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeValue(?string $raw): mixed
|
||||||
|
{
|
||||||
|
if ($raw === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmed = trim($raw);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($trimmed, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'value' => 'value 必须是合法 JSON',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,18 +163,17 @@ class GitMonitorService
|
|||||||
private function inspectRepository(string $repoKey, array $repoConfig, string $branch): array
|
private function inspectRepository(string $repoKey, array $repoConfig, string $branch): array
|
||||||
{
|
{
|
||||||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||||||
$restoreBranch = null;
|
|
||||||
|
|
||||||
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
||||||
throw new \RuntimeException("Project path {$path} is not a valid git repository");
|
throw new \RuntimeException("Project path {$path} is not a valid git repository");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$this->synchronizeRepository($path, $branch);
|
||||||
$restoreBranch = $this->synchronizeRepository($path, $branch);
|
$remoteBranch = 'origin/' . $branch;
|
||||||
$head = $this->runGit($path, ['git', 'rev-parse', 'HEAD']);
|
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
||||||
|
|
||||||
$lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey);
|
$lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey);
|
||||||
$commits = $this->collectCommits($path, $branch, $lastChecked);
|
$commits = $this->collectCommits($path, $remoteBranch, $lastChecked);
|
||||||
|
|
||||||
$issues = [
|
$issues = [
|
||||||
'develop_merges' => [],
|
'develop_merges' => [],
|
||||||
@@ -206,36 +205,15 @@ class GitMonitorService
|
|||||||
'commits_scanned' => count($commits),
|
'commits_scanned' => count($commits),
|
||||||
'issues' => $issues,
|
'issues' => $issues,
|
||||||
];
|
];
|
||||||
} finally {
|
|
||||||
if ($restoreBranch) {
|
|
||||||
try {
|
|
||||||
$this->runGit($path, ['git', 'checkout', $restoreBranch]);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::warning('Failed to restore branch', [
|
|
||||||
'repository' => $repoKey,
|
|
||||||
'branch' => $restoreBranch,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function synchronizeRepository(string $path, string $branch): ?string
|
private function synchronizeRepository(string $path, string $branch): void
|
||||||
{
|
{
|
||||||
$currentBranch = trim($this->runGit($path, ['git', 'rev-parse', '--abbrev-ref', 'HEAD']));
|
|
||||||
$restoreBranch = $currentBranch !== $branch && $currentBranch !== 'HEAD'
|
|
||||||
? $currentBranch
|
|
||||||
: null;
|
|
||||||
|
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin']);
|
$this->runGit($path, ['git', 'fetch', 'origin']);
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin', $branch]);
|
$this->runGit($path, ['git', 'fetch', 'origin', $branch]);
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin', self::DEVELOP_BRANCH]);
|
$this->runGit($path, ['git', 'fetch', 'origin', self::DEVELOP_BRANCH]);
|
||||||
|
|
||||||
$this->checkoutBranch($path, $branch);
|
$this->runGit($path, ['git', 'show-ref', '--verify', "refs/remotes/origin/{$branch}"]);
|
||||||
$this->runGit($path, ['git', 'pull', '--ff-only', 'origin', $branch]);
|
|
||||||
|
|
||||||
return $restoreBranch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkoutBranch(string $path, string $branch): void
|
private function checkoutBranch(string $path, string $branch): void
|
||||||
@@ -468,8 +446,41 @@ class GitMonitorService
|
|||||||
{
|
{
|
||||||
$projects = $this->configService->get('workspace.repositories');
|
$projects = $this->configService->get('workspace.repositories');
|
||||||
|
|
||||||
if (!is_array($projects) || empty($projects)) {
|
if ($projects === null) {
|
||||||
throw new \RuntimeException('configs 表未设置 workspace.repositories。');
|
$fallback = $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||||||
|
if (!empty($fallback)) {
|
||||||
|
Log::warning('configs 表未设置 workspace.repositories,已使用 git-monitor.enabled_projects。');
|
||||||
|
}
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($projects)) {
|
||||||
|
Log::warning('configs 表 workspace.repositories 配置格式不正确,已降级使用 git-monitor.enabled_projects。');
|
||||||
|
return $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function buildFallbackProjects(array $enabled): array
|
||||||
|
{
|
||||||
|
$projects = [];
|
||||||
|
$jiraKeyMap = [
|
||||||
|
'portal-be' => 'WP',
|
||||||
|
'agent-be' => 'AM',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($enabled as $repoKey) {
|
||||||
|
if (!is_string($repoKey) || $repoKey === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$projects[$repoKey] = [
|
||||||
|
'jira_project' => $jiraKeyMap[$repoKey] ?? $repoKey,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $projects;
|
return $projects;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
return [
|
return [
|
||||||
'enabled_projects' => array_values(array_filter(array_map(
|
'enabled_projects' => array_values(array_filter(array_map(
|
||||||
'trim',
|
'trim',
|
||||||
explode(',', env('GIT_MONITOR_PROJECTS', 'service,portal-be,agent-be'))
|
explode(',', env('GIT_MONITOR_PROJECTS', 'portal-be,agent-be'))
|
||||||
))),
|
))),
|
||||||
|
|
||||||
'commit_scan_limit' => 30,
|
'commit_scan_limit' => 30,
|
||||||
|
|||||||
23
database/factories/IpUserMappingFactory.php
Normal file
23
database/factories/IpUserMappingFactory.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\IpUserMapping;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<IpUserMapping>
|
||||||
|
*/
|
||||||
|
class IpUserMappingFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = IpUserMapping::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ip_address' => $this->faker->unique()->ipv4(),
|
||||||
|
'user_name' => $this->faker->userName(),
|
||||||
|
'remark' => $this->faker->optional()->sentence(6),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
database/factories/OperationLogFactory.php
Normal file
35
database/factories/OperationLogFactory.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\OperationLog;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<OperationLog>
|
||||||
|
*/
|
||||||
|
class OperationLogFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = OperationLog::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$payload = $this->faker->optional()->randomElement([
|
||||||
|
['action' => 'create', 'status' => 'ok'],
|
||||||
|
['item_id' => 1, 'count' => 2],
|
||||||
|
['note' => 'sample'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ip_address' => $this->faker->ipv4(),
|
||||||
|
'user_label' => $this->faker->optional()->userName(),
|
||||||
|
'method' => $this->faker->randomElement(['POST', 'PUT', 'PATCH', 'DELETE']),
|
||||||
|
'path' => '/' . $this->faker->slug(2),
|
||||||
|
'route_name' => $this->faker->optional()->slug(2),
|
||||||
|
'status_code' => $this->faker->randomElement([200, 201, 204, 400, 403, 422, 500]),
|
||||||
|
'duration_ms' => $this->faker->numberBetween(1, 5000),
|
||||||
|
'request_payload' => $payload,
|
||||||
|
'user_agent' => $this->faker->userAgent(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 系统设置页面 -->
|
<!-- 系统设置页面 -->
|
||||||
<system-settings v-else-if="currentPage === 'settings'" />
|
<system-settings v-else-if="currentPage === 'settings'" :is-admin="isAdmin" />
|
||||||
|
|
||||||
<!-- 操作日志页面 -->
|
<!-- 操作日志页面 -->
|
||||||
<operation-logs v-else-if="currentPage === 'logs'" :is-admin="isAdmin" :current-ip="currentIp" />
|
<operation-logs v-else-if="currentPage === 'logs'" :is-admin="isAdmin" :current-ip="currentIp" />
|
||||||
|
|||||||
@@ -1,75 +1,234 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
<div class="space-y-4">
|
||||||
<div class="mb-6">
|
<!-- Header -->
|
||||||
<h3 class="text-lg font-medium text-gray-900">系统设置</h3>
|
<div class="flex items-center justify-between bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
||||||
<p class="text-gray-500 mt-1">仅对当前浏览器生效的本地配置。</p>
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-800">系统设置</h3>
|
||||||
|
<p class="text-xs text-gray-500">管理本地偏好与服务端全局配置</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="jira.loading || configs.loading" class="text-xs text-blue-600 animate-pulse">
|
||||||
|
数据同步中...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
<div class="border border-gray-200 rounded-lg p-6">
|
<!-- Left Column: Local/JIRA Settings -->
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="lg:col-span-1 space-y-4">
|
||||||
<div>
|
<!-- JIRA Card -->
|
||||||
<h4 class="text-base font-semibold text-gray-900">JIRA</h4>
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
|
||||||
设置“JIRA 默认查询用户”(仅本地浏览器生效)。填写后将优先用于周报/工时等页面的默认用户名。
|
<h4 class="font-semibold text-gray-700 text-sm">JIRA 偏好</h4>
|
||||||
</p>
|
<span class="text-[10px] text-gray-400 uppercase tracking-wider">Local</span>
|
||||||
</div>
|
|
||||||
<div v-if="jira.loading" class="text-sm text-gray-400">加载中...</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="p-4 space-y-4">
|
||||||
|
<!-- Default User Input -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">默认查询用户(本地)</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">默认查询用户 (本地)</label>
|
||||||
|
<div class="flex gap-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="jira.localDefaultQueryUserDraft"
|
v-model="jira.localDefaultQueryUserDraft"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
class="flex-1 min-w-0 block w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||||
placeholder="例如: your.name"
|
placeholder="如: zhangsan"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-gray-500 mt-2">留空并保存(或点击清除)将使用服务端默认值。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="bg-gray-50 border border-gray-200 rounded-md p-4">
|
|
||||||
<div class="text-sm text-gray-700">
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<span class="text-gray-500">服务端默认查询用户</span>
|
|
||||||
<span class="font-mono text-gray-900">{{ jira.serverDefaultUser || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-2 mt-2">
|
|
||||||
<span class="text-gray-500">当前生效</span>
|
|
||||||
<span class="font-mono text-gray-900">{{ effectiveDefaultQueryUser || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-2 mt-2">
|
|
||||||
<span class="text-gray-500">JIRA Host</span>
|
|
||||||
<span class="font-mono text-gray-900">{{ jira.host || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
<button
|
||||||
@click="save"
|
@click="save"
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
:disabled="jira.saving"
|
:disabled="jira.saving"
|
||||||
|
class="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
|
title="保存本地设置"
|
||||||
>
|
>
|
||||||
{{ jira.saving ? '保存中...' : '保存' }}
|
<svg v-if="!jira.saving" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span v-else>...</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="clear"
|
@click="clear"
|
||||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
:disabled="jira.saving"
|
:disabled="jira.saving"
|
||||||
|
class="px-2 py-1.5 bg-gray-100 text-gray-600 text-xs font-medium rounded hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
||||||
|
title="清除并使用默认值"
|
||||||
>
|
>
|
||||||
清除
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-1 text-[10px] text-gray-400">留空则使用服务端默认值。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="jira.message" class="text-sm text-green-700 bg-green-50 border border-green-200 rounded-md px-3 py-2">
|
<!-- Messages -->
|
||||||
|
<div v-if="jira.message" class="text-xs text-green-600 bg-green-50 px-2 py-1 rounded border border-green-100 flex items-center gap-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
{{ jira.message }}
|
{{ jira.message }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="jira.error" class="text-sm text-red-700 bg-red-50 border border-red-200 rounded-md px-3 py-2">
|
<div v-if="jira.error" class="text-xs text-red-600 bg-red-50 px-2 py-1 rounded border border-red-100">
|
||||||
{{ jira.error }}
|
{{ jira.error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Info Compact -->
|
||||||
|
<div class="bg-gray-50 rounded border border-gray-200 p-3 space-y-2">
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<span class="text-gray-500">生效用户:</span>
|
||||||
|
<span class="font-mono font-medium text-gray-800 bg-white px-1.5 py-0.5 rounded border border-gray-200">{{ effectiveDefaultQueryUser || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-200 my-1"></div>
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<span class="text-gray-500">服务端默认:</span>
|
||||||
|
<span class="font-mono text-gray-700">{{ jira.serverDefaultUser || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-start text-xs gap-2">
|
||||||
|
<span class="text-gray-500 shrink-0">Host:</span>
|
||||||
|
<span class="font-mono text-gray-700 text-right break-all">{{ jira.host || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Database Configs -->
|
||||||
|
<div class="lg:col-span-2 space-y-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col h-full">
|
||||||
|
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
|
||||||
|
<h4 class="font-semibold text-gray-700 text-sm">数据库配置 (Configs)</h4>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="!isAdmin" class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded border border-gray-200">Read Only</span>
|
||||||
|
<button
|
||||||
|
v-if="isAdmin"
|
||||||
|
@click="loadConfigs"
|
||||||
|
:disabled="configs.loading"
|
||||||
|
class="text-gray-500 hover:text-blue-600 transition-colors p-1 rounded hover:bg-gray-200"
|
||||||
|
title="刷新列表"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4" :class="{'animate-spin': configs.loading}">
|
||||||
|
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0v2.433l-.31-.31a7 7 0 00-11.712 3.138.75.75 0 001.449.39 5.5 5.5 0 019.201-2.466l.312.312h-2.433a.75.75 0 000 1.5h4.185a.75.75 0 00.53-.219z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Non-Admin Placeholder -->
|
||||||
|
<div v-if="!isAdmin" class="p-8 text-center text-gray-400 text-sm">
|
||||||
|
需要管理员权限才能管理数据库配置。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Content -->
|
||||||
|
<div v-else class="flex-1 flex flex-col min-h-0">
|
||||||
|
<!-- Status Messages -->
|
||||||
|
<div v-if="configs.message" class="mx-4 mt-4 text-xs text-green-600 bg-green-50 px-2 py-1.5 rounded border border-green-100">
|
||||||
|
{{ configs.message }}
|
||||||
|
</div>
|
||||||
|
<div v-if="configs.error" class="mx-4 mt-4 text-xs text-red-600 bg-red-50 px-2 py-1.5 rounded border border-red-100">
|
||||||
|
{{ configs.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add New Config Form -->
|
||||||
|
<div class="p-4 bg-white border-b border-gray-100">
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 items-start md:items-center">
|
||||||
|
<input
|
||||||
|
v-model="configs.newConfig.key"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 w-full md:w-auto px-2 py-1.5 text-xs font-mono border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
||||||
|
placeholder="Key (e.g. workspace.repo)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="configs.newConfig.description"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 w-full md:w-auto px-2 py-1.5 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
||||||
|
placeholder="描述"
|
||||||
|
/>
|
||||||
|
<div class="relative flex-grow-[2] w-full md:w-auto">
|
||||||
|
<input
|
||||||
|
v-model="configs.newConfig.valueText"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-2 py-1.5 text-xs font-mono border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
||||||
|
placeholder='Value (JSON)'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="createConfig"
|
||||||
|
:disabled="configs.saving"
|
||||||
|
class="w-full md:w-auto px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700 disabled:opacity-50 flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
|
||||||
|
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||||
|
</svg>
|
||||||
|
新增
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="overflow-x-auto flex-1">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">Key</th>
|
||||||
|
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">描述</th>
|
||||||
|
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value (JSON)</th>
|
||||||
|
<th scope="col" class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider w-20">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr v-for="config in configs.items" :key="config.id" class="hover:bg-gray-50 group">
|
||||||
|
<td class="px-3 py-1.5 align-top">
|
||||||
|
<input
|
||||||
|
v-model="config.key"
|
||||||
|
type="text"
|
||||||
|
class="w-full text-xs font-mono bg-transparent border-none p-0 focus:ring-0 text-gray-900 placeholder-gray-400 group-hover:bg-white focus:bg-white rounded"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 align-top">
|
||||||
|
<input
|
||||||
|
v-model="config.description"
|
||||||
|
type="text"
|
||||||
|
class="w-full text-xs bg-transparent border-none p-0 focus:ring-0 text-gray-600 group-hover:bg-white focus:bg-white rounded"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 align-top">
|
||||||
|
<textarea
|
||||||
|
v-model="config.valueText"
|
||||||
|
rows="1"
|
||||||
|
class="w-full text-xs font-mono bg-transparent border-transparent focus:border-blue-300 focus:ring-1 focus:ring-blue-200 text-gray-600 rounded px-1 py-0.5 resize-y min-h-[1.5rem]"
|
||||||
|
@focus="$event.target.rows = 3"
|
||||||
|
@blur="$event.target.rows = 1"
|
||||||
|
></textarea>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 align-top text-right whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
@click="updateConfig(config)"
|
||||||
|
:disabled="config._saving"
|
||||||
|
class="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||||
|
title="保存"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteConfig(config)"
|
||||||
|
:disabled="config._deleting"
|
||||||
|
class="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="configs.items.length === 0">
|
||||||
|
<td colspan="4" class="px-4 py-8 text-center text-xs text-gray-400">
|
||||||
|
暂无配置数据
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,6 +246,12 @@ import {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SystemSettings',
|
name: 'SystemSettings',
|
||||||
|
props: {
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jira: {
|
jira: {
|
||||||
@@ -98,6 +263,19 @@ export default {
|
|||||||
host: '',
|
host: '',
|
||||||
message: '',
|
message: '',
|
||||||
error: ''
|
error: ''
|
||||||
|
},
|
||||||
|
configs: {
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
items: [],
|
||||||
|
message: '',
|
||||||
|
error: '',
|
||||||
|
loadedOnce: false,
|
||||||
|
newConfig: {
|
||||||
|
key: '',
|
||||||
|
description: '',
|
||||||
|
valueText: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -111,8 +289,52 @@ export default {
|
|||||||
this.jira.localDefaultQueryUserDraft = savedOverride;
|
this.jira.localDefaultQueryUserDraft = savedOverride;
|
||||||
this.jira.localDefaultQueryUserSaved = savedOverride;
|
this.jira.localDefaultQueryUserSaved = savedOverride;
|
||||||
await this.loadServerConfig();
|
await this.loadServerConfig();
|
||||||
|
if (this.isAdmin) {
|
||||||
|
await this.loadConfigs();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isAdmin(value) {
|
||||||
|
if (value && !this.configs.loadedOnce) {
|
||||||
|
this.loadConfigs();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async parseJsonResponse(response) {
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(text || '响应不是JSON');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
getErrorMessage(data, fallback) {
|
||||||
|
if (!data) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (data.message) {
|
||||||
|
return data.message;
|
||||||
|
}
|
||||||
|
if (data.errors && typeof data.errors === 'object') {
|
||||||
|
const firstKey = Object.keys(data.errors)[0];
|
||||||
|
const firstError = firstKey ? data.errors[firstKey]?.[0] : '';
|
||||||
|
if (firstError) {
|
||||||
|
return firstError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
},
|
||||||
|
formatConfigValue(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadServerConfig() {
|
async loadServerConfig() {
|
||||||
this.jira.loading = true;
|
this.jira.loading = true;
|
||||||
this.jira.error = '';
|
this.jira.error = '';
|
||||||
@@ -134,6 +356,179 @@ export default {
|
|||||||
this.jira.loading = false;
|
this.jira.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async loadConfigs() {
|
||||||
|
if (!this.isAdmin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configs.loading = true;
|
||||||
|
this.configs.error = '';
|
||||||
|
this.configs.message = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/configs', {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await this.parseJsonResponse(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.configs.error = this.getErrorMessage(data, '加载失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
this.configs.error = data.message || '加载失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configs.items = (data.data.configs || []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
description: item.description || '',
|
||||||
|
valueText: this.formatConfigValue(item.value),
|
||||||
|
_saving: false,
|
||||||
|
_deleting: false
|
||||||
|
}));
|
||||||
|
this.configs.loadedOnce = true;
|
||||||
|
} catch (error) {
|
||||||
|
this.configs.error = error.message;
|
||||||
|
} finally {
|
||||||
|
this.configs.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createConfig() {
|
||||||
|
if (!this.configs.newConfig.key.trim()) {
|
||||||
|
this.configs.error = 'key 不能为空';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configs.saving = true;
|
||||||
|
this.configs.error = '';
|
||||||
|
this.configs.message = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/configs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: this.configs.newConfig.key.trim(),
|
||||||
|
description: this.configs.newConfig.description || '',
|
||||||
|
value: this.configs.newConfig.valueText
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await this.parseJsonResponse(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.configs.error = this.getErrorMessage(data, '创建失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
this.configs.error = data.message || '创建失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = data.data.config;
|
||||||
|
this.configs.items.push({
|
||||||
|
...created,
|
||||||
|
description: created.description || '',
|
||||||
|
valueText: this.formatConfigValue(created.value),
|
||||||
|
_saving: false,
|
||||||
|
_deleting: false
|
||||||
|
});
|
||||||
|
this.configs.message = '已新增配置';
|
||||||
|
this.configs.newConfig = {
|
||||||
|
key: '',
|
||||||
|
description: '',
|
||||||
|
valueText: ''
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.configs.error = error.message;
|
||||||
|
} finally {
|
||||||
|
this.configs.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateConfig(config) {
|
||||||
|
config._saving = true;
|
||||||
|
this.configs.error = '';
|
||||||
|
this.configs.message = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/configs/${config.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: config.key,
|
||||||
|
description: config.description || '',
|
||||||
|
value: config.valueText
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await this.parseJsonResponse(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.configs.error = this.getErrorMessage(data, '保存失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
this.configs.error = data.message || '保存失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = data.data.config;
|
||||||
|
config.key = updated.key;
|
||||||
|
config.description = updated.description || '';
|
||||||
|
config.valueText = this.formatConfigValue(updated.value);
|
||||||
|
this.configs.message = '已保存配置';
|
||||||
|
} catch (error) {
|
||||||
|
this.configs.error = error.message;
|
||||||
|
} finally {
|
||||||
|
config._saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteConfig(config) {
|
||||||
|
if (!window.confirm(`确认删除配置 ${config.key} 吗?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config._deleting = true;
|
||||||
|
this.configs.error = '';
|
||||||
|
this.configs.message = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/configs/${config.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await this.parseJsonResponse(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.configs.error = this.getErrorMessage(data, '删除失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
this.configs.error = data.message || '删除失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configs.items = this.configs.items.filter((item) => item.id !== config.id);
|
||||||
|
this.configs.message = '已删除配置';
|
||||||
|
} catch (error) {
|
||||||
|
this.configs.error = error.message;
|
||||||
|
} finally {
|
||||||
|
config._deleting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
this.jira.saving = true;
|
this.jira.saving = true;
|
||||||
@@ -145,7 +540,12 @@ export default {
|
|||||||
const savedOverride = getJiraDefaultQueryUserOverride();
|
const savedOverride = getJiraDefaultQueryUserOverride();
|
||||||
this.jira.localDefaultQueryUserSaved = savedOverride;
|
this.jira.localDefaultQueryUserSaved = savedOverride;
|
||||||
this.jira.localDefaultQueryUserDraft = savedOverride;
|
this.jira.localDefaultQueryUserDraft = savedOverride;
|
||||||
this.jira.message = '已保存(仅本地浏览器生效)';
|
this.jira.message = '已保存';
|
||||||
|
|
||||||
|
// Auto clear message
|
||||||
|
setTimeout(() => {
|
||||||
|
this.jira.message = '';
|
||||||
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.jira.error = error.message;
|
this.jira.error = error.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -162,7 +562,12 @@ export default {
|
|||||||
clearJiraDefaultQueryUserOverride();
|
clearJiraDefaultQueryUserOverride();
|
||||||
this.jira.localDefaultQueryUserDraft = '';
|
this.jira.localDefaultQueryUserDraft = '';
|
||||||
this.jira.localDefaultQueryUserSaved = '';
|
this.jira.localDefaultQueryUserSaved = '';
|
||||||
this.jira.message = '已清除,将使用服务端默认值';
|
this.jira.message = '已清除默认值';
|
||||||
|
|
||||||
|
// Auto clear message
|
||||||
|
setTimeout(() => {
|
||||||
|
this.jira.message = '';
|
||||||
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.jira.error = error.message;
|
this.jira.error = error.message;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Http\Controllers\MessageSyncController;
|
|||||||
use App\Http\Controllers\MessageDispatchController;
|
use App\Http\Controllers\MessageDispatchController;
|
||||||
use App\Http\Controllers\SqlGeneratorController;
|
use App\Http\Controllers\SqlGeneratorController;
|
||||||
use App\Http\Controllers\Admin\AdminMetaController;
|
use App\Http\Controllers\Admin\AdminMetaController;
|
||||||
|
use App\Http\Controllers\Admin\ConfigController;
|
||||||
use App\Http\Controllers\Admin\IpUserMappingController;
|
use App\Http\Controllers\Admin\IpUserMappingController;
|
||||||
use App\Http\Controllers\Admin\OperationLogController;
|
use App\Http\Controllers\Admin\OperationLogController;
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ Route::get('/admin/meta', [AdminMetaController::class, 'show']);
|
|||||||
|
|
||||||
// 管理员IP白名单限定的后台接口
|
// 管理员IP白名单限定的后台接口
|
||||||
Route::prefix('admin')->middleware('admin.ip')->group(function () {
|
Route::prefix('admin')->middleware('admin.ip')->group(function () {
|
||||||
|
Route::get('/configs', [ConfigController::class, 'index']);
|
||||||
|
Route::post('/configs', [ConfigController::class, 'store']);
|
||||||
|
Route::put('/configs/{config}', [ConfigController::class, 'update']);
|
||||||
|
Route::delete('/configs/{config}', [ConfigController::class, 'destroy']);
|
||||||
Route::get('/ip-user-mappings', [IpUserMappingController::class, 'index']);
|
Route::get('/ip-user-mappings', [IpUserMappingController::class, 'index']);
|
||||||
Route::post('/ip-user-mappings', [IpUserMappingController::class, 'store']);
|
Route::post('/ip-user-mappings', [IpUserMappingController::class, 'store']);
|
||||||
Route::put('/ip-user-mappings/{mapping}', [IpUserMappingController::class, 'update']);
|
Route::put('/ip-user-mappings/{mapping}', [IpUserMappingController::class, 'update']);
|
||||||
|
|||||||
Reference in New Issue
Block a user