From 6a6c59e3e4493c513df5e420f9bf02c05eeb37b9 Mon Sep 17 00:00:00 2001 From: lizhuoran <625237490@qq.com> Date: Mon, 9 Mar 2026 10:59:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=98=B6=E6=AE=B5=E4=B8=89):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=BB=88=E7=AB=AF=E7=AE=A1=E7=90=86=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 TerminalResource 及其所有页面(列表、创建、编辑、查看) - 实现终端基本信息管理(名称、编码、IP、线站、组态图) - 添加显示配置管理(KeyValue 组件) - 实现在线状态显示和筛选 - 添加按线站分组功能 - 创建 TerminalPolicy 权限策略 - 支持搜索、排序、批量删除等功能 --- app/Filament/Resources/TerminalResource.php | 317 ++++++++++++++++++ .../TerminalResource/Pages/CreateTerminal.php | 43 +++ .../TerminalResource/Pages/EditTerminal.php | 73 ++++ .../TerminalResource/Pages/ListTerminals.php | 20 ++ .../TerminalResource/Pages/ViewTerminal.php | 162 +++++++++ app/Policies/TerminalPolicy.php | 98 ++++++ 6 files changed, 713 insertions(+) create mode 100644 app/Filament/Resources/TerminalResource.php create mode 100644 app/Filament/Resources/TerminalResource/Pages/CreateTerminal.php create mode 100644 app/Filament/Resources/TerminalResource/Pages/EditTerminal.php create mode 100644 app/Filament/Resources/TerminalResource/Pages/ListTerminals.php create mode 100644 app/Filament/Resources/TerminalResource/Pages/ViewTerminal.php create mode 100644 app/Policies/TerminalPolicy.php diff --git a/app/Filament/Resources/TerminalResource.php b/app/Filament/Resources/TerminalResource.php new file mode 100644 index 0000000..df0d61f --- /dev/null +++ b/app/Filament/Resources/TerminalResource.php @@ -0,0 +1,317 @@ +schema([ + Forms\Components\Section::make('基本信息') + ->schema([ + Forms\Components\TextInput::make('name') + ->label('终端名称') + ->required() + ->maxLength(255) + ->placeholder('例如: 生产线A-工位1') + ->helperText('终端的显示名称'), + + Forms\Components\TextInput::make('code') + ->label('终端编码') + ->required() + ->unique(ignoreRecord: true) + ->maxLength(100) + ->placeholder('例如: TERM-0001') + ->helperText('终端的唯一标识符') + ->regex('/^[A-Z0-9\-]+$/') + ->validationMessages([ + 'regex' => '终端编码只能包含大写字母、数字和连字符', + ]), + + Forms\Components\TextInput::make('ip_address') + ->label('IP地址') + ->ip() + ->maxLength(45) + ->placeholder('例如: 192.168.1.100') + ->helperText('终端的IP地址'), + + Forms\Components\TextInput::make('station_id') + ->label('线站ID') + ->numeric() + ->placeholder('请输入线站ID') + ->helperText('关联的生产线站ID'), + ]) + ->columns(2), + + Forms\Components\Section::make('组态图配置') + ->schema([ + Forms\Components\TextInput::make('diagram_url') + ->label('组态图URL') + ->url() + ->maxLength(500) + ->placeholder('https://example.com/diagram.png') + ->helperText('组态图的访问地址'), + ]), + + Forms\Components\Section::make('显示配置') + ->schema([ + Forms\Components\KeyValue::make('display_config') + ->label('显示参数') + ->keyLabel('参数名称') + ->valueLabel('参数值') + ->addActionLabel('添加参数') + ->helperText('配置终端的显示参数,如分辨率、刷新率等') + ->default([ + 'resolution' => '1920x1080', + 'refresh_rate' => '60', + 'orientation' => 'landscape', + 'brightness' => '80', + ]), + ]), + + Forms\Components\Section::make('知识库关联') + ->schema([ + Forms\Components\Repeater::make('knowledgeBaseAssociations') + ->label('关联知识库') + ->relationship('knowledgeBases') + ->schema([ + Forms\Components\Select::make('id') + ->label('知识库') + ->options(\App\Models\KnowledgeBase::where('status', 'active')->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\KnowledgeBase::find($state['id'])?->name ?? '未选择' + ) + ->collapsed() + ->collapsible() + ->helperText('可以关联多个知识库,并设置优先级。拖动或使用按钮调整顺序。'), + ]) + ->description('配置终端可以访问的知识库及其优先级'), + + Forms\Components\Section::make('AI提示词配置') + ->schema([ + Forms\Components\Grid::make(3) + ->schema([ + \AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor::make('prompt.prompt_template') + ->label('提示词模板') + ->language('markdown') + ->fontSize('14px') + ->helperText('编辑AI提示词模板,支持使用变量如 {user}, {station}, {time} 等') + ->placeholderText('请输入AI提示词模板...') + ->disablePreview() + ->columnSpan(2), + + Forms\Components\Grid::make(1) + ->schema([ + Forms\Components\Placeholder::make('template_selector') + ->label('模板库') + ->content(fn () => view('filament.components.prompt-template-selector')), + + Forms\Components\Placeholder::make('variable_helper') + ->label('变量参考') + ->content(fn () => view('filament.components.prompt-variable-helper')), + ]) + ->columnSpan(1), + ]), + ]) + ->description('配置终端的AI提示词模板,用于指导AI助手的行为') + ->collapsible(), + + Forms\Components\Section::make('状态信息') + ->schema([ + Forms\Components\Toggle::make('is_online') + ->label('在线状态') + ->helperText('终端是否在线') + ->default(false) + ->disabled() + ->dehydrated(false), + + Forms\Components\DateTimePicker::make('last_online_at') + ->label('最后在线时间') + ->disabled() + ->dehydrated(false), + ]) + ->columns(2) + ->visibleOn('edit'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label('终端名称') + ->searchable() + ->sortable() + ->weight('bold'), + + Tables\Columns\TextColumn::make('code') + ->label('终端编码') + ->searchable() + ->sortable() + ->copyable() + ->tooltip('点击复制'), + + Tables\Columns\TextColumn::make('ip_address') + ->label('IP地址') + ->searchable() + ->copyable() + ->placeholder('未设置'), + + Tables\Columns\TextColumn::make('station_id') + ->label('线站ID') + ->sortable() + ->placeholder('未绑定'), + + Tables\Columns\IconColumn::make('is_online') + ->label('在线状态') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + Tables\Columns\TextColumn::make('latestSyncLog.status') + ->label('同步状态') + ->badge() + ->formatStateUsing(fn (string $state): string => match ($state) { + 'pending' => '待同步', + 'syncing' => '同步中', + 'synced' => '已同步', + 'failed' => '失败', + default => '未知', + }) + ->color(fn (string $state): string => match ($state) { + 'pending' => 'warning', + 'syncing' => 'info', + 'synced' => 'success', + 'failed' => 'danger', + default => 'gray', + }) + ->icon(fn (string $state): string => match ($state) { + 'pending' => 'heroicon-o-clock', + 'syncing' => 'heroicon-o-arrow-path', + 'synced' => 'heroicon-o-check-circle', + 'failed' => 'heroicon-o-x-circle', + default => 'heroicon-o-question-mark-circle', + }) + ->placeholder('从未同步') + ->sortable(), + + Tables\Columns\TextColumn::make('last_online_at') + ->label('最后在线时间') + ->dateTime('Y-m-d H:i:s') + ->sortable() + ->placeholder('从未在线') + ->toggleable(), + + Tables\Columns\TextColumn::make('created_at') + ->label('创建时间') + ->dateTime('Y-m-d H:i:s') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + Tables\Columns\TextColumn::make('updated_at') + ->label('更新时间') + ->dateTime('Y-m-d H:i:s') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\TernaryFilter::make('is_online') + ->label('在线状态') + ->placeholder('全部') + ->trueLabel('在线') + ->falseLabel('离线'), + + Tables\Filters\Filter::make('station_id') + ->label('已绑定线站') + ->query(fn (Builder $query): Builder => $query->whereNotNull('station_id')), + + Tables\Filters\Filter::make('has_diagram') + ->label('已配置组态图') + ->query(fn (Builder $query): Builder => $query->whereNotNull('diagram_url')), + ]) + ->actions([ + SyncConfigAction::make(), + Tables\Actions\ViewAction::make() + ->label('查看'), + Tables\Actions\EditAction::make() + ->label('编辑'), + Tables\Actions\DeleteAction::make() + ->label('删除'), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + SyncConfigAction::makeBulk(), + Tables\Actions\DeleteBulkAction::make() + ->label('批量删除'), + ]), + ]) + ->defaultSort('created_at', 'desc') + ->groups([ + Tables\Grouping\Group::make('station_id') + ->label('按线站分组') + ->collapsible(), + Tables\Grouping\Group::make('is_online') + ->label('按在线状态分组') + ->getTitleFromRecordUsing(fn (Terminal $record): string => $record->is_online ? '在线' : '离线') + ->collapsible(), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTerminals::route('/'), + 'create' => Pages\CreateTerminal::route('/create'), + 'edit' => Pages\EditTerminal::route('/{record}/edit'), + 'view' => Pages\ViewTerminal::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/TerminalResource/Pages/CreateTerminal.php b/app/Filament/Resources/TerminalResource/Pages/CreateTerminal.php new file mode 100644 index 0000000..2f6a6d9 --- /dev/null +++ b/app/Filament/Resources/TerminalResource/Pages/CreateTerminal.php @@ -0,0 +1,43 @@ +getResource()::getUrl('index'); + } + + protected function getCreatedNotificationTitle(): ?string + { + return '终端创建成功'; + } + + protected function mutateFormDataBeforeCreate(array $data): array + { + // 提取提示词数据,稍后单独处理 + $this->promptData = $data['prompt'] ?? null; + unset($data['prompt']); + + return $data; + } + + protected function afterCreate(): void + { + // 创建终端后,创建或更新提示词 + if (!empty($this->promptData['prompt_template'])) { + $this->record->prompt()->create([ + 'prompt_template' => $this->promptData['prompt_template'], + 'variables' => $this->promptData['variables'] ?? [], + ]); + } + } + + private ?array $promptData = null; +} diff --git a/app/Filament/Resources/TerminalResource/Pages/EditTerminal.php b/app/Filament/Resources/TerminalResource/Pages/EditTerminal.php new file mode 100644 index 0000000..a6885e4 --- /dev/null +++ b/app/Filament/Resources/TerminalResource/Pages/EditTerminal.php @@ -0,0 +1,73 @@ +label('查看'), + Actions\DeleteAction::make() + ->label('删除'), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function getSavedNotificationTitle(): ?string + { + return '终端更新成功'; + } + + protected function mutateFormDataBeforeFill(array $data): array + { + // 加载提示词数据到表单 + if ($this->record->prompt) { + $data['prompt'] = [ + 'prompt_template' => $this->record->prompt->prompt_template, + 'variables' => $this->record->prompt->variables, + ]; + } + + return $data; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + // 提取提示词数据,稍后单独处理 + $this->promptData = $data['prompt'] ?? null; + unset($data['prompt']); + + return $data; + } + + protected function afterSave(): void + { + // 更新或创建提示词 + if (!empty($this->promptData['prompt_template'])) { + $this->record->prompt()->updateOrCreate( + ['terminal_id' => $this->record->id], + [ + 'prompt_template' => $this->promptData['prompt_template'], + 'variables' => $this->promptData['variables'] ?? [], + ] + ); + } elseif ($this->record->prompt) { + // 如果提示词模板为空,删除现有提示词 + $this->record->prompt()->delete(); + } + } + + private ?array $promptData = null; +} diff --git a/app/Filament/Resources/TerminalResource/Pages/ListTerminals.php b/app/Filament/Resources/TerminalResource/Pages/ListTerminals.php new file mode 100644 index 0000000..9c5f843 --- /dev/null +++ b/app/Filament/Resources/TerminalResource/Pages/ListTerminals.php @@ -0,0 +1,20 @@ +label('创建终端'), + ]; + } +} diff --git a/app/Filament/Resources/TerminalResource/Pages/ViewTerminal.php b/app/Filament/Resources/TerminalResource/Pages/ViewTerminal.php new file mode 100644 index 0000000..ba92d86 --- /dev/null +++ b/app/Filament/Resources/TerminalResource/Pages/ViewTerminal.php @@ -0,0 +1,162 @@ +label('编辑'), + Actions\DeleteAction::make() + ->label('删除'), + ]; + } + + public function infolist(Infolist $infolist): Infolist + { + return $infolist + ->schema([ + Infolists\Components\Section::make('基本信息') + ->schema([ + Infolists\Components\TextEntry::make('name') + ->label('终端名称'), + Infolists\Components\TextEntry::make('code') + ->label('终端编码') + ->copyable(), + Infolists\Components\TextEntry::make('ip_address') + ->label('IP地址') + ->copyable() + ->placeholder('未设置'), + Infolists\Components\TextEntry::make('station_id') + ->label('线站ID') + ->placeholder('未绑定'), + ]) + ->columns(2), + + Infolists\Components\Section::make('组态图配置') + ->schema([ + Infolists\Components\TextEntry::make('diagram_url') + ->label('组态图URL') + ->copyable() + ->placeholder('未设置') + ->url(fn ($state) => $state) + ->openUrlInNewTab(), + ]), + + Infolists\Components\Section::make('显示配置') + ->schema([ + Infolists\Components\KeyValueEntry::make('display_config') + ->label('显示参数') + ->keyLabel('参数名称') + ->valueLabel('参数值'), + ]), + + Infolists\Components\Section::make('知识库关联') + ->schema([ + Infolists\Components\RepeatableEntry::make('knowledgeBases') + ->label('关联的知识库') + ->schema([ + Infolists\Components\TextEntry::make('name') + ->label('知识库名称'), + Infolists\Components\TextEntry::make('pivot.priority') + ->label('优先级') + ->badge() + ->color('success'), + ]) + ->columns(2) + ->placeholder('未关联任何知识库'), + ]) + ->collapsible(), + + Infolists\Components\Section::make('AI提示词配置') + ->schema([ + Infolists\Components\TextEntry::make('prompt.prompt_template') + ->label('提示词模板') + ->markdown() + ->placeholder('未配置提示词'), + ]) + ->collapsible(), + + Infolists\Components\Section::make('状态信息') + ->schema([ + Infolists\Components\IconEntry::make('is_online') + ->label('在线状态') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger'), + Infolists\Components\TextEntry::make('last_online_at') + ->label('最后在线时间') + ->dateTime('Y-m-d H:i:s') + ->placeholder('从未在线'), + ]) + ->columns(2), + + Infolists\Components\Section::make('同步历史') + ->schema([ + Infolists\Components\RepeatableEntry::make('syncLogs') + ->label('同步记录') + ->schema([ + Infolists\Components\TextEntry::make('status') + ->label('状态') + ->badge() + ->formatStateUsing(fn (string $state): string => match ($state) { + 'pending' => '待同步', + 'syncing' => '同步中', + 'synced' => '已同步', + 'failed' => '失败', + default => '未知', + }) + ->color(fn (string $state): string => match ($state) { + 'pending' => 'warning', + 'syncing' => 'info', + 'synced' => 'success', + 'failed' => 'danger', + default => 'gray', + }), + Infolists\Components\TextEntry::make('created_at') + ->label('创建时间') + ->dateTime('Y-m-d H:i:s'), + Infolists\Components\TextEntry::make('synced_at') + ->label('同步完成时间') + ->dateTime('Y-m-d H:i:s') + ->placeholder('未完成'), + Infolists\Components\TextEntry::make('error_message') + ->label('错误信息') + ->placeholder('无') + ->color('danger') + ->columnSpanFull(), + ]) + ->columns(3) + ->placeholder('暂无同步记录') + ->contained(false), + ]) + ->description('显示最近的配置同步记录,按时间倒序排列') + ->collapsible() + ->collapsed(), + + Infolists\Components\Section::make('时间信息') + ->schema([ + Infolists\Components\TextEntry::make('created_at') + ->label('创建时间') + ->dateTime('Y-m-d H:i:s'), + Infolists\Components\TextEntry::make('updated_at') + ->label('更新时间') + ->dateTime('Y-m-d H:i:s'), + ]) + ->columns(2) + ->collapsed(), + ]); + } +} diff --git a/app/Policies/TerminalPolicy.php b/app/Policies/TerminalPolicy.php new file mode 100644 index 0000000..a54f374 --- /dev/null +++ b/app/Policies/TerminalPolicy.php @@ -0,0 +1,98 @@ +