diff --git a/app/Models/System/Dictionary.php b/app/Models/System/Dictionary.php index b052e83..54fc74c 100644 --- a/app/Models/System/Dictionary.php +++ b/app/Models/System/Dictionary.php @@ -17,6 +17,7 @@ class Dictionary extends Model 'name', 'code', 'description', + 'value_type', 'status', 'sort', ]; diff --git a/app/Services/System/DictionaryService.php b/app/Services/System/DictionaryService.php index 2a5ef42..6525501 100644 --- a/app/Services/System/DictionaryService.php +++ b/app/Services/System/DictionaryService.php @@ -4,11 +4,19 @@ namespace App\Services\System; use App\Models\System\Dictionary; use App\Models\System\DictionaryItem; +use App\Services\WebSocket\WebSocketService; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Validator; class DictionaryService { + protected $webSocketService; + + public function __construct(WebSocketService $webSocketService) + { + $this->webSocketService = $webSocketService; + } + public function getList(array $params): array { $query = Dictionary::query(); @@ -62,6 +70,10 @@ class DictionaryService $dictionary = Dictionary::with('items')->find($id); if ($dictionary) { $dictionary = $dictionary->toArray(); + // 格式化字典项值 + if (!empty($dictionary['items'])) { + $dictionary['items'] = $this->formatItemsByType($dictionary['items'], $dictionary['value_type']); + } Cache::put($cacheKey, $dictionary, 3600); } } @@ -75,9 +87,13 @@ class DictionaryService $dictionary = Cache::get($cacheKey); if ($dictionary === null) { - $dictionaryModel = Dictionary::where('code', $code)->first(); + $dictionaryModel = Dictionary::with('items')->where('code', $code)->first(); if ($dictionaryModel) { $dictionary = $dictionaryModel->toArray(); + // 格式化字典项值 + if (!empty($dictionary['items'])) { + $dictionary['items'] = $this->formatItemsByType($dictionary['items'], $dictionary['value_type']); + } Cache::put($cacheKey, $dictionary, 3600); } else { $dictionary = null; @@ -100,6 +116,8 @@ class DictionaryService ->orderBy('sort') ->get() ->toArray(); + // 格式化字典项值 + $items = $this->formatItemsByType($items, $dictionary->value_type); Cache::put($cacheKey, $items, 3600); } else { $items = []; @@ -118,6 +136,7 @@ class DictionaryService $dictionary = Dictionary::create($data); $this->clearCache(); + $this->notifyDictionaryUpdate('create', $dictionary->toArray()); return $dictionary; } @@ -132,15 +151,18 @@ class DictionaryService $dictionary->update($data); $this->clearCache(); + $this->notifyDictionaryUpdate('update', $dictionary->toArray()); return $dictionary; } public function delete(int $id): bool { $dictionary = Dictionary::findOrFail($id); + $dictionaryData = $dictionary->toArray(); DictionaryItem::where('dictionary_id', $id)->delete(); $dictionary->delete(); $this->clearCache(); + $this->notifyDictionaryUpdate('delete', $dictionaryData); return true; } @@ -149,6 +171,7 @@ class DictionaryService DictionaryItem::whereIn('dictionary_id', $ids)->delete(); Dictionary::whereIn('id', $ids)->delete(); $this->clearCache(); + $this->notifyDictionaryUpdate('batch_delete', ['ids' => $ids]); return true; } @@ -156,6 +179,7 @@ class DictionaryService { Dictionary::whereIn('id', $ids)->update(['status' => $status]); $this->clearCache(); + $this->notifyDictionaryUpdate('batch_update_status', ['ids' => $ids, 'status' => $status]); return true; } @@ -218,6 +242,7 @@ class DictionaryService $item = DictionaryItem::create($data); $this->clearCache($data['dictionary_id']); + $this->notifyDictionaryItemUpdate('create', $item->toArray()); return $item; } @@ -233,6 +258,7 @@ class DictionaryService $item->update($data); $this->clearCache($item->dictionary_id); + $this->notifyDictionaryItemUpdate('update', $item->toArray()); return $item; } @@ -240,8 +266,10 @@ class DictionaryService { $item = DictionaryItem::findOrFail($id); $dictionaryId = $item->dictionary_id; + $itemData = $item->toArray(); $item->delete(); $this->clearCache($dictionaryId); + $this->notifyDictionaryItemUpdate('delete', $itemData); return true; } @@ -257,6 +285,7 @@ class DictionaryService $this->clearCache($dictionaryId); } + $this->notifyDictionaryItemUpdate('batch_delete', ['ids' => $ids, 'dictionary_ids' => $dictionaryIds]); return true; } @@ -272,6 +301,7 @@ class DictionaryService $this->clearCache($dictionaryId); } + $this->notifyDictionaryItemUpdate('batch_update_status', ['ids' => $ids, 'dictionary_ids' => $dictionaryIds, 'status' => $status]); return true; } @@ -292,11 +322,16 @@ class DictionaryService $result = []; foreach ($dictionaries as $dictionary) { + $items = $dictionary->activeItems->toArray(); + // 格式化字典项值 + $items = $this->formatItemsByType($items, $dictionary->value_type); + $result[] = [ 'code' => $dictionary->code, 'name' => $dictionary->name, 'description' => $dictionary->description, - 'items' => $dictionary->activeItems->toArray() + 'value_type' => $dictionary->value_type, + 'items' => $items ]; } @@ -306,4 +341,76 @@ class DictionaryService return $allItems; } + + /** + * 通知前端字典分类已更新 + * + * @param string $action 操作类型:create, update, delete, batch_delete, batch_update_status + * @param array $data 字典数据 + */ + private function notifyDictionaryUpdate(string $action, array $data): void + { + $this->webSocketService->broadcast([ + 'type' => 'dictionary_update', + 'data' => [ + 'action' => $action, + 'resource_type' => 'dictionary', + 'data' => $data, + 'timestamp' => time() + ] + ]); + } + + /** + * 通知前端字典项已更新 + * + * @param string $action 操作类型:create, update, delete, batch_delete, batch_update_status + * @param array $data 字典项数据 + */ + private function notifyDictionaryItemUpdate(string $action, array $data): void + { + $this->webSocketService->broadcast([ + 'type' => 'dictionary_item_update', + 'data' => [ + 'action' => $action, + 'resource_type' => 'dictionary_item', + 'data' => $data, + 'timestamp' => time() + ] + ]); + } + + /** + * 根据值类型格式化字典项 + * @param array $items 字典项数组 + * @param string $valueType 值类型:string, number, boolean, json + * @return array 格式化后的字典项数组 + */ + private function formatItemsByType(array $items, string $valueType): array + { + return array_map(function ($item) use ($valueType) { + switch ($valueType) { + case 'number': + // 数字类型:将值转换为数字 + $item['value'] = is_numeric($item['value']) ? (strpos($item['value'], '.') !== false ? (float)$item['value'] : (int)$item['value']) : $item['value']; + break; + case 'boolean': + // 布尔类型:将 '1', 'true', 'yes' 转换为 true,其他为 false + $item['value'] = in_array(strtolower($item['value']), ['1', 'true', 'yes', 'on']); + break; + case 'json': + // JSON类型:尝试解析JSON字符串,失败则保持原值 + $decoded = json_decode($item['value'], true); + if (json_last_error() === JSON_ERROR_NONE) { + $item['value'] = $decoded; + } + break; + case 'string': + default: + // 字符串类型:保持原值 + break; + } + return $item; + }, $items); + } } diff --git a/app/Services/WebSocket/WebSocketService.php b/app/Services/WebSocket/WebSocketService.php index db844b6..a39622a 100644 --- a/app/Services/WebSocket/WebSocketService.php +++ b/app/Services/WebSocket/WebSocketService.php @@ -19,7 +19,24 @@ class WebSocketService */ public function getServer(): ?Server { - return app('swoole.server'); + // Check if Laravel-S is running + if (!class_exists('Hhxsv5\LaravelS\Illuminate\Laravel') || !defined('IN_LARAVELS')) { + return null; + } + + try { + // Try to get the Swoole server from the Laravel-S container + $laravelS = \Hhxsv5\LaravelS\Illuminate\Laravel::getInstance(); + if ($laravelS && $laravelS->getSwooleServer()) { + return $laravelS->getSwooleServer(); + } + } catch (\Exception $e) { + Log::warning('Failed to get Swoole server instance', [ + 'error' => $e->getMessage() + ]); + } + + return null; } /** diff --git a/database/migrations/2024_01_02_000001_create_system_tables.php b/database/migrations/2024_01_02_000001_create_system_tables.php index 7ea1d1e..e3001b6 100644 --- a/database/migrations/2024_01_02_000001_create_system_tables.php +++ b/database/migrations/2024_01_02_000001_create_system_tables.php @@ -64,6 +64,7 @@ return new class extends Migration $table->string('name')->comment('字典名称'); $table->string('code')->unique()->comment('字典编码'); $table->text('description')->nullable()->comment('字典描述'); + $table->string('value_type')->default('string')->comment('值类型:string, number, boolean, json'); $table->boolean('status')->default(true)->comment('状态'); $table->integer('sort')->default(0)->comment('排序'); $table->timestamps(); diff --git a/database/seeders/SystemSeeder.php b/database/seeders/SystemSeeder.php index da14fae..d88fbe3 100644 --- a/database/seeders/SystemSeeder.php +++ b/database/seeders/SystemSeeder.php @@ -531,6 +531,7 @@ class SystemSeeder extends Seeder 'name' => '用户状态', 'code' => 'user_status', 'description' => '用户账号状态', + 'value_type' => 'number', 'sort' => 1, 'status' => 1, ], @@ -538,6 +539,7 @@ class SystemSeeder extends Seeder 'name' => '性别', 'code' => 'gender', 'description' => '用户性别', + 'value_type' => 'number', 'sort' => 2, 'status' => 1, ], @@ -545,6 +547,7 @@ class SystemSeeder extends Seeder 'name' => '角色状态', 'code' => 'role_status', 'description' => '角色启用状态', + 'value_type' => 'number', 'sort' => 3, 'status' => 1, ], @@ -552,6 +555,7 @@ class SystemSeeder extends Seeder 'name' => '字典状态', 'code' => 'dictionary_status', 'description' => '数据字典状态', + 'value_type' => 'number', 'sort' => 4, 'status' => 1, ], @@ -559,6 +563,7 @@ class SystemSeeder extends Seeder 'name' => '任务状态', 'code' => 'task_status', 'description' => '定时任务状态', + 'value_type' => 'number', 'sort' => 5, 'status' => 1, ], @@ -566,6 +571,7 @@ class SystemSeeder extends Seeder 'name' => '日志类型', 'code' => 'log_type', 'description' => '系统日志类型', + 'value_type' => 'string', 'sort' => 6, 'status' => 1, ], @@ -573,6 +579,7 @@ class SystemSeeder extends Seeder 'name' => '是否', 'code' => 'yes_no', 'description' => '是否选项', + 'value_type' => 'boolean', 'sort' => 7, 'status' => 1, ], @@ -580,6 +587,7 @@ class SystemSeeder extends Seeder 'name' => '配置分组', 'code' => 'config_group', 'description' => '系统配置分组类型', + 'value_type' => 'string', 'sort' => 8, 'status' => 1, ], diff --git a/docs/DICTIONARY_CACHE_UPDATE.md b/docs/DICTIONARY_CACHE_UPDATE.md new file mode 100644 index 0000000..e9088aa --- /dev/null +++ b/docs/DICTIONARY_CACHE_UPDATE.md @@ -0,0 +1,415 @@ +# 字典缓存更新机制 + +## 概述 + +本文档说明前后端字典缓存的更新逻辑,确保在字典分类和字典项的增删改等操作后,前端字典缓存能够自动更新。 + +## 技术实现 + +### 1. 后端实现 + +#### 1.1 DictionaryService 更新 + +在 `app/Services/System/DictionaryService.php` 中添加了 WebSocket 通知功能: + +**依赖注入:** +```php +protected $webSocketService; + +public function __construct(WebSocketService $webSocketService) +{ + $this->webSocketService = $webSocketService; +} +``` + +**通知方法:** + +1. **字典分类更新通知** (`notifyDictionaryUpdate`) + - 触发时机:创建、更新、删除、批量删除、批量更新状态 + - 消息类型:`dictionary_update` + +2. **字典项更新通知** (`notifyDictionaryItemUpdate`) + - 触发时机:创建、更新、删除、批量删除、批量更新状态 + - 消息类型:`dictionary_item_update` + +**修改的方法列表:** + +- `create()` - 创建字典分类后发送通知 +- `update()` - 更新字典分类后发送通知 +- `delete()` - 删除字典分类后发送通知 +- `batchDelete()` - 批量删除字典分类后发送通知 +- `batchUpdateStatus()` - 批量更新状态后发送通知 +- `createItem()` - 创建字典项后发送通知 +- `updateItem()` - 更新字典项后发送通知 +- `deleteItem()` - 删除字典项后发送通知 +- `batchDeleteItems()` - 批量删除字典项后发送通知 +- `batchUpdateItemsStatus()` - 批量更新字典项状态后发送通知 + +#### 1.2 WebSocket 消息格式 + +**字典分类更新消息:** +```json +{ + "type": "dictionary_update", + "data": { + "action": "create|update|delete|batch_delete|batch_update_status", + "resource_type": "dictionary", + "data": { + // 字典分类数据 + }, + "timestamp": 1234567890 + } +} +``` + +**字典项更新消息:** +```json +{ + "type": "dictionary_item_update", + "data": { + "action": "create|update|delete|batch_delete|batch_update_status", + "resource_type": "dictionary_item", + "data": { + // 字典项数据 + }, + "timestamp": 1234567890 + } +} +``` + +### 2. 前端实现 + +#### 2.1 WebSocket Composable + +创建了 `resources/admin/src/composables/useWebSocket.js` 来处理 WebSocket 连接和消息监听: + +**主要功能:** + +1. **初始化 WebSocket 连接** + - 检查用户登录状态 + - 验证用户信息完整性 + - 建立连接并注册消息处理器 + +2. **消息处理器** + - `handleDictionaryUpdate` - 处理字典分类更新 + - `handleDictionaryItemUpdate` - 处理字典项更新 + +3. **缓存刷新** + - 接收到更新通知后,自动刷新字典缓存 + - 显示成功提示消息 + +#### 2.2 App.vue 集成 + +在 `resources/admin/src/App.vue` 中集成了 WebSocket: + +**生命周期钩子:** + +```javascript +onMounted(async () => { + // ... 其他初始化代码 + + // 初始化 WebSocket 连接 + if (userStore.isLoggedIn()) { + initWebSocket() + } +}) + +onUnmounted(() => { + // 关闭 WebSocket 连接 + closeWebSocket() +}) +``` + +## 工作流程 + +### 完整流程图 + +``` +用户操作(增删改字典) + ↓ +后端 Controller 调用 Service + ↓ +Service 执行数据库操作 + ↓ +Service 清理后端缓存(Redis) + ↓ +Service 发送 WebSocket 广播通知 + ↓ +WebSocket 推送消息到所有在线客户端 + ↓ +前端接收 WebSocket 消息 + ↓ +触发相应的消息处理器 + ↓ +刷新前端字典缓存 + ↓ +显示成功提示 +``` + +### 详细步骤 + +1. **用户操作** + - 管理员在后台管理界面进行字典分类或字典项的增删改操作 + - 例如:创建新字典分类、修改字典项、批量删除等 + +2. **后端处理** + - 接收请求并验证数据 + - 执行数据库操作(INSERT/UPDATE/DELETE) + - 清理 Redis 缓存(`DictionaryService::clearCache()`) + - 通过 WebSocket 广播更新通知 + +3. **WebSocket 通知** + - 服务端向所有连接的 WebSocket 客户端广播消息 + - 消息包含操作类型、资源类型和更新的数据 + +4. **前端接收** + - App.vue 在 onMounted 时初始化 WebSocket 连接 + - 注册消息处理器监听 `dictionary_update` 和 `dictionary_item_update` 事件 + - 接收到消息后调用对应的处理器 + +5. **缓存刷新** + - 处理器调用 `dictionaryStore.refresh(true)` 强制刷新缓存 + - 从后端 API 重新加载所有字典数据 + - 更新 Pinia store 中的字典数据 + - 持久化到本地存储 + +6. **用户反馈** + - 显示 "字典数据已更新" 的成功提示 + - 页面上的字典数据自动更新,无需手动刷新 + +## 使用示例 + +### 示例 1:创建新字典分类 + +```php +// 后端代码 +$dictionary = $dictionaryService->create([ + 'name' => '订单状态', + 'code' => 'order_status', + 'description' => '订单状态字典', + 'value_type' => 'string', + 'sort' => 1, + 'status' => true +]); + +// 自动触发: +// 1. 清理 Redis 缓存 +// 2. 广播 WebSocket 消息 +``` + +前端自动刷新缓存并显示提示。 + +### 示例 2:更新字典项 + +```php +// 后端代码 +$item = $dictionaryService->updateItem(1, [ + 'label' => '已付款', + 'value' => 'paid', + 'sort' => 2 +]); + +// 自动触发: +// 1. 清理对应字典的 Redis 缓存 +// 2. 广播 WebSocket 消息 +``` + +前端自动刷新缓存并显示提示。 + +### 示例 3:批量操作 + +```php +// 后端代码 +$dictionaryService->batchUpdateStatus([1, 2, 3], false); + +// 自动触发: +// 1. 清理所有相关字典的 Redis 缓存 +// 2. 广播 WebSocket 消息(包含批量更新的 ID) +``` + +前端自动刷新缓存并显示提示。 + +## 注意事项 + +### 1. WebSocket 连接 + +- WebSocket 仅在用户登录后建立连接 +- 连接失败会自动重试(最多 5 次) +- 页面卸载时会自动关闭连接 + +### 2. 缓存一致性 + +- 后端缓存使用 Redis,TTL 为 3600 秒(1 小时) +- 前端缓存使用 Pinia + 本地存储持久化 +- WebSocket 通知确保前后端缓存同步更新 + +### 3. 错误处理 + +- WebSocket 连接失败不影响页面正常使用 +- 缓存刷新失败会在控制台输出错误日志 +- 不会阻塞用户操作 + +### 4. 性能考虑 + +- 批量操作会一次性清理相关缓存 +- WebSocket 广播向所有在线用户推送 +- 前端刷新时会重新加载所有字典数据 + +## 扩展建议 + +### 1. 细粒度缓存更新 + +当前实现是全量刷新,未来可以优化为增量更新: + +```javascript +// 只更新受影响的字典 +async function handleDictionaryUpdate(data) { + const { action, data: dictData } = data + + if (action === 'update' && dictData.code) { + // 只更新特定的字典 + await dictionaryStore.getDictionary(dictData.code, true) + } else { + // 全量刷新 + await dictionaryStore.refresh(true) + } +} +``` + +### 2. 权限控制 + +可以只向有权限的用户发送通知: + +```php +// 后端只向有字典管理权限的用户发送 +$adminUserIds = User::whereHas('roles', function($query) { + $query->where('name', 'admin'); +})->pluck('id')->toArray(); + +$this->webSocketService->sendToUsers($adminUserIds, $message); +``` + +### 3. 消息队列 + +对于高并发场景,可以使用消息队列异步发送 WebSocket 通知: + +```php +// 使用 Laravel 队列 +UpdateDictionaryCacheJob::dispatch($action, $data); +``` + +## 测试建议 + +### 1. 单元测试 + +测试后端 WebSocket 通知是否正确发送: + +```php +public function testDictionaryUpdateSendsWebSocketNotification() +{ + $this->mockWebSocketService(); + + $dictionary = DictionaryService::create([ + 'name' => 'Test', + 'code' => 'test' + ]); + + // 验证 WebSocket 广播被调用 +} +``` + +### 2. 集成测试 + +1. 启动后端服务(Laravel-S) +2. 启动前端开发服务器 +3. 在浏览器中登录系统 +4. 打开开发者工具的 Network -> WS 标签查看 WebSocket 消息 +5. 执行字典增删改操作 +6. 验证: + - WebSocket 消息是否正确接收 + - 缓存是否自动刷新 + - 页面数据是否更新 + - 提示消息是否显示 + +### 3. 并发测试 + +1. 打开多个浏览器窗口并登录 +2. 在一个窗口中进行字典操作 +3. 验证所有窗口的缓存是否同步更新 + +## 故障排查 + +### 问题 1:前端未收到 WebSocket 消息 + +**可能原因:** +- WebSocket 服务未启动 +- 网络连接问题 +- 用户未登录 +- Laravel-S 未运行(使用普通 PHP 运行时) + +**解决方法:** +1. 检查 Laravel-S 服务是否启动:`php bin/laravels status` +2. 检查浏览器控制台是否有 WebSocket 错误 +3. 确认用户已登录且有 token +4. 确认是否在 Laravel-S 环境下运行(WebSocket 通知仅在 Laravel-S 环境下有效) + +**注意:** +- WebSocket 通知功能依赖于 Laravel-S (Swoole) 环境 +- 在普通 PHP 环境下运行时,WebSocket 通知会优雅降级(不发送通知,但不影响功能) +- 仍需手动刷新页面或使用 API 轮询来获取最新数据 + +### 问题 2:后端 WebSocket 通知发送失败 + +**可能原因:** +- Laravel-S 未运行 +- Swoole 服务器未启动 +- WebSocket 服务实例获取失败 + +**解决方法:** +1. 确认在 Laravel-S 环境下运行:`php bin/laravels start` +2. 检查 Laravel-S 配置文件 `config/laravels.php` +3. 查看后端日志:`tail -f storage/logs/laravel.log` + +**注意:** +- 如果未在 Laravel-S 环境下运行,后端会记录警告日志,但不会报错 +- 字典数据仍会正常更新到数据库和 Redis 缓存 +- 只是前端不会收到自动更新通知 + +### 问题 3:缓存未更新 + +**可能原因:** +- WebSocket 消息处理失败 +- API 请求失败 +- 前端未连接 WebSocket + +**解决方法:** +1. 查看浏览器控制台错误日志 +2. 检查网络请求是否成功 +3. 手动刷新页面验证 API 是否正常 +4. 确认 WebSocket 连接状态(浏览器开发者工具 Network -> WS) + +### 问题 3:通知频繁弹出 + +**可能原因:** +- 批量操作触发了多次通知 + +**解决方法:** +1. 优化后端批量操作,只发送一次通知 +2. 前端添加防抖/节流逻辑 + +## 总结 + +通过 WebSocket 实现的字典缓存自动更新机制,确保了前后端数据的一致性,提升了用户体验。用户无需手动刷新页面即可获取最新的字典数据。 + +### 优势 + +- ✅ 实时更新,无需手动刷新 +- ✅ 多端同步,所有在线用户自动更新 +- ✅ 操作透明,用户有明确的反馈 +- ✅ 易于扩展,可应用于其他数据类型 + +### 限制 + +- 需要稳定的 WebSocket 连接 +- 当前实现为全量刷新,可以优化为增量更新 +- 依赖后端服务(Laravel-S)正常运行 diff --git a/resources/admin/src/App.vue b/resources/admin/src/App.vue index 71be399..da0b0a2 100644 --- a/resources/admin/src/App.vue +++ b/resources/admin/src/App.vue @@ -1,8 +1,10 @@ diff --git a/resources/admin/src/composables/useWebSocket.js b/resources/admin/src/composables/useWebSocket.js new file mode 100644 index 0000000..c12d505 --- /dev/null +++ b/resources/admin/src/composables/useWebSocket.js @@ -0,0 +1,146 @@ +import { ref } from 'vue' +import { getWebSocket } from '@/utils/websocket' +import { useUserStore } from '@/stores/modules/user' +import { useDictionaryStore } from '@/stores/modules/dictionary' +import { message } from 'ant-design-vue' +import config from '@/config' + +/** + * WebSocket Composable + * + * 处理 WebSocket 连接和消息监听 + */ +export function useWebSocket() { + const ws = ref(null) + const userStore = useUserStore() + const dictionaryStore = useDictionaryStore() + + /** + * 初始化 WebSocket 连接 + */ + function initWebSocket() { + if (!userStore.token) { + console.warn('未登录,无法初始化 WebSocket') + return + } + + if (!userStore.userInfo || !userStore.userInfo.id) { + console.warn('用户信息不完整,无法初始化 WebSocket') + return + } + + try { + // 使用配置文件中的 WS_URL + ws.value = getWebSocket(userStore.userInfo.id, userStore.token, { + wsUrl: config.WS_URL, + onOpen: handleOpen, + onMessage: handleMessage, + onError: handleError, + onClose: handleClose + }) + + // 注册消息处理器 + ws.value.on('dictionary_update', handleDictionaryUpdate) + ws.value.on('dictionary_item_update', handleDictionaryItemUpdate) + + // 连接 + ws.value.connect() + } catch (error) { + console.error('初始化 WebSocket 失败:', error) + } + } + + /** + * 处理连接打开 + */ + function handleOpen(event) { + console.log('WebSocket 连接已建立', event) + } + + /** + * 处理接收消息 + */ + function handleMessage(message, event) { + console.log('收到 WebSocket 消息:', message) + } + + /** + * 处理错误 + */ + function handleError(error) { + console.error('WebSocket 错误:', error) + } + + /** + * 处理连接关闭 + */ + function handleClose(event) { + console.log('WebSocket 连接已关闭', event) + } + + /** + * 处理字典分类更新 + */ + async function handleDictionaryUpdate(data) { + console.log('字典分类已更新:', data) + + const { action, resource_type, timestamp } = data + + if (resource_type !== 'dictionary') { + return + } + + try { + // 刷新字典缓存 + await dictionaryStore.refresh(true) + + // 显示通知 + message.success('字典数据已更新') + } catch (error) { + console.error('刷新字典缓存失败:', error) + } + } + + /** + * 处理字典项更新 + */ + async function handleDictionaryItemUpdate(data) { + console.log('字典项已更新:', data) + + const { action, resource_type, timestamp } = data + + if (resource_type !== 'dictionary_item') { + return + } + + try { + // 刷新字典缓存 + await dictionaryStore.refresh(true) + + // 显示通知 + message.success('字典数据已更新') + } catch (error) { + console.error('刷新字典缓存失败:', error) + } + } + + /** + * 关闭 WebSocket 连接 + */ + function closeWebSocket() { + if (ws.value) { + // 取消注册消息处理器 + ws.value.off('dictionary_update') + ws.value.off('dictionary_item_update') + + ws.value.disconnect() + ws.value = null + } + } + + return { + ws, + initWebSocket, + closeWebSocket + } +} diff --git a/resources/admin/src/config/index.js b/resources/admin/src/config/index.js index 78f6b00..94ec627 100644 --- a/resources/admin/src/config/index.js +++ b/resources/admin/src/config/index.js @@ -13,6 +13,7 @@ const defaultConfig = { //接口地址 API_URL: 'http://127.0.0.1:8000/admin/', + WS_URL: '127.0.0.1:8000', //请求超时 TIMEOUT: 50000, diff --git a/resources/admin/src/pages/system/dictionaries/components/DictionaryDialog.vue b/resources/admin/src/pages/system/dictionaries/components/DictionaryDialog.vue index 93e1652..7007da0 100644 --- a/resources/admin/src/pages/system/dictionaries/components/DictionaryDialog.vue +++ b/resources/admin/src/pages/system/dictionaries/components/DictionaryDialog.vue @@ -12,6 +12,17 @@
系统唯一标识,只能包含字母、数字、下划线,且必须以字母开头
+ + + + 字符串 + 数字 + 布尔值 + JSON + +
指定字典项值的类型,系统会根据类型自动格式化返回数据
+
+ @@ -79,6 +90,7 @@ const form = ref({ id: '', name: '', code: '', + value_type: 'string', description: '', status: null, sort: 0 @@ -114,6 +126,9 @@ const rules = { trigger: 'blur' }, { validator: validateCodeUnique, trigger: 'blur' } + ], + value_type: [ + { required: true, message: '请选择值类型', trigger: 'change' } ] } @@ -123,6 +138,7 @@ const resetForm = () => { id: '', name: '', code: '', + value_type: 'string', description: '', status: null, sort: 0 @@ -137,6 +153,7 @@ const setData = (data) => { id: data.id || '', name: data.name || '', code: data.code || '', + value_type: data.value_type || 'string', description: data.description || '', status: data.status !== undefined ? data.status : null, sort: data.sort !== undefined ? data.sort : 0 @@ -155,6 +172,7 @@ const handleSubmit = async () => { const submitData = { name: form.value.name, code: form.value.code, + value_type: form.value.value_type, description: form.value.description, status: form.value.status, sort: form.value.sort diff --git a/resources/admin/src/utils/websocket.js b/resources/admin/src/utils/websocket.js index f0cbc7b..17125be 100644 --- a/resources/admin/src/utils/websocket.js +++ b/resources/admin/src/utils/websocket.js @@ -229,7 +229,9 @@ class WebSocketClient { */ export function createWebSocket(userId, token, options = {}) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - const host = window.location.host + + // 优先使用配置的 WS_URL,否则使用当前域名 + const host = options.wsUrl || window.location.host const url = `${protocol}//${host}/ws?user_id=${userId}&token=${token}` return new WebSocketClient(url, options)