refactor: 修复知识库和操作指引
This commit is contained in:
@@ -65,7 +65,7 @@ class ActivityLogExport implements FromQuery, WithHeadings, WithMapping, WithSty
|
||||
'Document' => '文档',
|
||||
'Group' => '分组',
|
||||
'Terminal' => '终端',
|
||||
'SopTemplate' => 'SOP模板',
|
||||
'Guide' => '操作指引',
|
||||
default => $className,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class ArchiveSopTemplateAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'archive';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('归档模板');
|
||||
|
||||
$this->icon('heroicon-o-archive-box');
|
||||
|
||||
$this->color('warning');
|
||||
|
||||
$this->requiresConfirmation();
|
||||
|
||||
$this->modalHeading('归档 SOP 模板');
|
||||
|
||||
$this->modalDescription('归档后,模板将不再对用户显示。确定要归档吗?');
|
||||
|
||||
$this->modalSubmitActionLabel('确认归档');
|
||||
|
||||
$this->visible(function ($record) {
|
||||
return $record instanceof SopTemplate && $record->status === 'published';
|
||||
});
|
||||
|
||||
$this->action(function (SopTemplate $record) {
|
||||
$record->update([
|
||||
'status' => 'archived',
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('归档成功')
|
||||
->body('SOP 模板已成功归档')
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Services\SopTemplateService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
|
||||
class ExportSopTemplateAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'export';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('导出模板');
|
||||
|
||||
$this->icon('heroicon-o-arrow-down-tray');
|
||||
|
||||
$this->color('info');
|
||||
|
||||
$this->action(function (SopTemplate $record) {
|
||||
$service = app(SopTemplateService::class);
|
||||
$json = $service->exportToJson($record);
|
||||
|
||||
$fileName = sprintf(
|
||||
'sop_template_%s_%s.json',
|
||||
$record->id,
|
||||
now()->format('YmdHis')
|
||||
);
|
||||
|
||||
Notification::make()
|
||||
->title('导出成功')
|
||||
->body('SOP 模板已成功导出')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return Response::streamDownload(function () use ($json) {
|
||||
echo $json;
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'application/json',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Services\SopTemplateService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ImportSopTemplateAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('导入模板');
|
||||
|
||||
$this->icon('heroicon-o-arrow-up-tray');
|
||||
|
||||
$this->color('success');
|
||||
|
||||
$this->modalHeading('导入 SOP 模板');
|
||||
|
||||
$this->modalDescription('从 JSON 文件导入 SOP 模板');
|
||||
|
||||
$this->modalSubmitActionLabel('导入');
|
||||
|
||||
$this->form([
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->label('选择文件')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->required()
|
||||
->maxSize(5120), // 5MB
|
||||
]);
|
||||
|
||||
$this->action(function (array $data) {
|
||||
try {
|
||||
$filePath = storage_path('app/public/' . $data['file']);
|
||||
$json = file_get_contents($filePath);
|
||||
|
||||
$service = app(SopTemplateService::class);
|
||||
$template = $service->importFromJson($json);
|
||||
|
||||
// 删除临时文件
|
||||
@unlink($filePath);
|
||||
|
||||
Notification::make()
|
||||
->title('导入成功')
|
||||
->body("SOP 模板「{$template->name}」已成功导入")
|
||||
->success()
|
||||
->send();
|
||||
|
||||
// 重定向到编辑页面
|
||||
return redirect()->route('filament.admin.resources.sop-templates.edit', ['record' => $template]);
|
||||
} catch (ValidationException $e) {
|
||||
Notification::make()
|
||||
->title('导入失败')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->title('导入失败')
|
||||
->body('文件格式错误或数据无效')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Infolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
|
||||
class PreviewSopTemplateAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'preview';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('预览模板');
|
||||
|
||||
$this->icon('heroicon-o-eye');
|
||||
|
||||
$this->color('info');
|
||||
|
||||
$this->modalHeading('SOP 模板预览');
|
||||
|
||||
$this->modalWidth('7xl');
|
||||
|
||||
$this->modalSubmitAction(false);
|
||||
|
||||
$this->modalCancelActionLabel('关闭');
|
||||
|
||||
$this->infolist(function (SopTemplate $record): Infolist {
|
||||
return Infolist::make()
|
||||
->record($record)
|
||||
->schema([
|
||||
Infolists\Components\Section::make('模板信息')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('name')
|
||||
->label('模板名称')
|
||||
->size(Infolists\Components\TextEntry\TextEntrySize::Large)
|
||||
->weight('bold'),
|
||||
|
||||
Infolists\Components\TextEntry::make('description')
|
||||
->label('模板描述')
|
||||
->columnSpanFull(),
|
||||
|
||||
Infolists\Components\TextEntry::make('category')
|
||||
->label('分类')
|
||||
->badge(),
|
||||
|
||||
Infolists\Components\TextEntry::make('version')
|
||||
->label('版本'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Infolists\Components\Section::make('操作步骤')
|
||||
->schema([
|
||||
Infolists\Components\RepeatableEntry::make('steps')
|
||||
->label('')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('step_number')
|
||||
->label('步骤')
|
||||
->badge()
|
||||
->color('primary'),
|
||||
|
||||
Infolists\Components\TextEntry::make('title')
|
||||
->label('标题')
|
||||
->weight('bold'),
|
||||
|
||||
Infolists\Components\TextEntry::make('content')
|
||||
->label('内容')
|
||||
->html()
|
||||
->columnSpanFull(),
|
||||
|
||||
Infolists\Components\TextEntry::make('is_required')
|
||||
->label('必需')
|
||||
->badge()
|
||||
->formatStateUsing(fn (bool $state): string => $state ? '是' : '否')
|
||||
->color(fn (bool $state): string => $state ? 'success' : 'gray'),
|
||||
])
|
||||
->columns(3)
|
||||
->contained(false),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\SopTemplateVersion;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class PublishSopTemplateAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'publish';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label('发布模板');
|
||||
|
||||
$this->icon('heroicon-o-check-circle');
|
||||
|
||||
$this->color('success');
|
||||
|
||||
$this->requiresConfirmation();
|
||||
|
||||
$this->modalHeading('发布 SOP 模板');
|
||||
|
||||
$this->modalDescription('发布后,模板将对所有用户可见。确定要发布吗?');
|
||||
|
||||
$this->modalSubmitActionLabel('确认发布');
|
||||
|
||||
$this->form([
|
||||
Forms\Components\Textarea::make('change_log')
|
||||
->label('变更说明')
|
||||
->placeholder('请描述本次发布的主要变更内容...')
|
||||
->rows(3),
|
||||
]);
|
||||
|
||||
$this->visible(function ($record) {
|
||||
return $record instanceof SopTemplate && $record->status === 'draft';
|
||||
});
|
||||
|
||||
$this->action(function (SopTemplate $record, array $data) {
|
||||
// 验证模板是否有步骤
|
||||
if ($record->steps()->count() === 0) {
|
||||
Notification::make()
|
||||
->title('发布失败')
|
||||
->body('模板至少需要包含一个步骤才能发布')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建版本快照
|
||||
SopTemplateVersion::create([
|
||||
'sop_template_id' => $record->id,
|
||||
'version' => $record->version,
|
||||
'change_log' => $data['change_log'] ?? '首次发布',
|
||||
'content_snapshot' => [
|
||||
'template' => $record->toArray(),
|
||||
'steps' => $record->steps->toArray(),
|
||||
],
|
||||
'created_by' => auth()->id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
// 更新模板状态
|
||||
$record->update([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('发布成功')
|
||||
->body('SOP 模板已成功发布')
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\Group;
|
||||
use App\Models\KnowledgeBase;
|
||||
use App\Services\DocumentSearchService;
|
||||
use App\Services\DocumentService;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -41,6 +42,7 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
public ?string $searchQuery = null;
|
||||
public ?string $documentType = null;
|
||||
public ?int $groupId = null;
|
||||
public ?int $knowledgeBaseId = null;
|
||||
|
||||
// 搜索结果
|
||||
public $searchResults = null;
|
||||
@@ -55,6 +57,7 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
'searchQuery' => '',
|
||||
'documentType' => null,
|
||||
'groupId' => null,
|
||||
'knowledgeBaseId' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -86,8 +89,15 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
->options(Group::pluck('name', 'id'))
|
||||
->searchable()
|
||||
->native(false),
|
||||
|
||||
Select::make('knowledgeBaseId')
|
||||
->label('知识库')
|
||||
->placeholder('全部知识库')
|
||||
->options(KnowledgeBase::pluck('name', 'id'))
|
||||
->searchable()
|
||||
->native(false),
|
||||
])
|
||||
->columns(3);
|
||||
->columns(4);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,6 +208,9 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
if ($this->groupId) {
|
||||
$filters['group_id'] = $this->groupId;
|
||||
}
|
||||
if ($this->knowledgeBaseId) {
|
||||
$filters['knowledge_base_id'] = $this->knowledgeBaseId;
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
$results = $searchService->search($this->searchQuery, $user, $filters);
|
||||
@@ -236,6 +249,7 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
$this->searchQuery = $data['searchQuery'];
|
||||
$this->documentType = $data['documentType'];
|
||||
$this->groupId = $data['groupId'];
|
||||
$this->knowledgeBaseId = $data['knowledgeBaseId'] ?? null;
|
||||
$this->hasSearched = true;
|
||||
|
||||
// 重置表格分页
|
||||
@@ -256,11 +270,13 @@ class SearchPage extends Page implements HasForms, HasTable
|
||||
'searchQuery' => '',
|
||||
'documentType' => null,
|
||||
'groupId' => null,
|
||||
'knowledgeBaseId' => null,
|
||||
]);
|
||||
|
||||
$this->searchQuery = null;
|
||||
$this->documentType = null;
|
||||
$this->groupId = null;
|
||||
$this->knowledgeBaseId = null;
|
||||
$this->hasSearched = false;
|
||||
|
||||
$this->resetTable();
|
||||
|
||||
@@ -99,7 +99,7 @@ class ActivityLogResource extends Resource
|
||||
'Document' => '文档',
|
||||
'Group' => '分组',
|
||||
'Terminal' => '终端',
|
||||
'SopTemplate' => 'SOP模板',
|
||||
'Guide' => '操作指引',
|
||||
default => $className,
|
||||
};
|
||||
})
|
||||
@@ -195,7 +195,7 @@ class ActivityLogResource extends Resource
|
||||
'App\\Models\\Document' => '文档',
|
||||
'App\\Models\\Group' => '分组',
|
||||
'App\\Models\\Terminal' => '终端',
|
||||
'App\\Models\\SopTemplate' => 'SOP模板',
|
||||
'App\\Models\\Guide' => '操作指引',
|
||||
])
|
||||
->placeholder('全部类型'),
|
||||
])
|
||||
|
||||
@@ -61,7 +61,7 @@ class ViewActivityLog extends ViewRecord
|
||||
'Document' => '文档',
|
||||
'Group' => '分组',
|
||||
'Terminal' => '终端',
|
||||
'SopTemplate' => 'SOP模板',
|
||||
'Guide' => '操作指引',
|
||||
default => $className,
|
||||
};
|
||||
}),
|
||||
|
||||
204
app/Filament/Resources/GuideResource.php
Normal file
204
app/Filament/Resources/GuideResource.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\GuideResource\Pages;
|
||||
use App\Models\Guide;
|
||||
use App\Models\Terminal;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class GuideResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Guide::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-book-open';
|
||||
|
||||
protected static ?string $navigationLabel = '操作指引';
|
||||
|
||||
protected static ?string $modelLabel = '指引';
|
||||
|
||||
protected static ?string $pluralModelLabel = '指引';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $navigationGroup = '业务管理';
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return auth()->user()?->can('guide.view') ?? false;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('基本信息')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('指引名称')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('例如: 如何用光'),
|
||||
|
||||
Forms\Components\Select::make('category')
|
||||
->label('分类')
|
||||
->required()
|
||||
->options([
|
||||
'operation' => '操作指引',
|
||||
'fault_handling' => '故障处理',
|
||||
'training' => '培训教程',
|
||||
'safety' => '安全规范',
|
||||
'maintenance' => '维护保养',
|
||||
])
|
||||
->default('operation'),
|
||||
|
||||
Forms\Components\Select::make('status')
|
||||
->label('状态')
|
||||
->required()
|
||||
->options([
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
])
|
||||
->default('draft'),
|
||||
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->label('标签')
|
||||
->placeholder('输入标签后回车')
|
||||
->helperText('用于分类和搜索的关键词标签'),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('描述')
|
||||
->maxLength(1000)
|
||||
->placeholder('简要描述此指引的用途')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('关联终端')
|
||||
->schema([
|
||||
Forms\Components\CheckboxList::make('terminals')
|
||||
->label('适用终端')
|
||||
->relationship('terminals', 'name')
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->helperText('选择此指引适用的终端,未关联终端的指引不会在终端显示')
|
||||
->columns(3),
|
||||
])
|
||||
->description('配置此指引在哪些终端上可见'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('指引名称')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->label('分类')
|
||||
->badge()
|
||||
->formatStateUsing(fn(string $state): string => match ($state) {
|
||||
'operation' => '操作指引',
|
||||
'fault_handling' => '故障处理',
|
||||
'training' => '培训教程',
|
||||
'safety' => '安全规范',
|
||||
'maintenance' => '维护保养',
|
||||
default => $state,
|
||||
})
|
||||
->color(fn(string $state): string => match ($state) {
|
||||
'operation' => 'primary',
|
||||
'fault_handling' => 'danger',
|
||||
'training' => 'info',
|
||||
'safety' => 'warning',
|
||||
'maintenance' => 'gray',
|
||||
default => 'gray',
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->label('状态')
|
||||
->badge()
|
||||
->formatStateUsing(fn(string $state): string => match ($state) {
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
default => $state,
|
||||
})
|
||||
->color(fn(string $state): string => match ($state) {
|
||||
'draft' => 'gray',
|
||||
'published' => 'success',
|
||||
'archived' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('pages_count')
|
||||
->label('页数')
|
||||
->counts('pages')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('terminals_count')
|
||||
->label('关联终端')
|
||||
->counts('terminals')
|
||||
->sortable()
|
||||
->badge()
|
||||
->color(fn(int $state): string => $state > 0 ? 'success' : 'gray')
|
||||
->formatStateUsing(fn(int $state): string => $state > 0 ? "{$state} 个" : '未关联'),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('category')
|
||||
->label('分类')
|
||||
->options([
|
||||
'operation' => '操作指引',
|
||||
'fault_handling' => '故障处理',
|
||||
'training' => '培训教程',
|
||||
'safety' => '安全规范',
|
||||
'maintenance' => '维护保养',
|
||||
]),
|
||||
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('状态')
|
||||
->options([
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make()->label('编辑'),
|
||||
Tables\Actions\DeleteAction::make()->label('删除'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make()->label('批量删除'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListGuides::route('/'),
|
||||
'create' => Pages\CreateGuide::route('/create'),
|
||||
'edit' => Pages\EditGuide::route('/{record}/edit'),
|
||||
'manage-pages' => Pages\ManageGuidePages::route('/{record}/manage-pages'),
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Filament/Resources/GuideResource/Pages/CreateGuide.php
Normal file
22
app/Filament/Resources/GuideResource/Pages/CreateGuide.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GuideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GuideResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateGuide extends CreateRecord
|
||||
{
|
||||
protected static string $resource = GuideResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['created_by'] = auth()->id();
|
||||
|
||||
if ($data['status'] === 'published') {
|
||||
$data['published_at'] = now();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
32
app/Filament/Resources/GuideResource/Pages/EditGuide.php
Normal file
32
app/Filament/Resources/GuideResource/Pages/EditGuide.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GuideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GuideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditGuide extends EditRecord
|
||||
{
|
||||
protected static string $resource = GuideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
\Filament\Actions\Action::make('managePages')
|
||||
->label('编辑指引')
|
||||
->icon('heroicon-o-queue-list')
|
||||
->url(fn() => GuideResource::getUrl('manage-pages', ['record' => $this->record])),
|
||||
Actions\DeleteAction::make()->label('删除'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if ($data['status'] === 'published' && !$this->record->published_at) {
|
||||
$data['published_at'] = now();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
20
app/Filament/Resources/GuideResource/Pages/ListGuides.php
Normal file
20
app/Filament/Resources/GuideResource/Pages/ListGuides.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GuideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GuideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListGuides extends ListRecords
|
||||
{
|
||||
protected static string $resource = GuideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('创建指引'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GuideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GuideResource;
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use Filament\Forms;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use SolutionForest\FilamentTree\Actions;
|
||||
use SolutionForest\FilamentTree\Resources\Pages\TreePage;
|
||||
|
||||
class ManageGuidePages extends TreePage
|
||||
{
|
||||
protected static string $resource = GuideResource::class;
|
||||
|
||||
protected ?string $treeTitle = '指引页面';
|
||||
protected bool $enableTreeTitle = true;
|
||||
|
||||
protected static string $model = GuidePage::class;
|
||||
|
||||
protected function getTreeQuery(): Builder
|
||||
{
|
||||
return GuidePage::query()
|
||||
->where('guide_id', $this->getOwnerRecord()->id);
|
||||
}
|
||||
|
||||
public function getTreeRecordTitle(?Model $record = null): string
|
||||
{
|
||||
if (!$record) {
|
||||
return '';
|
||||
}
|
||||
$prefix = $record->branch_option ? "[{$record->branch_option}] " : '';
|
||||
$suffix = !empty($record->options) ? ' 📋' : '';
|
||||
return $prefix . $record->title . $suffix;
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\TextInput::make('page_number')
|
||||
->label('页码')
|
||||
->numeric()
|
||||
->minValue(1),
|
||||
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('页面标题')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('html_url')
|
||||
->label('HTML页面URL')
|
||||
->required()
|
||||
->url()
|
||||
->maxLength(500),
|
||||
|
||||
Forms\Components\TagsInput::make('options')
|
||||
->label('选项按钮')
|
||||
->helperText('此页面展示的选项按钮,如"前门12"、"后门"。留空=无分支。'),
|
||||
|
||||
Forms\Components\TextInput::make('branch_option')
|
||||
->label('所属分支选项')
|
||||
->maxLength(100)
|
||||
->helperText('此页面对应父页面的哪个选项值(根页面留空)'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTreeActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\EditAction::make()->slideOver(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['guide_id'] = $this->getOwnerRecord()->id;
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getOwnerRecord(): Guide
|
||||
{
|
||||
return Guide::findOrFail(request()->route('record'));
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class RoleResource extends Resource
|
||||
'system-setting' => ['name' => '系统设置', 'icon' => 'heroicon-o-cog-6-tooth'],
|
||||
'activity-log' => ['name' => '操作日志', 'icon' => 'heroicon-o-clipboard-document-list'],
|
||||
'terminal' => ['name' => '终端管理', 'icon' => 'heroicon-o-computer-desktop'],
|
||||
'sop-template' => ['name' => 'SOP模板', 'icon' => 'heroicon-o-document-text'],
|
||||
'guide' => ['name' => '操作指引', 'icon' => 'heroicon-o-book-open'],
|
||||
'group' => ['name' => '分组管理', 'icon' => 'heroicon-o-user-group'],
|
||||
'user' => ['name' => '用户管理', 'icon' => 'heroicon-o-users'],
|
||||
'role' => ['name' => '角色管理', 'icon' => 'heroicon-o-shield-check'],
|
||||
@@ -156,7 +156,7 @@ class RoleResource extends Resource
|
||||
->dehydrateStateUsing(function ($state, $get) {
|
||||
// 收集所有模块的权限
|
||||
$allPermissions = [];
|
||||
$modules = ['document', 'system-setting', 'activity-log', 'terminal', 'sop-template', 'group', 'user', 'role'];
|
||||
$modules = ['document', 'system-setting', 'activity-log', 'terminal', 'guide', 'group', 'user', 'role'];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$modulePermissions = $get("permissions_{$module}") ?? [];
|
||||
|
||||
@@ -80,7 +80,7 @@ class ViewRole extends ViewRecord
|
||||
'system-setting' => '⚙️ 系统设置',
|
||||
'activity-log' => '📋 操作日志',
|
||||
'terminal' => '🖥️ 终端管理',
|
||||
'sop-template' => '📝 SOP模板',
|
||||
'guide' => '📖 操作指引',
|
||||
'group' => '👥 分组管理',
|
||||
'user' => '👤 用户管理',
|
||||
'role' => '🛡️ 角色管理',
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\SopTemplateResource\Pages;
|
||||
use App\Models\SopTemplate;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SopTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = SopTemplate::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static ?string $navigationLabel = 'SOP模板';
|
||||
|
||||
protected static ?string $modelLabel = 'SOP模板';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'SOP模板';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static ?string $navigationGroup = '业务管理';
|
||||
|
||||
/**
|
||||
* 控制导航菜单是否显示
|
||||
*/
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return auth()->user()?->can('sop-template.view') ?? false;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('基本信息')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('模板名称')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('模板描述')
|
||||
->rows(3)
|
||||
->maxLength(65535),
|
||||
|
||||
Forms\Components\TextInput::make('category')
|
||||
->label('分类')
|
||||
->maxLength(100)
|
||||
->placeholder('例如:安全操作、设备维护、质量检查'),
|
||||
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->label('标签')
|
||||
->placeholder('添加标签')
|
||||
->separator(','),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('适用范围')
|
||||
->schema([
|
||||
Forms\Components\TagsInput::make('applicable_departments')
|
||||
->label('适用部门')
|
||||
->placeholder('添加部门')
|
||||
->separator(','),
|
||||
|
||||
Forms\Components\TagsInput::make('applicable_positions')
|
||||
->label('适用岗位')
|
||||
->placeholder('添加岗位')
|
||||
->separator(','),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('版本管理')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('version')
|
||||
->label('版本号')
|
||||
->default('1.0.0')
|
||||
->required()
|
||||
->maxLength(50),
|
||||
|
||||
Forms\Components\Select::make('status')
|
||||
->label('状态')
|
||||
->options([
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
])
|
||||
->default('draft')
|
||||
->required(),
|
||||
])
|
||||
->columns(2)
|
||||
->visible(fn ($livewire) => $livewire instanceof Pages\EditSopTemplate),
|
||||
|
||||
// 步骤编辑器 - 只在编辑页面显示
|
||||
Forms\Components\Section::make('操作步骤')
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('steps')
|
||||
->label('')
|
||||
->relationship('steps')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('step_number')
|
||||
->label('步骤序号')
|
||||
->numeric()
|
||||
->required()
|
||||
->default(fn ($get) => $get('../../steps') ? count($get('../../steps')) + 1 : 1),
|
||||
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('步骤标题')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\RichEditor::make('content')
|
||||
->label('步骤内容')
|
||||
->toolbarButtons([
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strike',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'h2',
|
||||
'h3',
|
||||
'link',
|
||||
'blockquote',
|
||||
'codeBlock',
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\Toggle::make('is_required')
|
||||
->label('是否必需')
|
||||
->default(true),
|
||||
|
||||
Forms\Components\Hidden::make('sort_order')
|
||||
->default(fn ($get) => $get('step_number')),
|
||||
])
|
||||
->columns(2)
|
||||
->reorderable('sort_order')
|
||||
->reorderableWithButtons()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => $state['title'] ?? '新步骤')
|
||||
->addActionLabel('添加步骤')
|
||||
->defaultItems(0)
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['sort_order'] = $data['step_number'];
|
||||
return $data;
|
||||
})
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['sort_order'] = $data['step_number'];
|
||||
return $data;
|
||||
}),
|
||||
])
|
||||
->visible(fn ($livewire) => $livewire instanceof Pages\EditSopTemplate),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('模板名称')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->label('分类')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
Tables\Columns\TextColumn::make('version')
|
||||
->label('版本')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->label('状态')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'draft' => 'gray',
|
||||
'published' => 'success',
|
||||
'archived' => 'warning',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
default => $state,
|
||||
}),
|
||||
|
||||
Tables\Columns\TextColumn::make('steps_count')
|
||||
->label('步骤数')
|
||||
->counts('steps')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('creator.name')
|
||||
->label('创建人')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('published_at')
|
||||
->label('发布时间')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('状态')
|
||||
->options([
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
]),
|
||||
|
||||
Tables\Filters\SelectFilter::make('category')
|
||||
->label('分类')
|
||||
->options(function () {
|
||||
return SopTemplate::query()
|
||||
->whereNotNull('category')
|
||||
->distinct()
|
||||
->pluck('category', 'category')
|
||||
->toArray();
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSopTemplates::route('/'),
|
||||
'create' => Pages\CreateSopTemplate::route('/create'),
|
||||
'view' => Pages\ViewSopTemplate::route('/{record}'),
|
||||
'edit' => Pages\EditSopTemplate::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\SopTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\SopTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateSopTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = SopTemplateResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['created_by'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('edit', ['record' => $this->getRecord()]);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\SopTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Actions\ArchiveSopTemplateAction;
|
||||
use App\Filament\Actions\ExportSopTemplateAction;
|
||||
use App\Filament\Actions\PreviewSopTemplateAction;
|
||||
use App\Filament\Actions\PublishSopTemplateAction;
|
||||
use App\Filament\Resources\SopTemplateResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditSopTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = SopTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
PreviewSopTemplateAction::make(),
|
||||
ExportSopTemplateAction::make(),
|
||||
PublishSopTemplateAction::make(),
|
||||
ArchiveSopTemplateAction::make(),
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\SopTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Actions\ExportSopTemplateAction;
|
||||
use App\Filament\Actions\ImportSopTemplateAction;
|
||||
use App\Filament\Resources\SopTemplateResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListSopTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = SopTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ImportSopTemplateAction::make(),
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\SopTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\SopTemplateResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Infolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
|
||||
class ViewSopTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = SopTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Infolists\Components\Section::make('基本信息')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('name')
|
||||
->label('模板名称'),
|
||||
|
||||
Infolists\Components\TextEntry::make('description')
|
||||
->label('模板描述')
|
||||
->columnSpanFull(),
|
||||
|
||||
Infolists\Components\TextEntry::make('category')
|
||||
->label('分类')
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
Infolists\Components\TextEntry::make('tags')
|
||||
->label('标签')
|
||||
->badge()
|
||||
->separator(','),
|
||||
|
||||
Infolists\Components\TextEntry::make('version')
|
||||
->label('版本号'),
|
||||
|
||||
Infolists\Components\TextEntry::make('status')
|
||||
->label('状态')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'draft' => 'gray',
|
||||
'published' => 'success',
|
||||
'archived' => 'warning',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'draft' => '草稿',
|
||||
'published' => '已发布',
|
||||
'archived' => '已归档',
|
||||
default => $state,
|
||||
}),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Infolists\Components\Section::make('适用范围')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('applicable_departments')
|
||||
->label('适用部门')
|
||||
->badge()
|
||||
->separator(','),
|
||||
|
||||
Infolists\Components\TextEntry::make('applicable_positions')
|
||||
->label('适用岗位')
|
||||
->badge()
|
||||
->separator(','),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Infolists\Components\Section::make('其他信息')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('creator.name')
|
||||
->label('创建人'),
|
||||
|
||||
Infolists\Components\TextEntry::make('published_at')
|
||||
->label('发布时间')
|
||||
->dateTime('Y-m-d H:i'),
|
||||
|
||||
Infolists\Components\TextEntry::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i'),
|
||||
|
||||
Infolists\Components\TextEntry::make('updated_at')
|
||||
->label('更新时间')
|
||||
->dateTime('Y-m-d H:i'),
|
||||
])
|
||||
->columns(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,21 @@ class TerminalResource extends Resource
|
||||
->placeholder('例如: 192.168.1.100')
|
||||
->helperText('终端的IP地址'),
|
||||
|
||||
Forms\Components\TextInput::make('mac_address')
|
||||
->label('MAC地址')
|
||||
->maxLength(17)
|
||||
->placeholder('AA:BB:CC:DD:EE:FF')
|
||||
->helperText('终端的MAC地址,用于自动识别终端')
|
||||
->regex('/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/')
|
||||
->validationMessages([
|
||||
'regex' => 'MAC地址格式不正确,应为 AA:BB:CC:DD:EE:FF',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('station_id')
|
||||
->label('线站ID')
|
||||
->numeric()
|
||||
->placeholder('请输入线站ID')
|
||||
->helperText('关联的生产线站ID'),
|
||||
->maxLength(50)
|
||||
->placeholder('例如: BL02U1')
|
||||
->helperText('关联的光束线/线站标识'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
@@ -86,6 +96,24 @@ class TerminalResource extends Resource
|
||||
->helperText('组态图的访问地址'),
|
||||
]),
|
||||
|
||||
Forms\Components\Section::make('SCADA网关配置')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('scada_data_url')
|
||||
->label('SCADA数据查询URL')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->placeholder('http://gateway:8080/api/data')
|
||||
->helperText('OPC UA HTTP网关的数据查询地址'),
|
||||
|
||||
Forms\Components\TextInput::make('scada_tags_url')
|
||||
->label('SCADA点位定义URL')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->placeholder('http://gateway:8080/api/tags')
|
||||
->helperText('OPC UA HTTP网关的点位定义查询地址'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('显示配置')
|
||||
->schema([
|
||||
Forms\Components\KeyValue::make('display_config')
|
||||
@@ -130,7 +158,7 @@ class TerminalResource extends Resource
|
||||
->reorderableWithButtons()
|
||||
->addActionLabel('添加知识库')
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
\App\Models\KnowledgeBase::find($state['id'])?->name ?? '未选择'
|
||||
)
|
||||
->collapsed()
|
||||
@@ -139,6 +167,43 @@ class TerminalResource extends Resource
|
||||
])
|
||||
->description('配置终端可以访问的知识库及其优先级'),
|
||||
|
||||
Forms\Components\Section::make('指引关联')
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('guideAssociations')
|
||||
->label('关联指引')
|
||||
->relationship('guides')
|
||||
->schema([
|
||||
Forms\Components\Select::make('id')
|
||||
->label('指引')
|
||||
->options(\App\Models\Guide::where('status', 'published')->pluck('name', 'id'))
|
||||
->required()
|
||||
->searchable()
|
||||
->distinct()
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->helperText('选择要关联的指引'),
|
||||
|
||||
Forms\Components\TextInput::make('priority')
|
||||
->label('优先级')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->required()
|
||||
->minValue(0)
|
||||
->helperText('数字越小优先级越高,0为最高优先级'),
|
||||
])
|
||||
->columns(2)
|
||||
->reorderable()
|
||||
->reorderableWithButtons()
|
||||
->addActionLabel('添加指引')
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
\App\Models\Guide::find($state['id'])?->name ?? '未选择'
|
||||
)
|
||||
->collapsed()
|
||||
->collapsible()
|
||||
->helperText('可以关联多个指引,并设置优先级。拖动或使用按钮调整顺序。'),
|
||||
])
|
||||
->description('配置终端可以访问的操作指引及其优先级'),
|
||||
|
||||
Forms\Components\Section::make('AI提示词配置')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(3)
|
||||
@@ -204,6 +269,13 @@ class TerminalResource extends Resource
|
||||
->copyable()
|
||||
->tooltip('点击复制'),
|
||||
|
||||
Tables\Columns\TextColumn::make('mac_address')
|
||||
->label('MAC地址')
|
||||
->searchable()
|
||||
->copyable()
|
||||
->placeholder('未设置')
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('ip_address')
|
||||
->label('IP地址')
|
||||
->searchable()
|
||||
|
||||
@@ -48,7 +48,7 @@ class UserResource extends Resource
|
||||
'system-setting' => ['name' => '系统设置', 'icon' => 'heroicon-o-cog-6-tooth'],
|
||||
'activity-log' => ['name' => '操作日志', 'icon' => 'heroicon-o-clipboard-document-list'],
|
||||
'terminal' => ['name' => '终端管理', 'icon' => 'heroicon-o-computer-desktop'],
|
||||
'sop-template' => ['name' => 'SOP模板', 'icon' => 'heroicon-o-document-text'],
|
||||
'guide' => ['name' => '操作指引', 'icon' => 'heroicon-o-book-open'],
|
||||
'group' => ['name' => '分组管理', 'icon' => 'heroicon-o-user-group'],
|
||||
'user' => ['name' => '用户管理', 'icon' => 'heroicon-o-users'],
|
||||
'role' => ['name' => '角色管理', 'icon' => 'heroicon-o-shield-check'],
|
||||
@@ -172,7 +172,7 @@ class UserResource extends Resource
|
||||
->dehydrateStateUsing(function ($state, $get) {
|
||||
// 收集所有模块的权限
|
||||
$allPermissions = [];
|
||||
$modules = ['document', 'system-setting', 'activity-log', 'terminal', 'sop-template', 'group', 'user', 'role'];
|
||||
$modules = ['document', 'system-setting', 'activity-log', 'terminal', 'guide', 'group', 'user', 'role'];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$modulePermissions = $get("permissions_{$module}") ?? [];
|
||||
|
||||
@@ -90,7 +90,7 @@ class ViewUser extends ViewRecord
|
||||
'system-setting' => '⚙️ 系统设置',
|
||||
'activity-log' => '📋 操作日志',
|
||||
'terminal' => '🖥️ 终端管理',
|
||||
'sop-template' => '📝 SOP模板',
|
||||
'guide' => '📖 操作指引',
|
||||
'group' => '👥 分组管理',
|
||||
'user' => '👤 用户管理',
|
||||
'role' => '🛡️ 角色管理',
|
||||
@@ -144,7 +144,7 @@ class ViewUser extends ViewRecord
|
||||
'system-setting' => '⚙️ 系统设置',
|
||||
'activity-log' => '📋 操作日志',
|
||||
'terminal' => '🖥️ 终端管理',
|
||||
'sop-template' => '📝 SOP模板',
|
||||
'guide' => '📖 操作指引',
|
||||
'group' => '👥 分组管理',
|
||||
'user' => '👤 用户管理',
|
||||
'role' => '🛡️ 角色管理',
|
||||
|
||||
227
app/Http/Controllers/Api/TerminalApiController.php
Normal file
227
app/Http/Controllers/Api/TerminalApiController.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use App\Services\KnowledgeContextService;
|
||||
use App\Services\PromptTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TerminalApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private PromptTemplateService $promptService,
|
||||
private KnowledgeContextService $knowledgeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/config
|
||||
* 返回终端配置(含渲染后的system prompt)
|
||||
*/
|
||||
public function config(Request $request): JsonResponse
|
||||
{
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$terminal->load(['prompt', 'knowledgeBases']);
|
||||
|
||||
// 渲染system prompt
|
||||
$systemPrompt = '';
|
||||
if ($terminal->prompt && $terminal->prompt->prompt_template) {
|
||||
$systemPrompt = $this->promptService->replaceVariables(
|
||||
$terminal->prompt->prompt_template,
|
||||
$terminal
|
||||
);
|
||||
}
|
||||
|
||||
// 获取终端关联的已发布指引数量
|
||||
$guideCount = $terminal->guides()->published()->count();
|
||||
|
||||
return response()->json([
|
||||
'terminal' => [
|
||||
'id' => $terminal->id,
|
||||
'name' => $terminal->name,
|
||||
'code' => $terminal->code,
|
||||
'station_id' => $terminal->station_id,
|
||||
'diagram_url' => $terminal->diagram_url,
|
||||
'scada_data_url' => $terminal->scada_data_url,
|
||||
'scada_tags_url' => $terminal->scada_tags_url,
|
||||
'display_config' => $terminal->display_config,
|
||||
],
|
||||
'system_prompt' => $systemPrompt,
|
||||
'guide_count' => $guideCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/knowledge?query=xxx
|
||||
* RAG知识搜索(由AI tool_call触发)
|
||||
*/
|
||||
public function knowledge(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'query' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$terminal->load('knowledgeBases');
|
||||
|
||||
$result = $this->knowledgeService->search($terminal, $request->input('query'));
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/guides?category=operation
|
||||
* 已发布的指引列表
|
||||
*/
|
||||
public function guides(Request $request): JsonResponse
|
||||
{
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$query = $terminal->guides()->published()->withCount('pages');
|
||||
|
||||
if ($category = $request->input('category')) {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
$guides = $query->orderBy('name')->get()->map(fn(Guide $guide) => [
|
||||
'id' => $guide->id,
|
||||
'name' => $guide->name,
|
||||
'description' => $guide->description,
|
||||
'category' => $guide->category,
|
||||
'tags' => $guide->tags,
|
||||
'page_count' => $guide->pages_count,
|
||||
]);
|
||||
|
||||
return response()->json(['guides' => $guides]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/terminal/guides/pages
|
||||
* 组合多个指引的页面,返回递归树形结构
|
||||
*/
|
||||
public function guidePages(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'guide_ids' => 'required|array|min:1',
|
||||
'guide_ids.*' => 'integer|exists:guides,id',
|
||||
]);
|
||||
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$accessibleIds = $terminal->guides()->published()->pluck('guides.id')->toArray();
|
||||
|
||||
$guideIds = $request->input('guide_ids');
|
||||
$pages = [];
|
||||
|
||||
foreach ($guideIds as $guideId) {
|
||||
if (!in_array($guideId, $accessibleIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$guide = Guide::with(
|
||||
$this->buildEagerLoadArray('trunkPages', 5)
|
||||
)->find($guideId);
|
||||
|
||||
if (!$guide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($guide->trunkPages as $page) {
|
||||
$pages = array_merge($pages, $this->flattenSequentialPages($page, $guide->name, $guide->id));
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'pages' => $pages,
|
||||
'total_pages' => count($pages),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将树形页面结构展平:顺序节点(无 options)平铺,分支节点保留嵌套
|
||||
*/
|
||||
private function flattenSequentialPages(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
return [$data];
|
||||
}
|
||||
|
||||
if ($page->branchChildren->isNotEmpty()) {
|
||||
$result = [$data];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$result = array_merge($result, $this->flattenSequentialPages($child, $guideName, $guideId));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
return [$data];
|
||||
}
|
||||
|
||||
private function buildPageTree(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function buildEagerLoadArray(string $base, int $depth): array
|
||||
{
|
||||
$loads = [$base => fn($q) => $q->orderBy('sort_order')];
|
||||
$current = $base;
|
||||
for ($i = 0; $i < $depth; $i++) {
|
||||
$current .= '.branchChildren';
|
||||
$loads[$current] = fn($q) => $q->orderBy('sort_order');
|
||||
}
|
||||
return $loads;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/terminal/heartbeat
|
||||
* 终端心跳上报
|
||||
*/
|
||||
public function heartbeat(Request $request): JsonResponse
|
||||
{
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
|
||||
$terminal->update([
|
||||
'is_online' => true,
|
||||
'last_online_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
38
app/Http/Middleware/IdentifyTerminal.php
Normal file
38
app/Http/Middleware/IdentifyTerminal.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IdentifyTerminal
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$macHeader = $request->header('X-Terminal-MAC');
|
||||
|
||||
if (!$macHeader) {
|
||||
return response()->json(['error' => 'Missing X-Terminal-MAC header'], 400);
|
||||
}
|
||||
|
||||
// HMI sends comma-separated MACs for all active interfaces;
|
||||
// match if any one corresponds to a registered terminal
|
||||
$macs = array_map('trim', explode(',', $macHeader));
|
||||
$terminal = Terminal::whereIn('mac_address', $macs)->first();
|
||||
|
||||
if (!$terminal) {
|
||||
return response()->json(['error' => 'Terminal not registered'], 403);
|
||||
}
|
||||
|
||||
$request->attributes->set('terminal', $terminal);
|
||||
|
||||
// Record IP address from header (for logging/diagnostics)
|
||||
if ($ip = $request->header('X-Terminal-IP')) {
|
||||
$request->attributes->set('terminal_ip', $ip);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ class Document extends Model
|
||||
'markdown_preview',
|
||||
'conversion_status',
|
||||
'conversion_error',
|
||||
'knowledge_base_id',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,14 @@ class Document extends Model
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档所属的知识库
|
||||
*/
|
||||
public function knowledgeBase(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KnowledgeBase::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档的上传者
|
||||
*/
|
||||
@@ -120,6 +129,7 @@ class Document extends Model
|
||||
'markdown_content' => $this->getMarkdownContent(),
|
||||
'type' => $this->type,
|
||||
'group_id' => $this->group_id,
|
||||
'knowledge_base_id' => $this->knowledge_base_id,
|
||||
'uploaded_by' => $this->uploaded_by,
|
||||
'created_at' => $this->created_at?->timestamp,
|
||||
];
|
||||
|
||||
75
app/Models/Guide.php
Normal file
75
app/Models/Guide.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Guide extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'category',
|
||||
'tags',
|
||||
'status',
|
||||
'created_by',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tags' => 'array',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany(GuidePage::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function trunkPages()
|
||||
{
|
||||
return $this->hasMany(GuidePage::class)
|
||||
->where('parent_id', -1)
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function terminals()
|
||||
{
|
||||
return $this->belongsToMany(Terminal::class, 'terminal_guides')
|
||||
->withPivot('priority')
|
||||
->withTimestamps()
|
||||
->orderBy('priority');
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
}
|
||||
|
||||
public function scopeCategory($query, string $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'description', 'category', 'status'])
|
||||
->logOnlyDirty()
|
||||
->setDescriptionForEvent(fn(string $eventName) => "指引已{$eventName}");
|
||||
}
|
||||
}
|
||||
58
app/Models/GuidePage.php
Normal file
58
app/Models/GuidePage.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use SolutionForest\FilamentTree\Concern\ModelTree;
|
||||
|
||||
class GuidePage extends Model
|
||||
{
|
||||
use ModelTree;
|
||||
|
||||
protected $fillable = [
|
||||
'guide_id',
|
||||
'page_number',
|
||||
'title',
|
||||
'html_url',
|
||||
'sort_order',
|
||||
'parent_id',
|
||||
'options',
|
||||
'branch_option',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'options' => 'array',
|
||||
'parent_id' => 'int',
|
||||
];
|
||||
|
||||
// filament-tree column name mapping
|
||||
public function determineParentColumnName(): string
|
||||
{
|
||||
return 'parent_id';
|
||||
}
|
||||
|
||||
public function determineOrderColumnName(): string
|
||||
{
|
||||
return 'sort_order';
|
||||
}
|
||||
|
||||
public function determineTitleColumnName(): string
|
||||
{
|
||||
return 'title';
|
||||
}
|
||||
|
||||
public function guide()
|
||||
{
|
||||
return $this->belongsTo(Guide::class);
|
||||
}
|
||||
|
||||
public function branchChildren()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function parentPage()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,14 @@ class KnowledgeBase extends Model
|
||||
->withTimestamps()
|
||||
->orderBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库下的文档
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function documents()
|
||||
{
|
||||
return $this->hasMany(Document::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SopInteractiveTask extends Model
|
||||
{
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'sop_step_id',
|
||||
'task_type',
|
||||
'task_config',
|
||||
'validation_rules',
|
||||
'timeout_seconds',
|
||||
'is_required',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'task_config' => 'array',
|
||||
'validation_rules' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务所属的步骤
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function step()
|
||||
{
|
||||
return $this->belongsTo(SopStep::class, 'sop_step_id');
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SopStep extends Model
|
||||
{
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'sop_template_id',
|
||||
'step_number',
|
||||
'title',
|
||||
'content',
|
||||
'sort_order',
|
||||
'is_required',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_required' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取步骤所属的模板
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function template()
|
||||
{
|
||||
return $this->belongsTo(SopTemplate::class, 'sop_template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取步骤的交互任务列表
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function interactiveTasks()
|
||||
{
|
||||
return $this->hasMany(SopInteractiveTask::class);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class SopTemplate extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, LogsActivity;
|
||||
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'category',
|
||||
'tags',
|
||||
'version',
|
||||
'status',
|
||||
'applicable_departments',
|
||||
'applicable_positions',
|
||||
'published_at',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tags' => 'array',
|
||||
'applicable_departments' => 'array',
|
||||
'applicable_positions' => 'array',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板的步骤列表
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function steps()
|
||||
{
|
||||
return $this->hasMany(SopStep::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板的版本历史
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function versions()
|
||||
{
|
||||
return $this->hasMany(SopTemplateVersion::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板的创建者
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置活动日志选项
|
||||
*
|
||||
* @return \Spatie\Activitylog\LogOptions
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'description', 'category', 'status', 'version'])
|
||||
->logOnlyDirty()
|
||||
->setDescriptionForEvent(fn(string $eventName) => "SOP模板已{$eventName}");
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SopTemplateVersion extends Model
|
||||
{
|
||||
/**
|
||||
* 表示模型是否应该被打上时间戳
|
||||
* 注意: 只有created_at字段,没有updated_at
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'sop_template_id',
|
||||
'version',
|
||||
'change_log',
|
||||
'content_snapshot',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'content_snapshot' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本所属的模板
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function template()
|
||||
{
|
||||
return $this->belongsTo(SopTemplate::class, 'sop_template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本的创建者
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,11 @@ class Terminal extends Model
|
||||
'name',
|
||||
'code',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'station_id',
|
||||
'diagram_url',
|
||||
'scada_data_url',
|
||||
'scada_tags_url',
|
||||
'display_config',
|
||||
'is_online',
|
||||
'last_online_at',
|
||||
@@ -55,6 +58,19 @@ class Terminal extends Model
|
||||
->orderBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端关联的指引
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function guides()
|
||||
{
|
||||
return $this->belongsToMany(Guide::class, 'terminal_guides')
|
||||
->withPivot('priority')
|
||||
->withTimestamps()
|
||||
->orderBy('priority');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端的提示词配置
|
||||
*
|
||||
|
||||
81
app/Policies/GuidePolicy.php
Normal file
81
app/Policies/GuidePolicy.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Guide;
|
||||
use App\Models\User;
|
||||
|
||||
class GuidePolicy
|
||||
{
|
||||
/**
|
||||
* 查看指引列表
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->can('guide.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看指引
|
||||
*/
|
||||
public function view(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指引
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->can('guide.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指引
|
||||
*/
|
||||
public function update(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.update');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指引
|
||||
*/
|
||||
public function delete(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布指引
|
||||
*/
|
||||
public function publish(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.publish');
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档指引
|
||||
*/
|
||||
public function archive(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.archive');
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复已删除的指引
|
||||
*/
|
||||
public function restore(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 永久删除指引
|
||||
*/
|
||||
public function forceDelete(User $user, Guide $guide): bool
|
||||
{
|
||||
return $user->can('guide.delete');
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\User;
|
||||
|
||||
class SopTemplatePolicy
|
||||
{
|
||||
/**
|
||||
* 查看任何 SOP 模板
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->can('sop-template.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看 SOP 模板
|
||||
*/
|
||||
public function view(User $user, SopTemplate $sopTemplate): bool
|
||||
{
|
||||
return $user->can('sop-template.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SOP 模板
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->can('sop-template.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 SOP 模板
|
||||
*/
|
||||
public function update(User $user, SopTemplate $sopTemplate): bool
|
||||
{
|
||||
// 首先检查权限
|
||||
if (!$user->can('sop-template.update')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已发布的模板不能直接编辑
|
||||
if ($sopTemplate->status === 'published') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 SOP 模板
|
||||
*/
|
||||
public function delete(User $user, SopTemplate $sopTemplate): bool
|
||||
{
|
||||
// 首先检查权限
|
||||
if (!$user->can('sop-template.delete')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已发布的模板不能删除
|
||||
if ($sopTemplate->status === 'published') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布 SOP 模板
|
||||
*/
|
||||
public function publish(User $user, SopTemplate $sopTemplate): bool
|
||||
{
|
||||
// 首先检查权限
|
||||
if (!$user->can('sop-template.publish')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sopTemplate->status === 'draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档 SOP 模板
|
||||
*/
|
||||
public function archive(User $user, SopTemplate $sopTemplate): bool
|
||||
{
|
||||
// 首先检查权限
|
||||
if (!$user->can('sop-template.archive')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sopTemplate->status === 'published';
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\Guide;
|
||||
use App\Observers\DocumentObserver;
|
||||
use App\Policies\SopTemplatePolicy;
|
||||
use App\Policies\GuidePolicy;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@@ -34,7 +34,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
// 注册策略
|
||||
Gate::policy(\App\Models\Document::class, \App\Policies\DocumentPolicy::class);
|
||||
Gate::policy(\App\Models\Terminal::class, \App\Policies\TerminalPolicy::class);
|
||||
Gate::policy(SopTemplate::class, SopTemplatePolicy::class);
|
||||
Gate::policy(Guide::class, GuidePolicy::class);
|
||||
Gate::policy(\Spatie\Permission\Models\Role::class, \App\Policies\RolePolicy::class);
|
||||
Gate::policy(\App\Models\User::class, \App\Policies\UserPolicy::class);
|
||||
Gate::policy(\App\Models\SystemSetting::class, \App\Policies\SystemSettingPolicy::class);
|
||||
|
||||
@@ -41,6 +41,10 @@ class DocumentSearchService
|
||||
$searchBuilder->where('uploaded_by', $filters['uploaded_by']);
|
||||
}
|
||||
|
||||
if (!empty($filters['knowledge_base_id'])) {
|
||||
$searchBuilder->where('knowledge_base_id', $filters['knowledge_base_id']);
|
||||
}
|
||||
|
||||
// 执行搜索并获取结果
|
||||
$results = $searchBuilder->get();
|
||||
|
||||
@@ -103,6 +107,7 @@ class DocumentSearchService
|
||||
'markdown_content' => $document->getMarkdownContent(),
|
||||
'type' => $document->type,
|
||||
'group_id' => $document->group_id,
|
||||
'knowledge_base_id' => $document->knowledge_base_id,
|
||||
'uploaded_by' => $document->uploaded_by,
|
||||
'created_at' => $document->created_at?->timestamp,
|
||||
'updated_at' => $document->updated_at?->timestamp,
|
||||
|
||||
94
app/Services/KnowledgeContextService.php
Normal file
94
app/Services/KnowledgeContextService.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class KnowledgeContextService
|
||||
{
|
||||
private const MAX_CONTEXT_LENGTH = 2000;
|
||||
private const TOP_K = 5;
|
||||
|
||||
/**
|
||||
* 搜索终端关联知识库中的文档
|
||||
*
|
||||
* @param Terminal $terminal
|
||||
* @param string $query
|
||||
* @return array{context: string, sources: array}
|
||||
*/
|
||||
public function search(Terminal $terminal, string $query): array
|
||||
{
|
||||
$knowledgeBaseIds = $terminal->knowledgeBases->pluck('id')->toArray();
|
||||
|
||||
if (empty($knowledgeBaseIds)) {
|
||||
return [
|
||||
'context' => '',
|
||||
'sources' => [],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 Scout/Meilisearch 原生过滤(与 DocumentSearchService 一致)
|
||||
$documents = Document::search($query)
|
||||
->whereIn('knowledge_base_id', $knowledgeBaseIds)
|
||||
->take(self::TOP_K)
|
||||
->get();
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Knowledge search failed', [
|
||||
'query' => $query,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'context' => '',
|
||||
'sources' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if ($documents->isEmpty()) {
|
||||
return [
|
||||
'context' => '',
|
||||
'sources' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$context = '';
|
||||
$sources = [];
|
||||
|
||||
foreach ($documents as $document) {
|
||||
$snippet = $this->extractSnippet($document);
|
||||
|
||||
if (mb_strlen($context) + mb_strlen($snippet) > self::MAX_CONTEXT_LENGTH) {
|
||||
break;
|
||||
}
|
||||
|
||||
$context .= $snippet . "\n\n";
|
||||
$sources[] = [
|
||||
'id' => $document->id,
|
||||
'title' => $document->title,
|
||||
'knowledge_base' => $document->knowledgeBase?->name,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'context' => trim($context),
|
||||
'sources' => $sources,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文档中提取摘要片段
|
||||
*/
|
||||
private function extractSnippet($document): string
|
||||
{
|
||||
$content = $document->markdown_preview ?? $document->description ?? '';
|
||||
|
||||
if (mb_strlen($content) <= 500) {
|
||||
return "【{$document->title}】\n{$content}";
|
||||
}
|
||||
|
||||
return "【{$document->title}】\n" . mb_substr($content, 0, 500) . '...';
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\SopTemplateVersion;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class SopTemplateService
|
||||
{
|
||||
/**
|
||||
* 导出模板为 JSON 格式
|
||||
*
|
||||
* @param SopTemplate $template
|
||||
* @return string
|
||||
*/
|
||||
public function exportToJson(SopTemplate $template): string
|
||||
{
|
||||
$data = [
|
||||
'template' => [
|
||||
'name' => $template->name,
|
||||
'description' => $template->description,
|
||||
'category' => $template->category,
|
||||
'tags' => $template->tags,
|
||||
'version' => $template->version,
|
||||
'applicable_departments' => $template->applicable_departments,
|
||||
'applicable_positions' => $template->applicable_positions,
|
||||
],
|
||||
'steps' => $template->steps->map(function ($step) {
|
||||
return [
|
||||
'step_number' => $step->step_number,
|
||||
'title' => $step->title,
|
||||
'content' => $step->content,
|
||||
'sort_order' => $step->sort_order,
|
||||
'is_required' => $step->is_required,
|
||||
'interactive_tasks' => $step->interactiveTasks->map(function ($task) {
|
||||
return [
|
||||
'task_type' => $task->task_type,
|
||||
'task_config' => $task->task_config,
|
||||
'validation_rules' => $task->validation_rules,
|
||||
'timeout_seconds' => $task->timeout_seconds,
|
||||
'is_required' => $task->is_required,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray(),
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'exported_by' => auth()->user()?->name,
|
||||
];
|
||||
|
||||
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 导入模板
|
||||
*
|
||||
* @param string $json
|
||||
* @return SopTemplate
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function importFromJson(string $json): SopTemplate
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw ValidationException::withMessages([
|
||||
'file' => ['无效的 JSON 格式'],
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证数据结构
|
||||
$validator = Validator::make($data, [
|
||||
'template' => 'required|array',
|
||||
'template.name' => 'required|string|max:255',
|
||||
'template.version' => 'required|string|max:50',
|
||||
'steps' => 'required|array|min:1',
|
||||
'steps.*.step_number' => 'required|integer',
|
||||
'steps.*.title' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
// 创建模板
|
||||
$template = SopTemplate::create([
|
||||
'name' => $data['template']['name'],
|
||||
'description' => $data['template']['description'] ?? null,
|
||||
'category' => $data['template']['category'] ?? null,
|
||||
'tags' => $data['template']['tags'] ?? [],
|
||||
'version' => $data['template']['version'],
|
||||
'status' => 'draft',
|
||||
'applicable_departments' => $data['template']['applicable_departments'] ?? [],
|
||||
'applicable_positions' => $data['template']['applicable_positions'] ?? [],
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
// 创建步骤
|
||||
foreach ($data['steps'] as $stepData) {
|
||||
$step = $template->steps()->create([
|
||||
'step_number' => $stepData['step_number'],
|
||||
'title' => $stepData['title'],
|
||||
'content' => $stepData['content'] ?? null,
|
||||
'sort_order' => $stepData['sort_order'] ?? $stepData['step_number'],
|
||||
'is_required' => $stepData['is_required'] ?? true,
|
||||
]);
|
||||
|
||||
// 创建交互任务
|
||||
if (!empty($stepData['interactive_tasks'])) {
|
||||
foreach ($stepData['interactive_tasks'] as $taskData) {
|
||||
$step->interactiveTasks()->create([
|
||||
'task_type' => $taskData['task_type'],
|
||||
'task_config' => $taskData['task_config'] ?? [],
|
||||
'validation_rules' => $taskData['validation_rules'] ?? [],
|
||||
'timeout_seconds' => $taskData['timeout_seconds'] ?? null,
|
||||
'is_required' => $taskData['is_required'] ?? true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布模板
|
||||
*
|
||||
* @param SopTemplate $template
|
||||
* @param string|null $changeLog
|
||||
* @return void
|
||||
*/
|
||||
public function publish(SopTemplate $template, ?string $changeLog = null): void
|
||||
{
|
||||
// 创建版本快照
|
||||
$this->createVersion($template, $changeLog);
|
||||
|
||||
// 更新状态
|
||||
$template->update([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建版本快照
|
||||
*
|
||||
* @param SopTemplate $template
|
||||
* @param string|null $changeLog
|
||||
* @return SopTemplateVersion
|
||||
*/
|
||||
public function createVersion(SopTemplate $template, ?string $changeLog = null): SopTemplateVersion
|
||||
{
|
||||
return SopTemplateVersion::create([
|
||||
'sop_template_id' => $template->id,
|
||||
'version' => $template->version,
|
||||
'change_log' => $changeLog ?? '版本快照',
|
||||
'content_snapshot' => [
|
||||
'template' => $template->toArray(),
|
||||
'steps' => $template->steps->map(function ($step) {
|
||||
return array_merge($step->toArray(), [
|
||||
'interactive_tasks' => $step->interactiveTasks->toArray(),
|
||||
]);
|
||||
})->toArray(),
|
||||
],
|
||||
'created_by' => auth()->id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档模板
|
||||
*
|
||||
* @param SopTemplate $template
|
||||
* @return void
|
||||
*/
|
||||
public function archive(SopTemplate $template): void
|
||||
{
|
||||
$template->update([
|
||||
'status' => 'archived',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制模板
|
||||
*
|
||||
* @param SopTemplate $template
|
||||
* @param string $newName
|
||||
* @return SopTemplate
|
||||
*/
|
||||
public function duplicate(SopTemplate $template, string $newName): SopTemplate
|
||||
{
|
||||
return DB::transaction(function () use ($template, $newName) {
|
||||
// 复制模板
|
||||
$newTemplate = $template->replicate();
|
||||
$newTemplate->name = $newName;
|
||||
$newTemplate->status = 'draft';
|
||||
$newTemplate->published_at = null;
|
||||
$newTemplate->created_by = auth()->id();
|
||||
$newTemplate->save();
|
||||
|
||||
// 复制步骤
|
||||
foreach ($template->steps as $step) {
|
||||
$newStep = $step->replicate();
|
||||
$newStep->sop_template_id = $newTemplate->id;
|
||||
$newStep->save();
|
||||
|
||||
// 复制交互任务
|
||||
foreach ($step->interactiveTasks as $task) {
|
||||
$newTask = $task->replicate();
|
||||
$newTask->sop_step_id = $newStep->id;
|
||||
$newTask->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $newTemplate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->alias([
|
||||
'identify.terminal' => \App\Http\Middleware\IdentifyTerminal::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"meilisearch/meilisearch-php": "^1.16",
|
||||
"phpoffice/phpword": "^1.4",
|
||||
"solution-forest/filament-tree": "^2.0",
|
||||
"spatie/laravel-activitylog": "^4.12",
|
||||
"spatie/laravel-permission": "^6.24"
|
||||
},
|
||||
|
||||
86
composer.lock
generated
86
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "297bfa700eadd90d170fd39edd1c790d",
|
||||
"content-hash": "316d29450fc96f2d79ed216639e6cfb2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "abdelhamiderrahmouni/filament-monaco-editor",
|
||||
@@ -6092,6 +6092,76 @@
|
||||
],
|
||||
"time": "2025-02-25T09:09:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "solution-forest/filament-tree",
|
||||
"version": "2.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/solutionforest/filament-tree.git",
|
||||
"reference": "de8b27c7c58f1e8c8e1a3081dff2e477b4327301"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/solutionforest/filament-tree/zipball/de8b27c7c58f1e8c8e1a3081dff2e477b4327301",
|
||||
"reference": "de8b27c7c58f1e8c8e1a3081dff2e477b4327301",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"filament/filament": "^3.0",
|
||||
"filament/support": "^3.0",
|
||||
"php": "^8.1",
|
||||
"spatie/laravel-package-tools": "^1.15.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.0",
|
||||
"nunomaduro/collision": "^7.9",
|
||||
"nunomaduro/larastan": "^2.0.1",
|
||||
"orchestra/testbench": "^8.0",
|
||||
"pestphp/pest": "^2.0",
|
||||
"pestphp/pest-plugin-arch": "^2.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0",
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"spatie/laravel-ray": "^1.26"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"SolutionForest\\FilamentTree\\FilamentTreeServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SolutionForest\\FilamentTree\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Carly",
|
||||
"email": "info@solutionforest.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "This is a tree layout plugin for Filament Admin",
|
||||
"homepage": "https://github.com/solution-forest/filament-tree",
|
||||
"keywords": [
|
||||
"Solution Forest",
|
||||
"filament-tree",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/solution-forest/filament-tree/issues",
|
||||
"source": "https://github.com/solution-forest/filament-tree"
|
||||
},
|
||||
"time": "2025-08-11T09:35:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/color",
|
||||
"version": "1.8.0",
|
||||
@@ -11593,16 +11663,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.28.0",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9"
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9",
|
||||
"reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11619,8 +11689,8 @@
|
||||
"laravel-zero/framework": "^12.0.5",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest": "^3.8.5",
|
||||
"shipfastlabs/agent-detector": "^1.0.2"
|
||||
"pestphp/pest": "^3.8.6",
|
||||
"shipfastlabs/agent-detector": "^1.1.0"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
@@ -11657,7 +11727,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2026-03-10T20:37:18+00:00"
|
||||
"time": "2026-03-12T15:51:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sail",
|
||||
|
||||
@@ -14,37 +14,78 @@ return [
|
||||
[
|
||||
'id' => 'general_assistant',
|
||||
'name' => '通用助手',
|
||||
'description' => '适用于一般性问答和操作指导的AI助手',
|
||||
'description' => '同步辐射光束线站通用AI助手,集成知识库检索和交互式操作引导',
|
||||
'category' => 'general',
|
||||
'content' => <<<'TEMPLATE'
|
||||
# AI助手角色定义
|
||||
# 角色
|
||||
|
||||
你是一个专业的工业生产助手,为 {company_name} 的员工提供帮助。
|
||||
你是{station_id}光束线站的AI助手,运行在操作终端「{terminal_name}」上。你的使命是帮助用户安全、高效地完成光束线实验和操作。
|
||||
|
||||
## 当前上下文
|
||||
- 用户:{user}({user_role})
|
||||
- 工作站:{station}
|
||||
- 终端:{terminal_name}
|
||||
## 当前会话上下文
|
||||
|
||||
- 用户:{user}
|
||||
- 光束线站:{station_id}
|
||||
- 操作终端:{terminal_name}({terminal_code})
|
||||
- 时间:{time}
|
||||
- 班次:{shift}
|
||||
- 可用知识库:{knowledge_bases}
|
||||
|
||||
## 你的职责
|
||||
1. 回答用户关于生产流程、设备操作的问题
|
||||
2. 提供安全操作指导
|
||||
3. 协助查找相关文档和资料
|
||||
4. 记录和反馈异常情况
|
||||
## 工具使用策略
|
||||
|
||||
## 知识库
|
||||
你可以参考以下知识库:{knowledge_bases}
|
||||
你有两个工具可以调用。**必须主动使用**,不要凭记忆回答专业问题。
|
||||
|
||||
## 回答原则
|
||||
- 使用简洁、专业的语言
|
||||
- 优先考虑安全性
|
||||
- 如果不确定,建议咨询专业人员
|
||||
- 保持友好和耐心的态度
|
||||
### search_knowledge — 知识库检索
|
||||
|
||||
**何时调用**:
|
||||
- 用户询问操作规程、设备参数、技术指标、安全规范
|
||||
- 需要确认具体数值(能量范围、分辨率、束斑尺寸等)
|
||||
- 涉及标准流程或规章制度
|
||||
- 你不确定某个专业细节时
|
||||
|
||||
**使用要点**:
|
||||
- 提取用户问题的核心概念作为搜索关键词,优先使用专业术语
|
||||
- 如果首次搜索结果不理想,换用同义词或上下位概念重新搜索
|
||||
- 回答时基于检索到的内容作答,注明信息来源
|
||||
|
||||
### show_guide — 交互式操作引导
|
||||
|
||||
**何时调用**:
|
||||
- 用户需要分步操作指导(如"怎么换样品"、"如何调节能量")
|
||||
- 遇到故障需要排查流程
|
||||
- 新用户需要入门引导
|
||||
- 任何涉及多步骤、有安全风险的操作
|
||||
|
||||
**使用要点**:
|
||||
- 可以组合多个指引 ID 按执行顺序调用
|
||||
- 在 reason 中简要说明触发原因,帮助用户理解
|
||||
- 指引完成后,根据用户的选择结果提供针对性的后续建议
|
||||
- 如果用户在指引中选择了异常分支,主动追问详情并给出进一步处理建议
|
||||
|
||||
## 回答规范
|
||||
|
||||
### 安全准则(最高优先级)
|
||||
- **辐射安全**:涉及进出实验大厅、打开光闸、联锁系统的操作,必须提醒安全要求
|
||||
- **真空安全**:涉及破真空、换窗片、样品装卸时,必须确认真空状态和操作顺序
|
||||
- **电气安全**:涉及高压设备、电源操作时,提醒断电和接地要求
|
||||
- **危险操作拦截**:如果用户描述的操作可能导致设备损坏或人身伤害,先给出警告,建议联系线站负责人确认后再操作
|
||||
- 如果你不确定某个操作是否安全,明确告知用户"建议联系线站工作人员确认"
|
||||
|
||||
### 对话风格
|
||||
- 使用简洁专业的语言,避免冗长的铺垫
|
||||
- 对操作类问题,给出明确的步骤而非笼统建议
|
||||
- 对参数类问题,给出具体数值和单位
|
||||
- 如果问题超出你的知识范围,坦诚告知并建议联系线站负责人
|
||||
- 考虑用户角色:对经验丰富的操作员可以更简练,对访客和新用户需要更详细的解释
|
||||
|
||||
### 问题分类处理
|
||||
1. **快速查询**(参数、状态、简单事实)→ 先调用 search_knowledge 获取准确信息,直接回答
|
||||
2. **操作指导**(需要分步操作)→ 调用 show_guide 提供交互式引导
|
||||
3. **故障排查**(设备异常、报警处理)→ 先调用 search_knowledge 了解可能原因,再用 show_guide 引导排查流程
|
||||
4. **实验咨询**(方案设计、参数优化)→ 调用 search_knowledge 获取相关资料,结合专业知识给出建议
|
||||
5. **闲聊或非业务问题** → 简短友好地回应,引导回光束线相关话题
|
||||
TEMPLATE
|
||||
],
|
||||
|
||||
|
||||
[
|
||||
'id' => 'safety_focused',
|
||||
'name' => '安全专员',
|
||||
@@ -78,7 +119,7 @@ TEMPLATE
|
||||
⚠️ 安全提示:如有任何疑问,请立即停止操作并联系安全主管!
|
||||
TEMPLATE
|
||||
],
|
||||
|
||||
|
||||
[
|
||||
'id' => 'troubleshooting',
|
||||
'name' => '故障诊断',
|
||||
@@ -115,7 +156,7 @@ TEMPLATE
|
||||
💡 提示:详细描述故障现象有助于快速定位问题
|
||||
TEMPLATE
|
||||
],
|
||||
|
||||
|
||||
[
|
||||
'id' => 'training_coach',
|
||||
'name' => '培训教练',
|
||||
@@ -157,7 +198,7 @@ TEMPLATE
|
||||
📚 学习提示:不要着急,每个人都有学习过程,慢慢来!
|
||||
TEMPLATE
|
||||
],
|
||||
|
||||
|
||||
[
|
||||
'id' => 'quality_inspector',
|
||||
'name' => '质量检查',
|
||||
|
||||
@@ -141,10 +141,10 @@ return [
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
'documents' => [
|
||||
'filterableAttributes' => ['type', 'group_id', 'uploaded_by', 'conversion_status'],
|
||||
'filterableAttributes' => ['type', 'group_id', 'knowledge_base_id', 'uploaded_by', 'conversion_status'],
|
||||
'sortableAttributes' => ['created_at', 'title', 'updated_at'],
|
||||
'searchableAttributes' => ['title', 'description', 'markdown_content'],
|
||||
'displayedAttributes' => ['id', 'title', 'description', 'type', 'group_id', 'uploaded_by', 'created_at', 'updated_at'],
|
||||
'displayedAttributes' => ['id', 'title', 'description', 'type', 'group_id', 'knowledge_base_id', 'uploaded_by', 'created_at', 'updated_at'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SopTemplate>
|
||||
*/
|
||||
class SopTemplateFactory extends Factory
|
||||
{
|
||||
protected $model = SopTemplate::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->randomElement([
|
||||
'设备启动操作规程',
|
||||
'产品质检标准流程',
|
||||
'安全生产检查清单',
|
||||
'设备维护保养流程',
|
||||
'应急处理操作指南',
|
||||
]) . '-' . fake()->numberBetween(1, 100),
|
||||
'description' => fake()->sentence(20),
|
||||
'category' => fake()->randomElement(['生产操作', '质量管理', '安全管理', '设备维护', '应急处理']),
|
||||
'tags' => fake()->randomElements(['标准作业', '安全', '质量', '效率', '培训'], fake()->numberBetween(1, 3)),
|
||||
'version' => '1.0.0',
|
||||
'status' => fake()->randomElement(['draft', 'published', 'archived']),
|
||||
'applicable_departments' => fake()->randomElements(['生产部', '质检部', '设备部', '安全部'], fake()->numberBetween(1, 2)),
|
||||
'applicable_positions' => fake()->randomElements(['操作员', '质检员', '班组长', '技术员'], fake()->numberBetween(1, 2)),
|
||||
'published_at' => fake()->optional(0.6)->dateTimeBetween('-6 months', 'now'),
|
||||
'created_by' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function draft(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'draft',
|
||||
'published_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function published(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function archived(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'archived',
|
||||
'published_at' => fake()->dateTimeBetween('-1 year', '-1 month'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ return new class extends Migration
|
||||
$table->string('name')->comment('终端名称');
|
||||
$table->string('code', 100)->unique()->comment('终端编码');
|
||||
$table->string('ip_address', 45)->nullable()->comment('IP地址');
|
||||
$table->unsignedBigInteger('station_id')->nullable()->comment('线站ID');
|
||||
$table->string('station_id', 50)->nullable()->comment('线站ID');
|
||||
$table->string('diagram_url', 500)->nullable()->comment('组态图URL');
|
||||
$table->json('display_config')->nullable()->comment('显示配置');
|
||||
$table->boolean('is_online')->default(false)->comment('在线状态');
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sop_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('模板名称');
|
||||
$table->text('description')->nullable()->comment('模板描述');
|
||||
$table->string('category', 100)->nullable()->comment('分类');
|
||||
$table->json('tags')->nullable()->comment('标签');
|
||||
$table->string('version', 50)->default('1.0.0')->comment('版本号');
|
||||
$table->enum('status', ['draft', 'published', 'archived'])->default('draft')->comment('状态');
|
||||
$table->json('applicable_departments')->nullable()->comment('适用部门');
|
||||
$table->json('applicable_positions')->nullable()->comment('适用岗位');
|
||||
$table->timestamp('published_at')->nullable()->comment('发布时间');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('创建人');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 添加索引
|
||||
$table->index('status', 'idx_sop_templates_status');
|
||||
$table->index('category', 'idx_sop_templates_category');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sop_templates');
|
||||
}
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sop_steps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('sop_template_id')->comment('模板ID');
|
||||
$table->integer('step_number')->comment('步骤序号');
|
||||
$table->string('title')->comment('步骤标题');
|
||||
$table->text('content')->nullable()->comment('步骤内容');
|
||||
$table->integer('sort_order')->default(0)->comment('排序');
|
||||
$table->boolean('is_required')->default(true)->comment('是否必需');
|
||||
$table->timestamps();
|
||||
|
||||
// 添加外键约束
|
||||
$table->foreign('sop_template_id')
|
||||
->references('id')
|
||||
->on('sop_templates')
|
||||
->onDelete('cascade');
|
||||
|
||||
// 添加索引
|
||||
$table->index(['sop_template_id', 'sort_order'], 'idx_template_sort');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sop_steps');
|
||||
}
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sop_interactive_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('sop_step_id')->comment('步骤ID');
|
||||
$table->enum('task_type', ['confirm', 'input', 'select', 'photo', 'scan'])->comment('任务类型');
|
||||
$table->json('task_config')->nullable()->comment('任务配置');
|
||||
$table->json('validation_rules')->nullable()->comment('验证规则');
|
||||
$table->integer('timeout_seconds')->nullable()->comment('超时时间');
|
||||
$table->boolean('is_required')->default(true)->comment('是否必需');
|
||||
$table->timestamps();
|
||||
|
||||
// 添加外键约束
|
||||
$table->foreign('sop_step_id')
|
||||
->references('id')
|
||||
->on('sop_steps')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sop_interactive_tasks');
|
||||
}
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sop_template_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('sop_template_id')->comment('模板ID');
|
||||
$table->string('version', 50)->comment('版本号');
|
||||
$table->text('change_log')->nullable()->comment('变更说明');
|
||||
$table->json('content_snapshot')->nullable()->comment('内容快照');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('创建人');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
// 添加外键约束
|
||||
$table->foreign('sop_template_id')
|
||||
->references('id')
|
||||
->on('sop_templates')
|
||||
->onDelete('cascade');
|
||||
|
||||
// 添加索引
|
||||
$table->index(['sop_template_id', 'version'], 'idx_template_version');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sop_template_versions');
|
||||
}
|
||||
};
|
||||
@@ -24,8 +24,8 @@ return new class extends Migration
|
||||
// 终端管理
|
||||
'terminal.viewAny' => 'terminal.view',
|
||||
|
||||
// SOP模板
|
||||
'sop-template.viewAny' => 'sop-template.view',
|
||||
// 操作指引
|
||||
'guide.viewAny' => 'guide.view',
|
||||
|
||||
// 分组管理
|
||||
'group.viewAny' => 'group.view',
|
||||
@@ -78,7 +78,7 @@ return new class extends Migration
|
||||
'system-setting.view',
|
||||
'activity-log.view',
|
||||
'terminal.view',
|
||||
'sop-template.view',
|
||||
'guide.view',
|
||||
'group.view',
|
||||
'user.view',
|
||||
'role.view',
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('guides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('指引名称');
|
||||
$table->text('description')->nullable()->comment('指引描述');
|
||||
$table->string('category', 50)->default('operation')->comment('分类: operation/fault_handling/training');
|
||||
$table->json('tags')->nullable()->comment('标签');
|
||||
$table->string('status', 20)->default('draft')->comment('状态: draft/published/archived');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('category');
|
||||
$table->index('status');
|
||||
});
|
||||
|
||||
Schema::create('guide_pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('guide_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('page_number')->comment('页码');
|
||||
$table->string('title')->comment('页面标题');
|
||||
$table->string('html_url', 500)->comment('HTML页面链接');
|
||||
$table->integer('parent_id')->default(-1);
|
||||
$table->unsignedInteger('sort_order')->default(0)->comment('排序');
|
||||
$table->json('options')->nullable();
|
||||
$table->string('branch_option', 100)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('parent_id');
|
||||
$table->index(['guide_id', 'sort_order']);
|
||||
});
|
||||
|
||||
Schema::create('terminal_guides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('terminal_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('guide_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('priority')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['terminal_id', 'guide_id'], 'uk_terminal_guide');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('terminal_guides');
|
||||
Schema::dropIfExists('guide_pages');
|
||||
Schema::dropIfExists('guides');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('terminals', function (Blueprint $table) {
|
||||
$table->string('mac_address', 17)->nullable()->unique()->after('ip_address')
|
||||
->comment('MAC地址 (AA:BB:CC:DD:EE:FF)');
|
||||
$table->string('scada_data_url', 500)->nullable()->after('diagram_url')
|
||||
->comment('OPC UA网关数据查询地址');
|
||||
$table->string('scada_tags_url', 500)->nullable()->after('scada_data_url')
|
||||
->comment('OPC UA网关点位定义查询地址');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('terminals', function (Blueprint $table) {
|
||||
$table->dropColumn(['mac_address', 'scada_data_url', 'scada_tags_url']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->foreignId('knowledge_base_id')
|
||||
->nullable()
|
||||
->after('group_id')
|
||||
->constrained('knowledge_bases')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index('knowledge_base_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->dropForeign(['knowledge_base_id']);
|
||||
$table->dropColumn('knowledge_base_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -33,7 +33,7 @@ class DatabaseSeeder extends Seeder
|
||||
'email' => 'admin@example.com',
|
||||
'password' => Hash::make('TRG}E^5BvPcbyErc'),
|
||||
]);
|
||||
|
||||
|
||||
// 为管理员分配 super-admin 角色
|
||||
$admin->assignRole('super-admin');
|
||||
|
||||
@@ -218,13 +218,13 @@ class DatabaseSeeder extends Seeder
|
||||
]);
|
||||
|
||||
$this->command->info('演示数据生成完成!');
|
||||
|
||||
|
||||
// 9. 创建终端数据
|
||||
$this->call(TerminalSeeder::class);
|
||||
|
||||
// 10. 创建SOP模板数据
|
||||
$this->call(SopTemplateSeeder::class);
|
||||
|
||||
|
||||
// 10. 创建操作指引数据
|
||||
$this->call(GuideSeeder::class);
|
||||
|
||||
$this->command->newLine();
|
||||
$this->command->info('=== 生成的数据摘要 ===');
|
||||
$this->command->info('用户数量: ' . User::count());
|
||||
|
||||
219
database/seeders/GuideSeeder.php
Normal file
219
database/seeders/GuideSeeder.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use App\Models\Terminal;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class GuideSeeder extends Seeder
|
||||
{
|
||||
private const BASE_URL = 'https://ssrf.9z.work/guides';
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('开始创建操作指引数据...');
|
||||
|
||||
$admin = User::where('email', 'admin@example.com')->first();
|
||||
$terminals = Terminal::all();
|
||||
|
||||
// 1. 如何用光(带分支)
|
||||
$guide1 = $this->createHowToUseBeamGuide($admin);
|
||||
|
||||
// 2. 真空阀门故障处理
|
||||
$guide2 = $this->createVacuumValveIssueGuide($admin);
|
||||
|
||||
// 3. 漏水报警处理
|
||||
$guide3 = $this->createWaterLeakAlarmGuide($admin);
|
||||
|
||||
// 将所有指引关联到所有终端
|
||||
$this->command->info('关联指引到所有终端...');
|
||||
foreach ($terminals as $terminal) {
|
||||
$terminal->guides()->attach([
|
||||
$guide1->id => ['priority' => 1],
|
||||
$guide2->id => ['priority' => 2],
|
||||
$guide3->id => ['priority' => 3],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->command->info('操作指引数据创建完成!');
|
||||
$this->command->info(' - 指引数量: ' . Guide::count());
|
||||
$this->command->info(' - 指引页面数量: ' . GuidePage::count());
|
||||
$this->command->info(' - 关联终端数量: ' . $terminals->count());
|
||||
}
|
||||
|
||||
private function createHowToUseBeamGuide(User $admin): Guide
|
||||
{
|
||||
$this->command->info('创建指引: 如何用光...');
|
||||
|
||||
$guide = Guide::create([
|
||||
'name' => '如何用光',
|
||||
'description' => '光束线用光操作完整流程指引,包含前门12和后门两条路径',
|
||||
'category' => 'operation',
|
||||
'tags' => ['用光', '光闸', 'PS1', '光学棚屋'],
|
||||
'status' => 'published',
|
||||
'created_by' => $admin->id,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$baseUrl = self::BASE_URL . '/how-to-use-beam';
|
||||
|
||||
// 步骤1: 打开光子光闸 PS1(根节点)
|
||||
$step1 = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 1,
|
||||
'title' => '打开光子光闸 PS1',
|
||||
'html_url' => "{$baseUrl}/step-1.html",
|
||||
'parent_id' => -1,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
// 步骤2: 搜索光学棚屋(带选项:前门12 / 后门)
|
||||
$step2 = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 2,
|
||||
'title' => '搜索光学棚屋',
|
||||
'html_url' => "{$baseUrl}/step-2.html",
|
||||
'parent_id' => $step1->id,
|
||||
'sort_order' => 1,
|
||||
'options' => ['前门12', '后门'],
|
||||
]);
|
||||
|
||||
// 步骤3a: 前门12路径 - 检查设备状态
|
||||
$step3a = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 3,
|
||||
'title' => '前门12路径 - 检查设备状态',
|
||||
'html_url' => "{$baseUrl}/step-3a.html",
|
||||
'parent_id' => $step2->id,
|
||||
'sort_order' => 0,
|
||||
'branch_option' => '前门12',
|
||||
]);
|
||||
|
||||
// 步骤3b: 后门路径 - 安全确认
|
||||
$step3b = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 3,
|
||||
'title' => '后门路径 - 安全确认',
|
||||
'html_url' => "{$baseUrl}/step-3b.html",
|
||||
'parent_id' => $step2->id,
|
||||
'sort_order' => 1,
|
||||
'branch_option' => '后门',
|
||||
]);
|
||||
|
||||
// 步骤4a: 前门12路径 - 打开实验站光闸
|
||||
GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 4,
|
||||
'title' => '前门12路径 - 打开实验站光闸',
|
||||
'html_url' => "{$baseUrl}/step-4a.html",
|
||||
'parent_id' => $step3a->id,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
// 步骤4b: 后门路径 - 设备检查
|
||||
GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 4,
|
||||
'title' => '后门路径 - 设备检查',
|
||||
'html_url' => "{$baseUrl}/step-4b.html",
|
||||
'parent_id' => $step3b->id,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
// 步骤5: 完成(根节点,最终汇合)
|
||||
GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => 5,
|
||||
'title' => '完成',
|
||||
'html_url' => "{$baseUrl}/step-5.html",
|
||||
'parent_id' => -1,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
return $guide;
|
||||
}
|
||||
|
||||
private function createVacuumValveIssueGuide(User $admin): Guide
|
||||
{
|
||||
$this->command->info('创建指引: 真空阀门故障处理...');
|
||||
|
||||
$guide = Guide::create([
|
||||
'name' => '真空阀门故障处理',
|
||||
'description' => '真空阀门异常时的排查和处理流程',
|
||||
'category' => 'fault_handling',
|
||||
'tags' => ['真空', '阀门', '故障', '联锁', '气动'],
|
||||
'status' => 'published',
|
||||
'created_by' => $admin->id,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$baseUrl = self::BASE_URL . '/vacuum-valve-issue';
|
||||
$steps = [
|
||||
['title' => '检查真空度', 'file' => 'step-1.html'],
|
||||
['title' => '检查联锁状态', 'file' => 'step-2.html'],
|
||||
['title' => '尝试手动复位', 'file' => 'step-3.html'],
|
||||
['title' => '检查气动系统', 'file' => 'step-4.html'],
|
||||
['title' => '联系维护人员', 'file' => 'step-5.html'],
|
||||
];
|
||||
|
||||
$parentId = -1;
|
||||
foreach ($steps as $i => $step) {
|
||||
$page = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => $i + 1,
|
||||
'title' => $step['title'],
|
||||
'html_url' => "{$baseUrl}/{$step['file']}",
|
||||
'parent_id' => $parentId,
|
||||
'sort_order' => $parentId === -1 ? $i : 0,
|
||||
]);
|
||||
$parentId = $page->id;
|
||||
}
|
||||
|
||||
return $guide;
|
||||
}
|
||||
|
||||
private function createWaterLeakAlarmGuide(User $admin): Guide
|
||||
{
|
||||
$this->command->info('创建指引: 漏水报警处理...');
|
||||
|
||||
$guide = Guide::create([
|
||||
'name' => '漏水报警处理',
|
||||
'description' => '漏水报警时的应急处理和复位流程',
|
||||
'category' => 'fault_handling',
|
||||
'tags' => ['漏水', '报警', '应急', '复位'],
|
||||
'status' => 'published',
|
||||
'created_by' => $admin->id,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$baseUrl = self::BASE_URL . '/water-leak-alarm';
|
||||
$steps = [
|
||||
['title' => '确认报警位置', 'file' => 'step-1.html'],
|
||||
['title' => '搜索光学棚屋', 'file' => 'step-2.html'],
|
||||
['title' => '定位并处理漏水点', 'file' => 'step-3.html'],
|
||||
['title' => '复位报警', 'file' => 'step-4.html'],
|
||||
['title' => '完成', 'file' => 'step-5.html'],
|
||||
];
|
||||
|
||||
$parentId = -1;
|
||||
foreach ($steps as $i => $step) {
|
||||
$page = GuidePage::create([
|
||||
'guide_id' => $guide->id,
|
||||
'page_number' => $i + 1,
|
||||
'title' => $step['title'],
|
||||
'html_url' => "{$baseUrl}/{$step['file']}",
|
||||
'parent_id' => $parentId,
|
||||
'sort_order' => $parentId === -1 ? $i : 0,
|
||||
]);
|
||||
$parentId = $page->id;
|
||||
}
|
||||
|
||||
return $guide;
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,13 @@ class PermissionSeeder extends Seeder
|
||||
'terminal.delete' => '删除终端',
|
||||
'terminal.sync' => '同步终端配置',
|
||||
|
||||
// SOP模板权限
|
||||
'sop-template.view' => '查看SOP模板',
|
||||
'sop-template.create' => '创建SOP',
|
||||
'sop-template.update' => '编辑SOP',
|
||||
'sop-template.delete' => '删除SOP',
|
||||
'sop-template.publish' => '发布SOP',
|
||||
'sop-template.archive' => '归档SOP',
|
||||
// 操作指引权限
|
||||
'guide.view' => '查看指引',
|
||||
'guide.create' => '创建指引',
|
||||
'guide.update' => '编辑指引',
|
||||
'guide.delete' => '删除指引',
|
||||
'guide.publish' => '发布指引',
|
||||
'guide.archive' => '归档指引',
|
||||
|
||||
// 分组管理权限
|
||||
'group.view' => '查看分组',
|
||||
@@ -129,13 +129,13 @@ class PermissionSeeder extends Seeder
|
||||
'terminal.delete',
|
||||
'terminal.sync',
|
||||
|
||||
// SOP模板
|
||||
'sop-template.view',
|
||||
'sop-template.create',
|
||||
'sop-template.update',
|
||||
'sop-template.delete',
|
||||
'sop-template.publish',
|
||||
'sop-template.archive',
|
||||
// 操作指引
|
||||
'guide.view',
|
||||
'guide.create',
|
||||
'guide.update',
|
||||
'guide.delete',
|
||||
'guide.publish',
|
||||
'guide.archive',
|
||||
|
||||
// 分组管理
|
||||
'group.view',
|
||||
@@ -173,8 +173,8 @@ class PermissionSeeder extends Seeder
|
||||
// 终端管理(仅查看)
|
||||
'terminal.view',
|
||||
|
||||
// SOP模板(仅查看)
|
||||
'sop-template.view',
|
||||
// 操作指引(仅查看)
|
||||
'guide.view',
|
||||
|
||||
// 分组管理(仅查看)
|
||||
'group.view',
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\SopStep;
|
||||
use App\Models\SopInteractiveTask;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SopTemplateSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('开始创建SOP模板数据...');
|
||||
|
||||
// 获取或创建一个用户作为创建者
|
||||
$user = User::first();
|
||||
if (!$user) {
|
||||
$this->command->warn('未找到用户,跳过SOP模板创建');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 光束线开机流程
|
||||
$this->command->info('创建光束线开机流程...');
|
||||
$template1 = SopTemplate::create([
|
||||
'name' => '光束线标准开机流程',
|
||||
'description' => '本流程规定了光束线开机前的检查项目、开机步骤和注意事项,确保光束线安全、正常启动。',
|
||||
'category' => '光束线操作',
|
||||
'tags' => ['标准作业', '开机流程', '安全'],
|
||||
'version' => '1.0.0',
|
||||
'status' => 'published',
|
||||
'applicable_departments' => ['BL02U1', 'BL07U', 'BL08U', 'BL13HB', 'BL13U', 'BL14B', 'BL14W', 'BL15U', 'BL16B', 'BL16U1'],
|
||||
'applicable_positions' => ['操作员', '值班员'],
|
||||
'published_at' => now()->subMonths(2),
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
|
||||
$steps1 = [
|
||||
[
|
||||
'step_number' => 1,
|
||||
'title' => '开机前安全检查',
|
||||
'content' => '<p>检查光束线周围环境,确保无障碍物和安全隐患。</p><ul><li>检查光束线外观是否完好</li><li>检查安全联锁装置是否正常</li><li>检查急停按钮是否正常</li><li>确认辐射防护门关闭</li></ul>',
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 2,
|
||||
'title' => '真空系统检查',
|
||||
'content' => '<p>检查真空系统状态。</p><ul><li>确认真空泵运行正常</li><li>检查真空度读数</li><li>检查真空阀门状态</li></ul>',
|
||||
'sort_order' => 2,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 3,
|
||||
'title' => '冷却水系统检查',
|
||||
'content' => '<p>检查冷却水系统。</p><ul><li>确认冷却水流量正常</li><li>检查水温是否在正常范围</li><li>检查冷却水压力</li></ul>',
|
||||
'sort_order' => 3,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 4,
|
||||
'title' => '启动光束线',
|
||||
'content' => '<p>按照正确顺序启动光束线。</p><ol><li>打开控制系统</li><li>等待系统自检完成</li><li>启动束流</li><li>观察束流参数</li></ol>',
|
||||
'sort_order' => 4,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 5,
|
||||
'title' => '运行状态确认',
|
||||
'content' => '<p>确认光束线正常运行。</p><ul><li>检查束流强度</li><li>检查束流位置</li><li>检查各项参数是否在正常范围</li></ul>',
|
||||
'sort_order' => 5,
|
||||
'is_required' => true,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($steps1 as $stepData) {
|
||||
$step = SopStep::create(array_merge($stepData, ['sop_template_id' => $template1->id]));
|
||||
|
||||
// 为第1步添加确认任务
|
||||
if ($stepData['step_number'] == 1) {
|
||||
SopInteractiveTask::create([
|
||||
'sop_step_id' => $step->id,
|
||||
'task_type' => 'confirm',
|
||||
'task_config' => [
|
||||
'title' => '安全检查确认',
|
||||
'message' => '我已完成所有安全检查项目,确认无安全隐患',
|
||||
],
|
||||
'validation_rules' => [],
|
||||
'timeout_seconds' => 300,
|
||||
'is_required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// 为第4步添加拍照任务
|
||||
if ($stepData['step_number'] == 4) {
|
||||
SopInteractiveTask::create([
|
||||
'sop_step_id' => $step->id,
|
||||
'task_type' => 'photo',
|
||||
'task_config' => [
|
||||
'title' => '拍摄控制系统状态',
|
||||
'message' => '请拍摄控制系统界面照片',
|
||||
'min_photos' => 1,
|
||||
'max_photos' => 3,
|
||||
],
|
||||
'validation_rules' => [],
|
||||
'timeout_seconds' => 180,
|
||||
'is_required' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 用户实验准备流程
|
||||
$this->command->info('创建用户实验准备流程...');
|
||||
$template2 = SopTemplate::create([
|
||||
'name' => '用户实验准备标准流程',
|
||||
'description' => '本流程规定了用户实验前的准备工作、样品安装和参数设置步骤。',
|
||||
'category' => '实验操作',
|
||||
'tags' => ['用户实验', '标准作业', '样品准备'],
|
||||
'version' => '1.0.0',
|
||||
'status' => 'published',
|
||||
'applicable_departments' => ['BL02U1', 'BL07U', 'BL08U', 'BL13HB', 'BL13U', 'BL14B', 'BL14W', 'BL15U', 'BL16B', 'BL16U1'],
|
||||
'applicable_positions' => ['操作员', '实验员'],
|
||||
'published_at' => now()->subMonth(),
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
|
||||
$steps2 = [
|
||||
[
|
||||
'step_number' => 1,
|
||||
'title' => '扫描用户机时单',
|
||||
'content' => '<p>使用扫码枪扫描用户机时单二维码,获取实验信息。</p>',
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 2,
|
||||
'title' => '样品安全检查',
|
||||
'content' => '<p>检查样品安全性。</p><ul><li>确认样品无放射性</li><li>确认样品无毒性</li><li>确认样品符合实验要求</li></ul>',
|
||||
'sort_order' => 2,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 3,
|
||||
'title' => '样品安装',
|
||||
'content' => '<p>将样品安装到样品台。</p><ul><li>调整样品位置</li><li>固定样品</li><li>对准光束中心</li></ul>',
|
||||
'sort_order' => 3,
|
||||
'is_required' => true,
|
||||
],
|
||||
[
|
||||
'step_number' => 4,
|
||||
'title' => '实验参数设置',
|
||||
'content' => '<p>在控制系统中设置实验参数。</p>',
|
||||
'sort_order' => 4,
|
||||
'is_required' => true,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($steps2 as $stepData) {
|
||||
$step = SopStep::create(array_merge($stepData, ['sop_template_id' => $template2->id]));
|
||||
|
||||
// 为第1步添加扫码任务
|
||||
if ($stepData['step_number'] == 1) {
|
||||
SopInteractiveTask::create([
|
||||
'sop_step_id' => $step->id,
|
||||
'task_type' => 'scan',
|
||||
'task_config' => [
|
||||
'title' => '扫描机时单二维码',
|
||||
'scan_type' => 'qrcode',
|
||||
],
|
||||
'validation_rules' => [
|
||||
'pattern' => '^[A-Z0-9]{10,20}$',
|
||||
],
|
||||
'timeout_seconds' => 60,
|
||||
'is_required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// 为第2步添加选择任务
|
||||
if ($stepData['step_number'] == 2) {
|
||||
SopInteractiveTask::create([
|
||||
'sop_step_id' => $step->id,
|
||||
'task_type' => 'select',
|
||||
'task_config' => [
|
||||
'title' => '样品安全检查结果',
|
||||
'options' => ['通过', '不通过'],
|
||||
],
|
||||
'validation_rules' => [],
|
||||
'timeout_seconds' => 120,
|
||||
'is_required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// 为第3步添加拍照任务
|
||||
if ($stepData['step_number'] == 3) {
|
||||
SopInteractiveTask::create([
|
||||
'sop_step_id' => $step->id,
|
||||
'task_type' => 'photo',
|
||||
'task_config' => [
|
||||
'title' => '拍摄样品安装照片',
|
||||
'message' => '请拍摄样品安装完成后的照片',
|
||||
'min_photos' => 1,
|
||||
'max_photos' => 2,
|
||||
],
|
||||
'validation_rules' => [],
|
||||
'timeout_seconds' => 180,
|
||||
'is_required' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建一个草稿模板
|
||||
$this->command->info('创建草稿模板...');
|
||||
SopTemplate::create([
|
||||
'name' => '光束线日常维护流程(草稿)',
|
||||
'description' => '光束线日常维护保养操作流程,包括清洁、检查、记录等内容。',
|
||||
'category' => '设备维护',
|
||||
'tags' => ['维护', '保养'],
|
||||
'version' => '0.1.0',
|
||||
'status' => 'draft',
|
||||
'applicable_departments' => ['BL02U1', 'BL07U', 'BL08U'],
|
||||
'applicable_positions' => ['维修工', '技术员'],
|
||||
'published_at' => null,
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
|
||||
$this->command->info('SOP模板数据创建完成!');
|
||||
$this->command->newLine();
|
||||
$this->command->info('=== 生成的SOP模板摘要 ===');
|
||||
$this->command->info('总模板数量: ' . SopTemplate::count());
|
||||
$this->command->info(' - 已发布: ' . SopTemplate::where('status', 'published')->count());
|
||||
$this->command->info(' - 草稿: ' . SopTemplate::where('status', 'draft')->count());
|
||||
$this->command->info(' - 已归档: ' . SopTemplate::where('status', 'archived')->count());
|
||||
$this->command->info('总步骤数量: ' . SopStep::count());
|
||||
$this->command->info('总交互任务数量: ' . SopInteractiveTask::count());
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class TerminalSeeder extends Seeder
|
||||
'code' => "SCREEN-{$beamline}",
|
||||
'ip_address' => $ipAddress,
|
||||
'station_id' => $beamline,
|
||||
'diagram_url' => "https://example.com/diagrams/{$beamline}.png",
|
||||
'diagram_url' => 'https://ssrf.9z.work/scada/demo.html',
|
||||
'display_config' => [
|
||||
'resolution' => '3840x2160',
|
||||
'refresh_rate' => 60,
|
||||
@@ -46,11 +46,11 @@ class TerminalSeeder extends Seeder
|
||||
'touch_enabled' => true,
|
||||
],
|
||||
'is_online' => in_array($beamline, ['BL02U1', 'BL07U', 'BL08U', 'BL13U', 'BL15U']),
|
||||
'last_online_at' => in_array($beamline, ['BL02U1', 'BL07U', 'BL08U', 'BL13U', 'BL15U'])
|
||||
? now()
|
||||
'last_online_at' => in_array($beamline, ['BL02U1', 'BL07U', 'BL08U', 'BL13U', 'BL15U'])
|
||||
? now()
|
||||
: now()->subHours(rand(1, 24)),
|
||||
]);
|
||||
|
||||
|
||||
// 为每个终端创建提示词
|
||||
TerminalPrompt::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
|
||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -66,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -83,9 +83,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -117,9 +117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -134,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -168,9 +168,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -202,9 +202,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -219,9 +219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -236,9 +236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -253,9 +253,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -287,9 +287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -321,9 +321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -338,9 +338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -389,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -406,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -423,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -440,9 +440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1419,9 +1419,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1432,32 +1432,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
"@esbuild/aix-ppc64": "0.27.4",
|
||||
"@esbuild/android-arm": "0.27.4",
|
||||
"@esbuild/android-arm64": "0.27.4",
|
||||
"@esbuild/android-x64": "0.27.4",
|
||||
"@esbuild/darwin-arm64": "0.27.4",
|
||||
"@esbuild/darwin-x64": "0.27.4",
|
||||
"@esbuild/freebsd-arm64": "0.27.4",
|
||||
"@esbuild/freebsd-x64": "0.27.4",
|
||||
"@esbuild/linux-arm": "0.27.4",
|
||||
"@esbuild/linux-arm64": "0.27.4",
|
||||
"@esbuild/linux-ia32": "0.27.4",
|
||||
"@esbuild/linux-loong64": "0.27.4",
|
||||
"@esbuild/linux-mips64el": "0.27.4",
|
||||
"@esbuild/linux-ppc64": "0.27.4",
|
||||
"@esbuild/linux-riscv64": "0.27.4",
|
||||
"@esbuild/linux-s390x": "0.27.4",
|
||||
"@esbuild/linux-x64": "0.27.4",
|
||||
"@esbuild/netbsd-arm64": "0.27.4",
|
||||
"@esbuild/netbsd-x64": "0.27.4",
|
||||
"@esbuild/openbsd-arm64": "0.27.4",
|
||||
"@esbuild/openbsd-x64": "0.27.4",
|
||||
"@esbuild/openharmony-arm64": "0.27.4",
|
||||
"@esbuild/sunos-x64": "0.27.4",
|
||||
"@esbuild/win32-arm64": "0.27.4",
|
||||
"@esbuild/win32-ia32": "0.27.4",
|
||||
"@esbuild/win32-x64": "0.27.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
||||
1
public/css/solution-forest/filament-tree/filament-tree-min.css
vendored
Normal file
1
public/css/solution-forest/filament-tree/filament-tree-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
84
public/guides/how-to-use-beam/step-1.html
Normal file
84
public/guides/how-to-use-beam/step-1.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 1 - 打开光子光闸 PS1</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 1: 打开光子光闸 PS1</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/ps1-panel.jpg" alt="PS1 控制面板" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="20%" y="15%" width="30%" height="25%" stroke="#f59e0b" />
|
||||
<text class="annotation-label" x="21%" y="12%" fill="#f59e0b">PS1 开关位置</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>首先需要打开光子光闸 PS1。请问安全光闸 SS1 是否能打开?如果 SS1 无法打开,需要先检查人员安全系统(PSS)状态。</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
92
public/guides/how-to-use-beam/step-2.html
Normal file
92
public/guides/how-to-use-beam/step-2.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 2 - 搜索光学棚屋</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ul { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 2: 搜索光学棚屋</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/optics-hutch-layout.jpg" alt="光学棚屋布局图" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="8%" y="20%" width="25%" height="20%" stroke="#3b82f6" />
|
||||
<text class="annotation-label" x="9%" y="17%" fill="#3b82f6">前门12</text>
|
||||
<rect class="annotation-box" x="60%" y="20%" width="25%" height="20%" stroke="#10b981" />
|
||||
<text class="annotation-label" x="61%" y="17%" fill="#10b981">后门</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>光学棚屋设置了 2 种搜索顺序,请选择您要使用的搜索路径:</p>
|
||||
<ul>
|
||||
<li><strong>前门12</strong>:从前门进入,适合常规实验</li>
|
||||
<li><strong>后门</strong>:从后门进入,适合特殊设备维护</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
72
public/guides/how-to-use-beam/step-3a.html
Normal file
72
public/guides/how-to-use-beam/step-3a.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 3 - 前门12路径 - 检查设备状态</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 3: 前门12路径 - 检查设备状态</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/frontend-equipment.jpg" alt="前端设备" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>从前门12进入后,请检查以下设备状态:</p>
|
||||
<ol>
|
||||
<li>白光挡板是否正常</li>
|
||||
<li>单色器冷却水流量</li>
|
||||
<li>光束位置监测器读数</li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
91
public/guides/how-to-use-beam/step-3b.html
Normal file
91
public/guides/how-to-use-beam/step-3b.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 3 - 后门路径 - 安全确认</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
.warning-box {
|
||||
background: #fef3c7;
|
||||
border-left: 4px solid #f59e0b;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box strong {
|
||||
color: #d97706;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 3: 后门路径 - 安全确认</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/backdoor-safety.jpg" alt="后门安全区域" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>从后门进入需要额外的安全确认:</p>
|
||||
<ol>
|
||||
<li>确认后门区域无人员</li>
|
||||
<li>检查后门联锁状态</li>
|
||||
<li>通知控制室操作人员</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 注意</strong>
|
||||
后门路径需要额外安全授权,请确保已通知控制室后再进入。
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
65
public/guides/how-to-use-beam/step-4a.html
Normal file
65
public/guides/how-to-use-beam/step-4a.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 4 - 前门12路径 - 打开实验站光闸</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 4: 前门12路径 - 打开实验站光闸</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/ss2-panel.jpg" alt="SS2 控制面板" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>确认所有设备状态正常后,打开实验站光闸 SS2。观察光束强度是否达到预期值。</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
72
public/guides/how-to-use-beam/step-4b.html
Normal file
72
public/guides/how-to-use-beam/step-4b.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 4 - 后门路径 - 设备检查</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 4: 后门路径 - 设备检查</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/backdoor-equipment.jpg" alt="后门区域设备" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>检查后门区域的关键设备:</p>
|
||||
<ol>
|
||||
<li>聚焦镜冷却系统</li>
|
||||
<li>狭缝位置</li>
|
||||
<li>光束诊断设备</li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
94
public/guides/how-to-use-beam/step-5.html
Normal file
94
public/guides/how-to-use-beam/step-5.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 5 - 完成</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-left: 4px solid #22c55e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-box .check {
|
||||
font-size: 20px;
|
||||
color: #16a34a;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.step-content ul { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
.param-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.param-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.param-table td:first-child {
|
||||
font-weight: bold;
|
||||
color: #0f172a;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.param-table td:last-child {
|
||||
color: #475569;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 5: 完成</div>
|
||||
|
||||
<div class="success-box">
|
||||
<div class="check">✓ 光束已成功送达实验站!</div>
|
||||
<div class="step-content">
|
||||
<p>当前光束参数:</p>
|
||||
<table class="param-table">
|
||||
<tr><td>能量</td><td>12.66 keV</td></tr>
|
||||
<tr><td>通量</td><td>1.2×10¹² ph/s</td></tr>
|
||||
<tr><td>光斑尺寸</td><td>0.15×0.08 mm²</td></tr>
|
||||
</table>
|
||||
<p style="margin-top: 16px;">您现在可以开始实验了。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
98
public/guides/vacuum-valve-issue/step-1.html
Normal file
98
public/guides/vacuum-valve-issue/step-1.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 1 - 检查真空度</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ul { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
.step-content code {
|
||||
background: #f1f5f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #0f172a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 1: 检查真空度</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/vacuum-gauge.jpg" alt="真空计读数面板" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="15%" y="22%" width="25%" height="20%" stroke="#3b82f6" />
|
||||
<text class="annotation-label" x="16%" y="19%" fill="#3b82f6">上游真空计</text>
|
||||
<rect class="annotation-box" x="58%" y="22%" width="25%" height="20%" stroke="#10b981" />
|
||||
<text class="annotation-label" x="59%" y="19%" fill="#10b981">下游真空计</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>真空阀门无法打开通常是因为真空度不满足联锁条件。</p>
|
||||
<p>请检查阀门两侧的真空度读数:</p>
|
||||
<ul>
|
||||
<li>上游真空度应 < <code>1×10⁻⁷ mbar</code></li>
|
||||
<li>下游真空度应 < <code>1×10⁻⁶ mbar</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
89
public/guides/vacuum-valve-issue/step-2.html
Normal file
89
public/guides/vacuum-valve-issue/step-2.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 2 - 检查联锁状态</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 2: 检查联锁状态</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/valve-control-panel.jpg" alt="阀门控制面板" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="30%" y="18%" width="35%" height="30%" stroke="#f59e0b" />
|
||||
<text class="annotation-label" x="31%" y="15%" fill="#f59e0b">联锁指示区</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>如果真空度满足要求但阀门仍无法打开,检查联锁系统:</p>
|
||||
<ol>
|
||||
<li>查看阀门控制面板的联锁指示灯</li>
|
||||
<li>确认相关设备(离子泵、分子泵)运行正常</li>
|
||||
<li>检查 EPS 系统是否有相关报警</li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
108
public/guides/vacuum-valve-issue/step-3.html
Normal file
108
public/guides/vacuum-valve-issue/step-3.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 3 - 尝试手动复位</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
.warning-box {
|
||||
background: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box strong {
|
||||
color: #dc2626;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 3: 尝试手动复位</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/valve-reset.jpg" alt="阀门复位操作" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="40%" y="40%" width="18%" height="12%" stroke="#ef4444" />
|
||||
<text class="annotation-label" x="41%" y="37%" fill="#ef4444">复位按钮</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>如果联锁状态异常,尝试手动复位:</p>
|
||||
<ol>
|
||||
<li>按下阀门控制面板上的"复位"按钮</li>
|
||||
<li>等待 5 秒</li>
|
||||
<li>再次尝试打开阀门</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 重要提醒</strong>
|
||||
复位前必须确认真空度满足要求!在真空度不达标时强行复位可能导致设备损坏。
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
96
public/guides/vacuum-valve-issue/step-4.html
Normal file
96
public/guides/vacuum-valve-issue/step-4.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 4 - 检查气动系统</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
.step-content code {
|
||||
background: #f1f5f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #0f172a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 4: 检查气动系统</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/pneumatic-system.jpg" alt="气动系统" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="22%" y="28%" width="30%" height="24%" stroke="#06b6d4" />
|
||||
<text class="annotation-label" x="23%" y="25%" fill="#06b6d4">气动控制单元</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>如果复位后仍无法打开,检查气动系统:</p>
|
||||
<ol>
|
||||
<li>确认压缩空气供应正常(压力 > <code>5 bar</code>)</li>
|
||||
<li>检查气动管路是否有泄漏</li>
|
||||
<li>查看电磁阀是否工作(应有"咔哒"声)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
123
public/guides/vacuum-valve-issue/step-5.html
Normal file
123
public/guides/vacuum-valve-issue/step-5.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 5 - 联系维护人员</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
|
||||
.contact-card {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.contact-card h4 {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #0c4a6e;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e0f2fe;
|
||||
font-size: 14px;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.contact-item:last-child { border-bottom: none; }
|
||||
|
||||
.contact-item strong {
|
||||
min-width: 140px;
|
||||
color: #0c4a6e;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #fefce8;
|
||||
border: 1px solid #fde68a;
|
||||
border-left: 4px solid #f59e0b;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.info-box h4 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #92400e;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin-left: 24px;
|
||||
color: #a16207;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-box li { margin-bottom: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 5: 联系维护人员</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>如果以上步骤都无法解决问题,可能是阀门机械故障或控制系统故障。请联系维护人员。</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-card">
|
||||
<h4>维护人员联系方式</h4>
|
||||
<div class="contact-item">
|
||||
<strong>真空系统负责人</strong>
|
||||
<span>内线 1234</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<strong>控制系统负责人</strong>
|
||||
<span>内线 5678</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h4>请记录以下信息以便维护人员排查:</h4>
|
||||
<ul>
|
||||
<li>故障时间</li>
|
||||
<li>真空度读数</li>
|
||||
<li>已执行的操作步骤</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
115
public/guides/water-leak-alarm/step-1.html
Normal file
115
public/guides/water-leak-alarm/step-1.html
Normal file
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 1 - 确认报警位置</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
.annotation-arrow { stroke-width: 3; fill: none; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
|
||||
.warning-box {
|
||||
background: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box strong {
|
||||
color: #dc2626;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
color: #991b1b;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 紧急处理流程</strong>
|
||||
<p>水泄漏报警触发后应立即按照以下步骤处理,防止设备损坏。如遇大面积漏水,请先关闭主水阀并通知值班负责人。</p>
|
||||
</div>
|
||||
|
||||
<div class="step-title">步骤 1: 确认报警位置</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/eps-cabinet.jpg" alt="EPS 机柜" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<polygon points="0 0, 10 3, 0 6" fill="#ef4444" />
|
||||
</marker>
|
||||
</defs>
|
||||
<rect class="annotation-box" x="30%" y="30%" width="35%" height="20%" stroke="#ef4444" />
|
||||
<text class="annotation-label" x="31%" y="27%" fill="#ef4444">水泄漏检测模块</text>
|
||||
<line class="annotation-arrow" x1="48%" y1="50%" x2="48%" y2="60%" stroke="#ef4444" marker-end="url(#arrowhead)" />
|
||||
<text class="annotation-label" x="50%" y="65%" fill="#ef4444">查看编号</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>水泄漏报警已触发。首先需要确认漏水点的具体位置。</p>
|
||||
<p>请查看设备保护系统(EPS)机柜上的水泄漏检测模块,记录显示的数字编号。</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
94
public/guides/water-leak-alarm/step-2.html
Normal file
94
public/guides/water-leak-alarm/step-2.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 2 - 搜索光学棚屋</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 14px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ul { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 2: 搜索光学棚屋</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/optics-hutch-layout.jpg" alt="光学棚屋布局图" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="15%" y="20%" width="20%" height="16%" stroke="#c4b5fd" />
|
||||
<text class="annotation-label" x="16%" y="17%" fill="#7c3aed">1-10</text>
|
||||
<rect class="annotation-box" x="42%" y="20%" width="20%" height="16%" stroke="#93c5fd" />
|
||||
<text class="annotation-label" x="43%" y="17%" fill="#2563eb">11-20</text>
|
||||
<rect class="annotation-box" x="68%" y="20%" width="20%" height="16%" stroke="#6ee7b7" />
|
||||
<text class="annotation-label" x="69%" y="17%" fill="#059669">21-30</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>根据检测模块显示的编号,进入光学棚屋找到对应的水泄漏检测线缆。</p>
|
||||
<p>线缆编号对应位置:</p>
|
||||
<ul>
|
||||
<li><strong>1-10</strong>:单色器区域</li>
|
||||
<li><strong>11-20</strong>:聚焦镜区域</li>
|
||||
<li><strong>21-30</strong>:实验站区域</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
109
public/guides/water-leak-alarm/step-3.html
Normal file
109
public/guides/water-leak-alarm/step-3.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 3 - 定位并处理漏水点</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
.warning-box {
|
||||
background: #fef3c7;
|
||||
border-left: 4px solid #f59e0b;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box strong {
|
||||
color: #d97706;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 3: 定位并处理漏水点</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/leak-detection-cable.jpg" alt="漏水检测线缆" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="25%" y="40%" width="50%" height="30%" stroke="#f59e0b" />
|
||||
<text class="annotation-label" x="26%" y="37%" fill="#f59e0b">检查此区域</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>找到对应编号的线缆后,沿线缆检查漏水点:</p>
|
||||
<ol>
|
||||
<li>查看线缆周围是否有水渍</li>
|
||||
<li>检查附近的冷却水管接头</li>
|
||||
<li>用干布擦干水渍</li>
|
||||
<li>如果是管道漏水,需要关闭该区域冷却水阀门并通知维护人员</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 注意</strong>
|
||||
如果是管道漏水,应立即关闭该区域冷却水阀门,并联系维护人员处理。
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
108
public/guides/water-leak-alarm/step-4.html
Normal file
108
public/guides/water-leak-alarm/step-4.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 4 - 复位报警</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-image-container {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotation-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation-box { stroke-width: 3; fill: none; }
|
||||
.annotation-label { font-weight: bold; font-size: 16px; }
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-content p { margin-bottom: 12px; }
|
||||
.step-content ol { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
|
||||
.warning-box {
|
||||
background: #fef3c7;
|
||||
border-left: 4px solid #f59e0b;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning-box strong {
|
||||
color: #d97706;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 4: 复位报警</div>
|
||||
|
||||
<div class="step-image-container">
|
||||
<img src="images/eps-reset.jpg" alt="EPS 复位面板" class="step-image"
|
||||
onerror="this.style.display='none'">
|
||||
|
||||
<svg class="annotation-layer">
|
||||
<rect class="annotation-box" x="40%" y="35%" width="18%" height="12%" stroke="#10b981" />
|
||||
<text class="annotation-label" x="41%" y="32%" fill="#10b981">复位按钮</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<p>处理完漏水点后,返回 EPS 机柜:</p>
|
||||
<ol>
|
||||
<li>确认水泄漏检测模块指示灯已熄灭</li>
|
||||
<li>按下"复位"按钮</li>
|
||||
<li>观察系统是否恢复正常</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 注意</strong>
|
||||
如果报警持续,说明漏水未完全处理,需要重新检查。
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
71
public/guides/water-leak-alarm/step-5.html
Normal file
71
public/guides/water-leak-alarm/step-5.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>步骤 5 - 完成</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
color: #0f172a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-left: 4px solid #22c55e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-box .check {
|
||||
font-size: 20px;
|
||||
color: #16a34a;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.step-content ul { margin-left: 24px; margin-top: 12px; }
|
||||
.step-content li { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="step-title">步骤 5: 完成</div>
|
||||
|
||||
<div class="success-box">
|
||||
<div class="check">✓ 水泄漏报警已成功处理!</div>
|
||||
<div class="step-content">
|
||||
<p>后续建议:</p>
|
||||
<ul>
|
||||
<li>记录漏水位置和处理方法</li>
|
||||
<li>如果是管道问题,提交维护工单</li>
|
||||
<li>增加该区域的巡检频率</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
BIN
public/scada/beamline-background.png
Normal file
BIN
public/scada/beamline-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
52
public/scada/demo.html
Normal file
52
public/scada/demo.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BL15U 光束线组态图</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
#diagram-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
#background-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="diagram-container">
|
||||
<img id="background-image" src="beamline-background.png" alt="光束线组态图">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
12
routes/api.php
Normal file
12
routes/api.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\TerminalApiController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('identify.terminal')->prefix('terminal')->group(function () {
|
||||
Route::get('/config', [TerminalApiController::class, 'config']);
|
||||
Route::get('/knowledge', [TerminalApiController::class, 'knowledge']);
|
||||
Route::get('/guides', [TerminalApiController::class, 'guides']);
|
||||
Route::post('/guides/pages', [TerminalApiController::class, 'guidePages']);
|
||||
Route::post('/heartbeat', [TerminalApiController::class, 'heartbeat']);
|
||||
});
|
||||
@@ -1,246 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\User;
|
||||
use App\Services\SopTemplateService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user);
|
||||
$this->service = new SopTemplateService();
|
||||
});
|
||||
|
||||
test('可以导出模板为 JSON', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'name' => '测试模板',
|
||||
'version' => '1.0.0',
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'content' => '内容',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$json = $this->service->exportToJson($template);
|
||||
$data = json_decode($json, true);
|
||||
|
||||
expect($data)->toHaveKey('template')
|
||||
->and($data)->toHaveKey('steps')
|
||||
->and($data['template']['name'])->toBe('测试模板')
|
||||
->and($data['steps'])->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('导出的 JSON 包含步骤信息', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'content' => '<p>这是内容</p>',
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
]);
|
||||
|
||||
$json = $this->service->exportToJson($template);
|
||||
$data = json_decode($json, true);
|
||||
|
||||
expect($data['steps'][0])->toHaveKey('step_number')
|
||||
->and($data['steps'][0])->toHaveKey('title')
|
||||
->and($data['steps'][0])->toHaveKey('content')
|
||||
->and($data['steps'][0]['title'])->toBe('第一步');
|
||||
});
|
||||
|
||||
test('可以从 JSON 导入模板', function () {
|
||||
$jsonData = [
|
||||
'template' => [
|
||||
'name' => '导入的模板',
|
||||
'description' => '这是描述',
|
||||
'category' => '测试分类',
|
||||
'version' => '1.0.0',
|
||||
],
|
||||
'steps' => [
|
||||
[
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'content' => '内容',
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode($jsonData);
|
||||
$template = $this->service->importFromJson($json);
|
||||
|
||||
expect($template)->toBeInstanceOf(SopTemplate::class)
|
||||
->and($template->name)->toBe('导入的模板')
|
||||
->and($template->status)->toBe('draft')
|
||||
->and($template->steps)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('导入时会创建步骤', function () {
|
||||
$jsonData = [
|
||||
'template' => [
|
||||
'name' => '测试模板',
|
||||
'version' => '1.0.0',
|
||||
],
|
||||
'steps' => [
|
||||
[
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'step_number' => 2,
|
||||
'title' => '第二步',
|
||||
'sort_order' => 2,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode($jsonData);
|
||||
$template = $this->service->importFromJson($json);
|
||||
|
||||
expect($template->steps)->toHaveCount(2)
|
||||
->and($template->steps->first()->title)->toBe('第一步')
|
||||
->and($template->steps->last()->title)->toBe('第二步');
|
||||
});
|
||||
|
||||
test('导入无效 JSON 会抛出异常', function () {
|
||||
$invalidJson = 'invalid json';
|
||||
|
||||
$this->service->importFromJson($invalidJson);
|
||||
})->throws(\Illuminate\Validation\ValidationException::class);
|
||||
|
||||
test('导入缺少必需字段会抛出异常', function () {
|
||||
$jsonData = [
|
||||
'template' => [
|
||||
'name' => '测试模板',
|
||||
// 缺少 version
|
||||
],
|
||||
'steps' => [],
|
||||
];
|
||||
|
||||
$json = json_encode($jsonData);
|
||||
|
||||
$this->service->importFromJson($json);
|
||||
})->throws(\Illuminate\Validation\ValidationException::class);
|
||||
|
||||
test('可以发布模板', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->service->publish($template, '首次发布');
|
||||
|
||||
expect($template->fresh()->status)->toBe('published')
|
||||
->and($template->fresh()->published_at)->not->toBeNull()
|
||||
->and($template->versions)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('发布时会创建版本快照', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'status' => 'draft',
|
||||
'version' => '1.0.0',
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->service->publish($template, '测试发布');
|
||||
|
||||
$version = $template->versions->first();
|
||||
|
||||
expect($version)->not->toBeNull()
|
||||
->and($version->version)->toBe('1.0.0')
|
||||
->and($version->change_log)->toBe('测试发布')
|
||||
->and($version->content_snapshot)->toHaveKey('template')
|
||||
->and($version->content_snapshot)->toHaveKey('steps');
|
||||
});
|
||||
|
||||
test('可以归档模板', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'status' => 'published',
|
||||
]);
|
||||
|
||||
$this->service->archive($template);
|
||||
|
||||
expect($template->fresh()->status)->toBe('archived');
|
||||
});
|
||||
|
||||
test('可以复制模板', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'name' => '原始模板',
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$newTemplate = $this->service->duplicate($template, '复制的模板');
|
||||
|
||||
expect($newTemplate->name)->toBe('复制的模板')
|
||||
->and($newTemplate->status)->toBe('draft')
|
||||
->and($newTemplate->steps)->toHaveCount(1)
|
||||
->and($newTemplate->id)->not->toBe($template->id);
|
||||
});
|
||||
|
||||
test('复制模板时会复制所有步骤', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 2,
|
||||
'title' => '第二步',
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$newTemplate = $this->service->duplicate($template, '复制的模板');
|
||||
|
||||
expect($newTemplate->steps)->toHaveCount(2)
|
||||
->and($newTemplate->steps->first()->title)->toBe('第一步')
|
||||
->and($newTemplate->steps->last()->title)->toBe('第二步');
|
||||
});
|
||||
|
||||
test('复制模板时会复制交互任务', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
|
||||
$step = $template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$step->interactiveTasks()->create([
|
||||
'task_type' => 'confirm',
|
||||
'task_config' => ['message' => '确认?'],
|
||||
'is_required' => true,
|
||||
]);
|
||||
|
||||
$newTemplate = $this->service->duplicate($template, '复制的模板');
|
||||
$newStep = $newTemplate->steps->first();
|
||||
|
||||
expect($newStep->interactiveTasks)->toHaveCount(1)
|
||||
->and($newStep->interactiveTasks->first()->task_type)->toBe('confirm');
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SopTemplate;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user);
|
||||
});
|
||||
|
||||
test('可以创建 SOP 模板', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'name' => '测试模板',
|
||||
'status' => 'draft',
|
||||
'created_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
expect($template->name)->toBe('测试模板')
|
||||
->and($template->status)->toBe('draft')
|
||||
->and($template->created_by)->toBe($this->user->id);
|
||||
});
|
||||
|
||||
test('可以为模板添加步骤', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
|
||||
$step = $template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'content' => '这是第一步的内容',
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
]);
|
||||
|
||||
expect($template->steps)->toHaveCount(1)
|
||||
->and($step->title)->toBe('第一步')
|
||||
->and($step->template->id)->toBe($template->id);
|
||||
});
|
||||
|
||||
test('步骤按 sort_order 排序', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 3,
|
||||
'title' => '第三步',
|
||||
'sort_order' => 3,
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$template->steps()->create([
|
||||
'step_number' => 2,
|
||||
'title' => '第二步',
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$steps = $template->fresh()->steps;
|
||||
|
||||
expect($steps)->toHaveCount(3)
|
||||
->and($steps->first()->title)->toBe('第一步')
|
||||
->and($steps->last()->title)->toBe('第三步');
|
||||
});
|
||||
|
||||
test('可以为步骤添加交互任务', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
$step = $template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$task = $step->interactiveTasks()->create([
|
||||
'task_type' => 'confirm',
|
||||
'task_config' => ['message' => '确认完成?'],
|
||||
'validation_rules' => [],
|
||||
'is_required' => true,
|
||||
]);
|
||||
|
||||
expect($step->interactiveTasks)->toHaveCount(1)
|
||||
->and($task->task_type)->toBe('confirm')
|
||||
->and($task->step->id)->toBe($step->id);
|
||||
});
|
||||
|
||||
test('模板状态可以从草稿变为已发布', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$template->update([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
expect($template->fresh()->status)->toBe('published')
|
||||
->and($template->fresh()->published_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('模板状态可以从已发布变为已归档', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$template->update(['status' => 'archived']);
|
||||
|
||||
expect($template->fresh()->status)->toBe('archived');
|
||||
});
|
||||
|
||||
test('可以创建模板版本快照', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
$template->steps()->create([
|
||||
'step_number' => 1,
|
||||
'title' => '第一步',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$version = $template->versions()->create([
|
||||
'version' => '1.0.0',
|
||||
'change_log' => '首次发布',
|
||||
'content_snapshot' => [
|
||||
'template' => $template->toArray(),
|
||||
'steps' => $template->steps->toArray(),
|
||||
],
|
||||
'created_by' => $this->user->id,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
expect($template->versions)->toHaveCount(1)
|
||||
->and($version->version)->toBe('1.0.0')
|
||||
->and($version->content_snapshot)->toHaveKey('template')
|
||||
->and($version->content_snapshot)->toHaveKey('steps');
|
||||
});
|
||||
|
||||
test('模板记录活动日志', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'name' => '原始名称',
|
||||
]);
|
||||
|
||||
$template->update(['name' => '新名称']);
|
||||
|
||||
$activities = $template->activities;
|
||||
|
||||
expect($activities)->toHaveCount(2) // created 和 updated
|
||||
->and($activities->last()->description)->toContain('updated');
|
||||
});
|
||||
|
||||
test('可以通过分类筛选模板', function () {
|
||||
SopTemplate::factory()->create(['category' => '安全操作']);
|
||||
SopTemplate::factory()->create(['category' => '设备维护']);
|
||||
SopTemplate::factory()->create(['category' => '安全操作']);
|
||||
|
||||
$safetyTemplates = SopTemplate::where('category', '安全操作')->get();
|
||||
|
||||
expect($safetyTemplates)->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('可以通过状态筛选模板', function () {
|
||||
SopTemplate::factory()->create(['status' => 'draft']);
|
||||
SopTemplate::factory()->create(['status' => 'published']);
|
||||
SopTemplate::factory()->create(['status' => 'draft']);
|
||||
|
||||
$draftTemplates = SopTemplate::where('status', 'draft')->get();
|
||||
|
||||
expect($draftTemplates)->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('模板的标签字段正确转换为数组', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'tags' => ['标签1', '标签2', '标签3'],
|
||||
]);
|
||||
|
||||
expect($template->fresh()->tags)->toBeArray()
|
||||
->and($template->fresh()->tags)->toHaveCount(3)
|
||||
->and($template->fresh()->tags)->toContain('标签1');
|
||||
});
|
||||
|
||||
test('模板的适用部门字段正确转换为数组', function () {
|
||||
$template = SopTemplate::factory()->create([
|
||||
'applicable_departments' => ['生产部', '质检部'],
|
||||
]);
|
||||
|
||||
expect($template->fresh()->applicable_departments)->toBeArray()
|
||||
->and($template->fresh()->applicable_departments)->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('可以软删除模板', function () {
|
||||
$template = SopTemplate::factory()->create();
|
||||
$templateId = $template->id;
|
||||
|
||||
$template->delete();
|
||||
|
||||
expect(SopTemplate::find($templateId))->toBeNull()
|
||||
->and(SopTemplate::withTrashed()->find($templateId))->not->toBeNull();
|
||||
});
|
||||
@@ -21,13 +21,13 @@ class TerminalResourceTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
|
||||
// 创建测试用户并赋予管理员权限
|
||||
$this->user = User::factory()->create();
|
||||
|
||||
|
||||
// 设置为Filament管理员
|
||||
config(['filament.auth.guard' => 'web']);
|
||||
|
||||
|
||||
$this->actingAs($this->user);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class TerminalResourceTest extends TestCase
|
||||
'code' => 'TEST-0001',
|
||||
'ip_address' => '192.168.1.100',
|
||||
'station_id' => 1,
|
||||
'diagram_url' => 'https://example.com/diagram.png',
|
||||
'diagram_url' => 'https://example.com/diagram.html',
|
||||
'display_config' => [
|
||||
'resolution' => '1920x1080',
|
||||
'refresh_rate' => '60',
|
||||
@@ -250,7 +250,7 @@ class TerminalResourceTest extends TestCase
|
||||
|
||||
// 测试分组功能是否可用
|
||||
$component = Livewire::test(ListTerminals::class);
|
||||
|
||||
|
||||
// 验证表格可以正常渲染
|
||||
$component->assertSuccessful();
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class TerminalSyncTest extends TestCase
|
||||
|
||||
// 断言任务已加入队列
|
||||
Queue::assertPushed(SyncTerminalConfigJob::class, function ($job) use ($terminal, $log) {
|
||||
return $job->terminal->id === $terminal->id
|
||||
return $job->terminal->id === $terminal->id
|
||||
&& $job->log->id === $log->id;
|
||||
});
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class TerminalSyncTest extends TestCase
|
||||
'code' => 'TEST-002',
|
||||
'ip_address' => '192.168.1.100',
|
||||
'station_id' => 1,
|
||||
'diagram_url' => 'https://example.com/diagram.png',
|
||||
'diagram_url' => 'https://example.com/diagram.html',
|
||||
'display_config' => ['resolution' => '1920x1080'],
|
||||
]);
|
||||
|
||||
@@ -117,7 +117,7 @@ class TerminalSyncTest extends TestCase
|
||||
|
||||
// 断言创建了3个同步日志
|
||||
$this->assertCount(3, $logs);
|
||||
|
||||
|
||||
// 断言每个终端都有同步日志
|
||||
foreach ($terminals as $terminal) {
|
||||
$this->assertDatabaseHas('terminal_sync_logs', [
|
||||
|
||||
Reference in New Issue
Block a user