diff --git a/backend/.gitignore b/backend/.gitignore index 5dca95d..7321ec3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -21,6 +21,8 @@ public/* plugin/* !plugin/example/ !plugin/demo/ +!plugin/produce/ +!plugin/article/ diff --git a/backend/plugin/article/Install.php b/backend/plugin/article/Install.php new file mode 100644 index 0000000..caa51ac --- /dev/null +++ b/backend/plugin/article/Install.php @@ -0,0 +1,59 @@ +importPluginMenus() - 导入菜单 + * - $this->importTemplates() - 复制模板 + * - $this->uninstallPluginMenus() - 卸载菜单 + */ +class Install extends BasePluginInstall +{ + /** + * 安装前回调 + */ + protected function beforeInstall(string $version): void + { + $this->output(" 🔄 Running beforeInstall...\n"); + } + + /** + * 安装后回调 - 按需导入菜单和模板 + */ + protected function afterInstall(string $version): void + { + $this->output(" 🔄 Running afterInstall...\n"); + + // 导入菜单 + $this->importPluginMenus(); + + // 复制模板资源 + $this->importTemplates(); + } + + /** + * 卸载前回调 + */ + protected function beforeUninstall(string $version): void + { + $this->output(" 🔄 Running beforeUninstall...\n"); + } + + /** + * 卸载后回调 - 清理菜单 + */ + protected function afterUninstall(string $version): void + { + $this->output(" 🔄 Running afterUninstall...\n"); + // 卸载菜单 + $this->uninstallPluginMenus(); + } +} diff --git a/backend/plugin/article/app/adminapi/controller/ArticleController.php b/backend/plugin/article/app/adminapi/controller/ArticleController.php new file mode 100644 index 0000000..d15f982 --- /dev/null +++ b/backend/plugin/article/app/adminapi/controller/ArticleController.php @@ -0,0 +1,231 @@ +currentUser = $currentUser; + } + + #[OA\Get( + path: '/article/legacy/index', + summary: '旧版内容列表示例', + tags: ['article-legacy'] + )] + #[DataResponse(description: '获取成功')] + #[Permission(code: 'article:legacy:index', description: '旧版内容列表示例')] + public function index(Request $request): Response + { + return Json::success('获取成功', [ + [ + 'title' => '欢迎使用文章管理插件', + 'category' => '默认分类', + 'tags' => '插件, 演示', + 'status' => '已发布', + 'updated_at' => '2026-07-25 17:50:00', + ], + ]); + } + + #[OA\Get( + path: '/article/legacy/category', + summary: '旧版内容分类列表示例', + tags: ['article-legacy'] + )] + #[DataResponse(description: '获取成功')] + #[Permission(code: 'article:legacy:category', description: '旧版内容分类列表示例')] + public function category(Request $request): Response + { + return Json::success('获取成功', [ + [ + 'name' => '默认分类', + 'slug' => 'default-category', + 'article_count' => 1, + 'sort' => 100, + 'updated_at' => '2026-07-25 17:50:00', + ], + ]); + } + + #[OA\Get( + path: '/article/legacy/tag', + summary: '旧版标签列表示例', + tags: ['article-legacy'] + )] + #[DataResponse(description: '获取成功')] + #[Permission(code: 'article:legacy:tag', description: '旧版标签列表示例')] + public function tag(Request $request): Response + { + return Json::success('获取成功', [ + [ + 'name' => '插件演示', + 'slug' => 'plugin-demo', + 'article_count' => 1, + 'heat' => 88, + 'updated_at' => '2026-07-25 17:50:00', + ], + ]); + } + + #[OA\Get( + path: '/article/recycle', + summary: '内容回收站列表', + tags: ['article-legacy'] + )] + #[DataResponse(description: '获取成功')] + #[Permission(code: 'article:recycle:index', description: '内容回收站权限')] + public function recycle(Request $request): Response + { + $keyword = trim((string) $request->input('LIKE_title', '')); + + $items = Content::query() + ->onlyTrashed() + ->with(['category']) + ->when($keyword !== '', function ($query) use ($keyword) { + $query->where('title', 'like', "%{$keyword}%"); + }) + ->orderByDesc('deleted_at') + ->get(); + + $operatorIds = $items->pluck('updated_by') + ->filter(static fn($id) => !empty($id)) + ->unique() + ->values() + ->all(); + + $operators = empty($operatorIds) + ? collect() + : Admin::query()->whereIn('id', $operatorIds)->get(['id', 'real_name']); + + $operatorMap = $operators->pluck('real_name', 'id'); + + $rows = $items->map(function (Content $item) use ($operatorMap) { + $deletedAt = $item->deleted_at; + return [ + 'id' => $item->id, + 'title' => $item->title, + 'category' => $item->category?->name ?? '', + 'deleted_by' => $operatorMap[$item->updated_by] ?? '', + 'deleted_at' => $deletedAt ? date('Y-m-d H:i:s', (int) $deletedAt) : '', + 'expire_at' => $deletedAt ? date('Y-m-d H:i:s', (int) $deletedAt + 30 * 24 * 60 * 60) : '', + ]; + })->values()->all(); + + return Json::success('获取成功', $rows); + } + + #[OA\Put( + path: '/article/recycle/{id}/restore', + summary: '恢复内容', + tags: ['article-legacy'] + )] + #[SimpleResponse(description: '恢复成功')] + #[Permission(code: 'article:recycle:restore', description: '恢复回收站内容')] + public function restore(Request $request): Response + { + try { + $id = $request->route?->param('id') ?: $request->input('id'); + $item = Content::withTrashed()->find($id); + if (!$item) { + return Json::fail('内容不存在'); + } + $item->restore(); + return Json::success('恢复成功'); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Delete( + path: '/article/recycle/{id}', + summary: '彻底删除内容', + tags: ['article-legacy'] + )] + #[SimpleResponse(description: '删除成功')] + #[Permission(code: 'article:recycle:delete', description: '彻底删除回收站内容')] + public function destroyRecycle(Request $request): Response + { + try { + $id = $request->route?->param('id') ?: $request->input('id'); + $item = Content::withTrashed()->find($id); + if (!$item) { + return Json::fail('内容不存在'); + } + $item->forceDelete(); + return Json::success('删除成功'); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Post( + path: '/article/legacy/create', + summary: '旧版创建示例', + tags: ['article-legacy'] + )] + #[SimpleResponse(description: '创建成功')] + #[Permission(code: 'article:legacy:create', description: '旧版创建示例')] + public function create(Request $request): Response + { + return Json::success('创建成功'); + } + + #[OA\Get( + path: '/article/legacy/info', + summary: '旧版详情示例', + tags: ['article-legacy'] + )] + #[DataResponse(description: '获取成功')] + #[Permission(code: 'article:legacy:info', description: '旧版详情示例')] + public function info(Request $request): Response + { + return Json::success('获取成功', []); + } + + #[OA\Post( + path: '/article/legacy/update', + summary: '旧版更新示例', + tags: ['article-legacy'] + )] + #[SimpleResponse(description: '更新成功')] + #[Permission(code: 'article:legacy:update', description: '旧版更新示例')] + public function update(Request $request): Response + { + return Json::success('更新成功'); + } + + #[OA\Post( + path: '/article/legacy/delete', + summary: '旧版删除示例', + tags: ['article-legacy'] + )] + #[SimpleResponse(description: '删除成功')] + #[Permission(code: 'article:legacy:delete', description: '旧版删除示例')] + public function delete(Request $request): Response + { + return Json::success('删除成功'); + } +} diff --git a/backend/plugin/article/app/adminapi/controller/CategoryController.php b/backend/plugin/article/app/adminapi/controller/CategoryController.php new file mode 100644 index 0000000..b1fdc7e --- /dev/null +++ b/backend/plugin/article/app/adminapi/controller/CategoryController.php @@ -0,0 +1,127 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get( + path: '/article/category', + summary: '内容分类列表', + tags: ['article-category'] + )] + #[Permission(code: 'article:category:list', description: '内容分类列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/article/category/{id}', + summary: '内容分类详情', + tags: ['article-category'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:category:read', description: '内容分类详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post( + path: '/article/category', + summary: '创建内容分类', + tags: ['article-category'] + )] + #[Permission(code: 'article:category:create', description: '创建内容分类')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/article/category/{id}', + summary: '更新内容分类', + tags: ['article-category'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:category:update', description: '更新内容分类')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post( + path: '/article/category/export', + summary: '导出内容分类', + tags: ['article-category'] + )] + #[Permission(code: 'article:category:list', description: '导出内容分类')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/article/category/{id}', + summary: '删除内容分类', + tags: ['article-category'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:category:delete', description: '删除内容分类')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/article/category', + summary: '批量删除内容分类', + tags: ['article-category'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'article:category:delete', description: '批量删除内容分类')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/article/app/adminapi/controller/ContentController.php b/backend/plugin/article/app/adminapi/controller/ContentController.php new file mode 100644 index 0000000..f88797e --- /dev/null +++ b/backend/plugin/article/app/adminapi/controller/ContentController.php @@ -0,0 +1,183 @@ +service = $service; + $this->validate = $validate; + $this->currentUser = Container::make(CurrentUser::class); + } + + #[OA\Get( + path: '/article/index', + summary: '内容中心列表', + tags: ['article-content'] + )] + #[Permission(code: 'article:list', description: '内容中心列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/article/index/{id}', + summary: '内容中心详情', + tags: ['article-content'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:read', description: '内容中心详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post( + path: '/article/index', + summary: '创建内容', + tags: ['article-content'] + )] + #[Permission(code: 'article:create', description: '创建内容')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/article/index/{id}', + summary: '更新内容', + tags: ['article-content'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:update', description: '更新内容')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post( + path: '/article/index/export', + summary: '导出内容', + tags: ['article-content'] + )] + #[Permission(code: 'article:list', description: '导出内容')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + protected function selectInput(Request $request, ?array $config = null): array + { + $result = parent::selectInput($request, $config); + if (empty($result[4])) { + $result[4] = 'published_at desc,id desc'; + } + return $result; + } + + protected function insertInput(Request $request): array + { + return $this->inputFilter($request->all(), ['tag_ids']); + } + + #[OA\Delete( + path: '/article/index/{id}', + summary: '删除内容', + tags: ['article-content'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:delete', description: '删除内容')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return $this->deleteContents($request); + } + + #[OA\Delete( + path: '/article/index', + summary: '批量删除内容', + tags: ['article-content'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'article:delete', description: '批量删除内容')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return $this->deleteContents($request); + } + + protected function deleteContents(Request $request): \support\Response + { + try { + $ids = $this->getDeleteIds($request); + if (empty($ids)) { + throw new \Exception('删除参数不能为空'); + } + + $result = $this->service->transaction(function () use ($ids) { + $data = is_array($ids) ? $ids : explode(',', $ids); + $deletedIds = []; + $currentUserId = $this->currentUser->id(); + foreach ($data as $id) { + $item = Content::query()->find($id); + if (!$item) { + continue; + } + + if ($currentUserId && $item->isFillable('updated_by')) { + $item->setAttribute('updated_by', $currentUserId); + $item->save(); + } + + $item->delete(); + $deletedIds[] = $item->id; + } + return $deletedIds; + }); + + return Json::success('ok', $result); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } +} diff --git a/backend/plugin/article/app/adminapi/controller/TagController.php b/backend/plugin/article/app/adminapi/controller/TagController.php new file mode 100644 index 0000000..7584ec2 --- /dev/null +++ b/backend/plugin/article/app/adminapi/controller/TagController.php @@ -0,0 +1,127 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get( + path: '/article/tag', + summary: '标签管理列表', + tags: ['article-tag'] + )] + #[Permission(code: 'article:tag:list', description: '标签管理列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/article/tag/{id}', + summary: '标签详情', + tags: ['article-tag'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:tag:read', description: '标签详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post( + path: '/article/tag', + summary: '创建标签', + tags: ['article-tag'] + )] + #[Permission(code: 'article:tag:create', description: '创建标签')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/article/tag/{id}', + summary: '更新标签', + tags: ['article-tag'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:tag:update', description: '更新标签')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post( + path: '/article/tag/export', + summary: '导出标签', + tags: ['article-tag'] + )] + #[Permission(code: 'article:tag:list', description: '导出标签')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/article/tag/{id}', + summary: '删除标签', + tags: ['article-tag'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'article:tag:delete', description: '删除标签')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/article/tag', + summary: '批量删除标签', + tags: ['article-tag'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'article:tag:delete', description: '批量删除标签')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/article/app/api/controller/.gitkeep b/backend/plugin/article/app/api/controller/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/dao/.gitkeep b/backend/plugin/article/app/dao/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/dao/CategoryDao.php b/backend/plugin/article/app/dao/CategoryDao.php new file mode 100644 index 0000000..e48fa4c --- /dev/null +++ b/backend/plugin/article/app/dao/CategoryDao.php @@ -0,0 +1,40 @@ +withCount(['contents as article_count']); + } + + return $query; + } +} diff --git a/backend/plugin/article/app/dao/ContentDao.php b/backend/plugin/article/app/dao/ContentDao.php new file mode 100644 index 0000000..5c35f4c --- /dev/null +++ b/backend/plugin/article/app/dao/ContentDao.php @@ -0,0 +1,26 @@ +withCount(['contents as article_count']); + } + + return $query; + } +} diff --git a/backend/plugin/article/app/model/.gitkeep b/backend/plugin/article/app/model/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/model/BaseModel.php b/backend/plugin/article/app/model/BaseModel.php new file mode 100644 index 0000000..aff93b4 --- /dev/null +++ b/backend/plugin/article/app/model/BaseModel.php @@ -0,0 +1,37 @@ +hasOne(Admin::class, 'id', 'created_by')->select('id', 'real_name as created_name'); + } + + public function updateds(): HasOne + { + return $this->hasOne(Admin::class, 'id', 'updated_by')->select('id', 'real_name as updated_name'); + } + + public function getCreatedNameAttribute(): string + { + return $this->createds?->created_name ?? ''; + } + + public function getUpdatedNameAttribute(): string + { + return $this->updateds?->updated_name ?? ''; + } +} diff --git a/backend/plugin/article/app/model/Category.php b/backend/plugin/article/app/model/Category.php new file mode 100644 index 0000000..f5582a6 --- /dev/null +++ b/backend/plugin/article/app/model/Category.php @@ -0,0 +1,41 @@ + 'integer', + 'sort' => 'integer', + 'enabled' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function contents(): HasMany + { + return $this->hasMany(Content::class, 'category_id', 'id'); + } +} diff --git a/backend/plugin/article/app/model/Content.php b/backend/plugin/article/app/model/Content.php new file mode 100644 index 0000000..2b383fe --- /dev/null +++ b/backend/plugin/article/app/model/Content.php @@ -0,0 +1,93 @@ + 'integer', + 'visibility' => 'integer', + 'status' => 'integer', + 'is_top' => 'integer', + 'is_recommend' => 'integer', + 'allow_comment' => 'integer', + 'sort' => 'integer', + 'view_count' => 'integer', + 'like_count' => 'integer', + 'comment_count' => 'integer', + 'favorite_count' => 'integer', + 'published_at' => 'integer', + 'offline_at' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + protected $appends = ['tag_names', 'tag_ids', 'category_name', 'created_date', 'updated_date', 'created_name', 'updated_name']; + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class, 'category_id', 'id'); + } + + public function tags(): BelongsToMany + { + return $this->belongsToMany(Tag::class, 'article_content_tag', 'content_id', 'tag_id'); + } + + public function getCategoryNameAttribute(): string + { + return $this->category?->name ?? ''; + } + + public function getTagNamesAttribute(): string + { + return $this->tags->pluck('name')->implode('、'); + } + + public function getTagIdsAttribute(): array + { + return $this->tags->pluck('id')->map(static fn($id) => is_numeric($id) ? (int)$id : $id)->values()->all(); + } +} diff --git a/backend/plugin/article/app/model/Tag.php b/backend/plugin/article/app/model/Tag.php new file mode 100644 index 0000000..d9e3301 --- /dev/null +++ b/backend/plugin/article/app/model/Tag.php @@ -0,0 +1,41 @@ + 'integer', + 'heat' => 'integer', + 'enabled' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function contents(): BelongsToMany + { + return $this->belongsToMany(Content::class, 'article_content_tag', 'tag_id', 'content_id'); + } +} diff --git a/backend/plugin/article/app/schema/request/.gitkeep b/backend/plugin/article/app/schema/request/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/schema/response/.gitkeep b/backend/plugin/article/app/schema/response/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/service/admin/.gitkeep b/backend/plugin/article/app/service/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/service/admin/CategoryService.php b/backend/plugin/article/app/service/admin/CategoryService.php new file mode 100644 index 0000000..ee3eb11 --- /dev/null +++ b/backend/plugin/article/app/service/admin/CategoryService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/article/app/service/admin/ContentService.php b/backend/plugin/article/app/service/admin/ContentService.php new file mode 100644 index 0000000..9ec9049 --- /dev/null +++ b/backend/plugin/article/app/service/admin/ContentService.php @@ -0,0 +1,129 @@ +dao = $dao; + } + + public function save(array $data): ?\plugin\article\app\model\Content + { + try { + return $this->transaction(function () use ($data) { + $payload = $this->normalizePayload($data); + $tagIds = $payload['tag_ids'] ?? []; + unset($payload['tag_ids']); + + $model = new Content(); + PropertyCopier::copyProperties((object) $payload, $model); + $model->save(); + + $this->syncTags($model, $tagIds); + + return $model->refresh()?->load(['category', 'tags', 'createds', 'updateds']); + }); + } catch (\Throwable $e) { + throw new AdminException($e->getMessage()); + } + } + + public function update($id, $data): void + { + try { + $this->transaction(function () use ($id, $data) { + $model = $this->dao->get($id); + if (!$model) { + throw new AdminException('内容不存在'); + } + + $payload = $this->normalizePayload($data, false); + $tagIds = $payload['tag_ids'] ?? null; + unset($payload['tag_ids']); + + PropertyCopier::copyProperties((object) $payload, $model); + $model->save(); + + if ($tagIds !== null) { + $this->syncTags($model, $tagIds); + } + }); + } catch (\Throwable $e) { + throw new AdminException($e->getMessage()); + } + } + + protected function normalizePayload(array $data, bool $isCreate = true): array + { + $payload = $data; + + $payload['content_type'] = $payload['content_type'] ?? 'article'; + $payload['channel'] = $payload['channel'] ?? 'official'; + $payload['visibility'] = isset($payload['visibility']) ? (int) $payload['visibility'] : 1; + $payload['source'] = $payload['source'] ?? 'original'; + $payload['status'] = isset($payload['status']) ? (int) $payload['status'] : 0; + $payload['is_top'] = isset($payload['is_top']) ? (int) $payload['is_top'] : 0; + $payload['is_recommend'] = isset($payload['is_recommend']) ? (int) $payload['is_recommend'] : 0; + $payload['allow_comment'] = isset($payload['allow_comment']) ? (int) $payload['allow_comment'] : 1; + $payload['sort'] = isset($payload['sort']) ? (int) $payload['sort'] : 100; + $payload['view_count'] = isset($payload['view_count']) ? (int) $payload['view_count'] : 0; + $payload['like_count'] = isset($payload['like_count']) ? (int) $payload['like_count'] : 0; + $payload['comment_count'] = isset($payload['comment_count']) ? (int) $payload['comment_count'] : 0; + $payload['favorite_count'] = isset($payload['favorite_count']) ? (int) $payload['favorite_count'] : 0; + $payload['category_id'] = isset($payload['category_id']) ? (int) $payload['category_id'] : 0; + + if (!empty($payload['slug'])) { + $payload['slug'] = trim((string) $payload['slug']); + } elseif ($isCreate && !empty($payload['title'])) { + $payload['slug'] = Str::slug((string) $payload['title']); + } + + if (array_key_exists('tag_ids', $payload)) { + $payload['tag_ids'] = collect($payload['tag_ids'] ?? []) + ->filter(static fn($id) => $id !== null && $id !== '' && $id !== 0 && $id !== '0') + ->map(static fn($id) => (int) $id) + ->unique() + ->values() + ->all(); + } + + return $payload; + } + + protected function syncTags(Content $model, array $tagIds): void + { + $tableName = $model->tags()->getTable(); + + Db::table($tableName)->where('content_id', $model->id)->delete(); + + if (empty($tagIds)) { + return; + } + + $now = time(); + $rows = []; + foreach ($tagIds as $tagId) { + $rows[] = [ + 'id' => (string) Snowflake::generate(), + 'content_id' => $model->id, + 'tag_id' => (int) $tagId, + 'created_at' => $now, + ]; + } + + Db::table($tableName)->insert($rows); + } +} diff --git a/backend/plugin/article/app/service/admin/TagService.php b/backend/plugin/article/app/service/admin/TagService.php new file mode 100644 index 0000000..979476f --- /dev/null +++ b/backend/plugin/article/app/service/admin/TagService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/article/app/service/api/.gitkeep b/backend/plugin/article/app/service/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/validate/.gitkeep b/backend/plugin/article/app/validate/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/app/validate/CategoryValidate.php b/backend/plugin/article/app/validate/CategoryValidate.php new file mode 100644 index 0000000..1067b26 --- /dev/null +++ b/backend/plugin/article/app/validate/CategoryValidate.php @@ -0,0 +1,45 @@ +route?->param('id'); + + return [ + 'name' => ['required', 'max:100', Rule::unique(Category::class, 'name')->ignore($id, 'id')], + 'slug' => ['nullable', 'max:100', Rule::unique(Category::class, 'slug')->ignore($id, 'id')], + 'parent_id' => ['nullable', 'integer'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'enabled' => ['nullable', 'integer', 'in:0,1'], + 'description' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'name.required' => '分类名称不能为空', + 'name.max' => '分类名称不能超过100个字符', + 'name.unique' => '分类名称已存在', + 'slug.max' => '分类标识不能超过100个字符', + 'slug.unique' => '分类标识已存在', + 'parent_id.integer' => '父级分类格式不正确', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'enabled.integer' => '状态格式不正确', + 'enabled.in' => '状态值不正确', + 'description.max' => '分类描述内容过长', + ]; + + protected array $scene = [ + 'store' => ['name', 'slug', 'parent_id', 'sort', 'enabled', 'description'], + 'update' => ['name', 'slug', 'parent_id', 'sort', 'enabled', 'description'], + ]; +} diff --git a/backend/plugin/article/app/validate/ContentValidate.php b/backend/plugin/article/app/validate/ContentValidate.php new file mode 100644 index 0000000..0efdaf3 --- /dev/null +++ b/backend/plugin/article/app/validate/ContentValidate.php @@ -0,0 +1,93 @@ +route?->param('id'); + + return [ + 'title' => ['required', 'max:200', Rule::unique(Content::class, 'title')->ignore($id, 'id')], + 'content_type' => ['nullable', 'max:50'], + 'slug' => ['nullable', 'max:200', Rule::unique(Content::class, 'slug')->ignore($id, 'id')], + 'category_id' => ['required', 'integer'], + 'channel' => ['nullable', 'max:100'], + 'visibility' => ['nullable', 'integer', 'in:1,2,3'], + 'author' => ['nullable', 'max:100'], + 'source' => ['nullable', 'max:100'], + 'cover' => ['nullable', 'max:255'], + 'seo_keywords' => ['nullable', 'max:255'], + 'seo_title' => ['nullable', 'max:200'], + 'seo_description' => ['nullable', 'max:500'], + 'summary' => ['nullable', 'max:500'], + 'content' => ['nullable'], + 'status' => ['nullable', 'integer', 'in:0,1,2'], + 'is_top' => ['nullable', 'integer', 'in:0,1'], + 'is_recommend' => ['nullable', 'integer', 'in:0,1'], + 'allow_comment' => ['nullable', 'integer', 'in:0,1'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'view_count' => ['nullable', 'integer', 'min:0'], + 'like_count' => ['nullable', 'integer', 'min:0'], + 'comment_count' => ['nullable', 'integer', 'min:0'], + 'favorite_count' => ['nullable', 'integer', 'min:0'], + 'published_at' => ['nullable', 'integer'], + 'offline_at' => ['nullable', 'integer'], + 'tag_ids' => ['nullable', 'array'], + ]; + } + + protected array $message = [ + 'title.required' => '文章标题不能为空', + 'title.max' => '文章标题不能超过200个字符', + 'title.unique' => '文章标题已存在', + 'content_type.max' => '内容类型不能超过50个字符', + 'slug.max' => '文章标识不能超过200个字符', + 'slug.unique' => '文章标识已存在', + 'category_id.required' => '请选择内容分类', + 'category_id.integer' => '内容分类格式不正确', + 'channel.max' => '所属频道不能超过100个字符', + 'visibility.integer' => '可见范围格式不正确', + 'visibility.in' => '可见范围值不正确', + 'author.max' => '作者不能超过100个字符', + 'source.max' => '内容来源不能超过100个字符', + 'cover.max' => '封面地址不能超过255个字符', + 'seo_keywords.max' => 'SEO关键词不能超过255个字符', + 'seo_title.max' => 'SEO标题不能超过200个字符', + 'seo_description.max' => 'SEO描述不能超过500个字符', + 'summary.max' => '摘要不能超过500个字符', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + 'is_top.integer' => '置顶状态格式不正确', + 'is_top.in' => '置顶状态值不正确', + 'is_recommend.integer' => '推荐状态格式不正确', + 'is_recommend.in' => '推荐状态值不正确', + 'allow_comment.integer' => '评论设置格式不正确', + 'allow_comment.in' => '评论设置值不正确', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'view_count.integer' => '浏览量必须为整数', + 'view_count.min' => '浏览量不能小于0', + 'like_count.integer' => '点赞量必须为整数', + 'like_count.min' => '点赞量不能小于0', + 'comment_count.integer' => '评论量必须为整数', + 'comment_count.min' => '评论量不能小于0', + 'favorite_count.integer' => '收藏量必须为整数', + 'favorite_count.min' => '收藏量不能小于0', + 'published_at.integer' => '发布时间格式不正确', + 'offline_at.integer' => '下线时间格式不正确', + 'tag_ids.array' => '标签数据格式不正确', + ]; + + protected array $scene = [ + 'store' => ['title', 'content_type', 'slug', 'category_id', 'channel', 'visibility', 'author', 'source', 'cover', 'seo_keywords', 'seo_title', 'seo_description', 'summary', 'content', 'status', 'is_top', 'is_recommend', 'allow_comment', 'sort', 'view_count', 'like_count', 'comment_count', 'favorite_count', 'published_at', 'offline_at', 'tag_ids'], + 'update' => ['title', 'content_type', 'slug', 'category_id', 'channel', 'visibility', 'author', 'source', 'cover', 'seo_keywords', 'seo_title', 'seo_description', 'summary', 'content', 'status', 'is_top', 'is_recommend', 'allow_comment', 'sort', 'view_count', 'like_count', 'comment_count', 'favorite_count', 'published_at', 'offline_at', 'tag_ids'], + ]; +} diff --git a/backend/plugin/article/app/validate/TagValidate.php b/backend/plugin/article/app/validate/TagValidate.php new file mode 100644 index 0000000..5fcc70a --- /dev/null +++ b/backend/plugin/article/app/validate/TagValidate.php @@ -0,0 +1,46 @@ +route?->param('id'); + + return [ + 'name' => ['required', 'max:100', Rule::unique(Tag::class, 'name')->ignore($id, 'id')], + 'slug' => ['nullable', 'max:100', Rule::unique(Tag::class, 'slug')->ignore($id, 'id')], + 'sort' => ['nullable', 'integer', 'min:0'], + 'heat' => ['nullable', 'integer', 'min:0'], + 'enabled' => ['nullable', 'integer', 'in:0,1'], + 'description' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'name.required' => '标签名称不能为空', + 'name.max' => '标签名称不能超过100个字符', + 'name.unique' => '标签名称已存在', + 'slug.max' => '标签标识不能超过100个字符', + 'slug.unique' => '标签标识已存在', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'heat.integer' => '热度必须为整数', + 'heat.min' => '热度不能小于0', + 'enabled.integer' => '状态格式不正确', + 'enabled.in' => '状态值不正确', + 'description.max' => '标签描述内容过长', + ]; + + protected array $scene = [ + 'store' => ['name', 'slug', 'sort', 'heat', 'enabled', 'description'], + 'update' => ['name', 'slug', 'sort', 'heat', 'enabled', 'description'], + ]; +} diff --git a/backend/plugin/article/config/app.php b/backend/plugin/article/config/app.php new file mode 100644 index 0000000..a2a027c --- /dev/null +++ b/backend/plugin/article/config/app.php @@ -0,0 +1,9 @@ + true, + 'debug' => true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '1.0.0', +]; diff --git a/backend/plugin/article/config/info.php b/backend/plugin/article/config/info.php new file mode 100644 index 0000000..7f3cf83 --- /dev/null +++ b/backend/plugin/article/config/info.php @@ -0,0 +1,20 @@ + 'article', + 'identifier' => 'article', + 'type' => 'madong:plugin', + 'version' => '1.0.0', + 'description' => '', + 'author' => 'mrlee', + 'author_email' => '', + 'website' => '', + 'uninstall' => [ + 'drop_tables' => false, + 'remove_dependencies' => false, + ], + ]; diff --git a/backend/plugin/article/config/installed.php b/backend/plugin/article/config/installed.php new file mode 100644 index 0000000..306dc4c --- /dev/null +++ b/backend/plugin/article/config/installed.php @@ -0,0 +1,4 @@ + '1.0.0', + 'installed_at' => '2026-07-26 13:45:43', +); \ No newline at end of file diff --git a/backend/plugin/article/config/route.php b/backend/plugin/article/config/route.php new file mode 100644 index 0000000..375431f --- /dev/null +++ b/backend/plugin/article/config/route.php @@ -0,0 +1,87 @@ +registerRoute([ + 'route_prefix' => '/article/openapi', + 'register_route' => true, + 'openapi_doc' => [ + 'scan_path' => [ + base_path('plugin/article/app/api'), + ], + 'modify' => function (OA\OpenApi $openapi) { + $openapi->info->title = '文章管理 API'; + $openapi->info->version = '1.0.0'; + $openapi->servers = [ + new OA\Server( + [ + 'url' => '/api', + 'description' => request()->host(), + ] + ), + ]; + /** @phpstan-ignore-next-line */ + if (!$openapi->components instanceof OA\Components) { + $openapi->components = new OA\Components([]); + } + $openapi->components->securitySchemes = [ + new OA\SecurityScheme([ + 'securityScheme' => 'api_key', + 'type' => 'apiKey', + 'name' => config('core.jwt.app.token_name', 'Authorization'), + 'in' => 'header', + ]), + ]; + }, + ], + ]); +}); + +/** + * 注册 AdminAPI 路由 + */ +Route::group('/adminapi', function () { + Swagger::create()->registerRoute([ + 'route_prefix' => '/article/openapi', + 'register_route' => true, + 'openapi_doc' => [ + 'scan_path' => [ + base_path('plugin/article/app/adminapi'), + ], + 'modify' => function (OA\OpenApi $openapi) { + $openapi->info->title = '文章管理 API'; + $openapi->info->version = '1.0.0'; + $openapi->servers = [ + new OA\Server( + [ + 'url' => '/adminapi', + 'description' => request()->host(), + ] + ), + ]; + /** @phpstan-ignore-next-line */ + if (!$openapi->components instanceof OA\Components) { + $openapi->components = new OA\Components([]); + } + $openapi->components->securitySchemes = [ + new OA\SecurityScheme([ + 'securityScheme' => 'api_key', + 'type' => 'apiKey', + 'name' => config('core.jwt.app.token_name', 'Authorization'), + 'in' => 'header', + ]), + ]; + }, + ], + ]); +}); diff --git a/backend/plugin/article/config/translation.php b/backend/plugin/article/config/translation.php new file mode 100644 index 0000000..50507b7 --- /dev/null +++ b/backend/plugin/article/config/translation.php @@ -0,0 +1,7 @@ + config('app.lang'), + 'fallback_locale' => ['zh_CN', 'en'], + 'path' => base_path() . '/plugin/article/resource/translations', +]; diff --git a/backend/plugin/article/public/cover.png b/backend/plugin/article/public/cover.png new file mode 100644 index 0000000..566f7d2 Binary files /dev/null and b/backend/plugin/article/public/cover.png differ diff --git a/backend/plugin/article/public/icon.png b/backend/plugin/article/public/icon.png new file mode 100644 index 0000000..786fbd3 Binary files /dev/null and b/backend/plugin/article/public/icon.png differ diff --git a/backend/plugin/article/resource/database/migrations/.gitkeep b/backend/plugin/article/resource/database/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/resource/database/migrations/2026_07_25_000001_create_article_tables.php b/backend/plugin/article/resource/database/migrations/2026_07_25_000001_create_article_tables.php new file mode 100644 index 0000000..422ab49 --- /dev/null +++ b/backend/plugin/article/resource/database/migrations/2026_07_25_000001_create_article_tables.php @@ -0,0 +1,94 @@ +hasTable('article_category')) { + $schema->create('article_category', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键'); + $table->string('name', 100)->comment('分类名称'); + $table->string('slug', 100)->nullable()->comment('分类标识'); + $table->bigInteger('parent_id')->default(0)->comment('父级分类ID'); + $table->integer('sort')->default(100)->comment('排序'); + $table->tinyInteger('enabled')->default(1)->comment('状态: 1启用 0禁用'); + $table->longText('description')->nullable()->comment('分类描述'); + $table->bigInteger('created_by')->nullable()->comment('创建者'); + $table->bigInteger('updated_by')->nullable()->comment('更新者'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->index('parent_id'); + $table->index('slug'); + $table->index('enabled'); + }); + } + + if (!$schema->hasTable('article_tag')) { + $schema->create('article_tag', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键'); + $table->string('name', 100)->comment('标签名称'); + $table->string('slug', 100)->nullable()->comment('标签标识'); + $table->integer('sort')->default(100)->comment('排序'); + $table->integer('heat')->default(0)->comment('热度'); + $table->tinyInteger('enabled')->default(1)->comment('状态: 1启用 0禁用'); + $table->longText('description')->nullable()->comment('标签描述'); + $table->bigInteger('created_by')->nullable()->comment('创建者'); + $table->bigInteger('updated_by')->nullable()->comment('更新者'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->index('slug'); + $table->index('enabled'); + $table->index('heat'); + }); + } + + if (!$schema->hasTable('article_content')) { + $schema->create('article_content', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键'); + $table->string('title', 200)->comment('文章标题'); + $table->string('slug', 200)->nullable()->comment('文章标识'); + $table->bigInteger('category_id')->default(0)->comment('分类ID'); + $table->string('cover', 255)->nullable()->comment('封面图'); + $table->string('summary', 500)->nullable()->comment('摘要'); + $table->longText('content')->nullable()->comment('文章内容'); + $table->tinyInteger('status')->default(1)->comment('状态: 0草稿 1发布 2下架'); + $table->tinyInteger('is_top')->default(0)->comment('是否置顶'); + $table->integer('sort')->default(100)->comment('排序'); + $table->integer('view_count')->default(0)->comment('浏览量'); + $table->bigInteger('published_at')->nullable()->comment('发布时间'); + $table->bigInteger('created_by')->nullable()->comment('创建者'); + $table->bigInteger('updated_by')->nullable()->comment('更新者'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->index('category_id'); + $table->index('status'); + $table->index('is_top'); + $table->index('published_at'); + }); + } + + if (!$schema->hasTable('article_content_tag')) { + $schema->create('article_content_tag', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键'); + $table->bigInteger('content_id')->comment('文章ID'); + $table->bigInteger('tag_id')->comment('标签ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->index('content_id'); + $table->index('tag_id'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('article_content_tag'); + $schema->dropIfExists('article_content'); + $schema->dropIfExists('article_tag'); + $schema->dropIfExists('article_category'); + } +}; diff --git a/backend/plugin/article/resource/database/migrations/2026_07_26_000002_alter_article_content_add_publish_fields.php b/backend/plugin/article/resource/database/migrations/2026_07_26_000002_alter_article_content_add_publish_fields.php new file mode 100644 index 0000000..9f28ee8 --- /dev/null +++ b/backend/plugin/article/resource/database/migrations/2026_07_26_000002_alter_article_content_add_publish_fields.php @@ -0,0 +1,93 @@ +hasTable('article_content')) { + return; + } + + $schema->table('article_content', function (Blueprint $table) use ($schema) { + if (!$schema->hasColumn('article_content', 'content_type')) { + $table->string('content_type', 50)->default('article')->comment('内容类型')->after('title'); + } + if (!$schema->hasColumn('article_content', 'channel')) { + $table->string('channel', 100)->default('official')->comment('所属频道')->after('category_id'); + } + if (!$schema->hasColumn('article_content', 'visibility')) { + $table->tinyInteger('visibility')->default(1)->comment('可见范围: 1公开 2登录可见 3私密')->after('channel'); + } + if (!$schema->hasColumn('article_content', 'author')) { + $table->string('author', 100)->nullable()->comment('作者')->after('visibility'); + } + if (!$schema->hasColumn('article_content', 'source')) { + $table->string('source', 100)->default('original')->comment('内容来源')->after('author'); + } + if (!$schema->hasColumn('article_content', 'offline_at')) { + $table->bigInteger('offline_at')->nullable()->comment('下线时间')->after('published_at'); + } + if (!$schema->hasColumn('article_content', 'seo_keywords')) { + $table->string('seo_keywords', 255)->nullable()->comment('SEO关键词')->after('source'); + } + if (!$schema->hasColumn('article_content', 'seo_title')) { + $table->string('seo_title', 200)->nullable()->comment('SEO标题')->after('seo_keywords'); + } + if (!$schema->hasColumn('article_content', 'seo_description')) { + $table->string('seo_description', 500)->nullable()->comment('SEO描述')->after('seo_title'); + } + if (!$schema->hasColumn('article_content', 'like_count')) { + $table->integer('like_count')->default(0)->comment('点赞量')->after('view_count'); + } + if (!$schema->hasColumn('article_content', 'comment_count')) { + $table->integer('comment_count')->default(0)->comment('评论量')->after('like_count'); + } + if (!$schema->hasColumn('article_content', 'favorite_count')) { + $table->integer('favorite_count')->default(0)->comment('收藏量')->after('comment_count'); + } + if (!$schema->hasColumn('article_content', 'is_recommend')) { + $table->tinyInteger('is_recommend')->default(0)->comment('是否推荐')->after('is_top'); + } + if (!$schema->hasColumn('article_content', 'allow_comment')) { + $table->tinyInteger('allow_comment')->default(1)->comment('是否允许评论')->after('is_recommend'); + } + }); + } + + public function down(Builder $schema): void + { + if (!$schema->hasTable('article_content')) { + return; + } + + $dropColumns = []; + foreach ([ + 'content_type', + 'channel', + 'visibility', + 'author', + 'source', + 'offline_at', + 'seo_keywords', + 'seo_title', + 'seo_description', + 'like_count', + 'comment_count', + 'favorite_count', + 'is_recommend', + 'allow_comment', + ] as $column) { + if ($schema->hasColumn('article_content', $column)) { + $dropColumns[] = $column; + } + } + + if (!empty($dropColumns)) { + $schema->table('article_content', function (Blueprint $table) use ($dropColumns) { + $table->dropColumn($dropColumns); + }); + } + } +}; diff --git a/backend/plugin/article/resource/database/seeds/.gitkeep b/backend/plugin/article/resource/database/seeds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/article/resource/menu/admin.php b/backend/plugin/article/resource/menu/admin.php new file mode 100644 index 0000000..237bee6 --- /dev/null +++ b/backend/plugin/article/resource/menu/admin.php @@ -0,0 +1,46 @@ + '文章管理', + 'code' => 'article', + 'app' => 'article', + 'path' => '/article', + 'icon' => 'plugin', + 'children' => [ + [ + 'name' => '内容中心', + 'code' => 'article:index', + 'app' => 'article', + 'path' => '/article/index', + 'component' => '/article/content/index', + ], + [ + 'name' => '内容分类', + 'code' => 'article:category:index', + 'app' => 'article', + 'path' => '/article/category', + 'component' => '/article/category/index', + ], + [ + 'name' => '标签管理', + 'code' => 'article:tag:index', + 'app' => 'article', + 'path' => '/article/tag', + 'component' => '/article/tag/index', + ], + [ + 'name' => '内容回收站', + 'code' => 'article:recycle:index', + 'app' => 'article', + 'path' => '/article/recycle', + 'component' => '/article/recycle/index', + ], + ], + ], +]; diff --git a/backend/plugin/article/resource/menu/frontend.php b/backend/plugin/article/resource/menu/frontend.php new file mode 100644 index 0000000..917fd5d --- /dev/null +++ b/backend/plugin/article/resource/menu/frontend.php @@ -0,0 +1,9 @@ + + tag_names?: string + created_date?: string + updated_date?: string +} + +export const ArticleContentService = { + ...BaseService({ baseUrl: '/article/index' }) +} + +export const ArticleCategoryService = { + ...BaseService({ baseUrl: '/article/category' }) +} + +export const ArticleTagService = { + ...BaseService({ baseUrl: '/article/tag' }) +} + +const pluginApi = { + async test(params = {}) { + return request.get<{ msg?: string; data?: unknown }>({ + url: '/article/index', + params + }) + }, + + contentList(params = {}) { + return ArticleContentService.list(params as Record) + }, + + categoryList(params = {}) { + return ArticleCategoryService.list(params as Record) + }, + + tagList(params = {}) { + return ArticleTagService.list(params as Record) + }, + + async recycleList(params = {}) { + return request.get<{ code: number; msg: string; data: Record[] }>({ + url: '/article/recycle', + params + }) + }, + + async recycleRestore(id: string | number) { + return request.put({ + url: `/article/recycle/${id}/restore`, + params: { id } + }) + }, + + async recycleDelete(id: string | number) { + return request.del({ + url: `/article/recycle/${id}`, + params: { id } + }) + } +} + +export default pluginApi diff --git a/backend/plugin/article/resource/template/admin/lang/en/en.json b/backend/plugin/article/resource/template/admin/lang/en/en.json new file mode 100644 index 0000000..1ab8908 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/lang/en/en.json @@ -0,0 +1,136 @@ +{ + "article": { + "title": "Article Management", + "description": "Article content operation and management plugin", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh", + "common": { + "list_mode": "List Mode", + "columns": { + "index": "No." + } + }, + "modules": { + "content": { + "title": "Content Center", + "description": "Manage articles, publish states, and content archives.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter article title, category, or tags" + }, + "form": { + "title": "Content Title", + "title_placeholder": "Please enter article title", + "slug": "Content Slug", + "slug_placeholder": "Please enter article slug", + "content_type": "Content Type", + "category_id": "Category", + "category_id_placeholder": "Please select category", + "channel": "Channel", + "visibility": "Visibility", + "author": "Author", + "author_placeholder": "Please enter author", + "source": "Source", + "cover": "Cover", + "cover_placeholder": "Please enter cover URL", + "summary": "Summary", + "summary_placeholder": "Please enter summary", + "content": "Content", + "content_placeholder": "Please enter content", + "status": "Status", + "is_top": "Pinned", + "is_recommend": "Recommend", + "allow_comment": "Comment Setting", + "sort": "Sort Order", + "sort_placeholder": "Please enter sort order", + "view_count": "View Count", + "like_count": "Like Count", + "comment_count": "Comment Count", + "favorite_count": "Favorite Count", + "published_at": "Published At", + "offline_at": "Offline At", + "tag_ids": "Content Tags", + "tag_ids_placeholder": "Please select content tags", + "seo_keywords": "SEO Keywords", + "seo_keywords_placeholder": "For search engine optimization", + "seo_title": "SEO Title", + "seo_title_placeholder": "Optional, defaults to content title", + "seo_description": "SEO Description", + "seo_description_placeholder": "Optional, highlight value and search keywords" + }, + "columns": { + "title": "Article Title", + "content_info": "Content Info", + "category": "Category", + "tags": "Tags", + "status": "Status", + "business_data": "Business Data", + "time_info": "Time", + "is_top": "Pinned", + "updated_at": "Last Updated" + } + }, + "category": { + "title": "Content Category", + "description": "Manage article category structure and ordering.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter category name or slug" + }, + "form": { + "name_placeholder": "Please enter category name", + "slug_placeholder": "Please enter category slug", + "parent_id": "Parent Category", + "parent_top": "Top Category", + "description": "Category Description", + "description_placeholder": "Please enter category description" + }, + "columns": { + "name": "Category Name", + "slug": "Category Slug", + "article_count": "Content Count", + "enabled": "Status", + "sort": "Sort", + "updated_at": "Last Updated" + } + }, + "tag": { + "title": "Tag Management", + "description": "Maintain article tags, popularity, and related article counts.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter tag name or slug" + }, + "form": { + "name_placeholder": "Please enter tag name", + "slug_placeholder": "Please enter tag slug", + "description": "Tag Description", + "description_placeholder": "Please enter tag description" + }, + "columns": { + "name": "Tag Name", + "slug": "Tag Slug", + "article_count": "Usage Count", + "enabled": "Status", + "heat": "Heat", + "updated_at": "Last Updated" + } + }, + "recycle": { + "title": "Content Recycle Bin", + "description": "Manage deleted articles for later recovery or cleanup.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter article title or deleter" + }, + "columns": { + "title": "Article Title", + "category": "Original Category", + "deleted_by": "Deleted By", + "deleted_at": "Deleted At", + "expire_at": "Expire At" + } + } + } + } +} diff --git a/backend/plugin/article/resource/template/admin/lang/en/index.json b/backend/plugin/article/resource/template/admin/lang/en/index.json new file mode 100644 index 0000000..a11b9a1 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/lang/en/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "Article Management", + "description": "Article plugin demo page", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh" + } +} \ No newline at end of file diff --git a/backend/plugin/article/resource/template/admin/lang/zh-cn/index.json b/backend/plugin/article/resource/template/admin/lang/zh-cn/index.json new file mode 100644 index 0000000..582d445 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/lang/zh-cn/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "文章管理", + "description": "文章插件示例页面", + "card_title": "文章插件数据", + "refresh_btn": "刷新" + } +} \ No newline at end of file diff --git a/backend/plugin/article/resource/template/admin/lang/zh-cn/zh.json b/backend/plugin/article/resource/template/admin/lang/zh-cn/zh.json new file mode 100644 index 0000000..1fcdb6e --- /dev/null +++ b/backend/plugin/article/resource/template/admin/lang/zh-cn/zh.json @@ -0,0 +1,136 @@ +{ + "article": { + "title": "文章管理", + "description": "文章内容运营与管理插件", + "card_title": "文章插件数据", + "refresh_btn": "刷新", + "common": { + "list_mode": "列表模式", + "columns": { + "index": "序号" + } + }, + "modules": { + "content": { + "title": "内容中心", + "description": "管理文章内容、发布状态与内容归档。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题、分类或标签" + }, + "form": { + "title": "内容标题", + "title_placeholder": "请输入文章标题", + "slug": "内容别名", + "slug_placeholder": "请输入文章标识", + "content_type": "内容类型", + "category_id": "内容分类", + "category_id_placeholder": "请选择内容分类", + "channel": "所属频道", + "visibility": "可见范围", + "author": "作者", + "author_placeholder": "请输入作者", + "source": "内容来源", + "cover": "封面图", + "cover_placeholder": "请输入封面图地址", + "summary": "摘要", + "summary_placeholder": "请输入摘要", + "content": "文章内容", + "content_placeholder": "请输入文章内容", + "status": "状态", + "is_top": "是否置顶", + "is_recommend": "推荐", + "allow_comment": "评论设置", + "sort": "内容排序", + "sort_placeholder": "请输入排序", + "view_count": "浏览量", + "like_count": "点赞量", + "comment_count": "评论量", + "favorite_count": "收藏量", + "published_at": "发布时间", + "offline_at": "下线时间", + "tag_ids": "内容标签", + "tag_ids_placeholder": "请选择内容标签", + "seo_keywords": "SEO 关键词", + "seo_keywords_placeholder": "用于搜索引擎优化", + "seo_title": "SEO 标题", + "seo_title_placeholder": "可选,默认继承内容标题", + "seo_description": "SEO 描述", + "seo_description_placeholder": "可选,建议突出商业价值与检索关键词" + }, + "columns": { + "title": "文章标题", + "content_info": "内容信息", + "category": "内容分类", + "tags": "标签", + "status": "状态", + "business_data": "经营数据", + "time_info": "时间", + "is_top": "置顶", + "updated_at": "最后更新" + } + }, + "category": { + "title": "内容分类", + "description": "管理文章分类结构与分类排序。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入分类名称或分类标识" + }, + "form": { + "name_placeholder": "请输入分类名称", + "slug_placeholder": "请输入分类标识", + "parent_id": "父级分类", + "parent_top": "顶级分类", + "description": "分类描述", + "description_placeholder": "请输入分类描述" + }, + "columns": { + "name": "分类名称", + "slug": "分类标识", + "article_count": "内容数", + "enabled": "状态", + "sort": "排序", + "updated_at": "最后更新" + } + }, + "tag": { + "title": "标签管理", + "description": "维护文章标签、热度与关联内容数量。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入标签名称或标签标识" + }, + "form": { + "name_placeholder": "请输入标签名称", + "slug_placeholder": "请输入标签标识", + "description": "标签描述", + "description_placeholder": "请输入标签描述" + }, + "columns": { + "name": "标签名称", + "slug": "标签标识", + "article_count": "使用次数", + "enabled": "状态", + "heat": "热度", + "updated_at": "最后更新" + } + }, + "recycle": { + "title": "内容回收站", + "description": "管理已删除文章,支持后续恢复或清理。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题或删除人" + }, + "columns": { + "title": "文章标题", + "category": "原分类", + "deleted_by": "删除人", + "deleted_at": "删除时间", + "expire_at": "过期清理时间" + } + } + } + } +} diff --git a/backend/plugin/article/resource/template/admin/routes/index.ts b/backend/plugin/article/resource/template/admin/routes/index.ts new file mode 100644 index 0000000..6d04b32 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/routes/index.ts @@ -0,0 +1,59 @@ +import { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '@/router/routesAlias' + +export default { + path: '/article', + name: 'Article', + component: RoutesAlias.Layout, + meta: { + title: '文章管理', + icon: 'ri:plugin-line', + module: 'article' + }, + children: [ + { + path: 'index', + name: 'ArticleIndex', + component: '/article/content/index', + meta: { + title: '内容中心', + icon: 'ri:article-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'category', + name: 'ArticleCategory', + component: '/article/category/index', + meta: { + title: '内容分类', + icon: 'ri:folder-2-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'tag', + name: 'ArticleTag', + component: '/article/tag/index', + meta: { + title: '标签管理', + icon: 'ri:price-tag-3-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'recycle', + name: 'ArticleRecycle', + component: '/article/recycle/index', + meta: { + title: '内容回收站', + icon: 'ri:delete-bin-6-line', + keepAlive: false, + module: 'article' + } + } + ] +} as AppRouteRecord diff --git a/backend/plugin/article/resource/template/admin/views/article/category/index.vue b/backend/plugin/article/resource/template/admin/views/article/category/index.vue new file mode 100644 index 0000000..d835801 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/category/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/article/resource/template/admin/views/article/category/schemas/index.tsx b/backend/plugin/article/resource/template/admin/views/article/category/schemas/index.tsx new file mode 100644 index 0000000..0d64db7 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/category/schemas/index.tsx @@ -0,0 +1,189 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleCategoryService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleCategoryService.list, + dialogTitle: $t('article.modules.category.title'), + crudApi: { + add: ArticleCategoryService.create, + edit: ArticleCategoryService.update, + remove: ArticleCategoryService.delete, + view: ArticleCategoryService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:category:create', + editAuth: 'article:category:update', + removeAuth: 'article:category:delete', + viewAuth: 'article:category:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.category.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.category.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.category.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + minWidth: 180, + }, + { + prop: 'article_count', + label: $t('article.modules.category.columns.article_count'), + width: 120, + }, + { + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.category.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.category.columns.name'), + componentProps: { + placeholder: $t('article.modules.category.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + componentProps: { + placeholder: $t('article.modules.category.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'ApiTreeSelect', + prop: 'parent_id', + label: $t('article.modules.category.form.parent_id'), + defaultValue: 0, + componentProps: { + api: '/article/category', + requestMethod: 'get', + params: { + format: 'tree', + }, + afterFetch: async (res: any) => { + if (!Array.isArray(res)) { + return res + } + return [ + { + id: 0, + name: $t('article.modules.category.form.parent_top'), + }, + ...res, + ] + }, + clearable: true, + checkStrictly: true, + class: 'w-full', + labelField: 'name', + valueField: 'id', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.category.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.category.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/article/resource/template/admin/views/article/content/index.vue b/backend/plugin/article/resource/template/admin/views/article/content/index.vue new file mode 100644 index 0000000..c375ed5 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/content/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/article/resource/template/admin/views/article/content/schemas/index.tsx b/backend/plugin/article/resource/template/admin/views/article/content/schemas/index.tsx new file mode 100644 index 0000000..fde1498 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/content/schemas/index.tsx @@ -0,0 +1,544 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleContentService } from '../../../../api/index' +import { h } from 'vue' + +const contentTypeOptions = [ + { label: '文章', value: 'article' }, + { label: '图文', value: 'news' }, + { label: '公告', value: 'notice' }, +] + +const channelOptions = [ + { label: '官网内容', value: 'official' }, + { label: '专题内容', value: 'topic' }, + { label: '活动内容', value: 'activity' }, +] + +const visibilityOptions = [ + { label: '公开', value: 1 }, + { label: '登录可见', value: 2 }, + { label: '私密', value: 3 }, +] + +const sourceOptions = [ + { label: '原创', value: 'original' }, + { label: '转载', value: 'repost' }, + { label: '投稿', value: 'contribution' }, +] + +const statusOptions = [ + { label: '草稿', value: 0 }, + { label: '发布', value: 1 }, + { label: '下线', value: 2 }, +] + +const getOptionLabel = (options: Array<{ label: string; value: string | number }>, value: string | number) => { + return options.find((item) => item.value === value)?.label || '-' +} + +const getStatusTagClass = (status: number) => { + if (status === 1) { + return 'inline-flex rounded-full bg-[#ecfdf5] px-2 py-[2px] text-xs text-[#10b981]' + } + if (status === 2) { + return 'inline-flex rounded-full bg-[#fef2f2] px-2 py-[2px] text-xs text-[#ef4444]' + } + return 'inline-flex rounded-full bg-[#f4f4f5] px-2 py-[2px] text-xs text-[#909399]' +} + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleContentService.list, + dialogTitle: $t('article.modules.content.title'), + exportFileName: '内容中心', + exportFields: [ + { field: 'title', title: '标题' }, + { field: 'content_type', title: '内容类型' }, + { field: 'category_name', title: '分类' }, + { field: 'tag_names', title: '标签' }, + { field: 'author', title: '作者' }, + { field: 'channel', title: '频道' }, + { field: 'visibility', title: '可见范围' }, + { field: 'source', title: '来源' }, + { field: 'status', title: '状态' }, + { field: 'is_top', title: '置顶' }, + { field: 'is_recommend', title: '推荐' }, + { field: 'view_count', title: '浏览数' }, + { field: 'like_count', title: '点赞数' }, + { field: 'comment_count', title: '评论数' }, + { field: 'favorite_count', title: '收藏数' }, + { field: 'published_at', title: '发布时间' }, + { field: 'updated_date', title: '更新时间' }, + { field: 'summary', title: '摘要' } + ], + crudApi: { + add: ArticleContentService.create, + edit: ArticleContentService.update, + remove: ArticleContentService.remove, + view: ArticleContentService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:create', + editAuth: 'article:update', + removeAuth: 'article:delete', + viewAuth: 'article:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.content.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.content.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'content_info', + label: $t('article.modules.content.columns.content_info'), + minWidth: 380, + formatter: (row: any) => { + const metaItems = [ + getOptionLabel(contentTypeOptions, row.content_type), + row.author || '管理员', + row.category_name, + row.tag_names, + ].filter(Boolean) + + const badgeNodes = [ + Number(row.is_top) === 1 + ? h('span', { class: 'inline-flex items-center rounded bg-[#fff7e6] px-1.5 py-[1px] text-[10px] text-[#fa8c16]' }, '置顶') + : null, + Number(row.is_recommend) === 1 + ? h('span', { class: 'inline-flex items-center rounded bg-[#ecfdf5] px-1.5 py-[1px] text-[10px] text-[#10b981]' }, '推荐') + : null, + ].filter(Boolean) + + return h('div', { class: 'flex gap-3 py-2' }, [ + row.cover + ? h('img', { + src: row.cover, + class: 'mt-1 h-14 w-20 flex-none rounded bg-[#f5f7fa] object-cover border border-[#f0f0f0]' + }) + : h( + 'div', + { + class: 'mt-1 h-14 w-20 flex-none rounded border border-[#f0f0f0] bg-[#fafafa] text-[10px] text-[#c0c4cc] flex items-center justify-center' + }, + '无上传封面' + ), + h('div', { class: 'min-w-0 flex-1 leading-6' }, [ + h('div', { class: 'flex items-center gap-2 text-sm font-medium text-[#1f2937]' }, [ + h('span', { class: 'line-clamp-1' }, row.title || '-'), + ...badgeNodes + ]), + h( + 'div', + { class: 'mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-[#909399]' }, + metaItems.length > 0 ? metaItems.map((item) => h('span', item)) : [h('span', '-')] + ), + h('div', { class: 'mt-1 line-clamp-2 text-xs text-[#606266]' }, row.summary || row.slug || '-') + ]) + ]) + } + }, + { + prop: 'status_info', + label: $t('article.modules.content.columns.status'), + width: 140, + align: 'center', + formatter: (row: any) => { + const statusText = getOptionLabel(statusOptions, row.status) + const visibilityText = getOptionLabel(visibilityOptions, row.visibility) + const sourceText = getOptionLabel(sourceOptions, row.source) + return h('div', { class: 'flex flex-col items-center py-2 text-sm leading-6' }, [ + h('div', [ + h('span', { class: getStatusTagClass(Number(row.status)) }, statusText) + ]), + h('div', { class: 'mt-1 text-xs text-[#606266]' }, visibilityText), + h('div', { class: 'mt-1 text-xs text-[#909399]' }, sourceText) + ]) + } + }, + { + prop: 'business_data', + label: $t('article.modules.content.columns.business_data'), + minWidth: 250, + formatter: (row: any) => { + const metricItems = [ + { label: '浏览', value: row.view_count ?? 0 }, + { label: '点赞', value: row.like_count ?? 0 }, + { label: '评论', value: row.comment_count ?? 0 }, + { label: '收藏', value: row.favorite_count ?? 0 }, + ] + + return h('div', { class: 'grid grid-cols-2 gap-2 py-2 text-xs text-[#606266]' }, [ + ...metricItems.map((item) => + h('div', { class: 'flex items-center justify-between rounded border border-[#e5e7eb] bg-[#eef2f6] px-3 py-1.5 transition-colors hover:bg-[#e5e7eb]' }, [ + h('span', { class: 'text-[#909399]' }, item.label), + h('span', item.value) + ]) + ) + ]) + } + }, + { + prop: 'time_info', + label: $t('article.modules.content.columns.time_info'), + minWidth: 190, + formatter: (row: any) => { + return h('div', { class: 'py-2 text-xs leading-6 text-[#606266]' }, [ + h('div', [ + h('span', { class: 'text-[#909399]' }, '更新 '), + h('span', row.updated_date || '-') + ]), + h('div', { class: 'mt-1' }, [ + h('span', { class: 'text-[#909399]' }, '发布 '), + h('span', row.created_date || '-') + ]) + ]) + } + }, + ], + tableActionColumn: { + width: 170, + }, + dialogProps: { + width: '70%', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'title', + label: $t('article.modules.content.columns.title'), + componentProps: { + placeholder: $t('article.modules.content.form.title_placeholder'), + maxlength: 200, + }, + rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.content.form.slug'), + componentProps: { + placeholder: $t('article.modules.content.form.slug_placeholder'), + maxlength: 200, + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'content_type', + label: $t('article.modules.content.form.content_type'), + defaultValue: 'article', + componentProps: { + options: contentTypeOptions, + class: 'w-full', + }, + rules: [{ required: true, message: $t('article.modules.content.form.content_type') }], + colSpan: 12, + }, + { + component: 'ApiTreeSelect', + prop: 'category_id', + label: $t('article.modules.content.form.category_id'), + componentProps: { + api: '/article/category', + requestMethod: 'get', + params: { + format: 'tree', + }, + teleported: true, + clearable: true, + checkStrictly: true, + class: 'w-full', + labelField: 'name', + valueField: 'id', + placeholder: $t('article.modules.content.form.category_id_placeholder'), + }, + rules: [{ required: true, message: $t('article.modules.content.form.category_id') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'channel', + label: $t('article.modules.content.form.channel'), + defaultValue: 'official', + componentProps: { + options: channelOptions, + class: 'w-full', + }, + rules: [{ required: true, message: $t('article.modules.content.form.channel') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'author', + label: $t('article.modules.content.form.author'), + componentProps: { + placeholder: $t('article.modules.content.form.author_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.content.form.author_placeholder') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'visibility', + label: $t('article.modules.content.form.visibility'), + defaultValue: 1, + componentProps: { + options: visibilityOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'status', + label: $t('article.modules.content.form.status'), + defaultValue: 0, + componentProps: { + options: statusOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'source', + label: $t('article.modules.content.form.source'), + defaultValue: 'original', + componentProps: { + options: sourceOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Avatar', + prop: 'cover', + label: $t('article.modules.content.form.cover'), + componentProps: { + shape: 'square', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'summary', + label: $t('article.modules.content.form.summary'), + componentProps: { + type: 'textarea', + rows: 4, + maxlength: 500, + placeholder: $t('article.modules.content.form.summary_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiSelect', + prop: 'tag_ids', + label: $t('article.modules.content.form.tag_ids'), + componentProps: { + api: '/article/tag', + requestMethod: 'get', + params: { + format: 'select', + }, + labelField: 'label', + valueField: 'value', + multiple: true, + clearable: true, + filterable: true, + class: 'w-full', + placeholder: $t('article.modules.content.form.tag_ids_placeholder'), + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_keywords', + label: $t('article.modules.content.form.seo_keywords'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_keywords_placeholder'), + maxlength: 255, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_title', + label: $t('article.modules.content.form.seo_title'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_title_placeholder'), + maxlength: 200, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_description', + label: $t('article.modules.content.form.seo_description'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_description_placeholder'), + maxlength: 500, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.content.form.sort'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'DatePicker', + prop: 'published_at', + label: $t('article.modules.content.form.published_at'), + componentProps: { + type: 'datetime', + valueFormat: 'X', + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'DatePicker', + prop: 'offline_at', + label: $t('article.modules.content.form.offline_at'), + componentProps: { + type: 'datetime', + valueFormat: 'X', + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'InputNumber', + prop: 'view_count', + label: $t('article.modules.content.form.view_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'like_count', + label: $t('article.modules.content.form.like_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'comment_count', + label: $t('article.modules.content.form.comment_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'favorite_count', + label: $t('article.modules.content.form.favorite_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'ApiDict', + prop: 'is_top', + label: $t('article.modules.content.form.is_top'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'ApiDict', + prop: 'is_recommend', + label: $t('article.modules.content.form.is_recommend'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'ApiDict', + prop: 'allow_comment', + label: $t('article.modules.content.form.allow_comment'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'Editor', + prop: 'content', + label: $t('article.modules.content.form.content'), + componentProps: { + placeholder: $t('article.modules.content.form.content_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/article/resource/template/admin/views/article/index/index.vue b/backend/plugin/article/resource/template/admin/views/article/index/index.vue new file mode 100644 index 0000000..2617d29 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/backend/plugin/article/resource/template/admin/views/article/recycle/index.vue b/backend/plugin/article/resource/template/admin/views/article/recycle/index.vue new file mode 100644 index 0000000..68ba3c5 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/recycle/index.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/backend/plugin/article/resource/template/admin/views/article/recycle/schemas/index.tsx b/backend/plugin/article/resource/template/admin/views/article/recycle/schemas/index.tsx new file mode 100644 index 0000000..9d6c771 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/recycle/schemas/index.tsx @@ -0,0 +1,65 @@ +import { CrudSchema } from '@/components/crud' +import { $t } from '@/locales' +import pluginApi from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: async (params: any) => { + const response = await pluginApi.recycleList(params) + const rows = Array.isArray(response) ? response : [] + return { + recordCount: rows.length, + totalPage: 1, + rows, + } + }, + useCrud: false, + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.recycle.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.recycle.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'title', + label: $t('article.modules.recycle.columns.title'), + minWidth: 220, + }, + { + prop: 'category', + label: $t('article.modules.recycle.columns.category'), + minWidth: 160, + }, + { + prop: 'deleted_by', + label: $t('article.modules.recycle.columns.deleted_by'), + width: 140, + }, + { + prop: 'deleted_at', + label: $t('article.modules.recycle.columns.deleted_at'), + minWidth: 180, + }, + { + prop: 'expire_at', + label: $t('article.modules.recycle.columns.expire_at'), + minWidth: 180, + }, + ], + } +} diff --git a/backend/plugin/article/resource/template/admin/views/article/tag/index.vue b/backend/plugin/article/resource/template/admin/views/article/tag/index.vue new file mode 100644 index 0000000..c763b60 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/tag/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/article/resource/template/admin/views/article/tag/schemas/index.tsx b/backend/plugin/article/resource/template/admin/views/article/tag/schemas/index.tsx new file mode 100644 index 0000000..3031030 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/article/tag/schemas/index.tsx @@ -0,0 +1,169 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleTagService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleTagService.list, + dialogTitle: $t('article.modules.tag.title'), + crudApi: { + add: ArticleTagService.create, + edit: ArticleTagService.update, + remove: ArticleTagService.delete, + view: ArticleTagService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:tag:create', + editAuth: 'article:tag:update', + removeAuth: 'article:tag:delete', + viewAuth: 'article:tag:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.tag.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.tag.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.tag.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + minWidth: 180, + }, + { + prop: 'article_count', + label: $t('article.modules.tag.columns.article_count'), + width: 120, + }, + { + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.tag.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.tag.columns.name'), + componentProps: { + placeholder: $t('article.modules.tag.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + componentProps: { + placeholder: $t('article.modules.tag.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.tag.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.tag.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/article/resource/template/admin/views/index/index.vue b/backend/plugin/article/resource/template/admin/views/index/index.vue new file mode 100644 index 0000000..1d702c1 --- /dev/null +++ b/backend/plugin/article/resource/template/admin/views/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/backend/plugin/article/resource/template/web/.gitkeep b/backend/plugin/article/resource/template/web/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/Install.php b/backend/plugin/produce/Install.php new file mode 100644 index 0000000..349ef1a --- /dev/null +++ b/backend/plugin/produce/Install.php @@ -0,0 +1,59 @@ +importPluginMenus() - 导入菜单 + * - $this->importTemplates() - 复制模板 + * - $this->uninstallPluginMenus() - 卸载菜单 + */ +class Install extends BasePluginInstall +{ + /** + * 安装前回调 + */ + protected function beforeInstall(string $version): void + { + $this->output(" 🔄 Running beforeInstall...\n"); + } + + /** + * 安装后回调 - 按需导入菜单和模板 + */ + protected function afterInstall(string $version): void + { + $this->output(" 🔄 Running afterInstall...\n"); + + // 导入菜单 + $this->importPluginMenus(); + + // 复制模板资源 + $this->importTemplates(); + } + + /** + * 卸载前回调 + */ + protected function beforeUninstall(string $version): void + { + $this->output(" 🔄 Running beforeUninstall...\n"); + } + + /** + * 卸载后回调 - 清理菜单 + */ + protected function afterUninstall(string $version): void + { + $this->output(" 🔄 Running afterUninstall...\n"); + // 卸载菜单 + $this->uninstallPluginMenus(); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ExceptionAnalysisController.php b/backend/plugin/produce/app/adminapi/controller/ExceptionAnalysisController.php new file mode 100644 index 0000000..ffefb12 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ExceptionAnalysisController.php @@ -0,0 +1,32 @@ +service->overview($request->all())); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ExceptionRecordController.php b/backend/plugin/produce/app/adminapi/controller/ExceptionRecordController.php new file mode 100644 index 0000000..be4585b --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ExceptionRecordController.php @@ -0,0 +1,102 @@ + true, + 'keyword_fields' => ['order_no', 'user_code', 'reporter', 'type_name', 'reason_name', 'issue_name', 'remark'], + ]; + + public function __construct(ExceptionRecordService $service, ExceptionRecordValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/exception-record', summary: '异常数据列表', tags: ['produce-exception-record'])] + #[Permission(code: 'produce:exception-data:list', description: '异常数据列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get(path: '/produce/exception-record/{id}', summary: '异常数据详情', tags: ['produce-exception-record'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-data:read', description: '异常数据详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/exception-record', summary: '创建异常数据', tags: ['produce-exception-record'])] + #[Permission(code: 'produce:exception-data:create', description: '创建异常数据')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put(path: '/produce/exception-record/{id}', summary: '更新异常数据', tags: ['produce-exception-record'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-data:update', description: '更新异常数据')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/exception-record/export', summary: '导出异常数据', tags: ['produce-exception-record'])] + #[Permission(code: 'produce:exception-data:list', description: '导出异常数据')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete(path: '/produce/exception-record/{id}', summary: '删除异常数据', tags: ['produce-exception-record'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-data:delete', description: '删除异常数据')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete(path: '/produce/exception-record', summary: '批量删除异常数据', tags: ['produce-exception-record'], x: [ + SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class, + ])] + #[Permission(code: 'produce:exception-data:delete', description: '批量删除异常数据')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ExceptionSettingController.php b/backend/plugin/produce/app/adminapi/controller/ExceptionSettingController.php new file mode 100644 index 0000000..14169dc --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ExceptionSettingController.php @@ -0,0 +1,94 @@ + true, + 'keyword_fields' => ['code', 'name', 'remark'], + ]; + + public function __construct(ExceptionSettingService $service, ExceptionSettingValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/exception-setting', summary: '异常设置列表', tags: ['produce-exception-setting'])] + #[Permission(code: 'produce:exception-setting:list', description: '异常设置列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get(path: '/produce/exception-setting/{id}', summary: '异常设置详情', tags: ['produce-exception-setting'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-setting:read', description: '异常设置详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/exception-setting', summary: '创建异常设置', tags: ['produce-exception-setting'])] + #[Permission(code: 'produce:exception-setting:create', description: '创建异常设置')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put(path: '/produce/exception-setting/{id}', summary: '更新异常设置', tags: ['produce-exception-setting'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-setting:update', description: '更新异常设置')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Delete(path: '/produce/exception-setting/{id}', summary: '删除异常设置', tags: ['produce-exception-setting'], x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ])] + #[Permission(code: 'produce:exception-setting:delete', description: '删除异常设置')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete(path: '/produce/exception-setting', summary: '批量删除异常设置', tags: ['produce-exception-setting'], x: [ + SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class, + ])] + #[Permission(code: 'produce:exception-setting:delete', description: '批量删除异常设置')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/HardwareFaultDescriptionController.php b/backend/plugin/produce/app/adminapi/controller/HardwareFaultDescriptionController.php new file mode 100644 index 0000000..aeda737 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/HardwareFaultDescriptionController.php @@ -0,0 +1,91 @@ + true, + 'keyword_fields' => ['description', 'remark'], + ]; + + public function __construct(HardwareFaultDescriptionService $service, HardwareFaultDescriptionValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/hardware-fault-description', summary: '硬件故障描述列表', tags: ['produce-hardware-fault-description'])] + #[Permission(code: 'produce:hardware-fault-description:list', description: '硬件故障描述列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response { return parent::index($request); } + + #[OA\Post(path: '/produce/hardware-fault-description/import', summary: '导入硬件故障描述', tags: ['produce-hardware-fault-description'])] + #[Permission(code: 'produce:hardware-fault-description:import', description: '导入硬件故障描述')] + #[SimpleResponse(schema: [], example: [])] + public function import(Request $request): \support\Response + { + try { + $file = $request->file('file'); + if (!$file) { + return Json::fail('请选择Excel文件'); + } + if ($file->getSize() > 5 * 1024 * 1024) { + return Json::fail('Excel文件不能超过5MB'); + } + $extension = strtolower((string) $file->getUploadExtension()); + if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { + return Json::fail('只支持xlsx、xls或csv格式文件'); + } + + $result = $this->service->importFromExcel($file->getPathname()); + return Json::success('导入成功', $result); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Get(path: '/produce/hardware-fault-description/{id}', summary: '硬件故障描述详情', tags: ['produce-hardware-fault-description'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:hardware-fault-description:read', description: '硬件故障描述详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response { return parent::show($request); } + + #[OA\Post(path: '/produce/hardware-fault-description', summary: '创建硬件故障描述', tags: ['produce-hardware-fault-description'])] + #[Permission(code: 'produce:hardware-fault-description:create', description: '创建硬件故障描述')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response { return parent::store($request); } + + #[OA\Put(path: '/produce/hardware-fault-description/{id}', summary: '更新硬件故障描述', tags: ['produce-hardware-fault-description'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:hardware-fault-description:update', description: '更新硬件故障描述')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response { return parent::update($request); } + + #[OA\Delete(path: '/produce/hardware-fault-description/{id}', summary: '删除硬件故障描述', tags: ['produce-hardware-fault-description'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:hardware-fault-description:delete', description: '删除硬件故障描述')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response { return parent::destroy($request); } + + #[OA\Delete(path: '/produce/hardware-fault-description', summary: '批量删除硬件故障描述', tags: ['produce-hardware-fault-description'], x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class])] + #[Permission(code: 'produce:hardware-fault-description:delete', description: '批量删除硬件故障描述')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response { return parent::destroy($request); } +} diff --git a/backend/plugin/produce/app/adminapi/controller/MaterialCategoryController.php b/backend/plugin/produce/app/adminapi/controller/MaterialCategoryController.php new file mode 100644 index 0000000..ad1773b --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/MaterialCategoryController.php @@ -0,0 +1,65 @@ + true, + 'keyword_fields' => ['category_name'], + ]; + + public function __construct(MaterialCategoryService $service, MaterialCategoryValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/material-category', summary: '物料分类列表', tags: ['produce-material-category'])] + #[Permission(code: 'produce:material-category:list', description: '物料分类列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response { return parent::index($request); } + + #[OA\Get(path: '/produce/material-category/{id}', summary: '物料分类详情', tags: ['produce-material-category'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material-category:read', description: '物料分类详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response { return parent::show($request); } + + #[OA\Post(path: '/produce/material-category', summary: '创建物料分类', tags: ['produce-material-category'])] + #[Permission(code: 'produce:material-category:create', description: '创建物料分类')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response { return parent::store($request); } + + #[OA\Put(path: '/produce/material-category/{id}', summary: '更新物料分类', tags: ['produce-material-category'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material-category:update', description: '更新物料分类')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response { return parent::update($request); } + + #[OA\Delete(path: '/produce/material-category/{id}', summary: '删除物料分类', tags: ['produce-material-category'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material-category:delete', description: '删除物料分类')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response { return parent::destroy($request); } + + #[OA\Delete(path: '/produce/material-category', summary: '批量删除物料分类', tags: ['produce-material-category'], x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class])] + #[Permission(code: 'produce:material-category:delete', description: '批量删除物料分类')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response { return parent::destroy($request); } +} diff --git a/backend/plugin/produce/app/adminapi/controller/MaterialController.php b/backend/plugin/produce/app/adminapi/controller/MaterialController.php new file mode 100644 index 0000000..3367f24 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/MaterialController.php @@ -0,0 +1,96 @@ + true, + 'keyword_fields' => ['product_code', 'product_name'], + ]; + + public function __construct(MaterialService $service, MaterialValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/material', summary: '物料列表', tags: ['produce-material'])] + #[Permission(code: 'produce:material:list', description: '物料列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response { return parent::index($request); } + + #[OA\Post(path: '/produce/material/import', summary: '导入物料', tags: ['produce-material'])] + #[Permission(code: 'produce:material:import', description: '导入物料')] + #[SimpleResponse(schema: [], example: [])] + public function import(Request $request): \support\Response + { + try { + $file = $request->file('file'); + if (!$file) { + return Json::fail('请选择Excel文件'); + } + if ($file->getSize() > 5 * 1024 * 1024) { + return Json::fail('Excel文件不能超过5MB'); + } + $extension = strtolower((string) $file->getUploadExtension()); + if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { + return Json::fail('只支持xlsx、xls或csv格式文件'); + } + + $result = $this->service->importFromExcel($file->getPathname()); + return Json::success('导入成功', $result); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Get(path: '/produce/material/{id}', summary: '物料详情', tags: ['produce-material'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material:read', description: '物料详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response { return parent::show($request); } + + #[OA\Post(path: '/produce/material', summary: '创建物料', tags: ['produce-material'])] + #[Permission(code: 'produce:material:create', description: '创建物料')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response { return parent::store($request); } + + #[OA\Put(path: '/produce/material/{id}', summary: '更新物料', tags: ['produce-material'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material:update', description: '更新物料')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response { return parent::update($request); } + + #[OA\Post(path: '/produce/material/export', summary: '导出物料', tags: ['produce-material'])] + #[Permission(code: 'produce:material:list', description: '导出物料')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response { return parent::export($request); } + + #[OA\Delete(path: '/produce/material/{id}', summary: '删除物料', tags: ['produce-material'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:material:delete', description: '删除物料')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response { return parent::destroy($request); } + + #[OA\Delete(path: '/produce/material', summary: '批量删除物料', tags: ['produce-material'], x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class])] + #[Permission(code: 'produce:material:delete', description: '批量删除物料')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response { return parent::destroy($request); } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ProcessController.php b/backend/plugin/produce/app/adminapi/controller/ProcessController.php new file mode 100644 index 0000000..5147ff7 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ProcessController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/process', summary: '工序管理列表', tags: ['produce-process'])] + #[Permission(code: 'produce:process:list', description: '工序管理列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/process/{id}', + summary: '工序管理详情', + tags: ['produce-process'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process:read', description: '工序管理详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/process', summary: '创建工序', tags: ['produce-process'])] + #[Permission(code: 'produce:process:create', description: '创建工序')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/process/{id}', + summary: '更新工序', + tags: ['produce-process'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process:update', description: '更新工序')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/process/export', summary: '导出工序', tags: ['produce-process'])] + #[Permission(code: 'produce:process:list', description: '导出工序')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/process/{id}', + summary: '删除工序', + tags: ['produce-process'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process:delete', description: '删除工序')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/process', + summary: '批量删除工序', + tags: ['produce-process'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:process:delete', description: '批量删除工序')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ProcessStepController.php b/backend/plugin/produce/app/adminapi/controller/ProcessStepController.php new file mode 100644 index 0000000..933b153 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ProcessStepController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/process-step', summary: '工序步骤列表', tags: ['produce-process-step'])] + #[Permission(code: 'produce:process-step:list', description: '工序步骤列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/process-step/{id}', + summary: '工序步骤详情', + tags: ['produce-process-step'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process-step:read', description: '工序步骤详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/process-step', summary: '创建工序步骤', tags: ['produce-process-step'])] + #[Permission(code: 'produce:process-step:create', description: '创建工序步骤')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/process-step/{id}', + summary: '更新工序步骤', + tags: ['produce-process-step'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process-step:update', description: '更新工序步骤')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/process-step/export', summary: '导出工序步骤', tags: ['produce-process-step'])] + #[Permission(code: 'produce:process-step:list', description: '导出工序步骤')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/process-step/{id}', + summary: '删除工序步骤', + tags: ['produce-process-step'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:process-step:delete', description: '删除工序步骤')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/process-step', + summary: '批量删除工序步骤', + tags: ['produce-process-step'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:process-step:delete', description: '批量删除工序步骤')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ProduceConfigController.php b/backend/plugin/produce/app/adminapi/controller/ProduceConfigController.php new file mode 100644 index 0000000..1ba8f6c --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ProduceConfigController.php @@ -0,0 +1,60 @@ +input('group_code', ''); + if ($groupCode === '') { + return Json::fail('配置组代码不能为空'); + } + + return Json::success('获取成功', $this->service->getConfigByGroup($groupCode)); + } + + #[OA\Post(path: '/produce/config/save', summary: '保存生产配置', tags: ['produce-config'])] + #[SimpleResponse(description: '保存成功')] + #[Permission(code: 'produce:config:update', description: '保存生产配置')] + public function save(Request $request): Response + { + $payload = $request->post(); + $groupCode = (string) ($payload['group_code'] ?? ''); + $configs = $payload['configs'] ?? []; + + if ($groupCode === '') { + return Json::fail('配置组代码不能为空'); + } + + if (!is_array($configs) || $configs === []) { + return Json::fail('配置数据不能为空'); + } + + $this->service->saveConfig($groupCode, $configs); + + return Json::success('保存成功'); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ProduceController.php b/backend/plugin/produce/app/adminapi/controller/ProduceController.php new file mode 100644 index 0000000..5da949c --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ProduceController.php @@ -0,0 +1,82 @@ + true, + 'keyword_fields' => ['process_order_no', 'product_code', 'operator', 'remark'], + ]; + + public function __construct(ProductRecordService $service, ProductRecordValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/product-record', summary: '整机数据列表', tags: ['produce-product-record'])] + #[Permission(code: 'produce:product-record:list', description: '整机数据列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/product-record/{id}', + summary: '整机数据详情', + tags: ['produce-product-record'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:product-record:read', description: '整机数据详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/product-record', summary: '创建整机数据', tags: ['produce-product-record'])] + #[Permission(code: 'produce:product-record:create', description: '创建整机数据')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/product-record/{id}', + summary: '更新整机数据', + tags: ['produce-product-record'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:product-record:update', description: '更新整机数据')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/product-record/export', summary: '导出整机数据', tags: ['produce-product-record'])] + #[Permission(code: 'produce:product-record:list', description: '导出整机数据')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/product-record/{id}', + summary: '删除整机数据', + tags: ['produce-product-record'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:product-record:delete', description: '删除整机数据')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/product-record', + summary: '批量删除整机数据', + tags: ['produce-product-record'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:product-record:delete', description: '批量删除整机数据')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php b/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php new file mode 100644 index 0000000..6ef2cc5 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php @@ -0,0 +1,140 @@ + true, + 'keyword_fields' => ['repair_no', 'order_no', 'product_code', 'product_name', 'fault_description', 'operator', 'remark'], + ]; + + public function __construct(RepairRecordService $service, RepairRecordValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/repair-record', summary: '维修数据列表', tags: ['produce-repair-record'])] + #[Permission(code: 'produce:repair-record:list', description: '维修数据列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + try { + [$where, $format, $limit, $field, $order, $page] = $this->selectInput($request); + $methods = [ + 'select' => 'formatSelect', + 'tree' => 'formatTree', + 'table_tree' => 'formatTableTree', + 'normal' => 'formatNormal', + ]; + $order = $order ?: 'repaired_at DESC, id DESC'; + $total = $this->service->getCount($where); + $list = $this->service->selectList($where, $field, $page, $limit, $order, [], false); + return call_user_func([$this, $methods[$format] ?? 'formatNormal'], $list, $total); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Get(path: '/produce/repair-record/{id}', summary: '维修数据详情', tags: ['produce-repair-record'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-record:read', description: '维修数据详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response { return parent::show($request); } + + #[OA\Post(path: '/produce/repair-record', summary: '创建维修数据', tags: ['produce-repair-record'])] + #[Permission(code: 'produce:repair-record:create', description: '创建维修数据')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response { return parent::store($request); } + + #[OA\Put(path: '/produce/repair-record/{id}', summary: '更新维修数据', tags: ['produce-repair-record'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-record:update', description: '更新维修数据')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response { return parent::update($request); } + + protected function expandExportRows(iterable $items): iterable + { + foreach ($items as $item) { + $record = $item->toArray(); + $parts = is_array($record['parts'] ?? null) ? $record['parts'] : []; + + if (!$parts) { + yield $record; + continue; + } + + foreach ($parts as $part) { + $faultDescriptions = is_array($part['fault_descriptions'] ?? null) ? $part['fault_descriptions'] : []; + $record['order_product'] = implode('/', array_filter([(string) ($record['order_no'] ?? ''), (string) ($record['product_code'] ?? '')])); + $record['part_category'] = (string) ($part['category_name'] ?? '-'); + $record['part_code'] = (string) ($part['product_code'] ?? '-'); + $record['part_name'] = (string) ($part['product_name'] ?? '-'); + $record['part_fault'] = implode('、', array_filter(array_map( + static fn (array $fault): string => (string) ($fault['description'] ?? ''), + $faultDescriptions, + ))) ?: '-'; + $record['part_quantity'] = (string) ($part['quantity'] ?? 1); + $record['part_source'] = match ($part['source'] ?? '') { + 'new' => '新料', + 'old', 'after_sale', 'after_sales', 'after-sale' => '售后', + 'repair', 'refurbished' => '返修', + default => (string) ($part['source'] ?? '-'), + }; + yield $record; + } + } + } + + #[OA\Post(path: '/produce/repair-record/export', summary: '导出维修数据', tags: ['produce-repair-record'])] + #[Permission(code: 'produce:repair-record:list', description: '导出维修数据')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete(path: '/produce/repair-record/{id}', summary: '删除维修数据', tags: ['produce-repair-record'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-record:delete', description: '删除维修数据')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + try { + $ids = $this->getDeleteIds($request); + if (!$ids) { + return Json::fail('删除参数不能为空'); + } + $this->service->transaction(function () use ($ids) { + foreach ($ids as $id) { + $this->service->deleteRecord($id); + } + }); + return Json::success('ok', $ids); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + } + + #[OA\Delete(path: '/produce/repair-record', summary: '批量删除维修数据', tags: ['produce-repair-record'], x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class])] + #[Permission(code: 'produce:repair-record:delete', description: '批量删除维修数据')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response { return $this->destroy($request); } +} diff --git a/backend/plugin/produce/app/adminapi/controller/RepairSettingController.php b/backend/plugin/produce/app/adminapi/controller/RepairSettingController.php new file mode 100644 index 0000000..6f8d0ec --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/RepairSettingController.php @@ -0,0 +1,65 @@ + true, + 'keyword_fields' => ['code', 'name', 'remark'], + ]; + + public function __construct(RepairSettingService $service, RepairSettingValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/repair-setting', summary: '维修设置列表', tags: ['produce-repair-setting'])] + #[Permission(code: 'produce:repair-setting:list', description: '维修设置列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response { return parent::index($request); } + + #[OA\Get(path: '/produce/repair-setting/{id}', summary: '维修设置详情', tags: ['produce-repair-setting'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-setting:read', description: '维修设置详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response { return parent::show($request); } + + #[OA\Post(path: '/produce/repair-setting', summary: '创建维修设置', tags: ['produce-repair-setting'])] + #[Permission(code: 'produce:repair-setting:create', description: '创建维修设置')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response { return parent::store($request); } + + #[OA\Put(path: '/produce/repair-setting/{id}', summary: '更新维修设置', tags: ['produce-repair-setting'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-setting:update', description: '更新维修设置')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response { return parent::update($request); } + + #[OA\Delete(path: '/produce/repair-setting/{id}', summary: '删除维修设置', tags: ['produce-repair-setting'], x: [SchemaConstants::X_PROPERTY_IN => 'id', SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class])] + #[Permission(code: 'produce:repair-setting:delete', description: '删除维修设置')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response { return parent::destroy($request); } + + #[OA\Delete(path: '/produce/repair-setting', summary: '批量删除维修设置', tags: ['produce-repair-setting'], x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class])] + #[Permission(code: 'produce:repair-setting:delete', description: '批量删除维修设置')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response { return parent::destroy($request); } +} diff --git a/backend/plugin/produce/app/adminapi/controller/ScanDataController.php b/backend/plugin/produce/app/adminapi/controller/ScanDataController.php new file mode 100644 index 0000000..a536404 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/ScanDataController.php @@ -0,0 +1,120 @@ + true, + 'keyword_fields' => ['order_no', 'user_code', 'operator', 'remark'], + ]; + + public function __construct(ScanDataService $service, ScanDataValidate $validate) + { + $this->service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/scan-data', summary: '扫码数据列表', tags: ['produce-scan-data'])] + #[Permission(code: 'produce:scan-data:list', description: '扫码数据列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/scan-data/{id}', + summary: '扫码数据详情', + tags: ['produce-scan-data'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:scan-data:read', description: '扫码数据详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/scan-data', summary: '创建扫码数据', tags: ['produce-scan-data'])] + #[Permission(code: 'produce:scan-data:create', description: '创建扫码数据')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/scan-data/{id}', + summary: '更新扫码数据', + tags: ['produce-scan-data'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:scan-data:update', description: '更新扫码数据')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/scan-data/export', summary: '导出扫码数据', tags: ['produce-scan-data'])] + #[Permission(code: 'produce:scan-data:list', description: '导出扫码数据')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/scan-data/{id}', + summary: '删除扫码数据', + tags: ['produce-scan-data'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:scan-data:delete', description: '删除扫码数据')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/scan-data', + summary: '批量删除扫码数据', + tags: ['produce-scan-data'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:scan-data:delete', description: '批量删除扫码数据')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/SpecificRuleController.php b/backend/plugin/produce/app/adminapi/controller/SpecificRuleController.php new file mode 100644 index 0000000..9279887 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/SpecificRuleController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/specific-rule', summary: '特定规则列表', tags: ['produce-specific-rule'])] + #[Permission(code: 'produce:specific-rule:list', description: '特定规则列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/specific-rule/{id}', + summary: '特定规则详情', + tags: ['produce-specific-rule'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:specific-rule:read', description: '特定规则详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/specific-rule', summary: '创建特定规则', tags: ['produce-specific-rule'])] + #[Permission(code: 'produce:specific-rule:create', description: '创建特定规则')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/specific-rule/{id}', + summary: '更新特定规则', + tags: ['produce-specific-rule'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:specific-rule:update', description: '更新特定规则')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/specific-rule/export', summary: '导出特定规则', tags: ['produce-specific-rule'])] + #[Permission(code: 'produce:specific-rule:list', description: '导出特定规则')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/specific-rule/{id}', + summary: '删除特定规则', + tags: ['produce-specific-rule'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:specific-rule:delete', description: '删除特定规则')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/specific-rule', + summary: '批量删除特定规则', + tags: ['produce-specific-rule'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:specific-rule:delete', description: '批量删除特定规则')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/StatusTransitionController.php b/backend/plugin/produce/app/adminapi/controller/StatusTransitionController.php new file mode 100644 index 0000000..0858f08 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/StatusTransitionController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/status-transition', summary: '状态转换规则列表', tags: ['produce-status-transition'])] + #[Permission(code: 'produce:status-transition:list', description: '状态转换规则列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/status-transition/{id}', + summary: '状态转换规则详情', + tags: ['produce-status-transition'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:status-transition:read', description: '状态转换规则详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/status-transition', summary: '创建状态转换规则', tags: ['produce-status-transition'])] + #[Permission(code: 'produce:status-transition:create', description: '创建状态转换规则')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/status-transition/{id}', + summary: '更新状态转换规则', + tags: ['produce-status-transition'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:status-transition:update', description: '更新状态转换规则')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/status-transition/export', summary: '导出状态转换规则', tags: ['produce-status-transition'])] + #[Permission(code: 'produce:status-transition:list', description: '导出状态转换规则')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/status-transition/{id}', + summary: '删除状态转换规则', + tags: ['produce-status-transition'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:status-transition:delete', description: '删除状态转换规则')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/status-transition', + summary: '批量删除状态转换规则', + tags: ['produce-status-transition'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:status-transition:delete', description: '批量删除状态转换规则')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/StepRelationController.php b/backend/plugin/produce/app/adminapi/controller/StepRelationController.php new file mode 100644 index 0000000..5e82edc --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/StepRelationController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/step-relation', summary: '步骤关系列表', tags: ['produce-step-relation'])] + #[Permission(code: 'produce:step-relation:list', description: '步骤关系列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/step-relation/{id}', + summary: '步骤关系详情', + tags: ['produce-step-relation'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-relation:read', description: '步骤关系详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/step-relation', summary: '创建步骤关系', tags: ['produce-step-relation'])] + #[Permission(code: 'produce:step-relation:create', description: '创建步骤关系')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/step-relation/{id}', + summary: '更新步骤关系', + tags: ['produce-step-relation'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-relation:update', description: '更新步骤关系')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/step-relation/export', summary: '导出步骤关系', tags: ['produce-step-relation'])] + #[Permission(code: 'produce:step-relation:list', description: '导出步骤关系')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/step-relation/{id}', + summary: '删除步骤关系', + tags: ['produce-step-relation'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-relation:delete', description: '删除步骤关系')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/step-relation', + summary: '批量删除步骤关系', + tags: ['produce-step-relation'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:step-relation:delete', description: '批量删除步骤关系')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/StepStatusController.php b/backend/plugin/produce/app/adminapi/controller/StepStatusController.php new file mode 100644 index 0000000..89c3b17 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/StepStatusController.php @@ -0,0 +1,115 @@ +service = $service; + $this->validate = $validate; + } + + #[OA\Get(path: '/produce/step-status', summary: '步骤状态列表', tags: ['produce-step-status'])] + #[Permission(code: 'produce:step-status:list', description: '步骤状态列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/step-status/{id}', + summary: '步骤状态详情', + tags: ['produce-step-status'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-status:read', description: '步骤状态详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/step-status', summary: '创建步骤状态', tags: ['produce-step-status'])] + #[Permission(code: 'produce:step-status:create', description: '创建步骤状态')] + #[SimpleResponse(schema: [], example: [])] + public function store(Request $request): \support\Response + { + return parent::store($request); + } + + #[OA\Put( + path: '/produce/step-status/{id}', + summary: '更新步骤状态', + tags: ['produce-step-status'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-status:update', description: '更新步骤状态')] + #[SimpleResponse(schema: [], example: [])] + public function update(Request $request): \support\Response + { + return parent::update($request); + } + + #[OA\Post(path: '/produce/step-status/export', summary: '导出步骤状态', tags: ['produce-step-status'])] + #[Permission(code: 'produce:step-status:list', description: '导出步骤状态')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } + + #[OA\Delete( + path: '/produce/step-status/{id}', + summary: '删除步骤状态', + tags: ['produce-step-status'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:step-status:delete', description: '删除步骤状态')] + #[SimpleResponse(schema: [], example: [])] + public function destroy(Request $request): \support\Response + { + return parent::destroy($request); + } + + #[OA\Delete( + path: '/produce/step-status', + summary: '批量删除步骤状态', + tags: ['produce-step-status'], + x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class] + )] + #[Permission(code: 'produce:step-status:delete', description: '批量删除步骤状态')] + #[SimpleResponse(schema: [], example: [])] + public function batchDelete(Request $request): \support\Response + { + return parent::destroy($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/TransitionLogController.php b/backend/plugin/produce/app/adminapi/controller/TransitionLogController.php new file mode 100644 index 0000000..47e093a --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/TransitionLogController.php @@ -0,0 +1,59 @@ +service = $service; + } + + #[OA\Get(path: '/produce/transition-log', summary: '转换日志列表', tags: ['produce-transition-log'])] + #[Permission(code: 'produce:transition-log:list', description: '转换日志列表')] + #[PageResponse(schema: [], example: [])] + public function index(Request $request): \support\Response + { + return parent::index($request); + } + + #[OA\Get( + path: '/produce/transition-log/{id}', + summary: '转换日志详情', + tags: ['produce-transition-log'], + x: [ + SchemaConstants::X_PROPERTY_IN => 'id', + SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class, + ] + )] + #[Permission(code: 'produce:transition-log:read', description: '转换日志详情')] + #[SimpleResponse(schema: [], example: [])] + public function show(Request $request): \support\Response + { + return parent::show($request); + } + + #[OA\Post(path: '/produce/transition-log/export', summary: '导出转换日志', tags: ['produce-transition-log'])] + #[Permission(code: 'produce:transition-log:list', description: '导出转换日志')] + #[SimpleResponse(schema: [], example: [])] + public function export(Request $request): \support\Response + { + return parent::export($request); + } +} diff --git a/backend/plugin/produce/app/adminapi/controller/WorkStatisticsController.php b/backend/plugin/produce/app/adminapi/controller/WorkStatisticsController.php new file mode 100644 index 0000000..8e133d2 --- /dev/null +++ b/backend/plugin/produce/app/adminapi/controller/WorkStatisticsController.php @@ -0,0 +1,64 @@ + $this->monthlyColumns((string) $request->input('month', date('Y-m'))), + 'items' => $this->service->monthly($request->all()), + ]); + } + + #[OA\Get(path: '/produce/statistics/daily', summary: '日工作统计', tags: ['produce-statistics'])] + #[DataResponse(description: '获取日工作统计成功')] + #[Permission(code: 'produce:statistics:daily', description: '日工作统计')] + public function daily(Request $request): Response + { + return Json::success('获取成功', [ + 'columns' => $this->dailyColumns(), + 'items' => $this->service->daily($request->all()), + ]); + } + + private function monthlyColumns(string $month): array + { + $month = preg_match('/^\d{4}-\d{2}$/', $month) ? $month : date('Y-m'); + $days = (int) date('t', strtotime($month . '-01')); + return array_map(static fn (int $day): array => [ + 'key' => (string) $day, + 'label' => str_pad((string) $day, 2, '0', STR_PAD_LEFT), + ], range(1, $days)); + } + + private function dailyColumns(): array + { + return array_map(static fn (int $hour): array => [ + 'key' => str_pad((string) $hour, 2, '0', STR_PAD_LEFT) . ':00', + 'label' => str_pad((string) $hour, 2, '0', STR_PAD_LEFT) . ':00', + ], range(0, 23)); + } +} diff --git a/backend/plugin/produce/app/api/controller/.gitkeep b/backend/plugin/produce/app/api/controller/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/api/controller/DataEntryController.php b/backend/plugin/produce/app/api/controller/DataEntryController.php new file mode 100644 index 0000000..867ef5e --- /dev/null +++ b/backend/plugin/produce/app/api/controller/DataEntryController.php @@ -0,0 +1,1698 @@ +validate = $validate; + $this->productRecordValidate = $productRecordValidate; + parent::__construct(); + } + + #[OA\Get(path: '/produce/process', summary: '获取工序下拉', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function processOptions(Request $request): Response + { + $items = Process::query() + ->where('status', 1) + ->orderBy('sort') + ->orderBy('id') + ->get(); + + return $this->formatSelect($items); + } + + #[OA\Get(path: '/produce/process-step', summary: '获取步骤下拉', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function stepOptions(Request $request): Response + { + $processId = (int) $request->input('process_id', 0); + + $query = ProcessStep::query()->where('status', 1); + if ($processId > 0) { + $query->where('process_id', $processId); + } + + $items = $query->orderBy('sort')->orderBy('id')->get(); + return $this->formatSelect($items); + } + + #[OA\Get(path: '/produce/step-status', summary: '获取状态下拉', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function statusOptions(Request $request): Response + { + $stepId = (int) $request->input('step_id', 0); + + $query = StepStatus::query()->where('status', 1); + if ($stepId > 0) { + $query->where('step_id', $stepId); + } + + $items = $query->orderByDesc('is_initial')->orderBy('sort')->orderBy('id')->get(); + return $this->formatSelect($items); + } + + #[OA\Get(path: '/produce/material/lookup', summary: '按商品编码查询物料', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function materialLookup(Request $request): Response + { + $productCode = trim((string) $request->input('product_code', '')); + if ($productCode === '') { + return Json::fail('商品编码不能为空'); + } + + $material = Material::query() + ->with('category:id,category_name') + ->where('product_code', $productCode) + ->first(); + if (!$material) { + return Json::fail('未找到对应物料'); + } + + return Json::success('ok', [ + 'id' => $material->id, + 'category_id' => $material->category_id, + 'category_name' => $material->category_name, + 'product_code' => $material->product_code, + 'product_name' => $material->product_name, + ]); + } + + #[OA\Get(path: '/produce/scanapp/material-category', summary: '获取扫码端物料分类选项', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function scanAppMaterialCategory(Request $request): Response + { + $items = MaterialCategory::query() + ->orderBy('category_name') + ->orderBy('id') + ->get(['id', 'category_name']) + ->map(static fn (MaterialCategory $item): array => [ + 'id' => (string) $item->id, + 'category_name' => (string) $item->category_name, + ]) + ->values() + ->all(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/hardware-fault-description/options', summary: '按物料分类获取硬件故障描述', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function hardwareFaultDescriptionOptions(Request $request): Response + { + $categoryIds = array_values(array_unique(array_filter(array_map( + 'intval', + explode(',', (string) $request->input('category_ids', '')) + )))); + if ($categoryIds === []) { + return Json::success('获取成功', []); + } + + $items = HardwareFaultDescription::query() + ->whereIn('category_id', $categoryIds) + ->where('is_enabled', 1) + ->orderByDesc('use_count') + ->orderBy('id') + ->get(['id', 'category_id', 'description', 'use_count']) + ->map(static function (HardwareFaultDescription $item): array { + return [ + 'id' => (string) $item->id, + 'category_id' => (string) $item->category_id, + 'description' => (string) $item->description, + 'use_count' => (int) $item->use_count, + ]; + }) + ->values() + ->all(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/config/info', summary: '获取前台生产配置', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function configInfo(Request $request): Response + { + $groupCode = (string) $request->input('group_code', ''); + if ($groupCode === '') { + return Json::fail('配置组代码不能为空'); + } + + $items = ProduceConfig::query() + ->where('group_code', $groupCode) + ->orderBy('sort') + ->orderBy('id') + ->get(); + + $data = []; + foreach ($items as $item) { + $data[$item->code] = $item->content; + } + + return Json::success('获取成功', $data); + } + + #[OA\Get(path: '/produce/config/data-entry', summary: '获取扫码输入页配置', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function dataEntryConfig(Request $request): Response + { + $processId = (string) $request->input('process_id', ''); + $stepId = (string) $request->input('step_id', ''); + $orderNo = trim((string) $request->input('order_no', '')); + $processOrderNo = trim((string) $request->input('process_order_no', '')); + $productCode = trim((string) $request->input('product_code', '')); + $userCode = trim((string) $request->input('user_code', '')); + $applyMode = trim((string) $request->input('apply_mode', 'order')); + $ruleContext = [ + 'mode' => $applyMode, + 'order_no' => $orderNo, + 'process_order_no' => $processOrderNo, + 'product_code' => $productCode, + 'user_code' => $userCode, + ]; + + $basicSettings = $this->getConfigGroupData('basic_setting'); + $stepRule = null; + if ($stepId !== '') { + $stepRule = ProcessStep::query()->with(['process'])->find($stepId); + } + + $specificRules = SpecificRule::query() + ->where('is_enabled', 1) + ->orderBy('sort') + ->orderBy('id') + ->get() + ->filter(function (SpecificRule $rule) use ($stepId, $ruleContext) { + return $this->matchSpecificRule($rule, $stepId, $ruleContext); + }) + ->values() + ->map(function (SpecificRule $rule) { + return [ + 'id' => $rule->id, + 'rule_name' => $rule->rule_name, + 'rule_pattern' => $rule->rule_pattern, + 'rule_type' => $rule->rule_type, + 'apply_mode' => (string) ($rule->apply_mode ?: 'order'), + 'match_field' => (string) ($rule->match_field ?: 'order_no'), + 'step_ids' => $rule->step_ids ?? [], + 'sort' => $rule->sort, + 'remark' => $rule->remark, + ]; + }); + + return Json::success('获取成功', [ + 'basic_setting' => $basicSettings, + 'step_rule' => $stepRule ? [ + 'id' => $stepRule->id, + 'process_id' => $stepRule->process_id, + 'process_name' => $stepRule->process?->process_name ?? '', + 'step_code' => $stepRule->step_code, + 'step_name' => $stepRule->step_name, + 'prerequisite_steps' => $stepRule->prerequisite_steps ?? [], + 'prerequisite_mode' => $stepRule->prerequisite_mode, + 'prerequisite_require_final' => (int) ($stepRule->prerequisite_require_final ?? 1), + 'allow_repeat_scan' => (int) ($stepRule->allow_repeat_scan ?? 0), + 'repeat_scan_statuses' => $stepRule->repeat_scan_statuses ?? [], + ] : null, + 'specific_rules' => $specificRules, + 'current' => [ + 'process_id' => $processId, + 'step_id' => $stepId, + 'order_no' => $orderNo, + 'process_order_no' => $processOrderNo, + 'product_code' => $productCode, + 'user_code' => $userCode, + 'apply_mode' => $applyMode, + ], + ]); + } + + #[OA\Get(path: '/produce/scan-data', summary: '前台最近扫码记录', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function recentRecords(Request $request): Response + { + $page = max(1, (int) $request->input('page', 1)); + $limit = max(1, min(50, (int) $request->input('limit', 10))); + + $query = ScanData::query()->with(['process', 'step', 'status']); + $this->applyScanDataFilter($query, $request); + + $total = (clone $query)->count(); + $items = $query->orderByDesc('scan_time')->orderByDesc('id') + ->forPage($page, $limit) + ->get(); + + return Json::success('获取成功', compact('items', 'total')); + } + + #[OA\Get(path: '/produce/scan-data/statistics', summary: '前台扫码统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function statistics(Request $request): Response + { + $query = ScanData::query(); + $this->applyScanDataFilter($query, $request); + + $todayTotal = (int) (clone $query)->sum('quantity'); + $successTotal = (int) (clone $query)->where('scan_result', 1)->sum('quantity'); + $failedTotal = (int) (clone $query)->where('scan_result', 0)->sum('quantity'); + + return Json::success('获取成功', [ + 'today_total' => $todayTotal, + 'success_total' => $successTotal, + 'failed_total' => $failedTotal, + ]); + } + + #[OA\Get(path: '/produce/scan-data/user-code-stats', summary: '前台用户扫码统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function userCodeStats(Request $request): Response + { + $query = ScanData::query() + ->selectRaw('user_code, operator, SUM(quantity) as count'); + $this->applyScanDataFilter($query, $request); + + $items = $query->groupBy('user_code', 'operator') + ->orderByDesc('count') + ->limit(20) + ->get(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/product-record/recent', summary: '前台最近整机记录', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function productRecentRecords(Request $request): Response + { + $page = max(1, (int) $request->input('page', 1)); + $limit = max(1, min(50, (int) $request->input('limit', 10))); + + $query = ProductRecord::query()->with(['process', 'step', 'status']); + $this->applyProductRecordFilter($query, $request); + + $total = (clone $query)->count(); + $items = $query->orderByDesc('created_at')->orderByDesc('id') + ->forPage($page, $limit) + ->get(); + + return Json::success('获取成功', compact('items', 'total')); + } + + #[OA\Get(path: '/produce/product-record/statistics', summary: '前台整机统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function productStatistics(Request $request): Response + { + $query = ProductRecord::query(); + $this->applyProductRecordFilter($query, $request); + + $todayTotal = (int) (clone $query)->sum('quantity'); + $successTotal = (int) (clone $query)->where('scan_result', 1)->sum('quantity'); + $failedTotal = (int) (clone $query)->where('scan_result', 0)->sum('quantity'); + + return Json::success('获取成功', [ + 'today_total' => $todayTotal, + 'success_total' => $successTotal, + 'failed_total' => $failedTotal, + ]); + } + + #[OA\Get(path: '/produce/product-record/user-code-stats', summary: '前台整机用户统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function productUserCodeStats(Request $request): Response + { + $query = ProductRecord::query() + ->selectRaw('user_code, operator, SUM(quantity) as count'); + $this->applyProductRecordFilter($query, $request); + + $items = $query->groupBy('user_code', 'operator') + ->orderByDesc('count') + ->limit(20) + ->get(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/member-user/check', summary: '校验用户编码对应会员', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function checkMemberUser(Request $request): Response + { + $userCode = trim((string) $request->input('user_code', '')); + if ($userCode === '') { + return Json::fail('用户编码不能为空'); + } + + $member = Member::query() + ->where('username', $userCode) + ->first(['id', 'username', 'nickname']); + + if (!$member) { + return Json::fail('用户编码不存在'); + } + + return Json::success('校验成功', [ + 'id' => (int) $member->id, + 'username' => (string) $member->username, + 'nickname' => (string) ($member->nickname ?: $member->username), + ]); + } + + #[OA\Post(path: '/produce/scan-data/record', summary: '前台提交扫码记录', tags: ['produce-web'])] + #[SimpleResponse(description: '录入成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function record(Request $request): Response + { + $data = $request->post(); + if (!$this->validate->scene('store')->check($data)) { + return Json::fail($this->validate->getError()); + } + + $userCode = trim((string) ($data['user_code'] ?? '')); + $member = Member::query() + ->where('username', $userCode) + ->first(); + + if (!$member) { + return Json::fail('用户编码不存在'); + } + + $processId = (string) ($data['process_id'] ?? ''); + $stepId = (string) ($data['step_id'] ?? ''); + $statusId = $data['status_id'] ?? null; + if (empty($statusId)) { + $statusId = StepStatus::query() + ->where('step_id', $stepId) + ->where('is_initial', 1) + ->value('id'); + } + + $entryMode = (string) ($data['entry_mode'] ?? 'order'); + $prerequisiteError = $this->validatePrerequisiteSteps( + $stepId, + (string) $data['order_no'] + ); + if ($prerequisiteError !== null) { + return Json::fail($prerequisiteError); + } + + // 一单多机仅增加按数量重复录入的能力,仍须遵守原有步骤规则。 + if ($entryMode !== 'multi_machine') { + $duplicateError = $this->validateRepeatScan( + $processId, + $stepId, + (string) $data['order_no'] + ); + if ($duplicateError !== null) { + return Json::fail($duplicateError); + } + } + + $quantity = max(1, (int) ($data['quantity'] ?? 1)); + $now = time(); + $result = Db::transaction(function () use ($data, $quantity, $userCode, $member, $processId, $stepId, $statusId, $request, $now) { + $quantityError = $this->validateOrderStepQuantity( + (string) $data['order_no'], + $processId, + $stepId, + $quantity, + (int) $data['scan_result'] === 1 + ); + if ($quantityError !== null) { + return ['error' => $quantityError]; + } + + $scanData = ScanData::query()->create([ + 'order_no' => (string) $data['order_no'], + 'quantity' => $quantity, + 'user_code' => $userCode, + 'operator' => (string) ($member->nickname ?: $member->username), + 'operator_id' => (int) $member->id, + 'process_id' => $processId === '' ? null : $processId, + 'step_id' => $stepId === '' ? null : $stepId, + 'status_id' => $statusId ? (int) $statusId : null, + 'scan_result' => (int) $data['scan_result'], + 'scan_time' => $now, + 'remark' => (string) ($data['remark'] ?? ''), + ]); + + $this->handleNextStepTransitions($scanData, $request, $member, $now); + + return ['model' => $scanData]; + }); + + if (isset($result['error'])) { + return Json::fail($result['error']); + } + + return Json::success('录入成功', ['id' => $result['model']->id]); + } + + #[OA\Post(path: '/produce/product-record/record', summary: '前台提交整机记录', tags: ['produce-web'])] + #[SimpleResponse(description: '录入成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function productRecord(Request $request): Response + { + $data = $request->post(); + if (!$this->productRecordValidate?->scene('store')->check($data)) { + return Json::fail($this->productRecordValidate?->getError() ?: '整机录入数据校验失败'); + } + + $userCode = trim((string) ($data['user_code'] ?? '')); + $member = Member::query() + ->where('username', $userCode) + ->first(); + + if (!$member) { + return Json::fail('用户编码不存在'); + } + + $processId = (string) ($data['process_id'] ?? ''); + $stepId = (string) ($data['step_id'] ?? ''); + $statusId = $data['status_id'] ?? null; + if (empty($statusId) && $stepId !== '') { + $statusId = StepStatus::query() + ->where('step_id', $stepId) + ->where('is_initial', 1) + ->value('id'); + } + + $model = Db::transaction(function () use ($data, $userCode, $member, $processId, $stepId, $statusId) { + return ProductRecord::query()->create([ + 'process_order_no' => trim((string) ($data['process_order_no'] ?? '')), + 'product_code' => trim((string) ($data['product_code'] ?? '')), + 'quantity' => max(1, (int) ($data['quantity'] ?? 1)), + 'user_code' => $userCode, + 'operator' => (string) ($member->nickname ?: $member->username), + 'operator_id' => (int) $member->id, + 'process_id' => $processId === '' ? null : $processId, + 'step_id' => $stepId === '' ? null : $stepId, + 'status_id' => $statusId ? (int) $statusId : null, + 'scan_result' => (int) ($data['scan_result'] ?? 1), + 'remark' => (string) ($data['remark'] ?? ''), + ]); + }); + + return Json::success('录入成功', ['id' => $model->id]); + } + + #[OA\Get(path: '/produce/exception/settings', summary: '获取异常设置', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function exceptionSettings(Request $request): Response + { + $startAt = time() - 30 * 24 * 60 * 60; + $issueFrequency = ExceptionRecord::query() + ->where('reported_at', '>=', $startAt) + ->whereNotNull('issue_id') + ->selectRaw('issue_id, COUNT(*) AS frequency') + ->groupBy('issue_id') + ->get() + ->mapWithKeys(static function (ExceptionRecord $record): array { + return [(string) $record->issue_id => (int) $record->frequency]; + }) + ->all(); + + $items = ExceptionSetting::query() + ->where('is_enabled', 1) + ->orderBy('setting_type') + ->orderBy('parent_id') + ->orderBy('sort') + ->orderBy('id') + ->get(['id', 'parent_id', 'setting_type', 'code', 'name', 'sort']) + ->map(static function (ExceptionSetting $item) use ($issueFrequency): array { + return [ + 'id' => (string) $item->id, + 'parent_id' => $item->parent_id === null ? null : (string) $item->parent_id, + 'setting_type' => $item->setting_type, + 'code' => $item->code, + 'name' => $item->name, + 'sort' => (int) $item->sort, + 'frequency' => $item->setting_type === 'issue' + ? ($issueFrequency[(string) $item->id] ?? 0) + : 0, + ]; + }) + ->values() + ->all(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/exception/statistics', summary: '获取异常统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function exceptionStatistics(Request $request): Response + { + $query = ExceptionRecord::query(); + $this->applyExceptionRecordFilter($query, $request); + + return Json::success('获取成功', [ + 'pending' => (clone $query)->where('status', 0)->count(), + 'review' => (clone $query)->where('status', 1)->count(), + 'done' => (clone $query)->where('status', 2)->count(), + ]); + } + + #[OA\Get(path: '/produce/exception/recent', summary: '获取最近异常记录', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function recentExceptions(Request $request): Response + { + $limit = max(1, min(20, (int) $request->input('limit', 4))); + $query = ExceptionRecord::query(); + $this->applyExceptionRecordFilter($query, $request); + + $items = $query->orderByDesc('reported_at')->orderByDesc('id') + ->limit($limit) + ->get(); + + return Json::success('获取成功', $items); + } + + #[OA\Get(path: '/produce/query/records', summary: '查询生产记录', tags: ['produce-web-query'])] + #[DataResponse(description: '查询成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function queryRecords(Request $request): Response + { + $recordType = trim((string) $request->input('record_type', 'scan')); + $orderNo = trim((string) $request->input('order_no', '')); + $keyword = trim((string) $request->input('keyword', '')); + $userCode = trim((string) $request->input('user_code', '')); + $status = $request->input('status', ''); + $scanResult = $request->input('scan_result', ''); + $page = max(1, (int) $request->input('page', 1)); + $limit = max(1, min(100, (int) $request->input('limit', 20))); + $startAt = $this->parseQueryDate($request->input('start_date')); + $endAt = $this->parseQueryDate($request->input('end_date'), true); + + if (!in_array($recordType, ['scan', 'product', 'exception', 'repair'], true)) { + return Json::fail('查询类型不正确'); + } + + $query = match ($recordType) { + 'scan' => ScanData::query()->with(['process', 'step', 'status']), + 'product' => ProductRecord::query()->with(['process', 'step', 'status']), + 'exception' => ExceptionRecord::query(), + 'repair' => RepairRecord::query()->with('exception'), + }; + + if ($recordType === 'scan') { + $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); + $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status_id', (int) $status)); + $query->when($scanResult !== '' && $scanResult !== null, fn ($builder) => $builder->where('scan_result', (int) $scanResult)); + $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { + $nested->where('order_no', 'like', "%{$keyword}%") + ->orWhere('user_code', 'like', "%{$keyword}%") + ->orWhere('operator', 'like', "%{$keyword}%") + ->orWhere('remark', 'like', "%{$keyword}%"); + })); + $query->when($startAt !== null, fn ($builder) => $builder->where('scan_time', '>=', $startAt)); + $query->when($endAt !== null, fn ($builder) => $builder->where('scan_time', '<=', $endAt)); + $query->orderByDesc('scan_time')->orderByDesc('id'); + } elseif ($recordType === 'product') { + $query->when($orderNo !== '', fn ($builder) => $builder->where('process_order_no', $orderNo)); + $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status_id', (int) $status)); + $query->when($scanResult !== '' && $scanResult !== null, fn ($builder) => $builder->where('scan_result', (int) $scanResult)); + $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { + $nested->where('process_order_no', 'like', "%{$keyword}%") + ->orWhere('product_code', 'like', "%{$keyword}%") + ->orWhere('user_code', 'like', "%{$keyword}%") + ->orWhere('operator', 'like', "%{$keyword}%") + ->orWhere('remark', 'like', "%{$keyword}%"); + })); + $query->when($startAt !== null, fn ($builder) => $builder->where('created_at', '>=', $startAt)); + $query->when($endAt !== null, fn ($builder) => $builder->where('created_at', '<=', $endAt)); + $query->orderByDesc('created_at')->orderByDesc('id'); + } elseif ($recordType === 'exception') { + $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); + $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status', (int) $status)); + $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { + $nested->where('order_no', 'like', "%{$keyword}%") + ->orWhere('user_code', 'like', "%{$keyword}%") + ->orWhere('reporter', 'like', "%{$keyword}%") + ->orWhere('type_name', 'like', "%{$keyword}%") + ->orWhere('reason_name', 'like', "%{$keyword}%") + ->orWhere('issue_name', 'like', "%{$keyword}%") + ->orWhere('remark', 'like', "%{$keyword}%"); + })); + $query->when($startAt !== null, fn ($builder) => $builder->where('reported_at', '>=', $startAt)); + $query->when($endAt !== null, fn ($builder) => $builder->where('reported_at', '<=', $endAt)); + $query->orderByDesc('reported_at')->orderByDesc('id'); + } else { + $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); + $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { + $nested->where('repair_no', 'like', "%{$keyword}%") + ->orWhere('order_no', 'like', "%{$keyword}%") + ->orWhere('product_code', 'like', "%{$keyword}%") + ->orWhere('product_name', 'like', "%{$keyword}%") + ->orWhere('operator', 'like', "%{$keyword}%") + ->orWhere('remark', 'like', "%{$keyword}%"); + })); + $query->when($startAt !== null, fn ($builder) => $builder->where('repaired_at', '>=', $startAt)); + $query->when($endAt !== null, fn ($builder) => $builder->where('repaired_at', '<=', $endAt)); + $query->orderByDesc('repaired_at')->orderByDesc('id'); + } + + $total = (clone $query)->count(); + $items = $query->forPage($page, $limit)->get()->map(fn ($item): array => $this->formatQueryRecord($recordType, $item))->values()->all(); + + return Json::success('查询成功', compact('items', 'total', 'page', 'limit')); + } + + #[OA\Get(path: '/produce/query/timeline', summary: '查询订单生产时间线', tags: ['produce-web-query'])] + #[DataResponse(description: '查询成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function queryTimeline(Request $request): Response + { + $orderNo = trim((string) $request->input('order_no', '')); + if ($orderNo === '') { + return Json::fail('订单号不能为空'); + } + + $timeline = collect(); + ScanData::query()->with(['process', 'step', 'status'])->where('order_no', $orderNo)->get()->each(function (ScanData $item) use ($timeline): void { + $timeline->push($this->formatTimelineRecord('scan', $item, (int) $item->scan_time)); + }); + ProductRecord::query()->with(['process', 'step', 'status'])->where('process_order_no', $orderNo)->get()->each(function (ProductRecord $item) use ($timeline): void { + $timeline->push($this->formatTimelineRecord('product', $item, (int) $item->created_at)); + }); + ExceptionRecord::query()->where('order_no', $orderNo)->get()->each(function (ExceptionRecord $item) use ($timeline): void { + $timeline->push($this->formatTimelineRecord('exception', $item, (int) $item->reported_at)); + }); + RepairRecord::query()->with('exception')->where('order_no', $orderNo)->get()->each(function (RepairRecord $item) use ($timeline): void { + $timeline->push($this->formatTimelineRecord('repair', $item, (int) $item->repaired_at ?: (int) $item->created_at)); + }); + + $items = $timeline->sortBy(fn (array $item): array => [$item['event_at'], (string) $item['id']])->values()->all(); + + return Json::success('查询成功', [ + 'order_no' => $orderNo, + 'total' => count($items), + 'items' => $items, + ]); + } + + #[OA\Get(path: '/produce/exception/order-check', summary: '校验异常提报单号', tags: ['produce-web'])] + #[DataResponse(description: '校验成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function checkExceptionOrder(Request $request): Response + { + $orderNo = trim((string) $request->input('order_no', '')); + if ($orderNo === '') { + return Json::fail('单号或条码不能为空'); + } + + $reference = $this->findExceptionOrderReference($orderNo); + if ($reference === null) { + return Json::fail('未找到对应订单或整机记录,不能提报异常'); + } + + return Json::success('校验成功', $reference); + } + + #[OA\Get(path: '/produce/repair-record/tasks', summary: '获取待维修任务列表', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function repairTasks(Request $request): Response + { + $limit = max(1, min(100, (int) $request->input('limit', 50))); + $tasks = ExceptionRecord::query() + ->where('status', 0) + ->orderBy('reported_at') + ->orderBy('id') + ->limit($limit) + ->get(); + + return Json::success('获取成功', $tasks->map(static fn (ExceptionRecord $task): array => [ + 'id' => (string) $task->id, + 'device_no' => (string) $task->order_no, + 'type_name' => (string) $task->type_name, + 'reason_name' => (string) $task->reason_name, + 'issue_name' => (string) ($task->issue_name ?? ''), + 'reporter' => (string) ($task->reporter ?: $task->user_code), + 'user_code' => (string) $task->user_code, + 'reported_date' => $task->reported_date, + 'status' => (int) $task->status, + 'status_name' => $task->status_name, + ])->values()->all()); + } + + #[OA\Get(path: '/produce/repair-record/statistics', summary: '获取维修统计', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function repairStatistics(Request $request): Response + { + $query = RepairRecord::query(); + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = $todayStart + 86400; + + return Json::success('获取成功', [ + 'pending' => ExceptionRecord::query()->where('status', 0)->count(), + 'processing' => ExceptionRecord::query()->where('status', 1)->count(), + 'completed_today' => (clone $query)->whereBetween('repaired_at', [$todayStart, $todayEnd - 1])->count(), + ]); + } + + #[OA\Get(path: '/produce/exception/repair-task', summary: '获取维修异常任务', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function repairTask(Request $request): Response + { + $deviceNo = trim((string) $request->input('device_no', '')); + if ($deviceNo === '') { + return Json::fail('设备编码或订单号不能为空'); + } + + $task = ExceptionRecord::query() + ->where('order_no', $deviceNo) + ->whereIn('status', [0, 1]) + ->orderByDesc('reported_at') + ->orderByDesc('id') + ->first(); + if (!$task) { + return Json::fail('未找到待维修的异常任务'); + } + + return Json::success('获取成功', [ + 'id' => (string) $task->id, + 'device_no' => (string) $task->order_no, + 'type_name' => (string) $task->type_name, + 'reason_name' => (string) $task->reason_name, + 'issue_name' => (string) ($task->issue_name ?? ''), + 'reporter' => (string) ($task->reporter ?: $task->user_code), + 'user_code' => (string) $task->user_code, + 'reported_date' => $task->reported_date, + 'status_name' => $task->status_name, + ]); + } + + #[OA\Post(path: '/produce/repair-record/record', summary: '提交维修记录', tags: ['produce-web'])] + #[SimpleResponse(description: '提交成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function repairRecord(Request $request, RepairRecordService $repairRecordService): Response + { + $data = $request->post(); + $exceptionId = trim((string) ($data['exception_id'] ?? '')); + $userCode = trim((string) ($data['user_code'] ?? '')); + $repairResult = trim((string) ($data['repair_result'] ?? '')); + $categoryIds = array_values(array_unique(array_filter(array_map(static fn ($id): string => trim((string) $id), (array) ($data['category_ids'] ?? $data['category_id'] ?? [])), static fn (string $id): bool => $id !== ''))); + $parts = $data['parts'] ?? []; + $images = array_values(array_filter((array) ($data['images'] ?? []), static function ($image): bool { + return is_string($image) && preg_match('/^https?:\/\//', $image) === 1; + })); + $operationFaultSteps = $data['operation_fault_steps'] ?? []; + $responsibleOperators = $data['responsible_operators'] ?? []; + + if ($exceptionId === '') { + return Json::fail('请先加载维修任务'); + } + if ($userCode === '') { + return Json::fail('维修人工号不能为空'); + } + if (!in_array($repairResult, ['material_defect', 'operation_defect', 'assembly_mismatch'], true)) { + return Json::fail('请选择有效的维修判定'); + } + if ($repairResult === 'assembly_mismatch' && !$categoryIds) { + return Json::fail('请选择搭配错误分类'); + } + if (!is_array($parts)) { + return Json::fail('更换备件格式不正确'); + } + if (!is_array($operationFaultSteps) || !is_array($responsibleOperators)) { + return Json::fail('作业不良责任信息格式不正确'); + } + + $member = Member::query()->where('username', $userCode)->first(); + if (!$member) { + return Json::fail('用户编码不存在'); + } + + try { + $record = Db::transaction(function () use ($exceptionId, $repairResult, $categoryIds, $parts, $images, $operationFaultSteps, $responsibleOperators, $data, $member, $repairRecordService) { + $exception = ExceptionRecord::query() + ->lockForUpdate() + ->whereKey($exceptionId) + ->whereIn('status', [0, 1]) + ->first(); + if (!$exception) { + throw new \InvalidArgumentException('维修任务不存在或已处理'); + } + + $partSnapshots = []; + $operationFaultStepSnapshots = []; + $responsibleOperatorSnapshots = []; + $primaryFault = null; + if ($repairResult === 'assembly_mismatch') { + $selectedCategories = MaterialCategory::query()->whereIn('id', $categoryIds)->get(['id', 'category_name'])->keyBy('id'); + if ($selectedCategories->count() !== count($categoryIds)) { + throw new \InvalidArgumentException('搭配错误分类不存在或已删除'); + } + } + if ($repairResult === 'operation_defect') { + $scanIds = array_values(array_unique(array_filter(array_merge($operationFaultSteps, $responsibleOperators), static fn ($id): bool => (string) $id !== ''))); + if (!$operationFaultSteps || !$responsibleOperators) { + throw new \InvalidArgumentException('请选择作业不良责任步骤和责任人员'); + } + if (!$scanIds) { + throw new \InvalidArgumentException('作业不良责任信息不能为空'); + } + $scanRecords = ScanData::query() + ->with(['process', 'step']) + ->where('order_no', $exception->order_no) + ->whereIn('id', $scanIds) + ->get() + ->keyBy('id'); + if ($scanRecords->count() !== count($scanIds)) { + throw new \InvalidArgumentException('责任步骤或责任人员不属于当前维修订单'); + } + foreach (array_values(array_unique(array_filter($operationFaultSteps, static fn ($id): bool => (string) $id !== ''))) as $scanId) { + $scan = $scanRecords->get($scanId); + $operationFaultStepSnapshots[] = [ + 'scan_id' => (string) $scan->id, + 'process_id' => (string) $scan->process_id, + 'process_name' => (string) $scan->process_name, + 'step_id' => (string) $scan->step_id, + 'step_name' => (string) $scan->step_name, + ]; + } + foreach (array_values(array_unique(array_filter($responsibleOperators, static fn ($id): bool => (string) $id !== ''))) as $scanId) { + $scan = $scanRecords->get($scanId); + $responsibleOperatorSnapshots[] = [ + 'scan_id' => (string) $scan->id, + 'operator_id' => (string) ($scan->operator_id ?? ''), + 'operator' => (string) $scan->operator, + 'user_code' => (string) $scan->user_code, + 'process_id' => (string) $scan->process_id, + 'process_name' => (string) $scan->process_name, + 'step_id' => (string) $scan->step_id, + 'step_name' => (string) $scan->step_name, + ]; + } + } + foreach ($parts as $part) { + if (!is_array($part)) { + throw new \InvalidArgumentException('更换备件格式不正确'); + } + $productCode = trim((string) ($part['product_code'] ?? $part['code'] ?? '')); + if ($productCode === '') { + throw new \InvalidArgumentException('更换备件商品编码不能为空'); + } + $material = Material::query()->where('product_code', $productCode)->first(); + if (!$material) { + throw new \InvalidArgumentException(sprintf('备件商品编码不存在:%s', $productCode)); + } + $source = trim((string) ($part['source'] ?? 'new')); + if (!in_array($source, ['new', 'after_sales', 'refurbished'], true)) { + throw new \InvalidArgumentException('备件来源不正确'); + } + $quantity = (int) ($part['quantity'] ?? 1); + if ($quantity < 1 || $quantity > 999) { + throw new \InvalidArgumentException('备件数量应在 1 到 999 之间'); + } + $faultIds = array_values(array_unique(array_filter((array) ($part['fault_description_ids'] ?? $part['selected_fault_point_ids'] ?? $part['selectedFaultPointIds'] ?? []), static fn ($id): bool => (string) $id !== ''))); + $faults = collect(); + if ($faultIds) { + $faults = HardwareFaultDescription::query() + ->whereIn('id', $faultIds) + ->where('category_id', $material->category_id) + ->get(['id', 'category_id', 'description']) + ->keyBy('id'); + if ($faults->count() !== count($faultIds)) { + throw new \InvalidArgumentException('备件故障点不存在或与物料分类不匹配'); + } + $primaryFault ??= $faults->first(); + } + $partSnapshots[] = [ + 'product_code' => (string) $material->product_code, + 'product_name' => (string) $material->product_name, + 'category_id' => (string) $material->category_id, + 'category_name' => (string) $material->category_name, + 'source' => $source, + 'quantity' => $quantity, + 'fault_descriptions' => $faults->map(static fn (HardwareFaultDescription $fault): array => [ + 'id' => (string) $fault->id, + 'description' => (string) $fault->description, + ])->values()->all(), + ]; + } + + $now = time(); + $datePrefix = date('Ymd', $now); + $sequence = RepairRecord::query() + ->withTrashed() + ->where('repair_no', 'like', sprintf('RP%s%%', $datePrefix)) + ->lockForUpdate() + ->count() + 1; + if ($sequence > 9999) { + throw new \RuntimeException('当天维修单号已超过9999条,无法生成新的维修单号'); + } + $repairNo = sprintf('RP%s%04d', $datePrefix, $sequence); + $repair = $repairRecordService->save([ + 'repair_no' => $repairNo, + 'exception_id' => (string) $exception->id, + 'exception_reason' => (string) $exception->reason_name, + 'exception_issue' => (string) ($exception->issue_name ?? ''), + 'order_no' => (string) $exception->order_no, + 'category_id' => $repairResult === 'assembly_mismatch' ? $categoryIds[0] : $primaryFault?->category_id, + 'category_ids' => $repairResult === 'assembly_mismatch' ? $categoryIds : null, + 'fault_description_id' => $primaryFault?->id, + 'repair_result' => $repairResult, + 'parts' => $partSnapshots, + 'images' => $images, + 'operation_fault_steps' => $operationFaultStepSnapshots, + 'responsible_operators' => $responsibleOperatorSnapshots, + 'operator' => (string) ($member->nickname ?: $member->username), + 'repaired_at' => $now, + 'remark' => trim((string) ($data['remark'] ?? '')), + ]); + + $exception->fill([ + 'status' => 2, + 'handler' => (string) ($member->nickname ?: $member->username), + 'handler_id' => $member->id, + 'handled_at' => $now, + ])->save(); + + return $repair; + }); + } catch (\Throwable $e) { + return Json::fail($e->getMessage()); + } + + return Json::success('提交成功', ['id' => (string) $record->id, 'repair_no' => $record->repair_no]); + } + + #[OA\Post(path: '/produce/exception/record', summary: '提交异常记录', tags: ['produce-web'])] + #[SimpleResponse(description: '提交成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function exceptionRecord(Request $request): Response + { + $data = $request->post(); + $orderNo = trim((string) ($data['order_no'] ?? '')); + $userCode = trim((string) ($data['user_code'] ?? '')); + $typeId = (string) ($data['type_id'] ?? ''); + $reasonId = (string) ($data['reason_id'] ?? ''); + $issueId = (string) ($data['issue_id'] ?? ''); + + if ($orderNo === '') { + return Json::fail('单号或条码不能为空'); + } + if ($userCode === '') { + return Json::fail('提报人工号不能为空'); + } + + if ($this->findExceptionOrderReference($orderNo) === null) { + return Json::fail('未找到对应订单或整机记录,不能提报异常'); + } + + $member = Member::query()->where('username', $userCode)->first(); + if (!$member) { + return Json::fail('用户编码不存在'); + } + + $type = ExceptionSetting::query() + ->whereKey($typeId) + ->where('setting_type', 'type') + ->where('is_enabled', 1) + ->first(); + if (!$type) { + return Json::fail('请选择有效的处理类型'); + } + + $reason = ExceptionSetting::query() + ->whereKey($reasonId) + ->where('parent_id', $type->id) + ->where('setting_type', 'reason') + ->where('is_enabled', 1) + ->first(); + if (!$reason) { + return Json::fail('请选择有效的异常原因'); + } + + $issue = null; + if ($issueId !== '') { + $issue = ExceptionSetting::query() + ->whereKey($issueId) + ->where('parent_id', $reason->id) + ->where('setting_type', 'issue') + ->where('is_enabled', 1) + ->first(); + if (!$issue) { + return Json::fail('具体问题与异常原因不匹配'); + } + } + + $images = array_values(array_filter((array) ($data['images'] ?? []), static function ($image): bool { + return is_string($image) && preg_match('/^https?:\/\//', $image) === 1; + })); + $record = ExceptionRecord::query()->create([ + 'order_no' => $orderNo, + 'user_code' => $userCode, + 'reporter' => (string) ($member->nickname ?: $member->username), + 'reporter_id' => (int) $member->id, + 'type_id' => $type->id, + 'type_code' => $type->code, + 'type_name' => $type->name, + 'reason_id' => $reason->id, + 'reason_code' => $reason->code, + 'reason_name' => $reason->name, + 'issue_id' => $issue?->id, + 'issue_code' => $issue?->code, + 'issue_name' => $issue?->name, + 'images' => $images, + 'remark' => trim((string) ($data['remark'] ?? '')), + 'status' => 0, + 'reported_at' => time(), + ]); + + return Json::success('提交成功', ['id' => $record->id]); + } + + /** + * 查找可用于异常提报的订单或整机记录。 + */ + private function findExceptionOrderReference(string $orderNo): ?array + { + $scanData = ScanData::query() + ->where('order_no', $orderNo) + ->orderByDesc('scan_time') + ->first(['id', 'order_no', 'process_id', 'step_id', 'scan_time']); + if ($scanData) { + return [ + 'reference_type' => 'order', + 'reference_name' => '订单记录', + 'reference_id' => (string) $scanData->id, + 'order_no' => (string) $scanData->order_no, + 'process_id' => $scanData->process_id === null ? null : (string) $scanData->process_id, + 'step_id' => $scanData->step_id === null ? null : (string) $scanData->step_id, + ]; + } + + $productRecord = ProductRecord::query() + ->where(function ($query) use ($orderNo) { + $query->where('process_order_no', $orderNo) + ->orWhere('product_code', $orderNo); + }) + ->orderByDesc('created_at') + ->first(['id', 'process_order_no', 'product_code', 'process_id', 'step_id']); + if ($productRecord) { + return [ + 'reference_type' => 'product', + 'reference_name' => '整机记录', + 'reference_id' => (string) $productRecord->id, + 'order_no' => (string) ($productRecord->process_order_no ?: $productRecord->product_code), + 'process_id' => $productRecord->process_id === null ? null : (string) $productRecord->process_id, + 'step_id' => $productRecord->step_id === null ? null : (string) $productRecord->step_id, + ]; + } + + return null; + } + + private function parseQueryDate(mixed $value, bool $endOfDay = false): ?int + { + $date = trim((string) $value); + if ($date === '') { + return null; + } + + try { + $parsed = new \DateTimeImmutable($date); + if ($endOfDay && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1) { + $parsed = $parsed->setTime(23, 59, 59); + } + return $parsed->getTimestamp(); + } catch (\Throwable) { + return null; + } + } + + private function formatQueryRecord(string $recordType, object $item): array + { + return match ($recordType) { + 'scan' => [ + 'id' => (string) $item->id, + 'record_type' => 'scan', + 'record_type_name' => '扫码记录', + 'order_no' => (string) $item->order_no, + 'product_code' => '', + 'process_name' => (string) $item->process_name, + 'step_name' => (string) $item->step_name, + 'status_name' => (string) $item->status_name, + 'result_name' => (int) $item->scan_result === 1 ? '成功' : '失败', + 'operator' => (string) $item->operator, + 'user_code' => (string) $item->user_code, + 'quantity' => (int) $item->quantity, + 'event_at' => (int) $item->scan_time, + 'event_date' => $item->scan_date, + 'remark' => (string) $item->remark, + ], + 'product' => [ + 'id' => (string) $item->id, + 'record_type' => 'product', + 'record_type_name' => '整机记录', + 'order_no' => (string) $item->process_order_no, + 'product_code' => (string) $item->product_code, + 'process_name' => (string) $item->process_name, + 'step_name' => (string) $item->step_name, + 'status_name' => (string) $item->status_name, + 'result_name' => (int) $item->scan_result === 1 ? '成功' : '失败', + 'operator' => (string) $item->operator, + 'user_code' => (string) $item->user_code, + 'quantity' => (int) $item->quantity, + 'event_at' => (int) $item->created_at, + 'event_date' => $item->created_date, + 'remark' => (string) $item->remark, + ], + 'exception' => [ + 'id' => (string) $item->id, + 'record_type' => 'exception', + 'record_type_name' => '异常提报', + 'order_no' => (string) $item->order_no, + 'product_code' => '', + 'process_name' => '', + 'step_name' => '', + 'status_name' => (string) $item->status_name, + 'result_name' => (string) $item->type_name, + 'operator' => (string) $item->reporter, + 'user_code' => (string) $item->user_code, + 'quantity' => null, + 'exception_reason' => (string) $item->reason_name, + 'exception_content' => (string) $item->issue_name, + 'event_at' => (int) $item->reported_at, + 'event_date' => $item->reported_date, + 'remark' => (string) $item->remark, + ], + default => [ + 'id' => (string) $item->id, + 'record_type' => 'repair', + 'record_type_name' => '维修记录', + 'order_no' => (string) $item->order_no, + 'product_code' => (string) $item->product_code, + 'process_name' => '', + 'step_name' => '', + 'status_name' => '已维修', + 'result_name' => (string) $item->repair_result_name, + 'operator' => (string) $item->operator, + 'user_code' => '', + 'quantity' => null, + 'material_category' => (string) ($item->category_name ?: $item->category_names), + 'fault_description' => (string) ($item->fault_description_name ?: $item->fault_description), + 'event_at' => (int) ($item->repaired_at ?: $item->created_at), + 'event_date' => $item->repaired_date ?: $item->created_date, + 'remark' => (string) $item->remark, + ], + }; + } + + private function formatTimelineRecord(string $recordType, object $item, int $eventAt): array + { + $record = $this->formatQueryRecord($recordType, $item); + $record['event_at'] = $eventAt; + $record['event_date'] = $eventAt > 0 ? date('Y-m-d H:i:s', $eventAt) : null; + return $record; + } + + protected function applyExceptionRecordFilter($query, Request $request): void + { + $userCode = trim((string) $request->input('user_code', '')); + if ($userCode !== '') { + $query->where('user_code', $userCode); + } + } + + protected function applyScanDataFilter($query, Request $request): void + { + $orderNo = trim((string) $request->input('order_no', '')); + $userCode = trim((string) $request->input('user_code', '')); + $processId = (int) $request->input('process_id', 0); + $stepId = (int) $request->input('step_id', 0); + $statusId = (int) $request->input('status_id', 0); + $scanResult = $request->input('scan_result', ''); + + if ($orderNo !== '') { + $query->where('order_no', $orderNo); + } + if ($userCode !== '') { + $query->where('user_code', $userCode); + } + if ($processId > 0) { + $query->where('process_id', $processId); + } + if ($stepId > 0) { + $query->where('step_id', $stepId); + } + if ($statusId > 0) { + $query->where('status_id', $statusId); + } + if ($scanResult !== '' && $scanResult !== null) { + $query->where('scan_result', (int) $scanResult); + } + + $recentOnly = (int) ProduceConfig::query() + ->where('group_code', 'basic_setting') + ->where('code', 'recent_records_today_only') + ->value('content'); + + if ($recentOnly === 1 && $orderNo === '') { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + $query->whereBetween('scan_time', [$start, $end]); + } + } + + protected function applyProductRecordFilter($query, Request $request): void + { + $processId = (int) $request->input('process_id', 0); + $stepId = (int) $request->input('step_id', 0); + $statusId = (int) $request->input('status_id', 0); + $scanResult = $request->input('scan_result', ''); + + if ($processId > 0) { + $query->where('process_id', $processId); + } + if ($stepId > 0) { + $query->where('step_id', $stepId); + } + if ($statusId > 0) { + $query->where('status_id', $statusId); + } + if ($scanResult !== '' && $scanResult !== null) { + $query->where('scan_result', (int) $scanResult); + } + + $recentOnly = (int) ProduceConfig::query() + ->where('group_code', 'basic_setting') + ->where('code', 'recent_records_today_only') + ->value('content'); + + if ($recentOnly === 1) { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + $query->whereBetween('created_at', [$start, $end]); + } + } + + protected function getConfigGroupData(string $groupCode): array + { + $items = ProduceConfig::query() + ->where('group_code', $groupCode) + ->orderBy('sort') + ->orderBy('id') + ->get(); + + $data = []; + foreach ($items as $item) { + $data[$item->code] = $item->content; + } + + return $data; + } + + protected function handleNextStepTransitions(ScanData $currentScanData, Request $request, Member $member, int $now): void + { + $previousRelations = \plugin\produce\app\model\StepRelation::query() + ->where('to_step_id', (string) $currentScanData->step_id) + ->where('status', 1) + ->orderBy('priority', 'asc') + ->orderBy('sort', 'asc') + ->orderBy('id', 'asc') + ->get(); + + foreach ($previousRelations as $relation) { + $previousScanData = ScanData::query() + ->where('order_no', (string) $currentScanData->order_no) + ->where('step_id', (string) $relation->from_step_id) + ->orderByDesc('scan_time') + ->orderByDesc('id') + ->first(); + + if (!$previousScanData || empty($previousScanData->status_id)) { + continue; + } + + $transition = StatusTransition::query() + ->where('step_id', (string) $relation->from_step_id) + ->where('from_status_id', (string) $previousScanData->status_id) + ->where('trigger_event', 'next_step_scan') + ->where('auto_trigger', 1) + ->where('status', 1) + ->orderBy('priority', 'desc') + ->orderBy('sort', 'asc') + ->orderBy('id', 'asc') + ->first(); + + if (!$transition) { + continue; + } + + $startedAt = microtime(true); + $fromStatusId = (string) $previousScanData->status_id; + $toStatusId = (string) $transition->to_status_id; + $isSuccess = 1; + $errorMessage = ''; + + try { + $previousScanData->status_id = $transition->to_status_id; + $previousScanData->save(); + } catch (\Throwable $throwable) { + $isSuccess = 0; + $errorMessage = $throwable->getMessage(); + } + + TransitionLog::query()->create([ + 'id' => Snowflake::generate(), + 'work_order_id' => $previousScanData->id, + 'work_order_no' => (string) $currentScanData->order_no, + 'step_id' => (string) $previousScanData->step_id, + 'transition_id' => $transition->id, + 'from_status_id' => $fromStatusId === '' ? null : $fromStatusId, + 'to_status_id' => $toStatusId === '' ? null : $toStatusId, + 'trigger_event' => 'next_step_scan', + 'trigger_data' => [ + 'order_no' => (string) $currentScanData->order_no, + 'user_code' => (string) $currentScanData->user_code, + 'operator' => (string) $currentScanData->operator, + 'operator_id' => (int) ($currentScanData->operator_id ?? 0), + 'process_id' => (string) ($currentScanData->process_id ?? ''), + 'current_scan_data_id' => $currentScanData->id, + 'current_step_id' => (string) $currentScanData->step_id, + 'current_status_id' => (string) ($currentScanData->status_id ?? ''), + 'scan_result' => (int) ($currentScanData->scan_result ?? 0), + 'remark' => (string) ($currentScanData->remark ?? ''), + 'previous_scan_data_id' => $previousScanData->id, + 'previous_step_id' => (string) $previousScanData->step_id, + 'previous_status_id' => $fromStatusId, + 'relation_id' => $relation->id, + 'relation_type' => $relation->relation_type, + ], + 'trigger_user' => (string) ($member->nickname ?: $member->username), + 'trigger_ip' => $request->getRealIp(), + 'is_auto' => 1, + 'is_success' => $isSuccess, + 'error_message' => $errorMessage !== '' ? $errorMessage : null, + 'duration' => (int) round((microtime(true) - $startedAt) * 1000), + 'remark' => '下一工序扫码自动触发状态转换', + 'created_by' => (int) $member->id, + 'created_at' => $now, + ]); + } + } + + /** + * 订单首笔成功记录所在的工序步骤作为数量来源步骤:该步骤可以持续累加; + * 后续每个步骤的成功累计数量不得超过来源步骤的成功累计数量。 + * + * @param bool $isSuccess 失败记录不计入步骤累计数量 + */ + protected function validateOrderStepQuantity( + string $orderNo, + string $processId, + string $stepId, + int $quantity, + bool $isSuccess + ): ?string + { + if (!$isSuccess) { + return null; + } + + // 在事务内锁定该订单既有成功记录,避免并发提交时基于旧累计数量重复放行。 + $records = ScanData::query() + ->where('order_no', $orderNo) + ->where('scan_result', 1) + ->orderBy('scan_time') + ->orderBy('id') + ->lockForUpdate() + ->get(['id', 'quantity', 'process_id', 'step_id']); + + // 首笔成功记录建立数量来源步骤,本次可直接写入。 + if ($records->isEmpty()) { + return null; + } + + $sourceRecord = $records->first(); + $sourceProcessId = (string) $sourceRecord->process_id; + $sourceStepId = (string) $sourceRecord->step_id; + + // 数量来源步骤允许多人、多次持续累加,以其最新累计数量作为后续步骤的上限。 + if ($processId === $sourceProcessId && $stepId === $sourceStepId) { + return null; + } + + $sourceQuantity = (int) $records + ->filter(static function (ScanData $record) use ($sourceProcessId, $sourceStepId) { + return (string) $record->process_id === $sourceProcessId + && (string) $record->step_id === $sourceStepId; + }) + ->sum('quantity'); + $completedQuantity = (int) $records + ->filter(static function (ScanData $record) use ($processId, $stepId) { + return (string) $record->process_id === $processId + && (string) $record->step_id === $stepId; + }) + ->sum('quantity'); + + if ($completedQuantity > $sourceQuantity) { + return sprintf( + '数量来源步骤累计为 %d,当前步骤历史累计已达 %d,存在超报数据,请先由管理员更正后再继续录入', + $sourceQuantity, + $completedQuantity + ); + } + + if ($completedQuantity + $quantity > $sourceQuantity) { + return sprintf( + '数量来源步骤累计为 %d,当前步骤已累计 %d,本次最多可录入 %d', + $sourceQuantity, + $completedQuantity, + $sourceQuantity - $completedQuantity + ); + } + + return null; + } + + protected function validatePrerequisiteSteps(string $stepId, string $orderNo): ?string + { + $stepRule = ProcessStep::query() + ->whereKey((string) $stepId) + ->first(); + if (!$stepRule) { + return null; + } + + $prerequisiteStepIds = is_array($stepRule->prerequisite_steps) ? $stepRule->prerequisite_steps : []; + $prerequisiteMode = (string) ($stepRule->prerequisite_mode ?: 'all'); + $requireFinal = (int) ($stepRule->prerequisite_require_final ?? 1) === 1; + + if ($prerequisiteMode === 'none' || $prerequisiteStepIds === []) { + return null; + } + + $prerequisiteSteps = ProcessStep::query() + ->whereIn('id', $prerequisiteStepIds) + ->get(['id', 'step_name']) + ->keyBy('id'); + + $matchedSteps = []; + $unmatchedNames = []; + + foreach ($prerequisiteStepIds as $prerequisiteStepId) { + $matched = $this->hasSatisfiedPrerequisiteStep((string) $prerequisiteStepId, $orderNo, $requireFinal); + if ($matched) { + $matchedSteps[] = (string) $prerequisiteStepId; + continue; + } + + $unmatchedNames[] = (string) ($prerequisiteSteps->get($prerequisiteStepId)?->step_name ?? ('步骤ID:' . $prerequisiteStepId)); + } + + if ($prerequisiteMode === 'all' && $unmatchedNames !== []) { + return '前置步骤未完成:' . implode('、', $unmatchedNames); + } + + if ($prerequisiteMode === 'any' && $matchedSteps === []) { + return '前置步骤未满足,至少需要完成一个前置步骤'; + } + + return null; + } + + protected function hasSatisfiedPrerequisiteStep(string $stepId, string $orderNo, bool $requireFinal): bool + { + $query = ScanData::query() + ->where('order_no', $orderNo) + ->where('step_id', $stepId) + ->orderByDesc('scan_time') + ->orderByDesc('id'); + + if (!$requireFinal) { + return $query->exists(); + } + + return $query->whereHas('status', function ($builder) { + $builder->where('is_final', 1); + })->exists(); + } + + protected function validateRepeatScan(string $processId, string $stepId, string $orderNo): ?string + { + $latestRecord = ScanData::query() + ->where('order_no', $orderNo) + ->where('process_id', $processId) + ->where('step_id', $stepId) + ->orderByDesc('scan_time') + ->orderByDesc('id') + ->first(); + + if (!$latestRecord) { + return null; + } + + $repeatPolicy = $this->resolveRepeatPolicy($stepId, $orderNo); + if (!$repeatPolicy['allow_repeat']) { + return '当前配置不允许重复扫码'; + } + + $allowedStatuses = array_values(array_filter(array_map('strval', is_array($repeatPolicy['repeat_scan_statuses']) ? $repeatPolicy['repeat_scan_statuses'] : []), static fn ($value) => $value !== '')); + $currentStatusId = (string) ($latestRecord->status_id ?? ''); + if ($allowedStatuses !== [] && ($currentStatusId === '' || !in_array($currentStatusId, $allowedStatuses, true))) { + return '当前状态不允许重复扫码'; + } + + $interval = max(0, (int) $repeatPolicy['repeat_scan_interval']); + if ($interval > 0 && (time() - (int) $latestRecord->scan_time) < $interval) { + return '重复扫码间隔不能小于 ' . $interval . ' 秒'; + } + + return null; + } + + protected function resolveRepeatPolicy(string $stepId, string $orderNo): array + { + $basicSettings = $this->getConfigGroupData('basic_setting'); + $policy = [ + 'allow_repeat' => (int) ($basicSettings['global_allow_repeat'] ?? 0) === 1, + 'repeat_scan_interval' => max(0, (int) ($basicSettings['repeat_scan_interval'] ?? 0)), + 'repeat_scan_statuses' => [], + ]; + + $stepRule = ProcessStep::query() + ->whereKey((string) $stepId) + ->first(); + if ($stepRule) { + $policy['allow_repeat'] = (int) ($stepRule->allow_repeat_scan ?? 0) === 1; + $policy['repeat_scan_statuses'] = is_array($stepRule->repeat_scan_statuses) ? $stepRule->repeat_scan_statuses : []; + } + + $specificRule = SpecificRule::query() + ->where('is_enabled', 1) + ->orderBy('sort') + ->orderBy('id') + ->get() + ->first(function (SpecificRule $rule) use ($stepId, $orderNo) { + return $this->matchSpecificRule($rule, $stepId, [ + 'mode' => 'order', + 'order_no' => $orderNo, + ]); + }); + + if ($specificRule) { + $policy['allow_repeat'] = (int) $specificRule->rule_type === 1; + } + + return $policy; + } + + protected function matchSpecificRule(SpecificRule $rule, string $stepId, array|string $context): bool + { + $ruleStepIds = $rule->step_ids ?? []; + $stepMatched = empty($ruleStepIds) + || in_array((string) $stepId, array_map('strval', $ruleStepIds), true); + + if (!$stepMatched) { + return false; + } + + $normalizedContext = is_array($context) + ? $context + : [ + 'mode' => 'order', + 'order_no' => (string) $context, + ]; + + $ruleMode = (string) ($rule->apply_mode ?: 'order'); + $currentMode = (string) ($normalizedContext['mode'] ?? 'order'); + if ($ruleMode !== 'both' && $ruleMode !== $currentMode) { + return false; + } + + $matchField = (string) ($rule->match_field ?: 'order_no'); + $matchValue = trim((string) ($normalizedContext[$matchField] ?? '')); + + $pattern = (string) $rule->rule_pattern; + if ($pattern === '' || $matchValue === '') { + return false; + } + + return @preg_match('/' . str_replace('/', '\\/', $pattern) . '/', $matchValue) === 1; + } + + protected function initialize(): void + { + } +} diff --git a/backend/plugin/produce/app/appapi/controller/DataEntryController.php b/backend/plugin/produce/app/appapi/controller/DataEntryController.php new file mode 100644 index 0000000..17b6593 --- /dev/null +++ b/backend/plugin/produce/app/appapi/controller/DataEntryController.php @@ -0,0 +1,227 @@ +delegate->processOptions($request); + } + + #[OA\Get(path: '/produce/process-step', summary: 'APP 获取步骤下拉', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function stepOptions(Request $request): Response + { + return $this->delegate->stepOptions($request); + } + + #[OA\Get(path: '/produce/step-status', summary: 'APP 获取状态下拉', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function statusOptions(Request $request): Response + { + return $this->delegate->statusOptions($request); + } + + #[OA\Get(path: '/produce/material/lookup', summary: 'APP 按商品编码查询物料', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function materialLookup(Request $request): Response + { + return $this->delegate->materialLookup($request); + } + + #[OA\Get(path: '/produce/scanapp/material-category', summary: 'APP 获取物料分类选项', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function scanAppMaterialCategory(Request $request): Response + { + return $this->delegate->scanAppMaterialCategory($request); + } + + #[OA\Get(path: '/produce/hardware-fault-description/options', summary: 'APP 按物料分类获取硬件故障描述', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function hardwareFaultDescriptionOptions(Request $request): Response + { + return $this->delegate->hardwareFaultDescriptionOptions($request); + } + + #[OA\Get(path: '/produce/config/data-entry', summary: 'APP 获取扫码输入页配置', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function dataEntryConfig(Request $request): Response + { + return $this->delegate->dataEntryConfig($request); + } + + #[OA\Get(path: '/produce/member-user/check', summary: 'APP 校验用户编码对应会员', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function checkMemberUser(Request $request): Response + { + return $this->delegate->checkMemberUser($request); + } + + #[OA\Get(path: '/produce/query/records', summary: 'APP 查询生产记录', tags: ['produce-app'])] + #[DataResponse(description: '查询成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function queryRecords(Request $request): Response + { + return $this->delegate->queryRecords($request); + } + + #[OA\Get(path: '/produce/query/timeline', summary: 'APP 查询订单生产时间线', tags: ['produce-app'])] + #[DataResponse(description: '查询成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function queryTimeline(Request $request): Response + { + return $this->delegate->queryTimeline($request); + } + + #[OA\Get(path: '/produce/scan-data', summary: 'APP 最近订单扫码记录', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function recentRecords(Request $request): Response + { + return $this->delegate->recentRecords($request); + } + + #[OA\Get(path: '/produce/scan-data/statistics', summary: 'APP 订单扫码统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function statistics(Request $request): Response + { + return $this->delegate->statistics($request); + } + + #[OA\Get(path: '/produce/scan-data/user-code-stats', summary: 'APP 用户扫码统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function userCodeStats(Request $request): Response + { + return $this->delegate->userCodeStats($request); + } + + #[OA\Post(path: '/produce/scan-data/record', summary: 'APP 提交订单扫码记录', tags: ['produce-app'])] + #[SimpleResponse(description: '录入成功')] + public function record(Request $request): Response + { + return $this->delegate->record($request); + } + + #[OA\Get(path: '/produce/exception/settings', summary: 'APP 获取异常设置', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function exceptionSettings(Request $request): Response + { + return $this->delegate->exceptionSettings($request); + } + + #[OA\Get(path: '/produce/exception/statistics', summary: 'APP 获取异常统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function exceptionStatistics(Request $request): Response + { + return $this->delegate->exceptionStatistics($request); + } + + #[OA\Get(path: '/produce/exception/recent', summary: 'APP 获取最近异常记录', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function recentExceptions(Request $request): Response + { + return $this->delegate->recentExceptions($request); + } + + #[OA\Get(path: '/produce/exception/order-check', summary: 'APP 校验异常提报单号', tags: ['produce-app'])] + #[DataResponse(description: '校验成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function checkExceptionOrder(Request $request): Response + { + return $this->delegate->checkExceptionOrder($request); + } + + #[OA\Get(path: '/produce/repair-record/tasks', summary: 'APP 获取待维修任务列表', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function repairTasks(Request $request): Response + { + return $this->delegate->repairTasks($request); + } + + #[OA\Get(path: '/produce/repair-record/statistics', summary: 'APP 获取维修统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function repairStatistics(Request $request): Response + { + return $this->delegate->repairStatistics($request); + } + + #[OA\Get(path: '/produce/exception/repair-task', summary: 'APP 获取维修异常任务', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function repairTask(Request $request): Response + { + return $this->delegate->repairTask($request); + } + + #[OA\Post(path: '/produce/repair-record/record', summary: 'APP 提交维修记录', tags: ['produce-app'])] + #[SimpleResponse(description: '提交成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function repairRecord(Request $request, RepairRecordService $repairRecordService): Response + { + return $this->delegate->repairRecord($request, $repairRecordService); + } + + #[OA\Post(path: '/produce/exception/record', summary: 'APP 提交异常记录', tags: ['produce-app'])] + #[SimpleResponse(description: '提交成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] + public function exceptionRecord(Request $request): Response + { + return $this->delegate->exceptionRecord($request); + } + + #[OA\Get(path: '/produce/product-record/recent', summary: 'APP 最近整机记录', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function productRecentRecords(Request $request): Response + { + return $this->delegate->productRecentRecords($request); + } + + #[OA\Get(path: '/produce/product-record/statistics', summary: 'APP 整机统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function productStatistics(Request $request): Response + { + return $this->delegate->productStatistics($request); + } + + #[OA\Get(path: '/produce/product-record/user-code-stats', summary: 'APP 整机用户统计', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function productUserCodeStats(Request $request): Response + { + return $this->delegate->productUserCodeStats($request); + } + + #[OA\Post(path: '/produce/product-record/record', summary: 'APP 提交整机记录', tags: ['produce-app'])] + #[SimpleResponse(description: '录入成功')] + public function productRecord(Request $request): Response + { + return $this->delegate->productRecord($request); + } +} + diff --git a/backend/plugin/produce/app/dao/.gitkeep b/backend/plugin/produce/app/dao/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/dao/ExceptionRecordDao.php b/backend/plugin/produce/app/dao/ExceptionRecordDao.php new file mode 100644 index 0000000..890ef9a --- /dev/null +++ b/backend/plugin/produce/app/dao/ExceptionRecordDao.php @@ -0,0 +1,72 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $keyword = '%' . $where['keyword'] . '%'; + $query->where(function (Builder $builder) use ($keyword) { + $builder->where('order_no', 'like', $keyword) + ->orWhere('user_code', 'like', $keyword) + ->orWhere('reporter', 'like', $keyword) + ->orWhere('type_name', 'like', $keyword) + ->orWhere('reason_name', 'like', $keyword) + ->orWhere('issue_name', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + foreach (['type_code', 'reason_code', 'issue_code', 'status'] as $field) { + if (($where[$field] ?? '') !== '') { + $query->where($field, $where[$field]); + } + } + + if (!empty($where['reported_at_range']) && is_array($where['reported_at_range']) && count($where['reported_at_range']) === 2) { + $query->whereBetween('reported_at', [ + strtotime((string) $where['reported_at_range'][0] . ' 00:00:00'), + strtotime((string) $where['reported_at_range'][1] . ' 23:59:59'), + ]); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/ExceptionSettingDao.php b/backend/plugin/produce/app/dao/ExceptionSettingDao.php new file mode 100644 index 0000000..256ef45 --- /dev/null +++ b/backend/plugin/produce/app/dao/ExceptionSettingDao.php @@ -0,0 +1,69 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $keyword = '%' . $where['keyword'] . '%'; + $query->where(function (Builder $builder) use ($keyword) { + $builder->where('code', 'like', $keyword) + ->orWhere('name', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + foreach (['setting_type', 'parent_id', 'is_enabled'] as $field) { + if (($where[$field] ?? '') !== '') { + $query->where($field, $where[$field]); + } + } + + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/HardwareFaultDescriptionDao.php b/backend/plugin/produce/app/dao/HardwareFaultDescriptionDao.php new file mode 100644 index 0000000..63582fc --- /dev/null +++ b/backend/plugin/produce/app/dao/HardwareFaultDescriptionDao.php @@ -0,0 +1,21 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $keyword = '%' . $where['keyword'] . '%'; + $query->where(function (Builder $builder) use ($keyword) { + $builder->where('product_code', 'like', $keyword) + ->orWhere('product_name', 'like', $keyword); + }); + } + if (($where['category_id'] ?? '') !== '') { + $query->where('category_id', $where['category_id']); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/ProcessDao.php b/backend/plugin/produce/app/dao/ProcessDao.php new file mode 100644 index 0000000..336215a --- /dev/null +++ b/backend/plugin/produce/app/dao/ProcessDao.php @@ -0,0 +1,42 @@ +selectModel($where, $field, $page, $limit, $order, $with, $search, $withoutScopes); + $query->withCount(['steps as step_count']); + + if ($page > 0 && $limit > 0) { + return $query->paginate($limit, ['*'], 'page', $page)->getCollection(); + } + + return $query->get(); + } + + protected function getKeywordFields(): array + { + return ['process_code', 'process_name', 'description']; + } +} diff --git a/backend/plugin/produce/app/dao/ProcessStepDao.php b/backend/plugin/produce/app/dao/ProcessStepDao.php new file mode 100644 index 0000000..5cc5fb1 --- /dev/null +++ b/backend/plugin/produce/app/dao/ProcessStepDao.php @@ -0,0 +1,35 @@ +getModel()::query() + ->where('group_code', $groupCode) + ->orderBy('sort') + ->orderBy('id') + ->get(); + + $result = []; + foreach ($items as $item) { + $result[$item->code] = $item->content; + } + + return $result; + } + + public function batchSave(string $groupCode, array $configs): bool + { + $timestamp = time(); + + foreach ($configs as $config) { + $this->getModel()::query()->updateOrCreate( + [ + 'group_code' => $groupCode, + 'code' => (string) ($config['code'] ?? ''), + ], + [ + 'name' => (string) ($config['name'] ?? ''), + 'content' => is_scalar($config['content'] ?? null) || $config['content'] === null + ? ($config['content'] ?? null) + : json_encode($config['content'], JSON_UNESCAPED_UNICODE), + 'sort' => (int) ($config['sort'] ?? 0), + 'remark' => (string) ($config['remark'] ?? ''), + 'updated_at' => $timestamp, + ] + ); + } + + return true; + } +} diff --git a/backend/plugin/produce/app/dao/ProductRecordDao.php b/backend/plugin/produce/app/dao/ProductRecordDao.php new file mode 100644 index 0000000..e6347d9 --- /dev/null +++ b/backend/plugin/produce/app/dao/ProductRecordDao.php @@ -0,0 +1,76 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $query->where(function (Builder $builder) use ($where) { + $keyword = '%' . $where['keyword'] . '%'; + $builder->where('process_order_no', 'like', $keyword) + ->orWhere('product_code', 'like', $keyword) + ->orWhere('operator', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + if (($where['process_id'] ?? '') !== '') { + $query->where('process_id', $where['process_id']); + } + + if (($where['step_id'] ?? '') !== '') { + $query->where('step_id', $where['step_id']); + } + + if (($where['status_id'] ?? '') !== '') { + $query->where('status_id', $where['status_id']); + } + + if (($where['scan_result'] ?? '') !== '') { + $query->where('scan_result', $where['scan_result']); + } + + if (!empty($where['created_time_range']) && is_array($where['created_time_range']) && count($where['created_time_range']) === 2) { + $query->whereBetween('created_at', [strtotime((string) $where['created_time_range'][0]), strtotime((string) $where['created_time_range'][1])]); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/RepairRecordDao.php b/backend/plugin/produce/app/dao/RepairRecordDao.php new file mode 100644 index 0000000..175bad6 --- /dev/null +++ b/backend/plugin/produce/app/dao/RepairRecordDao.php @@ -0,0 +1,21 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $keyword = '%' . $where['keyword'] . '%'; + $query->where(function (Builder $builder) use ($keyword) { + $builder->where('code', 'like', $keyword) + ->orWhere('name', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + foreach (['setting_type', 'parent_id', 'is_enabled'] as $field) { + if (($where[$field] ?? '') !== '') { + $query->where($field, $where[$field]); + } + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/ScanDataDao.php b/backend/plugin/produce/app/dao/ScanDataDao.php new file mode 100644 index 0000000..091e050 --- /dev/null +++ b/backend/plugin/produce/app/dao/ScanDataDao.php @@ -0,0 +1,76 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $query->where(function (Builder $builder) use ($where) { + $keyword = '%' . $where['keyword'] . '%'; + $builder->where('order_no', 'like', $keyword) + ->orWhere('user_code', 'like', $keyword) + ->orWhere('operator', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + if (($where['process_id'] ?? '') !== '') { + $query->where('process_id', $where['process_id']); + } + + if (($where['step_id'] ?? '') !== '') { + $query->where('step_id', $where['step_id']); + } + + if (($where['status_id'] ?? '') !== '') { + $query->where('status_id', $where['status_id']); + } + + if (($where['scan_result'] ?? '') !== '') { + $query->where('scan_result', $where['scan_result']); + } + + if (!empty($where['scan_time_range']) && is_array($where['scan_time_range']) && count($where['scan_time_range']) === 2) { + $query->whereBetween('scan_time', [strtotime((string) $where['scan_time_range'][0]), strtotime((string) $where['scan_time_range'][1])]); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/SpecificRuleDao.php b/backend/plugin/produce/app/dao/SpecificRuleDao.php new file mode 100644 index 0000000..3419973 --- /dev/null +++ b/backend/plugin/produce/app/dao/SpecificRuleDao.php @@ -0,0 +1,70 @@ +query(); + + if (($where['keyword'] ?? '') !== '') { + $query->where(function (Builder $builder) use ($where) { + $keyword = '%' . $where['keyword'] . '%'; + $builder->where('rule_name', 'like', $keyword) + ->orWhere('rule_pattern', 'like', $keyword) + ->orWhere('remark', 'like', $keyword); + }); + } + + if (($where['rule_type'] ?? '') !== '') { + $query->where('rule_type', $where['rule_type']); + } + + if (($where['is_enabled'] ?? '') !== '') { + $query->where('is_enabled', $where['is_enabled']); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/dao/StatusTransitionDao.php b/backend/plugin/produce/app/dao/StatusTransitionDao.php new file mode 100644 index 0000000..4f4cd30 --- /dev/null +++ b/backend/plugin/produce/app/dao/StatusTransitionDao.php @@ -0,0 +1,115 @@ +where('step_id', $where['step_id']); + } + + if (($where['from_status_id'] ?? '') !== '') { + $query->where('from_status_id', $where['from_status_id']); + } + + if (($where['to_status_id'] ?? '') !== '') { + $query->where('to_status_id', $where['to_status_id']); + } + + if (($where['trigger_event'] ?? '') !== '') { + $query->where('trigger_event', $where['trigger_event']); + } + + return $query; + } + + public function getAutoTransitionsByStep(int|string $stepId, int|string $fromStatusId): array + { + return $this->getModel()::query() + ->where('step_id', $stepId) + ->where('from_status_id', $fromStatusId) + ->where('auto_trigger', 1) + ->where('status', 1) + ->orderBy('priority', 'desc') + ->orderBy('sort', 'asc') + ->with(['toStatus:id,status_name']) + ->get() + ->map(function ($item) { + return [ + 'id' => $item->id, + 'step_id' => $item->step_id, + 'from_status_id' => $item->from_status_id, + 'to_status_id' => $item->to_status_id, + 'to_status_name' => $item->toStatus->status_name ?? '', + 'trigger_event' => $item->trigger_event, + 'event_config' => $item->event_config, + 'priority' => $item->priority, + ]; + }) + ->toArray(); + } + + public function getAutoTransitions(int|string $stepId, int|string $fromStatusId, string $triggerEvent): array + { + return $this->getModel()::query() + ->where('step_id', $stepId) + ->where('from_status_id', $fromStatusId) + ->where('trigger_event', $triggerEvent) + ->where('auto_trigger', 1) + ->where('status', 1) + ->orderBy('priority', 'desc') + ->orderBy('sort', 'asc') + ->with(['toStatus:id,status_name']) + ->get() + ->map(function ($item) { + return [ + 'id' => $item->id, + 'step_id' => $item->step_id, + 'from_status_id' => $item->from_status_id, + 'to_status_id' => $item->to_status_id, + 'to_status_name' => $item->toStatus->status_name ?? '', + 'trigger_event' => $item->trigger_event, + 'event_config' => $item->event_config, + 'priority' => $item->priority, + ]; + }) + ->toArray(); + } +} diff --git a/backend/plugin/produce/app/dao/StepRelationDao.php b/backend/plugin/produce/app/dao/StepRelationDao.php new file mode 100644 index 0000000..f9dede7 --- /dev/null +++ b/backend/plugin/produce/app/dao/StepRelationDao.php @@ -0,0 +1,69 @@ +where('from_step_id', $where['from_step_id']); + } + + if (($where['to_step_id'] ?? '') !== '') { + $query->where('to_step_id', $where['to_step_id']); + } + + if (($where['relation_type'] ?? '') !== '') { + $query->where('relation_type', $where['relation_type']); + } + + return $query; + } + + public function getPreviousSteps(int|string $currentStepId): array + { + return $this->getModel()::query() + ->where('to_step_id', $currentStepId) + ->where('status', 1) + ->orderBy('priority', 'asc') + ->orderBy('sort', 'asc') + ->get(['id', 'from_step_id', 'to_step_id', 'relation_type']) + ->toArray(); + } +} diff --git a/backend/plugin/produce/app/dao/StepStatusDao.php b/backend/plugin/produce/app/dao/StepStatusDao.php new file mode 100644 index 0000000..3c03054 --- /dev/null +++ b/backend/plugin/produce/app/dao/StepStatusDao.php @@ -0,0 +1,35 @@ +where('step_id', $where['step_id']); + } + + if (($where['trigger_event'] ?? '') !== '') { + $query->where('trigger_event', $where['trigger_event']); + } + + if (($where['is_success'] ?? '') !== '') { + $query->where('is_success', $where['is_success']); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/model/.gitkeep b/backend/plugin/produce/app/model/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/model/BaseModel.php b/backend/plugin/produce/app/model/BaseModel.php new file mode 100644 index 0000000..f635aa0 --- /dev/null +++ b/backend/plugin/produce/app/model/BaseModel.php @@ -0,0 +1,37 @@ +hasOne(Admin::class, 'id', 'created_by')->select('id', 'real_name as created_name'); + } + + public function updateds(): HasOne + { + return $this->hasOne(Admin::class, 'id', 'updated_by')->select('id', 'real_name as updated_name'); + } + + public function getCreatedNameAttribute(): string + { + return $this->createds?->created_name ?? ''; + } + + public function getUpdatedNameAttribute(): string + { + return $this->updateds?->updated_name ?? ''; + } +} diff --git a/backend/plugin/produce/app/model/ExceptionRecord.php b/backend/plugin/produce/app/model/ExceptionRecord.php new file mode 100644 index 0000000..47ba974 --- /dev/null +++ b/backend/plugin/produce/app/model/ExceptionRecord.php @@ -0,0 +1,81 @@ +reported_at)) { + $model->reported_at = time(); + } + }); + } + + protected $fillable = [ + 'id', + 'order_no', + 'user_code', + 'reporter', + 'reporter_id', + 'type_id', + 'type_code', + 'type_name', + 'reason_id', + 'reason_code', + 'reason_name', + 'issue_id', + 'issue_code', + 'issue_name', + 'images', + 'remark', + 'status', + 'handler', + 'handler_id', + 'handled_at', + 'reported_at', + 'created_by', + 'updated_by', + 'created_at', + 'updated_at', + 'deleted_at', + ]; + + protected $casts = [ + 'images' => 'array', + 'status' => 'integer', + 'handled_at' => 'integer', + 'reported_at' => 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getReportedDateAttribute(): ?string + { + return $this->reported_at ? date('Y-m-d H:i:s', (int) $this->reported_at) : null; + } + + public function getHandledDateAttribute(): ?string + { + return $this->handled_at ? date('Y-m-d H:i:s', (int) $this->handled_at) : null; + } + + public function getStatusNameAttribute(): string + { + return match ((int) $this->status) { + 1 => '处理中', + 2 => '已处理', + default => '待处理', + }; + } +} diff --git a/backend/plugin/produce/app/model/ExceptionSetting.php b/backend/plugin/produce/app/model/ExceptionSetting.php new file mode 100644 index 0000000..c60e93f --- /dev/null +++ b/backend/plugin/produce/app/model/ExceptionSetting.php @@ -0,0 +1,50 @@ + 'integer', + 'is_enabled' => 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id', 'id')->withTrashed(); + } + + public function getParentNameAttribute(): string + { + return (string) ($this->parent?->name ?? ''); + } +} diff --git a/backend/plugin/produce/app/model/HardwareFaultDescription.php b/backend/plugin/produce/app/model/HardwareFaultDescription.php new file mode 100644 index 0000000..2e8b50b --- /dev/null +++ b/backend/plugin/produce/app/model/HardwareFaultDescription.php @@ -0,0 +1,39 @@ + 'integer', + 'is_enabled' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(MaterialCategory::class, 'category_id', 'id')->withTrashed(); + } + + public function getCategoryNameAttribute(): string + { + return (string) ($this->category?->category_name ?? ''); + } +} diff --git a/backend/plugin/produce/app/model/Material.php b/backend/plugin/produce/app/model/Material.php new file mode 100644 index 0000000..51c7d13 --- /dev/null +++ b/backend/plugin/produce/app/model/Material.php @@ -0,0 +1,42 @@ + 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(MaterialCategory::class, 'category_id', 'id')->withTrashed(); + } + + public function getCategoryNameAttribute(): string + { + return (string) ($this->category?->category_name ?? ''); + } +} diff --git a/backend/plugin/produce/app/model/MaterialCategory.php b/backend/plugin/produce/app/model/MaterialCategory.php new file mode 100644 index 0000000..31cf037 --- /dev/null +++ b/backend/plugin/produce/app/model/MaterialCategory.php @@ -0,0 +1,38 @@ + 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->category_name; + } + + public function materials(): HasMany + { + return $this->hasMany(Material::class, 'category_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/Process.php b/backend/plugin/produce/app/model/Process.php new file mode 100644 index 0000000..b89ac0f --- /dev/null +++ b/backend/plugin/produce/app/model/Process.php @@ -0,0 +1,46 @@ + 'integer', + 'status' => 'integer', + 'step_count' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->process_name; + } + + public function steps(): HasMany + { + return $this->hasMany(ProcessStep::class, 'process_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/ProcessStep.php b/backend/plugin/produce/app/model/ProcessStep.php new file mode 100644 index 0000000..cebaadc --- /dev/null +++ b/backend/plugin/produce/app/model/ProcessStep.php @@ -0,0 +1,55 @@ + 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'prerequisite_steps' => 'array', + 'prerequisite_require_final' => 'integer', + 'allow_repeat_scan' => 'integer', + 'repeat_scan_statuses' => 'array', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->step_name; + } + + public function process(): BelongsTo + { + return $this->belongsTo(Process::class, 'process_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/ProduceConfig.php b/backend/plugin/produce/app/model/ProduceConfig.php new file mode 100644 index 0000000..e752979 --- /dev/null +++ b/backend/plugin/produce/app/model/ProduceConfig.php @@ -0,0 +1,39 @@ + 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->name; + } +} diff --git a/backend/plugin/produce/app/model/ProductRecord.php b/backend/plugin/produce/app/model/ProductRecord.php new file mode 100644 index 0000000..dc03814 --- /dev/null +++ b/backend/plugin/produce/app/model/ProductRecord.php @@ -0,0 +1,82 @@ + 'integer', + 'scan_result' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function process(): BelongsTo + { + return $this->belongsTo(Process::class, 'process_id', 'id')->withTrashed(); + } + + public function step(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'step_id', 'id')->withTrashed(); + } + + public function status(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'status_id', 'id')->withTrashed(); + } + + public function getProcessNameAttribute(): string + { + return (string) ($this->process?->process_name ?? ''); + } + + public function getStepNameAttribute(): string + { + return (string) ($this->step?->step_name ?? ''); + } + + public function getStatusNameAttribute(): string + { + return (string) ($this->status?->status_name ?? ''); + } + + public function getCreatedDateAttribute(): ?string + { + return $this->created_at ? date('Y-m-d H:i:s', (int) $this->created_at) : null; + } + + public function getUpdatedDateAttribute(): ?string + { + return $this->updated_at ? date('Y-m-d H:i:s', (int) $this->updated_at) : null; + } +} diff --git a/backend/plugin/produce/app/model/RepairRecord.php b/backend/plugin/produce/app/model/RepairRecord.php new file mode 100644 index 0000000..a57aa12 --- /dev/null +++ b/backend/plugin/produce/app/model/RepairRecord.php @@ -0,0 +1,160 @@ + 'array', + 'category_names' => 'array', + 'parts' => 'array', + 'images' => 'array', + 'operation_fault_steps' => 'array', + 'responsible_operators' => 'array', + 'repaired_at' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(MaterialCategory::class, 'category_id', 'id')->withTrashed(); + } + + public function exception(): BelongsTo + { + return $this->belongsTo(ExceptionRecord::class, 'exception_id', 'id')->withTrashed(); + } + + public function getExceptionTypeNameAttribute(): string + { + return (string) ($this->exception?->type_name ?? ''); + } + + public function faultDescription(): BelongsTo + { + return $this->belongsTo(HardwareFaultDescription::class, 'fault_description_id', 'id')->withTrashed(); + } + + public function getFaultDescriptionNameAttribute(): string + { + return (string) $this->fault_description; + } + + public function getOrderProductAttribute(): string + { + return implode('/', array_filter([(string) $this->order_no, (string) $this->product_code])); + } + + public function getRepairResultNameAttribute(): string + { + return match ($this->repair_result) { + 'material_defect' => '物料不良', + 'operation_defect' => '作业不良', + 'assembly_mismatch' => '搭配错误', + 'process_defect' => '工艺不良', + 'no_defect' => '不良不现', + default => (string) $this->repair_result, + }; + } + + public function getResponsibleOperatorNamesAttribute(): string + { + return implode("\n", array_filter(array_map( + static function (array $snapshot): string { + $step = (string) ($snapshot['step_name'] ?? ''); + $operator = (string) ($snapshot['operator'] ?? $snapshot['user_code'] ?? ''); + return implode('-', array_filter([$step, $operator])); + }, + is_array($this->responsible_operators) ? $this->responsible_operators : [], + ))); + } + + public function getPartCategoryAttribute(): string + { + return $this->formatPartValues(static fn (array $part): string => (string) ($part['category_name'] ?? '')); + } + + public function getPartCodeAttribute(): string + { + return $this->formatPartValues(static fn (array $part): string => (string) ($part['product_code'] ?? '')); + } + + public function getPartNameAttribute(): string + { + return $this->formatPartValues(static fn (array $part): string => (string) ($part['product_name'] ?? '')); + } + + public function getPartFaultAttribute(): string + { + return $this->formatPartValues(static function (array $part): string { + return implode('、', array_filter(array_map( + static fn (array $fault): string => (string) ($fault['description'] ?? ''), + is_array($part['fault_descriptions'] ?? null) ? $part['fault_descriptions'] : [], + ))); + }); + } + + public function getPartQuantityAttribute(): string + { + return $this->formatPartValues(static fn (array $part): string => (string) ($part['quantity'] ?? 1)); + } + + public function getPartSourceAttribute(): string + { + return $this->formatPartValues(static function (array $part): string { + return match ($part['source'] ?? '') { + 'new' => '新料', + 'old', 'after_sale', 'after_sales', 'after-sale' => '售后', + 'repair', 'refurbished' => '返修', + default => (string) ($part['source'] ?? '-'), + }; + }); + } + + private function formatPartValues(callable $callback): string + { + return implode("\n", array_map( + static fn (array $part): string => $callback($part) ?: '-', + is_array($this->parts) ? $this->parts : [], + )); + } + + public function getFaultDescriptionIdsAttribute(): array + { + return $this->fault_description_id ? [$this->fault_description_id] : []; + } + + public function getFaultDescriptionsAttribute(): array + { + return $this->fault_description ? [$this->fault_description] : []; + } + + public function getRepairedDateAttribute(): ?string + { + return $this->repaired_at ? date('Y-m-d H:i:s', (int) $this->repaired_at) : null; + } +} diff --git a/backend/plugin/produce/app/model/RepairSetting.php b/backend/plugin/produce/app/model/RepairSetting.php new file mode 100644 index 0000000..3029bbb --- /dev/null +++ b/backend/plugin/produce/app/model/RepairSetting.php @@ -0,0 +1,50 @@ + 'integer', + 'is_enabled' => 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id', 'id')->withTrashed(); + } + + public function getParentNameAttribute(): string + { + return (string) ($this->parent?->name ?? ''); + } +} diff --git a/backend/plugin/produce/app/model/ScanData.php b/backend/plugin/produce/app/model/ScanData.php new file mode 100644 index 0000000..e055d69 --- /dev/null +++ b/backend/plugin/produce/app/model/ScanData.php @@ -0,0 +1,88 @@ +scan_time)) { + $model->scan_time = time(); + } + }); + } + + protected $fillable = [ + 'id', + 'order_no', + 'quantity', + 'user_code', + 'operator', + 'operator_id', + 'process_id', + 'step_id', + 'status_id', + 'scan_result', + 'scan_time', + 'remark', + 'created_by', + 'updated_by', + 'created_at', + 'updated_at', + 'deleted_at', + ]; + + protected $casts = [ + 'quantity' => 'integer', + 'scan_result' => 'integer', + 'scan_time' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function process(): BelongsTo + { + return $this->belongsTo(Process::class, 'process_id', 'id')->withTrashed(); + } + + public function step(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'step_id', 'id')->withTrashed(); + } + + public function status(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'status_id', 'id')->withTrashed(); + } + + public function getProcessNameAttribute(): string + { + return (string) ($this->process?->process_name ?? ''); + } + + public function getStepNameAttribute(): string + { + return (string) ($this->step?->step_name ?? ''); + } + + public function getStatusNameAttribute(): string + { + return (string) ($this->status?->status_name ?? ''); + } + + public function getScanDateAttribute(): ?string + { + return $this->scan_time ? date('Y-m-d H:i:s', (int) $this->scan_time) : null; + } +} + diff --git a/backend/plugin/produce/app/model/SpecificRule.php b/backend/plugin/produce/app/model/SpecificRule.php new file mode 100644 index 0000000..71d5fe9 --- /dev/null +++ b/backend/plugin/produce/app/model/SpecificRule.php @@ -0,0 +1,47 @@ + 'integer', + 'is_enabled' => 'integer', + 'step_ids' => 'array', + 'sort' => 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->rule_name; + } +} diff --git a/backend/plugin/produce/app/model/StatusTransition.php b/backend/plugin/produce/app/model/StatusTransition.php new file mode 100644 index 0000000..a3ca886 --- /dev/null +++ b/backend/plugin/produce/app/model/StatusTransition.php @@ -0,0 +1,64 @@ + 'integer', + 'from_status_id' => 'integer', + 'to_status_id' => 'integer', + 'event_config' => 'array', + 'auto_trigger' => 'integer', + 'priority' => 'integer', + 'side_effect_config' => 'array', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function step(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'step_id', 'id'); + } + + public function fromStatus(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'from_status_id', 'id'); + } + + public function toStatus(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'to_status_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/StepRelation.php b/backend/plugin/produce/app/model/StepRelation.php new file mode 100644 index 0000000..0a8c613 --- /dev/null +++ b/backend/plugin/produce/app/model/StepRelation.php @@ -0,0 +1,51 @@ + 'integer', + 'to_step_id' => 'integer', + 'flow_condition' => 'array', + 'priority' => 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function fromStep(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'from_step_id', 'id'); + } + + public function toStep(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'to_step_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/StepStatus.php b/backend/plugin/produce/app/model/StepStatus.php new file mode 100644 index 0000000..2dd4991 --- /dev/null +++ b/backend/plugin/produce/app/model/StepStatus.php @@ -0,0 +1,50 @@ + 'integer', + 'is_initial' => 'integer', + 'is_final' => 'integer', + 'sort' => 'integer', + 'status' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getNameAttribute(): string + { + return (string) $this->status_name; + } + + public function step(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'step_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/model/TransitionLog.php b/backend/plugin/produce/app/model/TransitionLog.php new file mode 100644 index 0000000..9504134 --- /dev/null +++ b/backend/plugin/produce/app/model/TransitionLog.php @@ -0,0 +1,72 @@ + 'integer', + 'step_id' => 'integer', + 'transition_id' => 'integer', + 'from_status_id' => 'integer', + 'to_status_id' => 'integer', + 'trigger_data' => 'array', + 'is_auto' => 'integer', + 'is_success' => 'integer', + 'duration' => 'integer', + 'created_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function step(): BelongsTo + { + return $this->belongsTo(ProcessStep::class, 'step_id', 'id'); + } + + public function transition(): BelongsTo + { + return $this->belongsTo(StatusTransition::class, 'transition_id', 'id'); + } + + public function fromStatus(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'from_status_id', 'id'); + } + + public function toStatus(): BelongsTo + { + return $this->belongsTo(StepStatus::class, 'to_status_id', 'id'); + } +} diff --git a/backend/plugin/produce/app/schema/request/.gitkeep b/backend/plugin/produce/app/schema/request/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/schema/response/.gitkeep b/backend/plugin/produce/app/schema/response/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/service/admin/.gitkeep b/backend/plugin/produce/app/service/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/service/admin/ExceptionAnalysisService.php b/backend/plugin/produce/app/service/admin/ExceptionAnalysisService.php new file mode 100644 index 0000000..cbaf402 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ExceptionAnalysisService.php @@ -0,0 +1,85 @@ +query($params); + + return [ + 'total' => (clone $query)->count(), + 'pending' => (clone $query)->where('status', 0)->count(), + 'processing' => (clone $query)->where('status', 1)->count(), + 'completed' => (clone $query)->where('status', 2)->count(), + 'by_type' => $this->group($params, 'type_code', 'type_name'), + 'by_reason' => $this->group($params, 'reason_code', 'reason_name'), + 'by_issue' => $this->group($params, 'issue_code', 'issue_name'), + 'by_date' => $this->byDate($params), + ]; + } + + private function group(array $params, string $codeColumn, string $nameColumn): array + { + return $this->query($params) + ->selectRaw("{$codeColumn} as code, {$nameColumn} as name, COUNT(*) as total") + ->groupBy($codeColumn, $nameColumn) + ->orderByDesc('total') + ->orderBy($nameColumn) + ->limit(20) + ->get() + ->map(static fn (object $item): array => [ + 'code' => (string) $item->code, + 'name' => (string) $item->name, + 'total' => (int) $item->total, + ]) + ->all(); + } + + private function byDate(array $params): array + { + $model = new ExceptionRecord(); + $connection = $model->getConnection(); + $table = $connection->getTablePrefix() . $model->getTable(); + + return $this->query($params) + ->selectRaw("DATE_FORMAT(FROM_UNIXTIME({$table}.reported_at), '%Y-%m-%d') as date, COUNT(*) as total") + ->groupBy('date') + ->orderBy('date') + ->get() + ->map(static fn (object $item): array => [ + 'date' => (string) $item->date, + 'total' => (int) $item->total, + ]) + ->all(); + } + + private function query(array $params): Builder + { + $model = new ExceptionRecord(); + $query = $model->getConnection()->table($model->getTable())->whereNull('deleted_at'); + + foreach (['type_code', 'reason_code', 'issue_code', 'status'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($field, $params[$field]); + } + } + + if (!empty($params['reported_at_range']) && is_array($params['reported_at_range']) && count($params['reported_at_range']) === 2) { + $query->whereBetween('reported_at', [ + strtotime((string) $params['reported_at_range'][0] . ' 00:00:00'), + strtotime((string) $params['reported_at_range'][1] . ' 23:59:59'), + ]); + } else { + $query->whereBetween('reported_at', [strtotime('-29 days 00:00:00'), time()]); + } + + return $query; + } +} diff --git a/backend/plugin/produce/app/service/admin/ExceptionRecordService.php b/backend/plugin/produce/app/service/admin/ExceptionRecordService.php new file mode 100644 index 0000000..17bdde7 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ExceptionRecordService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/ExceptionSettingService.php b/backend/plugin/produce/app/service/admin/ExceptionSettingService.php new file mode 100644 index 0000000..276105f --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ExceptionSettingService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/HardwareFaultDescriptionService.php b/backend/plugin/produce/app/service/admin/HardwareFaultDescriptionService.php new file mode 100644 index 0000000..35ce940 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/HardwareFaultDescriptionService.php @@ -0,0 +1,84 @@ +dao = $dao; + } + + public function importFromExcel(string $filePath): array + { + $sheet = IOFactory::load($filePath)->getActiveSheet(); + $rows = $sheet->toArray('', true, true, false); + if (count($rows) < 2) { + throw new \InvalidArgumentException('导入文件至少应包含表头和一条硬件故障描述数据'); + } + + $headers = array_map(static fn ($value) => trim((string) $value), $rows[0]); + $indexes = array_flip($headers); + foreach (['分类', '描述'] as $header) { + if (!array_key_exists($header, $indexes)) { + throw new \InvalidArgumentException('导入表头必须包含:分类、描述'); + } + } + + return $this->transaction(function () use ($rows, $indexes) { + $created = 0; + $updated = 0; + $categories = []; + + foreach (array_slice($rows, 1) as $rowNumber => $row) { + $categoryName = trim((string) ($row[$indexes['分类']] ?? '')); + $description = trim((string) ($row[$indexes['描述']] ?? '')); + if ($categoryName === '' && $description === '') { + continue; + } + if ($categoryName === '' || $description === '') { + throw new \InvalidArgumentException('第 ' . ($rowNumber + 2) . ' 行的分类、描述均不能为空'); + } + + $category = $categories[$categoryName] ?? null; + if (!$category) { + $category = MaterialCategory::query()->withTrashed()->where('category_name', $categoryName)->first(); + if ($category?->trashed()) { + $category->restore(); + } + $category ??= MaterialCategory::query()->create(['category_name' => $categoryName]); + $categories[$categoryName] = $category; + } + + $faultDescription = HardwareFaultDescription::query() + ->withTrashed() + ->where('category_id', $category->id) + ->where('description', $description) + ->first(); + if ($faultDescription) { + if ($faultDescription->trashed()) { + $faultDescription->restore(); + } + $updated++; + continue; + } + + HardwareFaultDescription::query()->create([ + 'category_id' => $category->id, + 'description' => $description, + ]); + $created++; + } + + return ['created' => $created, 'updated' => $updated]; + }); + } +} diff --git a/backend/plugin/produce/app/service/admin/MaterialCategoryService.php b/backend/plugin/produce/app/service/admin/MaterialCategoryService.php new file mode 100644 index 0000000..9f1e269 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/MaterialCategoryService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/MaterialService.php b/backend/plugin/produce/app/service/admin/MaterialService.php new file mode 100644 index 0000000..1a4805e --- /dev/null +++ b/backend/plugin/produce/app/service/admin/MaterialService.php @@ -0,0 +1,86 @@ +dao = $dao; + } + + /** + * 从 Excel 导入物料;首行必须为:分类、商品编码、商品名称。 + * 相同商品编码会更新分类和名称,以便重复导入。 + */ + public function importFromExcel(string $filePath): array + { + $sheet = IOFactory::load($filePath)->getActiveSheet(); + $rows = $sheet->toArray('', true, true, false); + if (count($rows) < 2) { + throw new \InvalidArgumentException('导入文件至少应包含表头和一条物料数据'); + } + + $headers = array_map(static fn ($value) => trim((string) $value), $rows[0]); + $indexes = array_flip($headers); + foreach (['分类', '商品编码', '商品名称'] as $header) { + if (!array_key_exists($header, $indexes)) { + throw new \InvalidArgumentException('导入表头必须包含:分类、商品编码、商品名称'); + } + } + + return $this->transaction(function () use ($rows, $indexes) { + $created = 0; + $updated = 0; + $categories = []; + + foreach (array_slice($rows, 1) as $rowNumber => $row) { + $categoryName = trim((string) ($row[$indexes['分类']] ?? '')); + $productCode = trim((string) ($row[$indexes['商品编码']] ?? '')); + $productName = trim((string) ($row[$indexes['商品名称']] ?? '')); + if ($categoryName === '' && $productCode === '' && $productName === '') { + continue; + } + if ($categoryName === '' || $productCode === '' || $productName === '') { + throw new \InvalidArgumentException('第 ' . ($rowNumber + 2) . ' 行的分类、商品编码、商品名称均不能为空'); + } + + $category = $categories[$categoryName] ?? null; + if (!$category) { + $category = MaterialCategory::query()->withTrashed()->where('category_name', $categoryName)->first(); + if ($category?->trashed()) { + $category->restore(); + } + $category ??= MaterialCategory::query()->create(['category_name' => $categoryName]); + $categories[$categoryName] = $category; + } + $material = Material::query()->withTrashed()->where('product_code', $productCode)->first(); + if ($material) { + if ($material->trashed()) { + $material->restore(); + } + $material->fill(['category_id' => $category->id, 'product_name' => $productName])->save(); + $updated++; + continue; + } + + Material::query()->create([ + 'category_id' => $category->id, + 'product_code' => $productCode, + 'product_name' => $productName, + ]); + $created++; + } + + return ['created' => $created, 'updated' => $updated]; + }); + } +} diff --git a/backend/plugin/produce/app/service/admin/ProcessService.php b/backend/plugin/produce/app/service/admin/ProcessService.php new file mode 100644 index 0000000..657f589 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ProcessService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/ProcessStepService.php b/backend/plugin/produce/app/service/admin/ProcessStepService.php new file mode 100644 index 0000000..308795b --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ProcessStepService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/ProduceConfigService.php b/backend/plugin/produce/app/service/admin/ProduceConfigService.php new file mode 100644 index 0000000..1169c59 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ProduceConfigService.php @@ -0,0 +1,26 @@ +dao = $dao; + } + + public function getConfigByGroup(string $groupCode): array + { + return $this->dao->getByGroupCode($groupCode); + } + + public function saveConfig(string $groupCode, array $configs): bool + { + return $this->dao->batchSave($groupCode, $configs); + } +} diff --git a/backend/plugin/produce/app/service/admin/ProductRecordService.php b/backend/plugin/produce/app/service/admin/ProductRecordService.php new file mode 100644 index 0000000..9898293 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ProductRecordService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/RepairRecordService.php b/backend/plugin/produce/app/service/admin/RepairRecordService.php new file mode 100644 index 0000000..88de94c --- /dev/null +++ b/backend/plugin/produce/app/service/admin/RepairRecordService.php @@ -0,0 +1,121 @@ +dao = $dao; + } + + public function save(array $data): RepairRecord + { + return $this->transaction(function () use ($data) { + $data = $this->prepareData($data); + $record = RepairRecord::query()->create($data); + $this->changeFaultUsage($record->fault_description_id, 1); + return $record; + }); + } + + public function update(int|string $id, array $data): bool + { + return $this->transaction(function () use ($id, $data) { + $record = RepairRecord::query()->findOrFail($id); + $previousFaultId = $record->fault_description_id; + $data = $this->prepareData($data); + $record->fill($data)->save(); + if ((string) $previousFaultId !== (string) $record->fault_description_id) { + $this->changeFaultUsage($previousFaultId, -1); + $this->changeFaultUsage($record->fault_description_id, 1); + } + return true; + }); + } + + public function deleteRecord(int|string $id): bool + { + return $this->transaction(function () use ($id) { + $record = RepairRecord::query()->findOrFail($id); + $this->changeFaultUsage($record->fault_description_id, -1); + return (bool) $record->delete(); + }); + } + + private function prepareData(array $data): array + { + $hasCategoryIds = array_key_exists('category_ids', $data); + $categoryIds = array_values(array_unique(array_filter(array_map(static fn ($id): string => trim((string) $id), (array) ($data['category_ids'] ?? [])), static fn (string $id): bool => $id !== ''))); + if ($hasCategoryIds) { + $data['category_ids'] = $categoryIds ?: null; + $data['category_id'] = $categoryIds[0] ?? null; + if (!$categoryIds) { + $data['category_names'] = null; + } + } + + $categoryId = $data['category_id'] ?? null; + $faultId = $data['fault_description_id'] ?? null; + + if (isset($data['repaired_at']) && $data['repaired_at'] !== null && (int) $data['repaired_at'] > 9999999999) { + $data['repaired_at'] = (int) floor((int) $data['repaired_at'] / 1000); + } + if (array_key_exists('category_id', $data) && !$categoryId) { + $data['category_name'] = null; + $data['category_names'] = null; + } + if (array_key_exists('fault_description_id', $data) && !$faultId) { + $data['fault_description'] = null; + } + if ($categoryIds) { + $categories = MaterialCategory::query()->withTrashed()->whereIn('id', $categoryIds)->get(['id', 'category_name'])->keyBy('id'); + if ($categories->count() !== count($categoryIds)) { + throw new \InvalidArgumentException('物料分类不存在'); + } + $data['category_names'] = array_map(static fn (string $id): string => (string) $categories[$id]->category_name, $categoryIds); + $data['category_name'] = $data['category_names'][0]; + } elseif ($categoryId) { + $data['category_name'] = MaterialCategory::query()->withTrashed()->whereKey($categoryId)->value('category_name'); + } + if ($faultId) { + $fault = HardwareFaultDescription::query()->withTrashed()->find($faultId); + if (!$fault) { + throw new \InvalidArgumentException('硬件故障描述不存在'); + } + if ($categoryId && (string) $fault->category_id !== (string) $categoryId) { + throw new \InvalidArgumentException('硬件故障描述与物料分类不匹配'); + } + $data['category_id'] = $fault->category_id; + $data['category_ids'] = [$fault->category_id]; + $data['category_name'] = $fault->category?->category_name ?? $data['category_name'] ?? ''; + $data['category_names'] = [$data['category_name']]; + $data['fault_description'] = $fault->description; + } + if (isset($data['parts']) && is_string($data['parts'])) { + $data['parts'] = json_decode($data['parts'], true, 512, JSON_THROW_ON_ERROR); + } + return $data; + } + + private function changeFaultUsage(int|string|null $faultId, int $delta): void + { + if (!$faultId) { + return; + } + $fault = HardwareFaultDescription::query()->withTrashed()->find($faultId); + if (!$fault) { + return; + } + $fault->use_count = max(0, (int) $fault->use_count + $delta); + $fault->save(); + } +} diff --git a/backend/plugin/produce/app/service/admin/RepairSettingService.php b/backend/plugin/produce/app/service/admin/RepairSettingService.php new file mode 100644 index 0000000..4fe2274 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/RepairSettingService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/ScanDataService.php b/backend/plugin/produce/app/service/admin/ScanDataService.php new file mode 100644 index 0000000..1744f07 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/ScanDataService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/SpecificRuleService.php b/backend/plugin/produce/app/service/admin/SpecificRuleService.php new file mode 100644 index 0000000..bd7cfed --- /dev/null +++ b/backend/plugin/produce/app/service/admin/SpecificRuleService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/StatusTransitionService.php b/backend/plugin/produce/app/service/admin/StatusTransitionService.php new file mode 100644 index 0000000..fc8d384 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/StatusTransitionService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/StepRelationService.php b/backend/plugin/produce/app/service/admin/StepRelationService.php new file mode 100644 index 0000000..e32cd5e --- /dev/null +++ b/backend/plugin/produce/app/service/admin/StepRelationService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/StepStatusService.php b/backend/plugin/produce/app/service/admin/StepStatusService.php new file mode 100644 index 0000000..74d5480 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/StepStatusService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/TransitionLogService.php b/backend/plugin/produce/app/service/admin/TransitionLogService.php new file mode 100644 index 0000000..864590c --- /dev/null +++ b/backend/plugin/produce/app/service/admin/TransitionLogService.php @@ -0,0 +1,16 @@ +dao = $dao; + } +} diff --git a/backend/plugin/produce/app/service/admin/WorkStatisticsService.php b/backend/plugin/produce/app/service/admin/WorkStatisticsService.php new file mode 100644 index 0000000..fad7c40 --- /dev/null +++ b/backend/plugin/produce/app/service/admin/WorkStatisticsService.php @@ -0,0 +1,215 @@ +normalizeMonth($params['month'] ?? null); + $days = (int) date('t', strtotime($month . '-01')); + $start = strtotime($month . '-01 00:00:00'); + $end = strtotime(date('Y-m-t 23:59:59', $start)); + $rows = $this->collectRows($params, $start, $end, static fn (string $column): string => "DATE_FORMAT(FROM_UNIXTIME({$column}), '%Y-%m-%d')"); + + return $this->formatRows($rows, $days, static fn (int $index): string => (string) $index); + } + + public function daily(array $params): array + { + $date = $this->normalizeDate($params['date'] ?? null); + $start = strtotime($date . ' 00:00:00'); + $end = strtotime($date . ' 23:59:59'); + $rows = $this->collectRows($params, $start, $end, static fn (string $column): string => "HOUR(FROM_UNIXTIME({$column}))"); + + return $this->formatRows($rows, 24, static fn (int $index): string => str_pad((string) $index, 2, '0', STR_PAD_LEFT) . ':00'); + } + + private function collectRows(array $params, int $start, int $end, callable $periodExpression): array + { + $dataType = (string) ($params['data_type'] ?? 'all'); + $rows = []; + + if ($dataType === 'all' || $dataType === 'order') { + $rows = array_merge($rows, $this->queryScanData($params, $start, $end, $periodExpression)->get()->all()); + } + + if ($dataType === 'all' || $dataType === 'machine') { + $rows = array_merge($rows, $this->queryProductRecord($params, $start, $end, $periodExpression)->get()->all()); + } + + return $rows; + } + + private function queryScanData(array $params, int $start, int $end, callable $periodExpression): Builder + { + $scanDataModel = new ScanData(); + $connection = $scanDataModel->getConnection(); + $scanDataTable = $scanDataModel->getTable(); + $processTable = 'produce_process'; + $stepTable = 'produce_process_step'; + + $query = $connection->table($scanDataTable) + ->leftJoin($processTable, $processTable . '.id', '=', $scanDataTable . '.process_id') + ->leftJoin($stepTable, $stepTable . '.id', '=', $scanDataTable . '.step_id') + ->whereBetween($scanDataTable . '.scan_time', [$start, $end]) + ->whereNull($scanDataTable . '.deleted_at'); + + foreach (['process_id', 'step_id', 'scan_result'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($scanDataTable . '.' . $field, $params[$field]); + } + } + + foreach (['user_code', 'operator'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($scanDataTable . '.' . $field, 'like', '%' . $params[$field] . '%'); + } + } + + foreach (['process_name' => $processTable . '.process_name', 'step_name' => $stepTable . '.step_name'] as $field => $column) { + if (($params[$field] ?? '') !== '') { + $query->where($column, 'like', '%' . $params[$field] . '%'); + } + } + + return $query->select([ + $scanDataTable . '.user_code', + $scanDataTable . '.operator', + $connection->raw($connection->getTablePrefix() . $processTable . '.process_name as process_name'), + $connection->raw($connection->getTablePrefix() . $stepTable . '.step_name as step_name'), + ]) + ->selectRaw($periodExpression($connection->getTablePrefix() . $scanDataTable . '.scan_time') . ' as period') + ->selectRaw('SUM(' . $connection->getTablePrefix() . $scanDataTable . '.quantity) as amount') + ->groupBy('user_code', 'operator', 'process_name', 'step_name', 'period') + ->orderBy('user_code') + ->orderBy('process_name') + ->orderBy('step_name'); + } + + private function queryProductRecord(array $params, int $start, int $end, callable $periodExpression): Builder + { + $productRecordModel = new ProductRecord(); + $connection = $productRecordModel->getConnection(); + $productRecordTable = $productRecordModel->getTable(); + $processTable = 'produce_process'; + $stepTable = 'produce_process_step'; + + $query = $connection->table($productRecordTable) + ->leftJoin($processTable, $processTable . '.id', '=', $productRecordTable . '.process_id') + ->leftJoin($stepTable, $stepTable . '.id', '=', $productRecordTable . '.step_id') + ->whereBetween($productRecordTable . '.created_at', [$start, $end]) + ->whereNull($productRecordTable . '.deleted_at'); + + foreach (['process_id', 'step_id', 'scan_result'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($productRecordTable . '.' . $field, $params[$field]); + } + } + + foreach (['user_code', 'operator'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($productRecordTable . '.' . $field, 'like', '%' . $params[$field] . '%'); + } + } + + foreach (['process_name' => $processTable . '.process_name', 'step_name' => $stepTable . '.step_name'] as $field => $column) { + if (($params[$field] ?? '') !== '') { + $query->where($column, 'like', '%' . $params[$field] . '%'); + } + } + + return $query->select([ + $productRecordTable . '.user_code', + $productRecordTable . '.operator', + $connection->raw($connection->getTablePrefix() . $processTable . '.process_name as process_name'), + $connection->raw($connection->getTablePrefix() . $stepTable . '.step_name as step_name'), + ]) + ->selectRaw($periodExpression($connection->getTablePrefix() . $productRecordTable . '.created_at') . ' as period') + ->selectRaw('SUM(' . $connection->getTablePrefix() . $productRecordTable . '.quantity) as amount') + ->groupBy('user_code', 'operator', 'process_name', 'step_name', 'period') + ->orderBy('user_code') + ->orderBy('process_name') + ->orderBy('step_name'); + } + + private function formatRows(iterable $rows, int $periodCount, callable $periodLabel): array + { + $grouped = []; + $summaryRows = []; + + foreach ($rows as $row) { + $productionProcess = $this->formatProductionProcess((string) $row->process_name, (string) $row->step_name); + $key = implode('|', [(string) $row->user_code, (string) $row->operator, (string) $row->process_name, (string) $row->step_name]); + if (!isset($grouped[$key])) { + $grouped[$key] = [ + 'user_code' => $row->user_code, + 'operator' => $row->operator, + 'production_process' => $productionProcess, + 'process_name' => $row->process_name, + 'step_name' => $row->step_name, + 'total' => 0, + ]; + for ($index = 1; $index <= $periodCount; $index++) { + $grouped[$key][$periodLabel($index)] = 0; + } + } + if (!isset($summaryRows[$productionProcess])) { + $summaryRows[$productionProcess] = [ + 'user_code' => '总计', + 'operator' => '', + 'production_process' => $productionProcess, + 'process_name' => $row->process_name, + 'step_name' => $row->step_name, + 'total' => 0, + ]; + for ($index = 1; $index <= $periodCount; $index++) { + $summaryRows[$productionProcess][$periodLabel($index)] = 0; + } + } + $period = (string) $row->period; + $period = $periodCount === 24 ? str_pad($period, 2, '0', STR_PAD_LEFT) . ':00' : (string) ((int) substr($period, -2)); + if (array_key_exists($period, $grouped[$key])) { + $amount = (int) $row->amount; + $grouped[$key][$period] += $amount; + $grouped[$key]['total'] += $amount; + $summaryRows[$productionProcess][$period] += $amount; + $summaryRows[$productionProcess]['total'] += $amount; + } + } + + $items = array_values($grouped); + if ($items !== []) { + $items = array_merge($items, array_values($summaryRows)); + } + + return $items; + } + + private function formatProductionProcess(string $processName, string $stepName): string + { + if ($processName !== '' && $stepName !== '') { + return $processName . '-' . $stepName; + } + + return $processName !== '' ? $processName : $stepName; + } + + private function normalizeMonth(mixed $month): string + { + $month = is_string($month) && preg_match('/^\d{4}-\d{2}$/', $month) ? $month : date('Y-m'); + return date('Y-m', strtotime($month . '-01')); + } + + private function normalizeDate(mixed $date): string + { + $date = is_string($date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) ? $date : date('Y-m-d'); + return date('Y-m-d', strtotime($date)); + } +} diff --git a/backend/plugin/produce/app/service/api/.gitkeep b/backend/plugin/produce/app/service/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/validate/.gitkeep b/backend/plugin/produce/app/validate/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/app/validate/ExceptionRecordValidate.php b/backend/plugin/produce/app/validate/ExceptionRecordValidate.php new file mode 100644 index 0000000..340fda4 --- /dev/null +++ b/backend/plugin/produce/app/validate/ExceptionRecordValidate.php @@ -0,0 +1,66 @@ + ['required', 'max:100'], + 'user_code' => ['nullable', 'max:50'], + 'reporter' => ['nullable', 'max:100'], + 'type_id' => ['nullable', 'integer'], + 'type_code' => ['required', 'max:64'], + 'type_name' => ['required', 'max:100'], + 'reason_id' => ['nullable', 'integer'], + 'reason_code' => ['required', 'max:64'], + 'reason_name' => ['required', 'max:100'], + 'issue_id' => ['nullable', 'integer'], + 'issue_code' => ['nullable', 'max:64'], + 'issue_name' => ['nullable', 'max:100'], + 'images' => ['nullable'], + 'remark' => ['nullable', 'max:65535'], + 'status' => ['nullable', 'integer', 'in:0,1,2'], + 'handler' => ['nullable', 'max:100'], + 'handler_id' => ['nullable', 'integer'], + 'handled_at' => ['nullable', 'integer'], + 'reported_at' => ['nullable', 'integer'], + ]; + } + + protected array $message = [ + 'order_no.required' => '单号或条码不能为空', + 'order_no.max' => '单号或条码不能超过100个字符', + 'user_code.max' => '提报人工号不能超过50个字符', + 'reporter.max' => '提报人不能超过100个字符', + 'type_id.integer' => '处理类型格式不正确', + 'type_code.required' => '处理类型编码不能为空', + 'type_code.max' => '处理类型编码不能超过64个字符', + 'type_name.required' => '处理类型名称不能为空', + 'type_name.max' => '处理类型名称不能超过100个字符', + 'reason_id.integer' => '异常原因格式不正确', + 'reason_code.required' => '异常原因编码不能为空', + 'reason_code.max' => '异常原因编码不能超过64个字符', + 'reason_name.required' => '异常原因名称不能为空', + 'reason_name.max' => '异常原因名称不能超过100个字符', + 'issue_id.integer' => '具体问题格式不正确', + 'issue_code.max' => '具体问题编码不能超过64个字符', + 'issue_name.max' => '具体问题名称不能超过100个字符', + 'status.integer' => '处理状态格式不正确', + 'status.in' => '处理状态值不正确', + 'handler.max' => '处理人不能超过100个字符', + 'handler_id.integer' => '处理人ID格式不正确', + 'handled_at.integer' => '处理时间格式不正确', + 'reported_at.integer' => '提报时间格式不正确', + ]; + + protected array $scene = [ + 'store' => ['order_no', 'user_code', 'reporter', 'type_id', 'type_code', 'type_name', 'reason_id', 'reason_code', 'reason_name', 'issue_id', 'issue_code', 'issue_name', 'images', 'remark', 'status', 'handler', 'handler_id', 'handled_at', 'reported_at'], + 'update' => ['order_no', 'user_code', 'reporter', 'type_id', 'type_code', 'type_name', 'reason_id', 'reason_code', 'reason_name', 'issue_id', 'issue_code', 'issue_name', 'images', 'remark', 'status', 'handler', 'handler_id', 'handled_at', 'reported_at'], + ]; +} diff --git a/backend/plugin/produce/app/validate/ExceptionSettingValidate.php b/backend/plugin/produce/app/validate/ExceptionSettingValidate.php new file mode 100644 index 0000000..7538956 --- /dev/null +++ b/backend/plugin/produce/app/validate/ExceptionSettingValidate.php @@ -0,0 +1,43 @@ + ['nullable', 'integer'], + 'setting_type' => ['required', 'in:type,reason,issue'], + 'code' => ['required', 'max:64'], + 'name' => ['required', 'max:100'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'is_enabled' => ['nullable', 'integer', 'in:0,1'], + 'remark' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'parent_id.integer' => '父级设置格式不正确', + 'setting_type.required' => '设置类型不能为空', + 'setting_type.in' => '设置类型值不正确', + 'code.required' => '设置编码不能为空', + 'code.max' => '设置编码不能超过64个字符', + 'name.required' => '设置名称不能为空', + 'name.max' => '设置名称不能超过100个字符', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'is_enabled.integer' => '启用状态格式不正确', + 'is_enabled.in' => '启用状态值不正确', + 'remark.max' => '备注内容过长', + ]; + + protected array $scene = [ + 'store' => ['parent_id', 'setting_type', 'code', 'name', 'sort', 'is_enabled', 'remark'], + 'update' => ['parent_id', 'setting_type', 'code', 'name', 'sort', 'is_enabled', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/HardwareFaultDescriptionValidate.php b/backend/plugin/produce/app/validate/HardwareFaultDescriptionValidate.php new file mode 100644 index 0000000..e037185 --- /dev/null +++ b/backend/plugin/produce/app/validate/HardwareFaultDescriptionValidate.php @@ -0,0 +1,34 @@ + ['required', 'integer'], + 'description' => ['required', 'max:500'], + 'is_enabled' => ['integer', 'in:0,1'], + 'remark' => ['max:500'], + ]; + } + + protected array $message = [ + 'category_id.required' => '请选择物料分类', + 'category_id.integer' => '物料分类不正确', + 'description.required' => '硬件故障描述不能为空', + 'description.max' => '硬件故障描述不能超过500个字符', + 'is_enabled.in' => '启用状态不正确', + 'remark.max' => '备注不能超过500个字符', + ]; + + protected array $scene = [ + 'store' => ['category_id', 'description', 'is_enabled', 'remark'], + 'update' => ['category_id', 'description', 'is_enabled', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/MaterialCategoryValidate.php b/backend/plugin/produce/app/validate/MaterialCategoryValidate.php new file mode 100644 index 0000000..36a85b2 --- /dev/null +++ b/backend/plugin/produce/app/validate/MaterialCategoryValidate.php @@ -0,0 +1,25 @@ + ['required', 'max:100']]; + } + + protected array $message = [ + 'category_name.required' => '分类名称不能为空', + 'category_name.max' => '分类名称不能超过100个字符', + ]; + + protected array $scene = [ + 'store' => ['category_name'], + 'update' => ['category_name'], + ]; +} diff --git a/backend/plugin/produce/app/validate/MaterialValidate.php b/backend/plugin/produce/app/validate/MaterialValidate.php new file mode 100644 index 0000000..0ec14f4 --- /dev/null +++ b/backend/plugin/produce/app/validate/MaterialValidate.php @@ -0,0 +1,32 @@ + ['required'], + 'product_code' => ['required', 'max:100'], + 'product_name' => ['required', 'max:200'], + ]; + } + + protected array $message = [ + 'category_id.required' => '请选择分类', + 'product_code.required' => '商品编码不能为空', + 'product_code.max' => '商品编码不能超过100个字符', + 'product_name.required' => '商品名称不能为空', + 'product_name.max' => '商品名称不能超过200个字符', + ]; + + protected array $scene = [ + 'store' => ['category_id', 'product_code', 'product_name'], + 'update' => ['category_id', 'product_code', 'product_name'], + ]; +} diff --git a/backend/plugin/produce/app/validate/ProcessStepValidate.php b/backend/plugin/produce/app/validate/ProcessStepValidate.php new file mode 100644 index 0000000..27c9de2 --- /dev/null +++ b/backend/plugin/produce/app/validate/ProcessStepValidate.php @@ -0,0 +1,63 @@ +route?->param('id'); + $processId = (string) (request()->input('process_id') ?? ''); + + return [ + 'process_id' => ['required', 'integer'], + 'step_code' => [ + 'required', + 'max:50', + Rule::unique(ProcessStep::class, 'step_code') + ->where(fn ($query) => $query->where('process_id', $processId)) + ->ignore($id, 'id') + ], + 'step_name' => ['required', 'max:100'], + 'description' => ['nullable', 'max:65535'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'status' => ['nullable', 'integer', 'in:0,1'], + 'prerequisite_steps' => ['nullable'], + 'prerequisite_mode' => ['nullable', 'in:all,any,none'], + 'prerequisite_require_final' => ['nullable', 'integer', 'in:0,1'], + 'allow_repeat_scan' => ['nullable', 'integer', 'in:0,1'], + 'repeat_scan_statuses' => ['nullable'], + ]; + } + + protected array $message = [ + 'process_id.required' => '所属工序不能为空', + 'process_id.integer' => '所属工序格式不正确', + 'step_code.required' => '步骤编码不能为空', + 'step_code.max' => '步骤编码不能超过50个字符', + 'step_code.unique' => '步骤编码已存在', + 'step_name.required' => '步骤名称不能为空', + 'step_name.max' => '步骤名称不能超过100个字符', + 'description.max' => '步骤描述内容过长', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + 'prerequisite_mode.in' => '前置步骤验证模式值不正确', + 'prerequisite_require_final.integer' => '前置步骤终态要求格式不正确', + 'prerequisite_require_final.in' => '前置步骤终态要求值不正确', + 'allow_repeat_scan.integer' => '允许重复扫码格式不正确', + 'allow_repeat_scan.in' => '允许重复扫码值不正确', + ]; + + protected array $scene = [ + 'store' => ['process_id', 'step_code', 'step_name', 'description', 'sort', 'status', 'prerequisite_steps', 'prerequisite_mode', 'prerequisite_require_final', 'allow_repeat_scan', 'repeat_scan_statuses'], + 'update' => ['process_id', 'step_code', 'step_name', 'description', 'sort', 'status', 'prerequisite_steps', 'prerequisite_mode', 'prerequisite_require_final', 'allow_repeat_scan', 'repeat_scan_statuses'], + ]; +} diff --git a/backend/plugin/produce/app/validate/ProcessValidate.php b/backend/plugin/produce/app/validate/ProcessValidate.php new file mode 100644 index 0000000..7d89b55 --- /dev/null +++ b/backend/plugin/produce/app/validate/ProcessValidate.php @@ -0,0 +1,43 @@ +route?->param('id'); + + return [ + 'process_code' => ['required', 'max:50', Rule::unique(Process::class, 'process_code')->ignore($id, 'id')], + 'process_name' => ['required', 'max:100'], + 'description' => ['nullable', 'max:65535'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'status' => ['nullable', 'integer', 'in:0,1'], + ]; + } + + protected array $message = [ + 'process_code.required' => '工序编码不能为空', + 'process_code.max' => '工序编码不能超过50个字符', + 'process_code.unique' => '工序编码已存在', + 'process_name.required' => '工序名称不能为空', + 'process_name.max' => '工序名称不能超过100个字符', + 'description.max' => '工序描述内容过长', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + ]; + + protected array $scene = [ + 'store' => ['process_code', 'process_name', 'description', 'sort', 'status'], + 'update' => ['process_code', 'process_name', 'description', 'sort', 'status'], + ]; +} diff --git a/backend/plugin/produce/app/validate/ProductRecordValidate.php b/backend/plugin/produce/app/validate/ProductRecordValidate.php new file mode 100644 index 0000000..2c4c091 --- /dev/null +++ b/backend/plugin/produce/app/validate/ProductRecordValidate.php @@ -0,0 +1,51 @@ + ['required', 'max:100'], + 'product_code' => ['required', 'max:100'], + 'quantity' => ['required', 'integer', 'min:1'], + 'user_code' => ['required', 'max:50'], + 'operator' => ['nullable', 'max:100'], + 'process_id' => ['nullable', 'integer'], + 'step_id' => ['nullable', 'integer'], + 'status_id' => ['nullable', 'integer'], + 'scan_result' => ['required', 'integer', 'in:0,1'], + 'remark' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'process_order_no.required' => '加工单号不能为空', + 'process_order_no.max' => '加工单号不能超过100个字符', + 'product_code.required' => '商品编码不能为空', + 'product_code.max' => '商品编码不能超过100个字符', + 'quantity.required' => '数量不能为空', + 'quantity.integer' => '数量格式不正确', + 'quantity.min' => '数量必须大于0', + 'user_code.required' => '用户编码不能为空', + 'user_code.max' => '用户编码不能超过50个字符', + 'operator.max' => '操作人员不能超过100个字符', + 'process_id.integer' => '工序格式不正确', + 'step_id.integer' => '步骤格式不正确', + 'status_id.integer' => '状态格式不正确', + 'scan_result.required' => '扫码结果不能为空', + 'scan_result.integer' => '扫码结果格式不正确', + 'scan_result.in' => '扫码结果值不正确', + 'remark.max' => '备注内容过长', + ]; + + protected array $scene = [ + 'store' => ['process_order_no', 'product_code', 'quantity', 'user_code', 'operator', 'process_id', 'step_id', 'status_id', 'scan_result', 'remark'], + 'update' => ['process_order_no', 'product_code', 'quantity', 'user_code', 'operator', 'process_id', 'step_id', 'status_id', 'scan_result', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/RepairRecordValidate.php b/backend/plugin/produce/app/validate/RepairRecordValidate.php new file mode 100644 index 0000000..138d4ee --- /dev/null +++ b/backend/plugin/produce/app/validate/RepairRecordValidate.php @@ -0,0 +1,49 @@ + ['required', 'max:100'], + 'order_no' => ['max:100'], + 'product_code' => ['max:100'], + 'product_name' => ['max:200'], + 'category_id' => ['integer'], + 'category_ids' => ['array'], + 'category_ids.*' => ['integer'], + 'fault_description_id' => ['integer'], + 'repair_result' => ['max:50'], + 'operator' => ['max:100'], + 'repaired_at' => ['integer'], + 'remark' => ['max:1000'], + ]; + } + + protected array $message = [ + 'repair_no.required' => '维修单号不能为空', + 'repair_no.max' => '维修单号不能超过100个字符', + 'order_no.max' => '订单号不能超过100个字符', + 'product_code.max' => '商品编码不能超过100个字符', + 'product_name.max' => '商品名称不能超过200个字符', + 'category_id.integer' => '物料分类不正确', + 'category_ids.array' => '物料分类集合格式不正确', + 'category_ids.*.integer' => '物料分类不正确', + 'fault_description_id.integer' => '硬件故障描述不正确', + 'repair_result.max' => '维修判定不能超过50个字符', + 'operator.max' => '维修人员不能超过100个字符', + 'repaired_at.integer' => '维修时间不正确', + 'remark.max' => '备注不能超过1000个字符', + ]; + + protected array $scene = [ + 'store' => ['repair_no', 'order_no', 'product_code', 'product_name', 'category_id', 'category_ids', 'fault_description_id', 'repair_result', 'parts', 'operator', 'repaired_at', 'remark'], + 'update' => ['repair_no', 'order_no', 'product_code', 'product_name', 'category_id', 'category_ids', 'fault_description_id', 'repair_result', 'parts', 'operator', 'repaired_at', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/RepairSettingValidate.php b/backend/plugin/produce/app/validate/RepairSettingValidate.php new file mode 100644 index 0000000..3ebae2e --- /dev/null +++ b/backend/plugin/produce/app/validate/RepairSettingValidate.php @@ -0,0 +1,43 @@ + ['nullable', 'integer'], + 'setting_type' => ['required', 'in:type,reason,issue'], + 'code' => ['required', 'max:64'], + 'name' => ['required', 'max:100'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'is_enabled' => ['nullable', 'integer', 'in:0,1'], + 'remark' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'parent_id.integer' => '父级设置格式不正确', + 'setting_type.required' => '设置类型不能为空', + 'setting_type.in' => '设置类型值不正确', + 'code.required' => '设置编码不能为空', + 'code.max' => '设置编码不能超过64个字符', + 'name.required' => '设置名称不能为空', + 'name.max' => '设置名称不能超过100个字符', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'is_enabled.integer' => '启用状态格式不正确', + 'is_enabled.in' => '启用状态值不正确', + 'remark.max' => '备注内容过长', + ]; + + protected array $scene = [ + 'store' => ['parent_id', 'setting_type', 'code', 'name', 'sort', 'is_enabled', 'remark'], + 'update' => ['parent_id', 'setting_type', 'code', 'name', 'sort', 'is_enabled', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/ScanDataValidate.php b/backend/plugin/produce/app/validate/ScanDataValidate.php new file mode 100644 index 0000000..bd63576 --- /dev/null +++ b/backend/plugin/produce/app/validate/ScanDataValidate.php @@ -0,0 +1,49 @@ + ['required', 'max:100'], + 'quantity' => ['nullable', 'integer', 'min:1'], + 'user_code' => ['required', 'max:50'], + 'operator' => ['nullable', 'max:100'], + 'process_id' => ['required', 'integer'], + 'step_id' => ['required', 'integer'], + 'status_id' => ['nullable', 'integer'], + 'scan_result' => ['required', 'integer', 'in:0,1'], + 'remark' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'order_no.required' => '订单号不能为空', + 'order_no.max' => '订单号不能超过100个字符', + 'quantity.integer' => '本次数量格式不正确', + 'quantity.min' => '本次数量必须大于0', + 'user_code.required' => '用户编码不能为空', + 'user_code.max' => '用户编码不能超过50个字符', + 'operator.max' => '操作人员不能超过100个字符', + 'process_id.required' => '工序不能为空', + 'process_id.integer' => '工序格式不正确', + 'step_id.required' => '步骤不能为空', + 'step_id.integer' => '步骤格式不正确', + 'status_id.integer' => '状态格式不正确', + 'scan_result.required' => '扫码结果不能为空', + 'scan_result.integer' => '扫码结果格式不正确', + 'scan_result.in' => '扫码结果值不正确', + 'remark.max' => '备注内容过长', + ]; + + protected array $scene = [ + 'store' => ['order_no', 'quantity', 'user_code', 'operator', 'process_id', 'step_id', 'status_id', 'scan_result', 'remark'], + 'update' => ['order_no', 'quantity', 'user_code', 'operator', 'process_id', 'step_id', 'status_id', 'scan_result', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/SpecificRuleValidate.php b/backend/plugin/produce/app/validate/SpecificRuleValidate.php new file mode 100644 index 0000000..4834c3b --- /dev/null +++ b/backend/plugin/produce/app/validate/SpecificRuleValidate.php @@ -0,0 +1,47 @@ + ['required', 'max:255'], + 'rule_pattern' => ['required'], + 'rule_type' => ['required', 'integer'], + 'apply_mode' => ['required', 'in:order,machine,both'], + 'match_field' => ['required', 'in:order_no,process_order_no,product_code,user_code'], + 'is_enabled' => ['nullable', 'integer', 'in:0,1'], + 'step_ids' => ['nullable'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'remark' => ['nullable', 'max:65535'], + ]; + } + + protected array $message = [ + 'rule_name.required' => '规则名称不能为空', + 'rule_name.max' => '规则名称不能超过255个字符', + 'rule_pattern.required' => '规则表达式不能为空', + 'rule_type.required' => '规则类型不能为空', + 'rule_type.integer' => '规则类型格式不正确', + 'apply_mode.required' => '适用模式不能为空', + 'apply_mode.in' => '适用模式值不正确', + 'match_field.required' => '匹配字段不能为空', + 'match_field.in' => '匹配字段值不正确', + 'is_enabled.integer' => '启用状态格式不正确', + 'is_enabled.in' => '启用状态值不正确', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'remark.max' => '备注内容过长', + ]; + + protected array $scene = [ + 'store' => ['rule_name', 'rule_pattern', 'rule_type', 'apply_mode', 'match_field', 'is_enabled', 'step_ids', 'sort', 'remark'], + 'update' => ['rule_name', 'rule_pattern', 'rule_type', 'apply_mode', 'match_field', 'is_enabled', 'step_ids', 'sort', 'remark'], + ]; +} diff --git a/backend/plugin/produce/app/validate/StatusTransitionValidate.php b/backend/plugin/produce/app/validate/StatusTransitionValidate.php new file mode 100644 index 0000000..ac87f95 --- /dev/null +++ b/backend/plugin/produce/app/validate/StatusTransitionValidate.php @@ -0,0 +1,103 @@ + ['required', 'integer'], + 'from_status_id' => ['required', 'integer'], + 'to_status_id' => ['required', 'integer', 'different:from_status_id'], + 'trigger_event' => ['required', 'max:100', 'in:next_step_scan,scan_trigger,exception_trigger,manual_trigger,timer_trigger,complete_trigger'], + 'event_config' => [ + 'nullable', + function (string $attribute, mixed $value, \Closure $fail) { + if ($value === null || $value === '') { + return; + } + + if (is_array($value)) { + return; + } + + if (!is_string($value)) { + $fail('事件配置必须为有效的JSON字符串'); + return; + } + + json_decode($value, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $fail('事件配置必须为有效的JSON字符串'); + } + } + ], + 'auto_trigger' => ['nullable', 'integer', 'in:0,1'], + 'priority' => ['nullable', 'integer', 'min:0'], + 'transition_type' => ['required', 'max:20', 'in:primary,side_effect'], + 'side_effect_type' => ['nullable', 'max:50', 'in:log,notify,update_field,trigger_event'], + 'side_effect_config' => [ + 'nullable', + function (string $attribute, mixed $value, \Closure $fail) { + if ($value === null || $value === '') { + return; + } + + if (is_array($value)) { + return; + } + + if (!is_string($value)) { + $fail('副作用配置必须为有效的JSON字符串'); + return; + } + + json_decode($value, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $fail('副作用配置必须为有效的JSON字符串'); + } + } + ], + 'description' => ['nullable', 'max:65535'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'status' => ['nullable', 'integer', 'in:0,1'], + ]; + } + + protected array $message = [ + 'step_id.required' => '所属步骤不能为空', + 'step_id.integer' => '所属步骤格式不正确', + 'from_status_id.required' => '源状态不能为空', + 'from_status_id.integer' => '源状态格式不正确', + 'to_status_id.required' => '目标状态不能为空', + 'to_status_id.integer' => '目标状态格式不正确', + 'to_status_id.different' => '目标状态不能与源状态相同', + 'trigger_event.required' => '触发事件不能为空', + 'trigger_event.max' => '触发事件长度不能超过100个字符', + 'trigger_event.in' => '触发事件值不正确', + 'auto_trigger.integer' => '自动触发格式不正确', + 'auto_trigger.in' => '自动触发值不正确', + 'priority.integer' => '优先级必须为整数', + 'priority.min' => '优先级不能小于0', + 'transition_type.required' => '转换类型不能为空', + 'transition_type.max' => '转换类型长度不能超过20个字符', + 'transition_type.in' => '转换类型值不正确', + 'side_effect_type.max' => '副作用类型长度不能超过50个字符', + 'side_effect_type.in' => '副作用类型值不正确', + 'description.max' => '描述内容过长', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + ]; + + protected array $scene = [ + 'store' => ['step_id', 'from_status_id', 'to_status_id', 'trigger_event', 'event_config', 'auto_trigger', 'priority', 'transition_type', 'side_effect_type', 'side_effect_config', 'description', 'sort', 'status'], + 'update' => ['step_id', 'from_status_id', 'to_status_id', 'trigger_event', 'event_config', 'auto_trigger', 'priority', 'transition_type', 'side_effect_type', 'side_effect_config', 'description', 'sort', 'status'], + ]; +} diff --git a/backend/plugin/produce/app/validate/StepRelationValidate.php b/backend/plugin/produce/app/validate/StepRelationValidate.php new file mode 100644 index 0000000..0995bdb --- /dev/null +++ b/backend/plugin/produce/app/validate/StepRelationValidate.php @@ -0,0 +1,86 @@ +route?->param('id'); + $fromStepId = (string) (request()->input('from_step_id') ?? ''); + $toStepId = (string) (request()->input('to_step_id') ?? ''); + $relationType = (string) (request()->input('relation_type') ?? 'normal'); + + return [ + 'from_step_id' => ['required', 'integer'], + 'to_step_id' => ['required', 'integer', 'different:from_step_id'], + 'relation_type' => [ + 'required', + 'max:20', + Rule::in(['normal', 'exception']), + Rule::unique(StepRelation::class, 'relation_type') + ->where(fn ($query) => $query + ->where('from_step_id', $fromStepId) + ->where('to_step_id', $toStepId) + ) + ->ignore($id, 'id') + ], + 'flow_condition' => [ + 'nullable', + function (string $attribute, mixed $value, \Closure $fail) { + if ($value === null || $value === '') { + return; + } + + if (is_array($value)) { + return; + } + + if (!is_string($value)) { + $fail('流转条件必须为有效的JSON字符串'); + return; + } + + json_decode($value, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $fail('流转条件必须为有效的JSON字符串'); + } + } + ], + 'priority' => ['nullable', 'integer', 'min:0'], + 'description' => ['nullable', 'max:65535'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'status' => ['nullable', 'integer', 'in:0,1'], + ]; + } + + protected array $message = [ + 'from_step_id.required' => '源步骤不能为空', + 'from_step_id.integer' => '源步骤格式不正确', + 'to_step_id.required' => '目标步骤不能为空', + 'to_step_id.integer' => '目标步骤格式不正确', + 'to_step_id.different' => '目标步骤不能与源步骤相同', + 'relation_type.required' => '关系类型不能为空', + 'relation_type.max' => '关系类型长度不能超过20个字符', + 'relation_type.in' => '关系类型值不正确', + 'relation_type.unique' => '该步骤关系已存在', + 'priority.integer' => '优先级必须为整数', + 'priority.min' => '优先级不能小于0', + 'description.max' => '描述内容过长', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + ]; + + protected array $scene = [ + 'store' => ['from_step_id', 'to_step_id', 'relation_type', 'flow_condition', 'priority', 'description', 'sort', 'status'], + 'update' => ['from_step_id', 'to_step_id', 'relation_type', 'flow_condition', 'priority', 'description', 'sort', 'status'], + ]; +} diff --git a/backend/plugin/produce/app/validate/StepStatusValidate.php b/backend/plugin/produce/app/validate/StepStatusValidate.php new file mode 100644 index 0000000..6233431 --- /dev/null +++ b/backend/plugin/produce/app/validate/StepStatusValidate.php @@ -0,0 +1,59 @@ +route?->param('id'); + $stepId = (string) (request()->input('step_id') ?? ''); + + return [ + 'step_id' => ['required', 'integer'], + 'status_code' => [ + 'required', + 'max:50', + Rule::unique(StepStatus::class, 'status_code') + ->where(fn ($query) => $query->where('step_id', $stepId)) + ->ignore($id, 'id') + ], + 'status_name' => ['required', 'max:100'], + 'is_initial' => ['nullable', 'integer', 'in:0,1'], + 'is_final' => ['nullable', 'integer', 'in:0,1'], + 'description' => ['nullable', 'max:65535'], + 'sort' => ['nullable', 'integer', 'min:0'], + 'status' => ['nullable', 'integer', 'in:0,1'], + ]; + } + + protected array $message = [ + 'step_id.required' => '所属步骤不能为空', + 'step_id.integer' => '所属步骤格式不正确', + 'status_code.required' => '状态编码不能为空', + 'status_code.max' => '状态编码不能超过50个字符', + 'status_code.unique' => '状态编码已存在', + 'status_name.required' => '状态名称不能为空', + 'status_name.max' => '状态名称不能超过100个字符', + 'is_initial.integer' => '初始状态格式不正确', + 'is_initial.in' => '初始状态值不正确', + 'is_final.integer' => '终态格式不正确', + 'is_final.in' => '终态值不正确', + 'description.max' => '状态描述内容过长', + 'sort.integer' => '排序必须为整数', + 'sort.min' => '排序不能小于0', + 'status.integer' => '状态格式不正确', + 'status.in' => '状态值不正确', + ]; + + protected array $scene = [ + 'store' => ['step_id', 'status_code', 'status_name', 'is_initial', 'is_final', 'description', 'sort', 'status'], + 'update' => ['step_id', 'status_code', 'status_name', 'is_initial', 'is_final', 'description', 'sort', 'status'], + ]; +} diff --git a/backend/plugin/produce/config/app.php b/backend/plugin/produce/config/app.php new file mode 100644 index 0000000..a2a027c --- /dev/null +++ b/backend/plugin/produce/config/app.php @@ -0,0 +1,9 @@ + true, + 'debug' => true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '1.0.0', +]; diff --git a/backend/plugin/produce/config/info.php b/backend/plugin/produce/config/info.php new file mode 100644 index 0000000..a384834 --- /dev/null +++ b/backend/plugin/produce/config/info.php @@ -0,0 +1,20 @@ + 'produce', + 'identifier' => 'produce', + 'type' => 'madong:plugin', + 'version' => '1.0.0', + 'description' => '', + 'author' => 'mrlee', + 'author_email' => '', + 'website' => '', + 'uninstall' => [ + 'drop_tables' => false, + 'remove_dependencies' => false, + ], + ]; diff --git a/backend/plugin/produce/config/installed.php b/backend/plugin/produce/config/installed.php new file mode 100644 index 0000000..7bb33e2 --- /dev/null +++ b/backend/plugin/produce/config/installed.php @@ -0,0 +1,4 @@ + '1.0.0', + 'installed_at' => '2026-08-30 15:44:35', +); \ No newline at end of file diff --git a/backend/plugin/produce/config/middleware.php b/backend/plugin/produce/config/middleware.php new file mode 100644 index 0000000..1b12afb --- /dev/null +++ b/backend/plugin/produce/config/middleware.php @@ -0,0 +1,19 @@ + [ + ApiAccessTokenMiddleware::class, + ], + 'adminapi' => [ + AccessTokenMiddleware::class, + ], + '@' => [ + \app\middleware\AllowCrossOriginMiddleware::class, + ], +]; diff --git a/backend/plugin/produce/config/route.php b/backend/plugin/produce/config/route.php new file mode 100644 index 0000000..6c12a09 --- /dev/null +++ b/backend/plugin/produce/config/route.php @@ -0,0 +1,126 @@ +registerRoute([ + 'route_prefix' => '/produce/openapi', + 'register_route' => true, + 'openapi_doc' => [ + 'scan_path' => [ + base_path('plugin/produce/app/api'), + ], + 'modify' => function (OA\OpenApi $openapi) { + $openapi->info->title = '生产管理 API'; + $openapi->info->version = '1.0.0'; + $openapi->servers = [ + new OA\Server( + [ + 'url' => '/api', + 'description' => request()->host(), + ] + ), + ]; + /** @phpstan-ignore-next-line */ + if (!$openapi->components instanceof OA\Components) { + $openapi->components = new OA\Components([]); + } + $openapi->components->securitySchemes = [ + new OA\SecurityScheme([ + 'securityScheme' => 'api_key', + 'type' => 'apiKey', + 'name' => config('core.jwt.app.token_name', 'Authorization'), + 'in' => 'header', + ]), + ]; + }, + ], + ]); +}); + +/** + * 注册 AppAPI 路由 + */ +Route::group('/appapi', function () { + Swagger::create()->registerRoute([ + 'route_prefix' => '/produce/openapi', + 'register_route' => true, + 'openapi_doc' => [ + 'scan_path' => [ + base_path('plugin/produce/app/appapi'), + ], + 'modify' => function (OA\OpenApi $openapi) { + $openapi->info->title = '生产管理 APP API'; + $openapi->info->version = '1.0.0'; + $openapi->servers = [ + new OA\Server( + [ + 'url' => '/appapi', + 'description' => request()->host(), + ] + ), + ]; + /** @phpstan-ignore-next-line */ + if (!$openapi->components instanceof OA\Components) { + $openapi->components = new OA\Components([]); + } + $openapi->components->securitySchemes = [ + new OA\SecurityScheme([ + 'securityScheme' => 'api_key', + 'type' => 'apiKey', + 'name' => config('core.jwt.app.token_name', 'Authorization'), + 'in' => 'header', + ]), + ]; + }, + ], + ]); +}); + +/** + * 注册 AdminAPI 路由 + */ +Route::group('/adminapi', function () { + Swagger::create()->registerRoute([ + 'route_prefix' => '/produce/openapi', + 'register_route' => true, + 'openapi_doc' => [ + 'scan_path' => [ + base_path('plugin/produce/app/adminapi'), + ], + 'modify' => function (OA\OpenApi $openapi) { + $openapi->info->title = '生产管理 API'; + $openapi->info->version = '1.0.0'; + $openapi->servers = [ + new OA\Server( + [ + 'url' => '/adminapi', + 'description' => request()->host(), + ] + ), + ]; + /** @phpstan-ignore-next-line */ + if (!$openapi->components instanceof OA\Components) { + $openapi->components = new OA\Components([]); + } + $openapi->components->securitySchemes = [ + new OA\SecurityScheme([ + 'securityScheme' => 'api_key', + 'type' => 'apiKey', + 'name' => config('core.jwt.app.token_name', 'Authorization'), + 'in' => 'header', + ]), + ]; + }, + ], + ]); +}); diff --git a/backend/plugin/produce/config/translation.php b/backend/plugin/produce/config/translation.php new file mode 100644 index 0000000..9caa692 --- /dev/null +++ b/backend/plugin/produce/config/translation.php @@ -0,0 +1,7 @@ + config('app.lang'), + 'fallback_locale' => ['zh_CN', 'en'], + 'path' => base_path() . '/plugin/produce/resource/translations', +]; diff --git a/backend/plugin/produce/produce-replication-plan.md b/backend/plugin/produce/produce-replication-plan.md new file mode 100644 index 0000000..a6fe19d --- /dev/null +++ b/backend/plugin/produce/produce-replication-plan.md @@ -0,0 +1,1044 @@ +# Produce 插件六模块复刻方案 + +## 1. 文档目的 + +本文档用于梳理 `backend/plugin/produce` 插件需要优先复刻的 6 个模块,并结合旧项目的前端、后端、SQL 安装脚本,整理出一份可以直接执行的开发方案。 + +目标模块: + +1. 工序管理 `process` +2. 工序步骤 `process-step` +3. 步骤状态 `step-status` +4. 步骤关系 `step-relation` +5. 状态转换 `status-transition` +6. 转换日志 `transition-log` + +本文档基于以下真实参考: + +- 旧前端目录:`old/web/src/views/produce/*` +- 旧后端路由:`old/server/app/admin/route/produce.php` +- 旧后端控制器:`old/server/app/admin/controller/produce/*` +- 旧后端 Validate:`old/server/app/admin/validate/produce/*` +- 旧后端 DAO / Model / Service:`old/server/app/common/{dao,model,services}/produce/*` +- 旧库 SQL:`old/server/scripts/sql/install.sql` +- 当前插件骨架:`backend/plugin/produce/*` + +## 2. 当前 produce 插件现状 + +### 2.1 已存在结构 + +当前插件目录:`backend/plugin/produce` + +已有基础骨架: + +- `app/adminapi/controller/ProduceController.php` +- `config/app.php` +- `config/info.php` +- `config/route.php` +- `resource/menu/admin.php` +- `resource/template/admin/api` +- `resource/template/admin/views/produce/index` +- `resource/template/admin/lang` + +### 2.2 缺失结构 + +以下目录当前基本为空,尚不足以承载 6 个业务模块: + +- `app/model` +- `app/dao` +- `app/service` +- `app/validate` +- `app/schema` +- `resource/database/migrations` + +结论:当前 `produce` 插件仍是示例骨架,复刻这 6 个模块时需要按插件标准完整补齐后端数据层、控制器、菜单、模板源前端。 + +## 3. 旧项目总体结构结论 + +### 3.1 旧前端页面模式 + +旧前端每个模块基本采用如下模式: + +- `index.vue` +- `data.ts` +- `modules/form.vue` + +技术模式为: + +- `VXE Grid` +- 手写查询表单 +- Drawer Form +- 手写列表请求与弹窗逻辑 + +不适合直接复制到当前项目。当前插件应改造为: + +- `BasicCrud` +- `schemas/index.tsx` +- `api/index.ts` +- 插件模板源目录开发 + +### 3.2 旧后端路由模式 + +旧后端集中在:`old/server/app/admin/route/produce.php` + +6 个目标模块在旧项目均有独立路由分组,说明旧项目对这 6 个模块是完整拆分的,不是页面层虚拟模块。 + +### 3.3 旧数据库来源 + +旧项目这 6 张核心表的建表信息没有体现在 `old/server/scripts/migrations` 目录的独立 PHP migration 中,核心定义主要在 SQL 安装脚本中: + +- `old/server/scripts/sql/install.sql` + +因此本次字段与索引参考,优先以: + +1. SQL 安装脚本 +2. Model fillable / casts / relation +3. DAO / Service 业务逻辑 +4. 旧前端页面字段 + +作为综合依据。 + +## 4. 六模块真实表与后端入口对照 + +| 模块 | 表名(Model) | 旧控制器 | 旧服务/DAO | 旧前端目录 | +|---|---|---|---|---| +| 工序管理 | `produce_process` | `ProduceProcessController` | `ProduceProcessService` / `ProduceProcessDao` | `old/web/src/views/produce/process` | +| 工序步骤 | `produce_process_step` | `ProduceProcessStepController` | `ProduceProcessStepService` / `ProduceProcessStepDao` | `old/web/src/views/produce/process-step` | +| 步骤状态 | `produce_step_status` | `ProduceStepStatusController` | `ProduceStepStatusService` / `ProduceStepStatusDao` | `old/web/src/views/produce/step-status` | +| 步骤关系 | `produce_step_relation` | `ProduceStepRelationController` | `ProduceStepRelationService` / `ProduceStepRelationDao` | `old/web/src/views/produce/step-relation` | +| 状态转换 | `produce_status_transition` | `ProduceStatusTransitionController` | `ProduceStatusTransitionService` / `ProduceStatusTransitionDao` | `old/web/src/views/produce/status-transition` | +| 转换日志 | `produce_transition_log` | `ProduceTransitionLogController` | `ProduceTransitionLogService` / `ProduceTransitionLogDao` | `old/web/src/views/produce/transition-log` | + +## 5. 模块依赖关系 + +这 6 个模块存在清晰的业务依赖链: + +1. `process` +2. `process-step` 依赖 `process` +3. `step-status` 依赖 `process-step` +4. `step-relation` 依赖 `process-step` +5. `status-transition` 依赖: + - `process-step` + - `step-status` +6. `transition-log` 依赖: + - `process-step` + - `step-status` + - `status-transition` + - 生产执行侧产生的数据 + +推荐开发顺序必须遵循该依赖链。 + +## 6. 六模块详细分析 + +### 6.1 工序管理 `process` + +#### 6.1.1 旧前端参考 + +目录:`old/web/src/views/produce/process` + +查询字段: + +- `LIKE_process_code` +- `LIKE_process_name` +- `status` + +列表字段: + +- `process_code` +- `process_name` +- `description` +- `step_count` +- `sort` +- `status` +- `created_date` + +表单字段: + +- `process_code` +- `process_name` +- `sort` +- `status` +- `description` + +#### 6.1.2 旧后端模型字段 + +模型:`old/server/app/common/model/produce/ProduceProcess.php` + +真实字段: + +- `id` +- `tenant_id` +- `process_code` +- `process_name` +- `description` +- `sort` +- `status` +- `step_count` +- `created_by` +- `updated_by` +- `created_at` +- `updated_at` +- `deleted_at` + +关系: + +- `steps()` -> `hasMany(ProduceProcessStep::class)` + +特殊点: + +- `step_count` 在模型中带有自动统计能力,不只是纯存储字段。 + +#### 6.1.3 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_process` + +关键约束: + +- 唯一索引:`uk_process_code(process_code, tenant_id, deleted_at)` +- 常规索引:`tenant_id`、`status`、`sort` + +#### 6.1.4 复杂度评估 + +- 复杂度:低 +- 适合作为第一个落地模块 + +#### 6.1.5 新插件建议 + +模块名建议:`process` + +后端应提供: + +- 标准 CRUD +- 下拉接口 `select/options` + +前端建议: + +- 标准 `BasicCrud` +- 保留查询项与排序、状态字段 + +--- + +### 6.2 工序步骤 `process-step` + +#### 6.2.1 旧前端参考 + +目录:`old/web/src/views/produce/process-step` + +查询字段: + +- `process_id` +- `LIKE_step_code` +- `LIKE_step_name` +- `status` + +列表字段: + +- `process_id`(实际显示工序名称) +- `step_code` +- `step_name` +- `description` +- `sort` +- `status` +- `created_date` + +表单字段: + +- `process_id` +- `step_code` +- `step_name` +- `sort` +- `status` +- `description` + +#### 6.2.2 旧后端模型字段 + +模型:`old/server/app/common/model/produce/ProduceProcessStep.php` + +真实字段: + +- `id` +- `tenant_id` +- `process_id` +- `step_code` +- `step_name` +- `description` +- `sort` +- `status` +- `prerequisite_steps` +- `prerequisite_mode` +- `prerequisite_require_final` +- `allow_repeat_scan` +- `repeat_scan_statuses` +- `created_by` +- `updated_by` +- `created_at` +- `updated_at` +- `deleted_at` + +casts: + +- `prerequisite_steps` => `array` +- `repeat_scan_statuses` => `array` +- `allow_repeat_scan` => `integer` +- `prerequisite_require_final` => `integer` + +关系: + +- `process()` -> `belongsTo(ProduceProcess::class)` + +#### 6.2.3 旧前端与真实后端差异 + +旧前端页面只展示了基础字段,但后端模型里还有生产执行相关能力字段: + +- 前置必要步骤 +- 前置步骤验证模式 +- 是否要求前置终态 +- 是否允许重复扫码 +- 哪些状态允许重复扫码 + +这说明: + +- 如果只按旧页面复刻,会丢失隐藏业务能力 +- 新插件应优先保留这些字段,只是可以分阶段决定首版是否全部开放 UI + +#### 6.2.4 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_process_step` + +关键约束: + +- 唯一索引:`uk_step_code(step_code, process_id, tenant_id, deleted_at)` +- 常规索引通常包括:`tenant_id`、`process_id`、`status`、`sort` + +#### 6.2.5 复杂度评估 + +- 复杂度:中 +- 依赖 `process` +- 基础 CRUD 不复杂,但完整业务字段不应忽略 + +#### 6.2.6 新插件建议 + +模块名建议:`process-step` + +建议分两阶段: + +第一阶段: + +- 工序下拉 +- 步骤编码/名称 +- 排序/状态/描述 + +第二阶段: + +- 前置步骤配置 +- 重复扫码控制 + +--- + +### 6.3 步骤状态 `step-status` + +#### 6.3.1 旧前端参考 + +目录:`old/web/src/views/produce/step-status` + +查询字段: + +- `step_id` +- `LIKE_status_code` +- `LIKE_status_name` +- `status` + +列表字段: + +- `step_id`(显示步骤名称) +- `status_code` +- `status_name` +- `is_initial` +- `is_final` +- `description` +- `sort` +- `status` +- `created_date` + +表单字段: + +- `step_id` +- `status_code` +- `status_name` +- `is_initial` +- `is_final` +- `sort` +- `status` +- `description` + +#### 6.3.2 旧后端模型字段 + +模型:`old/server/app/common/model/produce/ProduceStepStatus.php` + +真实字段: + +- `id` +- `tenant_id` +- `step_id` +- `status_code` +- `status_name` +- `is_initial` +- `is_final` +- `description` +- `sort` +- `status` +- `created_by` +- `updated_by` +- `created_at` +- `updated_at` +- `deleted_at` + +关系: + +- `step()` -> `belongsTo(ProduceProcessStep::class)` + +#### 6.3.3 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_step_status` + +典型业务含义: + +- `is_initial`:步骤初始状态 +- `is_final`:步骤终态 + +这是状态机基础数据,不应简化掉。 + +#### 6.3.4 复杂度评估 + +- 复杂度:中 +- 依赖 `process-step` +- 是状态转换模块的基础配置表 + +#### 6.3.5 新插件建议 + +模块名建议:`step-status` + +建议保留: + +- 所属步骤联动下拉 +- 初始/终态布尔字段 +- 基础启停状态 + +--- + +### 6.4 步骤关系 `step-relation` + +#### 6.4.1 旧前端参考 + +目录:`old/web/src/views/produce/step-relation` + +查询字段: + +- `from_step_id` +- `to_step_id` +- `relation_type` +- `status` + +列表字段: + +- `from_step_id`(显示源步骤名称) +- `to_step_id`(显示目标步骤名称) +- `relation_type` +- `priority` +- `flow_condition` +- `description` +- `sort` +- `status` +- `created_date` + +表单字段: + +- `from_step_id` +- `to_step_id` +- `relation_type` +- `priority` +- `flow_condition` +- `sort` +- `status` +- `description` + +关系类型枚举: + +- `normal` 正常流转 +- `exception` 异常流转 + +#### 6.4.2 旧后端模型 / DAO 参考 + +模型:`ProduceStepRelation` + +真实表:`produce_step_relation` + +DAO:`ProduceStepRelationDao` + +体现出的真实业务能力: + +- 支持按 `from_step_id` / `to_step_id` / `relation_type` 搜索 +- 支持按 `priority asc, sort asc` 排序 +- 提供 `getPreviousSteps(int $currentStepId)` 能力 + +这说明该模块不仅是配置关系,还直接用于生产流转路径判断。 + +#### 6.4.3 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_step_relation` + +真实字段: + +- `from_step_id` +- `to_step_id` +- `relation_type` +- `flow_condition`(JSON 文本) +- `priority` +- `description` +- `sort` +- `status` + +#### 6.4.4 复杂度评估 + +- 复杂度:中偏高 +- 核心复杂点在 `flow_condition` +- 仍可标准 CRUD 化 + +#### 6.4.5 新插件建议 + +模块名建议:`step-relation` + +前端首版可先: + +- 使用多行文本录入 JSON +- 列表中直接字符串展示 + +后续增强: + +- JSON 校验 +- 条件构造器 UI + +--- + +### 6.5 状态转换 `status-transition` + +#### 6.5.1 旧前端参考 + +目录:`old/web/src/views/produce/status-transition` + +查询字段: + +- `step_id` +- `trigger_event` +- `status` + +列表字段: + +- `step_id` +- `from_status_id` +- `to_status_id` +- `trigger_event` +- `transition_type` +- `auto_trigger` +- `priority` +- `description` +- `sort` +- `status` +- `created_date` + +表单字段: + +- `step_id` +- `from_status_id` +- `to_status_id` +- `trigger_event` +- `transition_type` +- `side_effect_type` +- `side_effect_config` +- `auto_trigger` +- `priority` +- `event_config` +- `sort` +- `status` +- `description` + +触发事件枚举: + +- `next_step_scan` +- `scan_trigger` +- `exception_trigger` +- `manual_trigger` +- `timer_trigger` +- `complete_trigger` + +转换类型枚举: + +- `primary` +- `side_effect` + +副作用类型枚举: + +- `log` +- `notify` +- `update_field` +- `trigger_event` + +#### 6.5.2 旧后端模型 / DAO 参考 + +模型:`ProduceStatusTransition` + +真实字段: + +- `step_id` +- `from_status_id` +- `to_status_id` +- `trigger_event` +- `event_config` +- `auto_trigger` +- `priority` +- `transition_type` +- `side_effect_type` +- `side_effect_config` +- `description` +- `sort` +- `status` + +关系: + +- `step()` +- `fromStatus()` +- `toStatus()` + +JSON 处理: + +- `event_config` 自动 JSON 编解码 +- `side_effect_config` 自动 JSON 编解码 + +DAO 体现出的业务能力: + +- `getAutoTransitionsByStep(stepId, fromStatusId)` +- `getAutoTransitions(stepId, fromStatusId, triggerEvent)` + +排序规则: + +- `priority desc` / `sort asc` + +说明: + +- 该模块不只是配置表,而是生产执行流程中的规则引擎核心 + +#### 6.5.3 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_status_transition` + +真实字段里已经明确包含: + +- `transition_type` +- `side_effect_type` +- `side_effect_config` +- `event_config` + +#### 6.5.4 复杂度评估 + +- 复杂度:高 +- 是 6 个模块里最复杂的配置模块 + +复杂来源: + +- 依赖步骤与步骤状态双层联动 +- 表单中有条件显示字段 +- 存在 JSON 配置字段 +- 影响实际状态机执行逻辑 + +#### 6.5.5 新插件建议 + +模块名建议:`status-transition` + +建议实现分两层: + +第一层: + +- 完整字段 CRUD +- 文本方式维护 JSON +- 步骤切换后刷新状态选项 + +第二层: + +- JSON 结构校验 +- 副作用配置器 +- 事件配置器 + +--- + +### 6.6 转换日志 `transition-log` + +#### 6.6.1 旧前端参考 + +目录:`old/web/src/views/produce/transition-log` + +查询字段: + +- `LIKE_work_order_no` +- `step_id` +- `trigger_event` +- `LIKE_trigger_user` +- `is_success` + +列表字段: + +- `work_order_no` +- `step_id` +- `from_status_id` +- `to_status_id` +- `trigger_event` +- `trigger_user` +- `is_auto` +- `is_success` +- `duration` +- `error_message` +- `created_date` + +操作: + +- 只有 `detail` + +#### 6.6.2 旧后端模型 / Service 参考 + +模型:`ProduceTransitionLog` + +真实字段: + +- `work_order_id` +- `work_order_no` +- `step_id` +- `transition_id` +- `from_status_id` +- `to_status_id` +- `trigger_event` +- `trigger_data` +- `trigger_user` +- `trigger_ip` +- `is_auto` +- `is_success` +- `error_message` +- `duration` +- `remark` +- `created_by` +- `created_at` + +特征: + +- `UPDATED_AT = null` +- `trigger_data` 自动 JSON 解码 +- 服务 `read()` 会加载: + - `step` + - `transition` + - `fromStatus` + - `toStatus` + +旧路由里还存在额外查询接口: + +- `work-order-history` +- `step-statistics` + +说明: + +- 该模块是典型只读日志模块 +- 不适合做标准新增/编辑/删除页面 + +#### 6.6.3 SQL 参考 + +来源:`old/server/scripts/sql/install.sql` + +表:`ma_produce_transition_log` + +关键字段: + +- `trigger_data` JSON +- `trigger_ip` +- `duration` +- `error_message` +- `remark` + +#### 6.6.4 复杂度评估 + +- 复杂度:中 +- 不是 CRUD 复杂,而是业务只读特征与统计扩展 + +#### 6.6.5 新插件建议 + +模块名建议:`transition-log` + +实现建议: + +- 列表页 + 详情页 +- 默认只读 +- 不开放新增/编辑/删除 +- 后续可补: + - 工单历史 + - 步骤统计 + +## 7. 推荐复刻顺序 + +必须按依赖顺序推进: + +1. `process` +2. `process-step` +3. `step-status` +4. `step-relation` +5. `status-transition` +6. `transition-log` + +推荐分阶段: + +### 第一阶段:基础主数据 + +- `process` +- `process-step` +- `step-status` + +### 第二阶段:流程规则 + +- `step-relation` +- `status-transition` + +### 第三阶段:只读日志 + +- `transition-log` + +## 8. 新插件目录落点建议 + +### 8.1 后端 + +建议新增如下结构: + +- `backend/plugin/produce/app/model/Process.php` 等 +- `backend/plugin/produce/app/dao/*` +- `backend/plugin/produce/app/service/admin/*` +- `backend/plugin/produce/app/validate/*` +- `backend/plugin/produce/app/schema/request/*` +- `backend/plugin/produce/app/schema/response/*` +- `backend/plugin/produce/app/adminapi/controller/*` +- `backend/plugin/produce/resource/database/migrations/*` + +建议 6 模块一一对应生成: + +- `Process` +- `ProcessStep` +- `StepStatus` +- `StepRelation` +- `StatusTransition` +- `TransitionLog` + +### 8.2 前端模板源 + +正式维护目录必须放在: + +- `backend/plugin/produce/resource/template/admin/api/index.ts` +- `backend/plugin/produce/resource/template/admin/views/produce/process/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/process/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/views/produce/process-step/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/process-step/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/views/produce/step-status/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/step-status/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/views/produce/step-relation/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/step-relation/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/views/produce/status-transition/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/status-transition/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/views/produce/transition-log/index.vue` +- `backend/plugin/produce/resource/template/admin/views/produce/transition-log/schemas/index.tsx` +- `backend/plugin/produce/resource/template/admin/lang/zh-cn/zh.json` +- `backend/plugin/produce/resource/template/admin/lang/en/en.json` + +安装后再复制到: + +- `frontend/admin/src/apps/produce/*` + +## 9. 菜单与权限建议 + +当前插件菜单 `backend/plugin/produce/resource/menu/admin.php` 只有一个示例菜单项,不能满足这 6 个模块。 + +建议调整为: + +- 根菜单:`生产管理` +- 子菜单: + - `工序管理` + - `工序步骤` + - `步骤状态` + - `步骤关系` + - `状态转换` + - `转换日志` + +权限码建议统一前缀: + +- `produce:process:*` +- `produce:process-step:*` +- `produce:step-status:*` +- `produce:step-relation:*` +- `produce:status-transition:*` +- `produce:transition-log:*` + +注意: + +- 日志模块建议只保留 `list/detail` 等只读权限 +- 不建议给 `create/edit/delete` + +## 10. 接口风格迁移建议 + +旧项目接口风格: + +- `/admin/produce/process/list` +- `/admin/produce/process/get/{id}` +- `/admin/produce/process/create` +- `/admin/produce/process/update/{id}` +- `/admin/produce/process/delete/{id}` + +新插件建议按当前项目标准 CRUD 控制器收敛,避免继续沿用旧式 `list/get/create/update/delete` 命名。 + +建议资源路径: + +- `/adminapi/produce/process` +- `/adminapi/produce/process-step` +- `/adminapi/produce/step-status` +- `/adminapi/produce/step-relation` +- `/adminapi/produce/status-transition` +- `/adminapi/produce/transition-log` + +附加只读接口可补: + +- 下拉选项接口 +- 状态联动接口 +- 日志统计接口 + +## 11. 风险与注意事项 + +### 11.1 旧前端不等于真实后端能力 + +最典型的是 `process-step`: + +旧页面未暴露: + +- 前置步骤数组 +- 前置验证模式 +- 重复扫码控制 + +但后端模型与 SQL 已经存在这些字段。 + +结论: + +- 不能只按旧页面抄字段 +- 应优先以 SQL + Model 为准 + +### 11.2 `status-transition` 是规则引擎核心 + +该模块不是普通配置表,DAO 已体现真实执行用途: + +- 获取自动转换规则 +- 按优先级执行 +- 依赖事件与状态 + +因此该模块开发时应把: + +- 数据正确性 +- JSON 配置合法性 +- 联动逻辑 + +放在比页面美化更高的优先级。 + +### 11.3 `transition-log` 是只读模块 + +不要把它误做成标准增删改查。 + +推荐: + +- 列表 +- 详情 +- 历史/统计扩展 + +### 11.4 迁移来源差异 + +旧项目这几张表的核心定义主要在 SQL 安装脚本,不在当前已检索到的 PHP migration 单文件中。 + +因此新插件开发时: + +- 迁移文件需要重新按当前项目规范补建 +- 不能直接依赖旧 `scripts/migrations` 目录复用 +- 但可以严格参考 `install.sql` 的字段与索引 + +## 12. 推荐实施计划 + +### 12.1 第一批落地 + +先完成: + +1. `process` +2. `process-step` +3. `step-status` + +交付标准: + +- 后端 CRUD 可运行 +- 菜单可访问 +- 模板源前端完成 +- 运行目录联调通过 + +### 12.2 第二批落地 + +再完成: + +4. `step-relation` +5. `status-transition` + +重点: + +- 规则字段 +- JSON 输入 +- 状态联动 + +### 12.3 第三批落地 + +最后完成: + +6. `transition-log` + +重点: + +- 列表 +- 详情 +- 历史/统计接口是否保留 + +## 13. 待确认事项 + +正式开始开发前,建议你确认以下内容: + +1. 插件显示名称是否确定为“生产管理” +2. 这 6 个模块是否全部进入一期 +3. `process-step` 的隐藏字段是否一期就开放 UI +4. `status-transition` 的 JSON 字段是否先用文本方式维护 +5. `transition-log` 是否确认只读 +6. 是否要把旧路由里的 `select`、`statistics`、`work-order-history` 一并纳入一期 +7. 数据库主键是否统一按当前项目雪花 ID 规范重建,而不是继续使用旧 AUTO_INCREMENT 风格 + +## 14. 结论 + +结论如下: + +1. `produce` 插件当前只是骨架,无法直接承接旧功能。 +2. 旧项目这 6 个模块具备完整前后端参考,可复刻。 +3. 复刻时应以 `install.sql + Model + DAO/Service` 为准,不能只照搬旧页面字段。 +4. 最推荐的首批实现是: + - `process` + - `process-step` + - `step-status` +5. `status-transition` 是业务核心,复杂度最高,应在基础模块稳定后实现。 +6. `transition-log` 应按只读日志模块实现,而不是标准 CRUD。 diff --git a/backend/plugin/produce/public/cover.png b/backend/plugin/produce/public/cover.png new file mode 100644 index 0000000..162b1b5 Binary files /dev/null and b/backend/plugin/produce/public/cover.png differ diff --git a/backend/plugin/produce/public/icon.png b/backend/plugin/produce/public/icon.png new file mode 100644 index 0000000..b3430be Binary files /dev/null and b/backend/plugin/produce/public/icon.png differ diff --git a/backend/plugin/produce/resource/database/migrations/.gitkeep b/backend/plugin/produce/resource/database/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/resource/database/migrations/2026_07_30_000001_create_produce_basic_tables.php b/backend/plugin/produce/resource/database/migrations/2026_07_30_000001_create_produce_basic_tables.php new file mode 100644 index 0000000..bf09874 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_07_30_000001_create_produce_basic_tables.php @@ -0,0 +1,144 @@ +hasTable('produce_process')) { + $schema->create('produce_process', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('process_code', 50)->comment('工序编码'); + $table->string('process_name', 100)->comment('工序名称'); + $table->text('description')->nullable()->comment('工序描述'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('status')->default(1)->comment('状态:1启用,0禁用'); + $table->integer('step_count')->default(0)->comment('步骤数'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->unique(['process_code', 'deleted_at'], 'uk_process_code'); + $table->index('status', 'idx_status'); + $table->index('sort', 'idx_sort'); + }); + } + + if (!$schema->hasTable('produce_process_step')) { + $schema->create('produce_process_step', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('process_id')->comment('工序ID'); + $table->string('step_code', 50)->comment('步骤编码'); + $table->string('step_name', 100)->comment('步骤名称'); + $table->text('description')->nullable()->comment('步骤描述'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('status')->default(1)->comment('状态:1启用,0禁用'); + $table->json('prerequisite_steps')->nullable()->comment('前置必要步骤ID数组'); + $table->string('prerequisite_mode', 20)->default('all')->comment('前置步骤验证模式:all=全部完成,any=任意完成,none=无需验证'); + $table->tinyInteger('prerequisite_require_final')->default(1)->comment('前置步骤是否要求终态:0否1是'); + $table->tinyInteger('allow_repeat_scan')->default(0)->comment('是否允许重复扫码:0否1是'); + $table->json('repeat_scan_statuses')->nullable()->comment('允许重复扫码的状态数组'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->unique(['step_code', 'process_id', 'deleted_at'], 'uk_step_code'); + $table->index('process_id', 'idx_process_id'); + $table->index('status', 'idx_status'); + $table->index('sort', 'idx_sort'); + }); + } + + if (!$schema->hasTable('produce_step_status')) { + $schema->create('produce_step_status', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('step_id')->comment('所属步骤ID'); + $table->string('status_code', 50)->comment('状态编码'); + $table->string('status_name', 100)->comment('状态名称'); + $table->tinyInteger('is_initial')->default(0)->comment('是否初始状态:0否1是'); + $table->tinyInteger('is_final')->default(0)->comment('是否终态:0否1是'); + $table->text('description')->nullable()->comment('状态描述'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('status')->default(1)->comment('状态:0禁用1启用'); + $table->bigInteger('created_by')->nullable()->comment('创建人'); + $table->bigInteger('updated_by')->nullable()->comment('更新人'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->unique(['step_id', 'status_code', 'deleted_at'], 'uk_step_status_code'); + $table->index('step_id', 'idx_step_id'); + $table->index('status', 'idx_status'); + $table->index('sort', 'idx_sort'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_step_relation')) { + $schema->create('produce_step_relation', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('from_step_id')->comment('源步骤ID'); + $table->bigInteger('to_step_id')->comment('目标步骤ID'); + $table->string('relation_type', 20)->default('normal')->comment('关系类型:normal-正常流转,exception-异常流转'); + $table->text('flow_condition')->nullable()->comment('流转条件(JSON格式)'); + $table->integer('priority')->default(0)->comment('优先级(数字越小优先级越高)'); + $table->text('description')->nullable()->comment('描述'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('status')->default(1)->comment('状态:0-禁用,1-启用'); + $table->bigInteger('created_by')->nullable()->comment('创建人'); + $table->bigInteger('updated_by')->nullable()->comment('更新人'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->unique(['from_step_id', 'to_step_id', 'relation_type', 'deleted_at'], 'uk_step_relation'); + $table->index('from_step_id', 'idx_from_step_id'); + $table->index('to_step_id', 'idx_to_step_id'); + $table->index('relation_type', 'idx_relation_type'); + $table->index('status', 'idx_status'); + $table->index('priority', 'idx_priority'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_status_transition')) { + $schema->create('produce_status_transition', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('step_id')->comment('所属步骤ID'); + $table->bigInteger('from_status_id')->comment('源状态ID'); + $table->bigInteger('to_status_id')->comment('目标状态ID'); + $table->string('trigger_event', 100)->comment('触发事件'); + $table->text('event_config')->nullable()->comment('事件配置(JSON)'); + $table->tinyInteger('auto_trigger')->default(0)->comment('是否自动触发:0否1是'); + $table->integer('priority')->default(0)->comment('优先级(数字越大优先级越高)'); + $table->string('transition_type', 20)->default('primary')->comment('转换类型:primary-主转换,side_effect-副作用'); + $table->string('side_effect_type', 50)->nullable()->comment('副作用类型'); + $table->text('side_effect_config')->nullable()->comment('副作用配置(JSON)'); + $table->text('description')->nullable()->comment('描述'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('status')->default(1)->comment('状态:0禁用1启用'); + $table->bigInteger('created_by')->nullable()->comment('创建人'); + $table->bigInteger('updated_by')->nullable()->comment('更新人'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->index('step_id', 'idx_step_id'); + $table->index('from_status_id', 'idx_from_status_id'); + $table->index('to_status_id', 'idx_to_status_id'); + $table->index('trigger_event', 'idx_trigger_event'); + $table->index('status', 'idx_status'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_status_transition'); + $schema->dropIfExists('produce_step_relation'); + $schema->dropIfExists('produce_step_status'); + $schema->dropIfExists('produce_process_step'); + $schema->dropIfExists('produce_process'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_07_31_000002_create_produce_transition_log_table.php b/backend/plugin/produce/resource/database/migrations/2026_07_31_000002_create_produce_transition_log_table.php new file mode 100644 index 0000000..63232ff --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_07_31_000002_create_produce_transition_log_table.php @@ -0,0 +1,51 @@ +hasTable('produce_transition_log')) { + $schema->create('produce_transition_log', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('work_order_id')->nullable()->comment('工单ID'); + $table->string('work_order_no', 100)->nullable()->comment('工单编号'); + $table->bigInteger('step_id')->comment('所属步骤ID'); + $table->bigInteger('transition_id')->nullable()->comment('转换规则ID'); + $table->bigInteger('from_status_id')->comment('源状态ID'); + $table->bigInteger('to_status_id')->comment('目标状态ID'); + $table->string('trigger_event', 100)->comment('触发事件'); + $table->text('trigger_data')->nullable()->comment('触发数据(JSON)'); + $table->string('trigger_user', 100)->nullable()->comment('触发用户'); + $table->string('trigger_ip', 50)->nullable()->comment('触发IP'); + $table->tinyInteger('is_auto')->default(0)->comment('是否自动触发:0否1是'); + $table->tinyInteger('is_success')->default(1)->comment('是否成功:0失败1成功'); + $table->text('error_message')->nullable()->comment('错误信息'); + $table->integer('duration')->default(0)->comment('执行时长(毫秒)'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->index('work_order_id', 'idx_work_order_id'); + $table->index('work_order_no', 'idx_work_order_no'); + $table->index('step_id', 'idx_step_id'); + $table->index('transition_id', 'idx_transition_id'); + $table->index('from_status_id', 'idx_from_status_id'); + $table->index('to_status_id', 'idx_to_status_id'); + $table->index('trigger_event', 'idx_trigger_event'); + $table->index('is_success', 'idx_is_success'); + $table->index('created_at', 'idx_created_at'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_transition_log'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_07_31_000003_create_produce_config_and_scan_data_tables.php b/backend/plugin/produce/resource/database/migrations/2026_07_31_000003_create_produce_config_and_scan_data_tables.php new file mode 100644 index 0000000..aceb27b --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_07_31_000003_create_produce_config_and_scan_data_tables.php @@ -0,0 +1,94 @@ +hasTable('produce_config')) { + $schema->create('produce_config', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('group_code', 64)->comment('配置组代码'); + $table->string('code', 64)->comment('配置代码'); + $table->string('name', 255)->comment('配置名称'); + $table->text('content')->nullable()->comment('配置内容'); + $table->integer('sort')->default(0)->comment('排序'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique(['group_code', 'code', 'deleted_at'], 'uk_group_code_code'); + $table->index('group_code', 'idx_group_code'); + $table->index('sort', 'idx_sort'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_specific_rule')) { + $schema->create('produce_specific_rule', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('rule_name', 255)->comment('规则名称'); + $table->text('rule_pattern')->comment('规则表达式'); + $table->tinyInteger('rule_type')->default(1)->comment('规则类型'); + $table->tinyInteger('is_enabled')->default(1)->comment('是否启用:0否1是'); + $table->json('step_ids')->nullable()->comment('关联步骤ID数组'); + $table->integer('sort')->default(0)->comment('排序'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->index('rule_name', 'idx_rule_name'); + $table->index('rule_type', 'idx_rule_type'); + $table->index('is_enabled', 'idx_is_enabled'); + $table->index('sort', 'idx_sort'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_scan_data')) { + $schema->create('produce_scan_data', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('order_no', 100)->comment('订单号'); + $table->string('user_code', 50)->comment('用户编码'); + $table->string('operator', 100)->nullable()->comment('操作人员'); + $table->bigInteger('operator_id')->nullable()->comment('操作人员ID'); + $table->bigInteger('process_id')->comment('工序ID'); + $table->bigInteger('step_id')->comment('步骤ID'); + $table->bigInteger('status_id')->nullable()->comment('步骤状态ID'); + $table->tinyInteger('scan_result')->default(1)->comment('扫码结果:0失败1成功'); + $table->bigInteger('scan_time')->nullable()->comment('扫码时间'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->index('order_no', 'idx_order_no'); + $table->index('user_code', 'idx_user_code'); + $table->index('process_id', 'idx_process_id'); + $table->index('step_id', 'idx_step_id'); + $table->index('status_id', 'idx_status_id'); + $table->index('scan_result', 'idx_scan_result'); + $table->index('scan_time', 'idx_scan_time'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_scan_data'); + $schema->dropIfExists('produce_specific_rule'); + $schema->dropIfExists('produce_config'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_06_000001_create_produce_product_record_table.php b/backend/plugin/produce/resource/database/migrations/2026_08_06_000001_create_produce_product_record_table.php new file mode 100644 index 0000000..82e7783 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_06_000001_create_produce_product_record_table.php @@ -0,0 +1,49 @@ +hasTable('produce_product_record')) { + $schema->create('produce_product_record', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('process_order_no', 100)->comment('加工单号'); + $table->string('product_code', 100)->comment('商品编码'); + $table->integer('quantity')->default(1)->comment('数量'); + $table->bigInteger('process_id')->nullable()->comment('工序ID'); + $table->bigInteger('step_id')->nullable()->comment('步骤ID'); + $table->bigInteger('status_id')->nullable()->comment('状态ID'); + $table->string('user_code', 50)->nullable()->comment('用户编码'); + $table->string('operator', 100)->nullable()->comment('操作人员'); + $table->bigInteger('operator_id')->nullable()->comment('操作人员ID'); + $table->tinyInteger('scan_result')->default(1)->comment('扫码结果:0失败1成功'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->index('process_order_no', 'idx_process_order_no'); + $table->index('product_code', 'idx_product_code'); + $table->index('process_id', 'idx_process_id'); + $table->index('step_id', 'idx_step_id'); + $table->index('status_id', 'idx_status_id'); + $table->index('user_code', 'idx_user_code'); + $table->index('operator_id', 'idx_operator_id'); + $table->index('scan_result', 'idx_scan_result'); + $table->index('created_at', 'idx_created_at'); + $table->index('deleted_at', 'idx_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_product_record'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_07_000001_add_mode_fields_to_produce_specific_rule_table.php b/backend/plugin/produce/resource/database/migrations/2026_08_07_000001_add_mode_fields_to_produce_specific_rule_table.php new file mode 100644 index 0000000..7d1e5e8 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_07_000001_add_mode_fields_to_produce_specific_rule_table.php @@ -0,0 +1,53 @@ +hasTable('produce_specific_rule')) { + return; + } + + $hasApplyMode = $schema->hasColumn('produce_specific_rule', 'apply_mode'); + $hasMatchField = $schema->hasColumn('produce_specific_rule', 'match_field'); + if ($hasApplyMode && $hasMatchField) { + return; + } + + $schema->table('produce_specific_rule', function (Blueprint $table) use ($hasApplyMode, $hasMatchField) { + if (!$hasApplyMode) { + $table->string('apply_mode', 20)->default('order')->after('rule_type')->comment('适用模式:order订单 machine整机 both全部'); + } + if (!$hasMatchField) { + $table->string('match_field', 30)->default('order_no')->after('apply_mode')->comment('匹配字段:order_no process_order_no product_code user_code'); + } + }); + } + + public function down(Builder $schema): void + { + if (!$schema->hasTable('produce_specific_rule')) { + return; + } + + $hasApplyMode = $schema->hasColumn('produce_specific_rule', 'apply_mode'); + $hasMatchField = $schema->hasColumn('produce_specific_rule', 'match_field'); + if (!$hasApplyMode && !$hasMatchField) { + return; + } + + $schema->table('produce_specific_rule', function (Blueprint $table) use ($hasApplyMode, $hasMatchField) { + if ($hasMatchField) { + $table->dropColumn('match_field'); + } + if ($hasApplyMode) { + $table->dropColumn('apply_mode'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_12_000001_create_produce_exception_tables.php b/backend/plugin/produce/resource/database/migrations/2026_08_12_000001_create_produce_exception_tables.php new file mode 100644 index 0000000..91e8a27 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_12_000001_create_produce_exception_tables.php @@ -0,0 +1,136 @@ +hasTable('produce_exception_setting')) { + $createdExceptionSettingTable = true; + $schema->create('produce_exception_setting', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('parent_id')->nullable()->comment('父级ID'); + $table->string('setting_type', 20)->comment('设置类型:type处理类型、reason异常原因、issue具体问题'); + $table->string('code', 64)->comment('设置编码'); + $table->string('name', 100)->comment('设置名称'); + $table->integer('sort')->default(0)->comment('排序,值越小越靠前'); + $table->tinyInteger('is_enabled')->default(1)->comment('是否启用:0否1是'); + $table->text('remark')->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique(['parent_id', 'code', 'deleted_at'], 'uk_exception_setting_parent_code'); + $table->index('setting_type', 'idx_exception_setting_type'); + $table->index('parent_id', 'idx_exception_setting_parent'); + $table->index(['is_enabled', 'sort'], 'idx_exception_setting_enabled_sort'); + $table->index('deleted_at', 'idx_exception_setting_deleted'); + }); + } + + if ($createdExceptionSettingTable) { + $this->seedExceptionSettings($schema); + } + + if (!$schema->hasTable('produce_exception_record')) { + $schema->create('produce_exception_record', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('order_no', 100)->comment('单号或条码'); + $table->string('user_code', 50)->nullable()->comment('提报人工号'); + $table->string('reporter', 100)->nullable()->comment('提报人'); + $table->bigInteger('reporter_id')->nullable()->comment('提报人ID'); + $table->bigInteger('type_id')->nullable()->comment('处理类型设置ID'); + $table->string('type_code', 64)->comment('处理类型编码'); + $table->string('type_name', 100)->comment('处理类型名称'); + $table->bigInteger('reason_id')->nullable()->comment('异常原因设置ID'); + $table->string('reason_code', 64)->comment('异常原因编码'); + $table->string('reason_name', 100)->comment('异常原因名称'); + $table->bigInteger('issue_id')->nullable()->comment('具体问题设置ID'); + $table->string('issue_code', 64)->nullable()->comment('具体问题编码'); + $table->string('issue_name', 100)->nullable()->comment('具体问题名称'); + $table->json('images')->nullable()->comment('现场图片'); + $table->text('remark')->nullable()->comment('补充说明'); + $table->tinyInteger('status')->default(0)->comment('处理状态:0待处理1处理中2已处理'); + $table->string('handler', 100)->nullable()->comment('处理人'); + $table->bigInteger('handler_id')->nullable()->comment('处理人ID'); + $table->bigInteger('handled_at')->nullable()->comment('处理时间'); + $table->bigInteger('reported_at')->nullable()->comment('提报时间'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->index('order_no', 'idx_exception_record_order_no'); + $table->index('user_code', 'idx_exception_record_user_code'); + $table->index('type_code', 'idx_exception_record_type_code'); + $table->index('reason_code', 'idx_exception_record_reason_code'); + $table->index('issue_code', 'idx_exception_record_issue_code'); + $table->index('status', 'idx_exception_record_status'); + $table->index('reported_at', 'idx_exception_record_reported_at'); + $table->index('deleted_at', 'idx_exception_record_deleted'); + }); + } + } + + private function seedExceptionSettings(Builder $schema): void + { + $timestamp = time(); + $rows = []; + $add = static function (?string $parentId, string $type, string $code, string $name, int $sort) use (&$rows, $timestamp): string { + $id = (string) Snowflake::generate(); + $rows[] = [ + 'id' => $id, + 'parent_id' => $parentId, + 'setting_type' => $type, + 'code' => $code, + 'name' => $name, + 'sort' => $sort, + 'is_enabled' => 1, + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]; + return $id; + }; + + $repairTypeId = $add(null, 'type', 'defective_repair', '不良维修', 1); + $returnTypeId = $add(null, 'type', 'return_reconfigure', '退料重配', 2); + $testReasonId = $add($repairTypeId, 'reason', 'test_defect', '测试不良', 1); + $agingReasonId = $add($repairTypeId, 'reason', 'aging_defect', '老化不良', 2); + + foreach ([ + ['configuration_error', '配置错误'], + ['order_review_error', '审单错误'], + ['picking_error', '配货错误'], + ['order_change', '改单'], + ['return_refund', '退货退款'], + ] as $sort => [$code, $name]) { + $add($returnTypeId, 'reason', $code, $name, $sort + 1); + } + + $testIssues = ['不亮', '不进', '不通电', '死机', '卡logo', '机箱风扇不良', 'USB接口不良', '面板LED异常', '异响', '少配件', '配置不符', '风扇灯光异常', '机箱不良', '炸电源', '水冷坏', '声卡异常', '网卡异常', '不读硬盘', '硬盘超时', '掉盘', '机械格式化失败', '硬盘坏', '硬盘容量不对', '掉驱动', '打不上驱动', '不断电', '不认无线', '内存不识别', '闪屏', '断电', '花屏', '烤黑', '高温', '短路', '蓝屏', 'AIDA红', '烤断电', '灰屏', '烤机重启', '卡顿', '机械共振', '烤机使用核显', 'CPU主频低', '自动重启', '显存不符']; + foreach ($testIssues as $sort => $name) { + $add($testReasonId, 'issue', 'test_issue_' . str_pad((string) ($sort + 1), 2, '0', STR_PAD_LEFT), $name, $sort + 1); + } + + $agingIssues = ['烤花屏', '烤黑屏', 'CPU高温', '显卡高温', '烤蓝屏', 'AIDA红', '烤断电', '烤灰屏', '烤重启', '烤机使用核显', 'CPU主频低', '显卡掉帧', '掉驱动']; + foreach ($agingIssues as $sort => $name) { + $add($agingReasonId, 'issue', 'aging_issue_' . str_pad((string) ($sort + 1), 2, '0', STR_PAD_LEFT), $name, $sort + 1); + } + + $schema->getConnection()->table('produce_exception_setting')->insert($rows); + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_exception_record'); + $schema->dropIfExists('produce_exception_setting'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_13_000001_create_produce_material_tables.php b/backend/plugin/produce/resource/database/migrations/2026_08_13_000001_create_produce_material_tables.php new file mode 100644 index 0000000..4a206f6 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_13_000001_create_produce_material_tables.php @@ -0,0 +1,51 @@ +hasTable('produce_material_category')) { + $schema->create('produce_material_category', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('category_name', 100)->comment('分类名称'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique('category_name', 'uk_material_category_name'); + $table->index('deleted_at', 'idx_material_category_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_material')) { + $schema->create('produce_material', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('category_id')->nullable()->comment('分类ID'); + $table->string('product_code', 100)->comment('商品编码'); + $table->string('product_name', 200)->comment('商品名称'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique('product_code', 'uk_material_product_code'); + $table->index('category_id', 'idx_material_category_id'); + $table->index('product_name', 'idx_material_product_name'); + $table->index('deleted_at', 'idx_material_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_material'); + $schema->dropIfExists('produce_material_category'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_14_000001_create_produce_repair_management_tables.php b/backend/plugin/produce/resource/database/migrations/2026_08_14_000001_create_produce_repair_management_tables.php new file mode 100644 index 0000000..803d37e --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_14_000001_create_produce_repair_management_tables.php @@ -0,0 +1,92 @@ +hasTable('produce_repair_setting')) { + $schema->create('produce_repair_setting', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('setting_type', 50)->comment('设置类型'); + $table->string('setting_name', 100)->comment('设置名称'); + $table->string('setting_value', 255)->nullable()->comment('设置值'); + $table->integer('sort')->default(0)->comment('排序'); + $table->tinyInteger('is_enabled')->default(1)->comment('是否启用'); + $table->string('remark', 500)->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique(['setting_type', 'setting_name'], 'uk_repair_setting_type_name'); + $table->index(['setting_type', 'is_enabled'], 'idx_repair_setting_type_enabled'); + $table->index('deleted_at', 'idx_repair_setting_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_hardware_fault_description')) { + $schema->create('produce_hardware_fault_description', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->bigInteger('category_id')->comment('物料分类ID'); + $table->string('description', 500)->comment('硬件故障描述'); + $table->unsignedInteger('use_count')->default(0)->comment('使用次数'); + $table->tinyInteger('is_enabled')->default(1)->comment('是否启用'); + $table->string('remark', 500)->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique(['category_id', 'description'], 'uk_hardware_fault_category_description'); + $table->index(['category_id', 'is_enabled'], 'idx_hardware_fault_category_enabled'); + $table->index('use_count', 'idx_hardware_fault_use_count'); + $table->index('deleted_at', 'idx_hardware_fault_deleted_at'); + }); + } + + if (!$schema->hasTable('produce_repair_record')) { + $schema->create('produce_repair_record', function (Blueprint $table) { + $table->bigInteger('id')->primary()->comment('主键ID'); + $table->string('repair_no', 100)->comment('维修单号'); + $table->string('order_no', 100)->nullable()->comment('订单号'); + $table->string('product_code', 100)->nullable()->comment('商品编码'); + $table->string('product_name', 200)->nullable()->comment('商品名称'); + $table->bigInteger('category_id')->nullable()->comment('物料分类ID'); + $table->string('category_name', 100)->nullable()->comment('物料分类名称快照'); + $table->bigInteger('fault_description_id')->nullable()->comment('硬件故障描述ID'); + $table->string('fault_description', 500)->nullable()->comment('硬件故障描述快照'); + $table->string('repair_result', 50)->nullable()->comment('维修判定'); + $table->json('parts')->nullable()->comment('更换备件'); + $table->string('operator', 100)->nullable()->comment('维修人员'); + $table->bigInteger('repaired_at')->nullable()->comment('维修时间'); + $table->string('remark', 1000)->nullable()->comment('备注'); + $table->bigInteger('created_by')->nullable()->comment('创建人ID'); + $table->bigInteger('updated_by')->nullable()->comment('更新人ID'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + + $table->unique('repair_no', 'uk_repair_record_repair_no'); + $table->index('order_no', 'idx_repair_record_order_no'); + $table->index('product_code', 'idx_repair_record_product_code'); + $table->index('category_id', 'idx_repair_record_category_id'); + $table->index('fault_description_id', 'idx_repair_record_fault_description_id'); + $table->index('repaired_at', 'idx_repair_record_repaired_at'); + $table->index('deleted_at', 'idx_repair_record_deleted_at'); + }); + } + } + + public function down(Builder $schema): void + { + $schema->dropIfExists('produce_repair_record'); + $schema->dropIfExists('produce_hardware_fault_description'); + $schema->dropIfExists('produce_repair_setting'); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_15_000001_add_exception_snapshot_to_repair_record.php b/backend/plugin/produce/resource/database/migrations/2026_08_15_000001_add_exception_snapshot_to_repair_record.php new file mode 100644 index 0000000..6e74931 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_15_000001_add_exception_snapshot_to_repair_record.php @@ -0,0 +1,50 @@ +hasTable($table)) { + return; + } + + $schema->table($table, function (Blueprint $table) use ($schema) { + if (!$schema->hasColumn('produce_repair_record', 'exception_id')) { + $table->bigInteger('exception_id')->nullable()->comment('关联异常记录ID'); + $table->index('exception_id', 'idx_repair_record_exception_id'); + } + if (!$schema->hasColumn('produce_repair_record', 'exception_reason')) { + $table->string('exception_reason', 100)->nullable()->comment('异常原因快照'); + } + if (!$schema->hasColumn('produce_repair_record', 'exception_issue')) { + $table->string('exception_issue', 100)->nullable()->comment('具体问题快照'); + } + }); + } + + public function down(Builder $schema): void + { + $table = 'produce_repair_record'; + if (!$schema->hasTable($table)) { + return; + } + + $schema->table($table, function (Blueprint $table) use ($schema) { + if ($schema->hasColumn('produce_repair_record', 'exception_id')) { + $table->dropIndex('idx_repair_record_exception_id'); + $table->dropColumn('exception_id'); + } + if ($schema->hasColumn('produce_repair_record', 'exception_reason')) { + $table->dropColumn('exception_reason'); + } + if ($schema->hasColumn('produce_repair_record', 'exception_issue')) { + $table->dropColumn('exception_issue'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_16_000001_align_produce_repair_setting.php b/backend/plugin/produce/resource/database/migrations/2026_08_16_000001_align_produce_repair_setting.php new file mode 100644 index 0000000..36ae720 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_16_000001_align_produce_repair_setting.php @@ -0,0 +1,85 @@ +hasTable($table)) { + return; + } + + $hasParentId = $schema->hasColumn($table, 'parent_id'); + $hasCode = $schema->hasColumn($table, 'code'); + $hasName = $schema->hasColumn($table, 'name'); + $hasSettingType = $schema->hasColumn($table, 'setting_type'); + $hasSettingName = $schema->hasColumn($table, 'setting_name'); + + $schema->table($table, function (Blueprint $table) use ($hasParentId, $hasCode, $hasName) { + if (!$hasParentId) { + $table->bigInteger('parent_id')->nullable()->comment('父级ID'); + $table->index('parent_id', 'idx_repair_setting_parent'); + } + if (!$hasCode) { + $table->string('code', 64)->nullable()->comment('设置编码'); + } + if (!$hasName) { + $table->string('name', 100)->nullable()->comment('设置名称'); + } + }); + + $connection = $schema->getConnection(); + if (!$hasCode && $hasSettingType) { + $connection->table($table)->whereNull('code')->update([ + 'code' => $connection->raw('setting_type'), + ]); + } + if (!$hasName && $hasSettingName) { + $connection->table($table)->whereNull('name')->update([ + 'name' => $connection->raw('setting_name'), + ]); + } + if ($hasSettingType) { + $connection->table($table) + ->whereNotIn('setting_type', ['type', 'reason', 'issue']) + ->update(['setting_type' => 'type']); + } + + $schema->table($table, function (Blueprint $table) use ($hasCode, $hasName) { + if (!$hasCode) { + $table->string('code', 64)->nullable(false)->change(); + } + if (!$hasName) { + $table->string('name', 100)->nullable(false)->change(); + } + }); + } + + public function down(Builder $schema): void + { + $table = 'produce_repair_setting'; + if (!$schema->hasTable($table)) { + return; + } + + $hasLegacySettingType = $schema->hasColumn($table, 'setting_type'); + $hasLegacySettingName = $schema->hasColumn($table, 'setting_name'); + + $schema->table($table, function (Blueprint $table) use ($schema, $hasLegacySettingType, $hasLegacySettingName) { + if ($hasLegacySettingType && $schema->hasColumn('produce_repair_setting', 'parent_id')) { + $table->dropIndex('idx_repair_setting_parent'); + $table->dropColumn('parent_id'); + } + if ($hasLegacySettingType && $schema->hasColumn('produce_repair_setting', 'code')) { + $table->dropColumn('code'); + } + if ($hasLegacySettingName && $schema->hasColumn('produce_repair_setting', 'name')) { + $table->dropColumn('name'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_17_000001_add_operation_fault_responsibility_to_repair_record.php b/backend/plugin/produce/resource/database/migrations/2026_08_17_000001_add_operation_fault_responsibility_to_repair_record.php new file mode 100644 index 0000000..596bd5d --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_17_000001_add_operation_fault_responsibility_to_repair_record.php @@ -0,0 +1,42 @@ +hasTable($table)) { + return; + } + + $schema->table($table, function (Blueprint $table) use ($schema) { + if (!$schema->hasColumn('produce_repair_record', 'operation_fault_steps')) { + $table->json('operation_fault_steps')->nullable()->comment('作业不良责任步骤快照'); + } + if (!$schema->hasColumn('produce_repair_record', 'responsible_operators')) { + $table->json('responsible_operators')->nullable()->comment('作业不良责任人员快照'); + } + }); + } + + public function down(Builder $schema): void + { + $table = 'produce_repair_record'; + if (!$schema->hasTable($table)) { + return; + } + + $schema->table($table, function (Blueprint $table) use ($schema) { + if ($schema->hasColumn('produce_repair_record', 'operation_fault_steps')) { + $table->dropColumn('operation_fault_steps'); + } + if ($schema->hasColumn('produce_repair_record', 'responsible_operators')) { + $table->dropColumn('responsible_operators'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_18_000001_add_category_ids_to_produce_repair_record.php b/backend/plugin/produce/resource/database/migrations/2026_08_18_000001_add_category_ids_to_produce_repair_record.php new file mode 100644 index 0000000..7d9ea22 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_18_000001_add_category_ids_to_produce_repair_record.php @@ -0,0 +1,27 @@ +hasTable('produce_repair_record') && !$schema->hasColumn('produce_repair_record', 'category_ids')) { + $schema->table('produce_repair_record', function (Blueprint $table) { + $table->json('category_ids')->nullable()->after('category_id')->comment('物料分类ID集合'); + $table->json('category_names')->nullable()->after('category_name')->comment('物料分类名称快照集合'); + }); + } + } + + public function down(Builder $schema): void + { + if ($schema->hasTable('produce_repair_record') && $schema->hasColumn('produce_repair_record', 'category_ids')) { + $schema->table('produce_repair_record', function (Blueprint $table) { + $table->dropColumn(['category_ids', 'category_names']); + }); + } + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_22_000001_add_quantity_to_produce_scan_data_table.php b/backend/plugin/produce/resource/database/migrations/2026_08_22_000001_add_quantity_to_produce_scan_data_table.php new file mode 100644 index 0000000..58f0a5f --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_22_000001_add_quantity_to_produce_scan_data_table.php @@ -0,0 +1,26 @@ +hasTable('produce_scan_data') && !$schema->hasColumn('produce_scan_data', 'quantity')) { + $schema->table('produce_scan_data', function (Blueprint $table) { + $table->integer('quantity')->default(1)->after('order_no')->comment('本次数量'); + }); + } + } + + public function down(Builder $schema): void + { + if ($schema->hasTable('produce_scan_data') && $schema->hasColumn('produce_scan_data', 'quantity')) { + $schema->table('produce_scan_data', function (Blueprint $table) { + $table->dropColumn('quantity'); + }); + } + } +}; diff --git a/backend/plugin/produce/resource/database/migrations/2026_08_30_000001_add_images_to_produce_repair_record.php b/backend/plugin/produce/resource/database/migrations/2026_08_30_000001_add_images_to_produce_repair_record.php new file mode 100644 index 0000000..182507b --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_08_30_000001_add_images_to_produce_repair_record.php @@ -0,0 +1,26 @@ +hasTable('produce_repair_record') && !$schema->hasColumn('produce_repair_record', 'images')) { + $schema->table('produce_repair_record', function (Blueprint $table) { + $table->json('images')->nullable()->after('parts')->comment('维修现场图片'); + }); + } + } + + public function down(Builder $schema): void + { + if ($schema->hasTable('produce_repair_record') && $schema->hasColumn('produce_repair_record', 'images')) { + $schema->table('produce_repair_record', function (Blueprint $table) { + $table->dropColumn('images'); + }); + } + } +}; diff --git a/backend/plugin/produce/resource/database/seeds/.gitkeep b/backend/plugin/produce/resource/database/seeds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/resource/database/seeds/ProduceDemoDataSeeder.php b/backend/plugin/produce/resource/database/seeds/ProduceDemoDataSeeder.php new file mode 100644 index 0000000..5e54579 --- /dev/null +++ b/backend/plugin/produce/resource/database/seeds/ProduceDemoDataSeeder.php @@ -0,0 +1,630 @@ +forceDelete(); + StepRelation::withTrashed()->forceDelete(); + StepStatus::withTrashed()->forceDelete(); + ProcessStep::withTrashed()->forceDelete(); + Process::withTrashed()->forceDelete(); + + $processes = [ + 'production' => Process::create([ + 'id' => Snowflake::generate(), + 'process_code' => 'L1SC', + 'process_name' => '生产工序', + 'description' => '生产主流程示例数据', + 'sort' => 0, + 'status' => 1, + 'step_count' => 3, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'addon' => Process::create([ + 'id' => Snowflake::generate(), + 'process_code' => 'L1JZ', + 'process_name' => '加装工序', + 'description' => '加装分支示例数据', + 'sort' => 10, + 'status' => 1, + 'step_count' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'repair' => Process::create([ + 'id' => Snowflake::generate(), + 'process_code' => 'L1WX', + 'process_name' => '异常工序', + 'description' => '维修/异常处理示例数据', + 'sort' => 20, + 'status' => 1, + 'step_count' => 3, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + ]; + + $steps = [ + 'panel' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['production']->id, + 'step_code' => 'PB001', + 'step_name' => '拼板', + 'description' => '生产起始步骤', + 'sort' => 0, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'none', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 1, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'assemble' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['production']->id, + 'step_code' => 'ZZ001', + 'step_name' => '组装', + 'description' => '生产主流程第二步', + 'sort' => 10, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'all', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 1, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'test' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['production']->id, + 'step_code' => 'CS001', + 'step_name' => '测试', + 'description' => '生产主流程第三步', + 'sort' => 20, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'any', + 'prerequisite_require_final' => 0, + 'allow_repeat_scan' => 1, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'addon' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['addon']->id, + 'step_code' => 'JZ001', + 'step_name' => '加装', + 'description' => '加装支线步骤', + 'sort' => 0, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'all', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 0, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'repair' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['repair']->id, + 'step_code' => 'WX001', + 'step_name' => '维修', + 'description' => '异常维修步骤', + 'sort' => 0, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'all', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 1, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'rework' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['repair']->id, + 'step_code' => 'CP001', + 'step_name' => '重配', + 'description' => '异常重配步骤', + 'sort' => 10, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'all', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 0, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'cancel' => ProcessStep::create([ + 'id' => Snowflake::generate(), + 'process_id' => $processes['repair']->id, + 'step_code' => 'TD001', + 'step_name' => '退单', + 'description' => '异常退单步骤', + 'sort' => 20, + 'status' => 1, + 'prerequisite_steps' => [], + 'prerequisite_mode' => 'all', + 'prerequisite_require_final' => 1, + 'allow_repeat_scan' => 0, + 'repeat_scan_statuses' => [], + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + ]; + + $statuses = [ + 'panel_processing' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['panel']->id, + 'status_code' => 'PB_PROCESSING', + 'status_name' => '拼板中', + 'is_initial' => 1, + 'is_final' => 0, + 'description' => '拼板初始状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'panel_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['panel']->id, + 'status_code' => 'PB_COMPLETED', + 'status_name' => '拼板完成', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '拼板完成状态', + 'sort' => 10, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'panel_exception' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['panel']->id, + 'status_code' => 'PB_EXCEPTION', + 'status_name' => '拼板异常', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '拼板异常状态', + 'sort' => 20, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'assemble_processing' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['assemble']->id, + 'status_code' => 'ZZ_PROCESSING', + 'status_name' => '组装中', + 'is_initial' => 1, + 'is_final' => 0, + 'description' => '组装初始状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'assemble_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['assemble']->id, + 'status_code' => 'ZZ_COMPLETED', + 'status_name' => '组装完成', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '组装完成状态', + 'sort' => 10, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'assemble_exception' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['assemble']->id, + 'status_code' => 'ZZ_EXCEPTION', + 'status_name' => '组装异常', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '组装异常状态', + 'sort' => 20, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'test_processing' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['test']->id, + 'status_code' => 'CS_PROCESSING', + 'status_name' => '测试中', + 'is_initial' => 1, + 'is_final' => 0, + 'description' => '测试初始状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'test_passed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['test']->id, + 'status_code' => 'CS_PASSED', + 'status_name' => '测试通过', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '测试通过状态', + 'sort' => 10, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'test_failed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['test']->id, + 'status_code' => 'CS_FAILED', + 'status_name' => '测试不通过', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '测试失败状态', + 'sort' => 20, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'addon_processing' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['addon']->id, + 'status_code' => 'JZ_PROCESSING', + 'status_name' => '加装中', + 'is_initial' => 1, + 'is_final' => 0, + 'description' => '加装初始状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'addon_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['addon']->id, + 'status_code' => 'JZ_COMPLETED', + 'status_name' => '加装完成', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '加装完成状态', + 'sort' => 10, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'repair_processing' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['repair']->id, + 'status_code' => 'WX_PROCESSING', + 'status_name' => '维修中', + 'is_initial' => 1, + 'is_final' => 0, + 'description' => '维修初始状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'repair_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['repair']->id, + 'status_code' => 'WX_COMPLETED', + 'status_name' => '维修完成', + 'is_initial' => 0, + 'is_final' => 1, + 'description' => '维修完成状态', + 'sort' => 10, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'rework_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['rework']->id, + 'status_code' => 'CP_COMPLETED', + 'status_name' => '重配完成', + 'is_initial' => 1, + 'is_final' => 1, + 'description' => '重配完成状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + 'cancel_completed' => StepStatus::create([ + 'id' => Snowflake::generate(), + 'step_id' => $steps['cancel']->id, + 'status_code' => 'TD_COMPLETED', + 'status_name' => '退单完成', + 'is_initial' => 1, + 'is_final' => 1, + 'description' => '退单完成状态', + 'sort' => 0, + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]), + ]; + + $steps['assemble']->update([ + 'prerequisite_steps' => [$steps['panel']->id], + 'repeat_scan_statuses' => [$statuses['panel_processing']->id], + ]); + + $steps['test']->update([ + 'prerequisite_steps' => [$steps['assemble']->id, $steps['addon']->id], + 'repeat_scan_statuses' => [$statuses['assemble_processing']->id], + ]); + + $steps['panel']->update([ + 'repeat_scan_statuses' => [$statuses['rework_completed']->id, $statuses['cancel_completed']->id], + ]); + + $relations = [ + [ + 'from_step_id' => $steps['panel']->id, + 'to_step_id' => $steps['assemble']->id, + 'relation_type' => 'normal', + 'flow_condition' => [], + 'priority' => 0, + 'description' => '拼板完成后进入组装', + 'sort' => 0, + ], + [ + 'from_step_id' => $steps['assemble']->id, + 'to_step_id' => $steps['test']->id, + 'relation_type' => 'normal', + 'flow_condition' => [], + 'priority' => 0, + 'description' => '组装完成后进入测试', + 'sort' => 10, + ], + [ + 'from_step_id' => $steps['addon']->id, + 'to_step_id' => $steps['test']->id, + 'relation_type' => 'normal', + 'flow_condition' => [], + 'priority' => 0, + 'description' => '加装完成后可进入测试', + 'sort' => 20, + ], + [ + 'from_step_id' => $steps['test']->id, + 'to_step_id' => $steps['assemble']->id, + 'relation_type' => 'exception', + 'flow_condition' => ['reason' => 'assembly_rework'], + 'priority' => 0, + 'description' => '测试失败后回流组装', + 'sort' => 30, + ], + [ + 'from_step_id' => $steps['assemble']->id, + 'to_step_id' => $steps['repair']->id, + 'relation_type' => 'exception', + 'flow_condition' => ['reason' => 'repair_required'], + 'priority' => 0, + 'description' => '组装异常进入维修', + 'sort' => 40, + ], + [ + 'from_step_id' => $steps['test']->id, + 'to_step_id' => $steps['repair']->id, + 'relation_type' => 'exception', + 'flow_condition' => ['reason' => 'repair_required'], + 'priority' => 0, + 'description' => '测试异常进入维修', + 'sort' => 50, + ], + [ + 'from_step_id' => $steps['panel']->id, + 'to_step_id' => $steps['rework']->id, + 'relation_type' => 'exception', + 'flow_condition' => ['reason' => 'rework_required'], + 'priority' => 0, + 'description' => '拼板异常进入重配', + 'sort' => 60, + ], + [ + 'from_step_id' => $steps['assemble']->id, + 'to_step_id' => $steps['cancel']->id, + 'relation_type' => 'exception', + 'flow_condition' => ['reason' => 'cancel_required'], + 'priority' => 0, + 'description' => '组装异常进入退单', + 'sort' => 70, + ], + ]; + + foreach ($relations as $relation) { + StepRelation::create([ + 'id' => Snowflake::generate(), + 'from_step_id' => $relation['from_step_id'], + 'to_step_id' => $relation['to_step_id'], + 'relation_type' => $relation['relation_type'], + 'flow_condition' => $relation['flow_condition'], + 'priority' => $relation['priority'], + 'description' => $relation['description'], + 'sort' => $relation['sort'], + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + $transitions = [ + [ + 'step_id' => $steps['panel']->id, + 'from_status_id' => $statuses['panel_processing']->id, + 'to_status_id' => $statuses['panel_completed']->id, + 'trigger_event' => 'next_step_scan', + 'event_config' => null, + 'auto_trigger' => 1, + 'priority' => 0, + 'transition_type' => 'primary', + 'side_effect_type' => null, + 'side_effect_config' => null, + 'description' => '拼板完成后推进下一步', + 'sort' => 0, + ], + [ + 'step_id' => $steps['assemble']->id, + 'from_status_id' => $statuses['assemble_processing']->id, + 'to_status_id' => $statuses['assemble_completed']->id, + 'trigger_event' => 'next_step_scan', + 'event_config' => null, + 'auto_trigger' => 1, + 'priority' => 0, + 'transition_type' => 'primary', + 'side_effect_type' => null, + 'side_effect_config' => null, + 'description' => '组装完成后推进下一步', + 'sort' => 10, + ], + [ + 'step_id' => $steps['assemble']->id, + 'from_status_id' => $statuses['assemble_processing']->id, + 'to_status_id' => $statuses['assemble_exception']->id, + 'trigger_event' => 'exception_trigger', + 'event_config' => ['source' => 'manual'], + 'auto_trigger' => 1, + 'priority' => 5, + 'transition_type' => 'primary', + 'side_effect_type' => null, + 'side_effect_config' => null, + 'description' => '组装异常时转异常态', + 'sort' => 20, + ], + [ + 'step_id' => $steps['test']->id, + 'from_status_id' => $statuses['test_processing']->id, + 'to_status_id' => $statuses['test_passed']->id, + 'trigger_event' => 'scan_trigger', + 'event_config' => null, + 'auto_trigger' => 1, + 'priority' => 0, + 'transition_type' => 'primary', + 'side_effect_type' => null, + 'side_effect_config' => null, + 'description' => '测试通过', + 'sort' => 30, + ], + [ + 'step_id' => $steps['test']->id, + 'from_status_id' => $statuses['test_processing']->id, + 'to_status_id' => $statuses['test_failed']->id, + 'trigger_event' => 'exception_trigger', + 'event_config' => ['source' => 'manual'], + 'auto_trigger' => 1, + 'priority' => 5, + 'transition_type' => 'primary', + 'side_effect_type' => null, + 'side_effect_config' => null, + 'description' => '测试失败', + 'sort' => 40, + ], + ]; + + foreach ($transitions as $transition) { + StatusTransition::create([ + 'id' => Snowflake::generate(), + 'step_id' => $transition['step_id'], + 'from_status_id' => $transition['from_status_id'], + 'to_status_id' => $transition['to_status_id'], + 'trigger_event' => $transition['trigger_event'], + 'event_config' => $transition['event_config'], + 'auto_trigger' => $transition['auto_trigger'], + 'priority' => $transition['priority'], + 'transition_type' => $transition['transition_type'], + 'side_effect_type' => $transition['side_effect_type'], + 'side_effect_config' => $transition['side_effect_config'], + 'description' => $transition['description'], + 'sort' => $transition['sort'], + 'status' => 1, + 'created_by' => $adminId, + 'updated_by' => $adminId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } +} diff --git a/backend/plugin/produce/resource/menu/admin.php b/backend/plugin/produce/resource/menu/admin.php new file mode 100644 index 0000000..6d3e28d --- /dev/null +++ b/backend/plugin/produce/resource/menu/admin.php @@ -0,0 +1,178 @@ + '生产管理', + 'code' => 'produce', + 'app' => 'produce', + 'path' => '/produce', + 'icon' => 'plugin', + 'children' => [ + [ + 'name' => '物料分类', + 'code' => 'produce:material-category:index', + 'app' => 'produce', + 'path' => '/produce/material-category', + 'component' => '/produce/material-category/index', + ], + [ + 'name' => '物料库', + 'code' => 'produce:material:index', + 'app' => 'produce', + 'path' => '/produce/material', + 'component' => '/produce/material/index', + ], + [ + 'name' => '生产配置', + 'code' => 'produce:config:index', + 'app' => 'produce', + 'path' => '/produce/config', + 'component' => '/produce/config/index', + ], + [ + 'name' => '工序管理', + 'code' => 'produce:process:index', + 'app' => 'produce', + 'path' => '/produce/process', + 'component' => '/produce/process/index', + ], + [ + 'name' => '工序步骤', + 'code' => 'produce:process-step:index', + 'app' => 'produce', + 'path' => '/produce/process-step', + 'component' => '/produce/process-step/index', + ], + [ + 'name' => '步骤状态', + 'code' => 'produce:step-status:index', + 'app' => 'produce', + 'path' => '/produce/step-status', + 'component' => '/produce/step-status/index', + ], + [ + 'name' => '步骤关系', + 'code' => 'produce:step-relation:index', + 'app' => 'produce', + 'path' => '/produce/step-relation', + 'component' => '/produce/step-relation/index', + ], + [ + 'name' => '状态转换', + 'code' => 'produce:status-transition:index', + 'app' => 'produce', + 'path' => '/produce/status-transition', + 'component' => '/produce/status-transition/index', + ], + [ + 'name' => '整机数据', + 'code' => 'produce:product-record:index', + 'app' => 'produce', + 'path' => '/produce/product-record', + 'component' => '/produce/product-record/index', + ], + [ + 'name' => '扫码数据', + 'code' => 'produce:scan-data:index', + 'app' => 'produce', + 'path' => '/produce/scan-data', + 'component' => '/produce/scan-data/index', + ], + [ + 'name' => '转换日志', + 'code' => 'produce:transition-log:index', + 'app' => 'produce', + 'path' => '/produce/transition-log', + 'component' => '/produce/transition-log/index', + ], + [ + 'name' => '异常管理', + 'code' => 'produce:exception:index', + 'app' => 'produce', + 'path' => '/produce/exception', + 'icon' => 'warning', + 'children' => [ + [ + 'name' => '异常数据', + 'code' => 'produce:exception-data:list', + 'app' => 'produce', + 'path' => '/produce/exception-data', + 'component' => '/produce/exception/data/index', + ], + [ + 'name' => '异常设置', + 'code' => 'produce:exception-setting:list', + 'app' => 'produce', + 'path' => '/produce/exception-setting', + 'component' => '/produce/exception/setting/index', + ], + [ + 'name' => '数据分析', + 'code' => 'produce:exception-analysis:read', + 'app' => 'produce', + 'path' => '/produce/exception-analysis', + 'component' => '/produce/exception/analysis/index', + ], + ], + ], + [ + 'name' => '维修管理', + 'code' => 'produce:repair:index', + 'app' => 'produce', + 'path' => '/produce/repair', + 'icon' => 'tools', + 'children' => [ + [ + 'name' => '维修数据', + 'code' => 'produce:repair-record:list', + 'app' => 'produce', + 'path' => '/produce/repair/repair-data', + 'component' => '/produce/repair/repair-data/index', + ], + [ + 'name' => '维修设置', + 'code' => 'produce:repair-setting:list', + 'app' => 'produce', + 'path' => '/produce/repair/repair-setting', + 'component' => '/produce/repair/repair-setting/index', + ], + [ + 'name' => '硬件故障描述', + 'code' => 'produce:hardware-fault-description:list', + 'app' => 'produce', + 'path' => '/produce/repair/hardware-fault-description', + 'component' => '/produce/repair/hardware-fault-description/index', + ], + ], + ], + [ + 'name' => '数据统计', + 'code' => 'produce:statistics:index', + 'app' => 'produce', + 'path' => '/produce/statistics', + 'icon' => 'bar-chart-2', + 'children' => [ + [ + 'name' => '月工作统计', + 'code' => 'produce:statistics:monthly', + 'app' => 'produce', + 'path' => '/produce/statistics/monthly', + 'component' => '/produce/statistics/monthly/index', + ], + [ + 'name' => '日工作统计', + 'code' => 'produce:statistics:daily', + 'app' => 'produce', + 'path' => '/produce/statistics/daily', + 'component' => '/produce/statistics/daily/index', + ], + ], + ], + ], + ], +]; diff --git a/backend/plugin/produce/resource/menu/frontend.php b/backend/plugin/produce/resource/menu/frontend.php new file mode 100644 index 0000000..917fd5d --- /dev/null +++ b/backend/plugin/produce/resource/menu/frontend.php @@ -0,0 +1,9 @@ + 'web', + 'category' => 1, + 'source' => 'plugin', + 'code' => null, + 'name' => '生产录入', + 'url' => '/produce/data-entry', + 'icon' => 'ri:qr-scan-2-line', + 'level' => 1, + 'type' => 2, + 'sort' => 10, + 'target' => 1, + 'is_show' => 1, + 'enabled' => 1, + 'created_at' => time(), + 'updated_at' => time(), + 'deleted_at' => null, + ], + [ + 'app' => 'web', + 'category' => 1, + 'source' => 'plugin', + 'code' => null, + 'name' => '异常处理', + 'url' => '/produce/exception', + 'icon' => 'ri:error-warning-line', + 'level' => 1, + 'type' => 2, + 'sort' => 20, + 'target' => 1, + 'is_show' => 1, + 'enabled' => 1, + 'created_at' => time(), + 'updated_at' => time(), + 'deleted_at' => null, + ], + [ + 'app' => 'web', + 'category' => 1, + 'source' => 'plugin', + 'code' => null, + 'name' => '维修处理', + 'url' => '/produce/repair', + 'icon' => 'ri:tools-line', + 'level' => 1, + 'type' => 2, + 'sort' => 30, + 'target' => 1, + 'is_show' => 1, + 'enabled' => 1, + 'created_at' => time(), + 'updated_at' => time(), + 'deleted_at' => null, + ], + [ + 'app' => 'web', + 'category' => 1, + 'source' => 'plugin', + 'code' => null, + 'name' => '待维修订单', + 'url' => '/produce/repair/tasks', + 'icon' => 'ri:task-line', + 'level' => 1, + 'type' => 2, + 'sort' => 40, + 'target' => 1, + 'is_show' => 1, + 'enabled' => 1, + 'created_at' => time(), + 'updated_at' => time(), + 'deleted_at' => null, + ], + [ + 'app' => 'web', + 'category' => 1, + 'source' => 'plugin', + 'code' => null, + 'name' => '综合查询', + 'url' => '/produce/query', + 'icon' => 'ri:search-eye-line', + 'level' => 1, + 'type' => 2, + 'sort' => 50, + 'target' => 1, + 'is_show' => 1, + 'enabled' => 1, + 'created_at' => time(), + 'updated_at' => time(), + 'deleted_at' => null, + ], +]; diff --git a/backend/plugin/produce/resource/template/admin/.gitkeep b/backend/plugin/produce/resource/template/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/resource/template/admin/api/index.ts b/backend/plugin/produce/resource/template/admin/api/index.ts new file mode 100644 index 0000000..a182241 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/api/index.ts @@ -0,0 +1,519 @@ +import request from "@/utils/http"; + +/** + * produce 插件 API + */ +import BaseService from "@/api/core/base"; + +export interface ProduceProcessRow { + id?: string | number; + process_code: string; + process_name: string; + description?: string; + step_count?: number; + sort?: number; + status?: number; + created_date?: string; + updated_date?: string; +} + +export interface ProduceProcessStepRow { + id?: string | number; + process_id: string | number; + step_code: string; + step_name: string; + description?: string; + sort?: number; + status?: number; + prerequisite_steps?: Array | string | null; + prerequisite_mode?: "all" | "any" | "none"; + prerequisite_require_final?: number; + allow_repeat_scan?: number; + repeat_scan_statuses?: Array | string | null; + process?: { + id?: string | number; + process_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStepStatusRow { + id?: string | number; + step_id: string | number; + status_code: string; + status_name: string; + is_initial?: number; + is_final?: number; + description?: string; + sort?: number; + status?: number; + step?: { + id?: string | number; + step_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStepRelationRow { + id?: string | number; + from_step_id: string | number; + to_step_id: string | number; + relation_type: "normal" | "exception"; + flow_condition?: string | Record | null; + priority?: number; + description?: string; + sort?: number; + status?: number; + from_step?: { + id?: string | number; + step_name?: string; + }; + to_step?: { + id?: string | number; + step_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStatusTransitionRow { + id?: string | number; + step_id: string | number; + from_status_id: string | number; + to_status_id: string | number; + trigger_event: string; + event_config?: string | Record | null; + auto_trigger?: number; + priority?: number; + transition_type?: "primary" | "side_effect"; + side_effect_type?: string; + side_effect_config?: string | Record | null; + description?: string; + sort?: number; + status?: number; + step?: { + id?: string | number; + step_name?: string; + }; + from_status?: { + id?: string | number; + status_name?: string; + }; + to_status?: { + id?: string | number; + status_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceMaterialCategoryRow { + id?: string | number; + category_name: string; + created_date?: string; + updated_date?: string; +} + +export interface ProduceMaterialRow { + id?: string | number; + category_id: string | number; + category_name?: string; + product_code: string; + product_name: string; + created_date?: string; + updated_date?: string; +} + +export interface ProduceHardwareFaultDescriptionRow { + id?: string | number; + // ... other properties +} + +export const ProduceMaterialCategoryService = { + ...BaseService({ + baseUrl: "/produce/material-category", + }), +}; + +export const ProduceMaterialService = { + ...BaseService({ baseUrl: "/produce/material" }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + importExcel(data: FormData) { + return request.post<{ created: number; updated: number }>({ + url: "/produce/material/import", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, +}; + +export const ProduceProcessService = { + ...BaseService({ baseUrl: "/produce/process" }), +}; + +export const ProduceProcessStepService = { + ...BaseService({ baseUrl: "/produce/process-step" }), +}; + +export const ProduceStepStatusService = { + ...BaseService({ baseUrl: "/produce/step-status" }), +}; + +export const ProduceStepRelationService = { + ...BaseService({ baseUrl: "/produce/step-relation" }), +}; + +export const ProduceStatusTransitionService = { + ...BaseService({ + baseUrl: "/produce/status-transition", + }), +}; + +export interface ProduceTransitionLogRow { + id?: string | number; + work_order_id?: string | number | null; + work_order_no?: string; + step_id: string | number; + transition_id?: string | number | null; + from_status_id: string | number; + to_status_id: string | number; + trigger_event: string; + trigger_data?: Record | string | null; + trigger_user?: string; + trigger_ip?: string; + is_auto?: number; + is_success?: number; + error_message?: string | null; + duration?: number; + remark?: string | null; + created_by?: string | number | null; + created_name?: string; + created_date?: string; + step?: { + id?: string | number; + step_name?: string; + }; + transition?: { + id?: string | number; + trigger_event?: string; + }; + from_status?: { + id?: string | number; + status_name?: string; + }; + to_status?: { + id?: string | number; + status_name?: string; + }; +} + +export const ProduceTransitionLogService = { + ...BaseService({ + baseUrl: "/produce/transition-log", + allowedMethods: ["list", "get", "export"], + }), +}; + +export interface ProduceSpecificRuleRow { + id?: string | number; + rule_name: string; + rule_pattern: string; + rule_type: number; + apply_mode?: "order" | "machine" | "both"; + match_field?: "order_no" | "process_order_no" | "product_code" | "user_code"; + is_enabled?: number; + step_ids?: Array | string | null; + sort?: number; + remark?: string; + created_name?: string; + created_date?: string; +} + +export interface ProduceProductRecordRow { + id?: string | number; + process_order_no: string; + product_code: string; + quantity: number; + process_id?: string | number | null; + step_id?: string | number | null; + status_id?: string | number | null; + user_code?: string; + operator?: string; + operator_id?: string | number | null; + scan_result?: number; + remark?: string | null; + process?: { + id?: string | number; + process_name?: string; + }; + step?: { + id?: string | number; + step_name?: string; + }; + status?: { + id?: string | number; + status_name?: string; + }; + created_name?: string; + created_date?: string; +} + +export interface ProduceScanDataRow { + id?: string | number; + order_no: string; + quantity?: number; + user_code: string; + operator?: string; + operator_id?: string | number | null; + process_id: string | number; + step_id: string | number; + status_id?: string | number | null; + scan_result: number; + scan_time?: number; + scan_date?: string; + remark?: string | null; + process?: { + id?: string | number; + process_name?: string; + }; + step?: { + id?: string | number; + step_name?: string; + }; + status?: { + id?: string | number; + status_name?: string; + }; + created_name?: string; + created_date?: string; +} + +export const ProduceSpecificRuleService = { + ...BaseService({ baseUrl: "/produce/specific-rule" }), +}; + +export const ProduceProductRecordService = { + ...BaseService({ + baseUrl: "/produce/product-record", + }), +}; + +export const ProduceScanDataService = { + ...BaseService({ baseUrl: "/produce/scan-data" }), +}; + +export interface ProduceExceptionSettingRow { + id?: string | number; + parent_id?: string | number | null; + parent_name?: string; + setting_type: "type" | "reason" | "issue"; + code: string; + name: string; + sort?: number; + is_enabled?: number; + remark?: string | null; + created_name?: string; + created_date?: string; +} + +export interface ProduceExceptionRecordRow { + id?: string | number; + order_no: string; + user_code?: string | null; + reporter?: string | null; + type_id?: string | number | null; + type_code: string; + type_name: string; + reason_id?: string | number | null; + reason_code: string; + reason_name: string; + issue_id?: string | number | null; + issue_code?: string | null; + issue_name?: string | null; + images?: string[] | null; + remark?: string | null; + status?: number; + status_name?: string; + handler?: string | null; + handler_id?: string | number | null; + handled_at?: number | null; + handled_date?: string | null; + reported_at?: number | null; + reported_date?: string | null; + created_name?: string; + created_date?: string; +} + +export const ProduceExceptionSettingService = { + ...BaseService({ + baseUrl: "/produce/exception-setting", + }), +}; + +export interface ProduceRepairSettingRow { + id?: string | number; + parent_id?: string | number | null; + parent_name?: string; + setting_type: "type" | "reason" | "issue"; + code: string; + name: string; + sort?: number; + is_enabled?: number; + remark?: string | null; + created_name?: string; + created_date?: string; +} + +export interface ProduceHardwareFaultDescriptionRow { + id?: string | number; + category_id: string | number; + category_name?: string; + description: string; + use_count?: number; + is_enabled?: number; + remark?: string | null; + created_date?: string; +} + +export interface ProduceRepairRecordRow { + id?: string | number; + repair_no: string; + exception_id?: string | number | null; + exception_reason?: string | null; + exception_issue?: string | null; + exception_type_name?: string | null; + order_no?: string | null; + product_code?: string | null; + product_name?: string | null; + category_id?: string | number | null; + category_name?: string | null; + fault_description_id?: string | number | null; + fault_description_ids?: Array; + fault_description?: string | null; + fault_descriptions?: string[]; + repair_result?: string | null; + parts?: Array> | string | null; + responsible_operators?: Array> | string | null; + operator?: string | null; + repaired_at?: number | null; + repaired_date?: string | null; + remark?: string | null; + created_date?: string; +} + +export const ProduceRepairSettingService = { + ...BaseService({ + baseUrl: "/produce/repair-setting", + }), +}; + +export const ProduceHardwareFaultDescriptionService = { + ...BaseService({ + baseUrl: "/produce/hardware-fault-description", + }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + importExcel(data: FormData) { + return request.post<{ created: number; updated: number }>({ + url: "/produce/hardware-fault-description/import", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, +}; + +export const ProduceRepairRecordService = { + ...BaseService({ baseUrl: "/produce/repair-record" }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + faultDescriptionOptions(params: Record = {}) { + return ProduceHardwareFaultDescriptionService.list({ + limit: 999, + is_enabled: 1, + ...params, + }); + }, +}; + +export const ProduceExceptionRecordService = { + ...BaseService({ + baseUrl: "/produce/exception-record", + }), +}; + +export const ProduceConfigApi = { + info(params: Record) { + return request.get({ url: "/produce/config/info", params }); + }, + save(data: Record) { + return request.post({ url: "/produce/config/save", data }); + }, +}; + +export default { + materialCategoryList(params = {}) { + return ProduceMaterialCategoryService.list(params as Record); + }, + materialList(params = {}) { + return ProduceMaterialService.list(params as Record); + }, + processList(params = {}) { + return ProduceProcessService.list(params as Record); + }, + processStepList(params = {}) { + return ProduceProcessStepService.list(params as Record); + }, + stepStatusList(params = {}) { + return ProduceStepStatusService.list(params as Record); + }, + stepRelationList(params = {}) { + return ProduceStepRelationService.list(params as Record); + }, + statusTransitionList(params = {}) { + return ProduceStatusTransitionService.list(params as Record); + }, + transitionLogList(params = {}) { + return ProduceTransitionLogService.list(params as Record); + }, + specificRuleList(params = {}) { + return ProduceSpecificRuleService.list(params as Record); + }, + productRecordList(params = {}) { + return ProduceProductRecordService.list(params as Record); + }, + scanDataList(params = {}) { + return ProduceScanDataService.list(params as Record); + }, + exceptionRecordList(params = {}) { + return ProduceExceptionRecordService.list(params as Record); + }, + exceptionSettingList(params = {}) { + return ProduceExceptionSettingService.list(params as Record); + }, + repairSettingList(params = {}) { + return ProduceRepairSettingService.list(params as Record); + }, + hardwareFaultDescriptionList(params = {}) { + return ProduceHardwareFaultDescriptionService.list( + params as Record, + ); + }, + repairRecordList(params = {}) { + return ProduceRepairRecordService.list(params as Record); + }, +}; diff --git a/backend/plugin/produce/resource/template/admin/api/statistics.ts b/backend/plugin/produce/resource/template/admin/api/statistics.ts new file mode 100644 index 0000000..fd84d75 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/api/statistics.ts @@ -0,0 +1,50 @@ +import request from '@/utils/http' + +export interface WorkStatisticsQuery { + month?: string + date?: string + data_type?: string + process_id?: string | number + step_id?: string | number + scan_result?: string | number + user_code?: string + operator?: string + process_name?: string + step_name?: string +} + +export interface WorkStatisticsColumn { + key: string + label: string +} + +export interface WorkStatisticsOption { + label: string + value: string | number +} + +export interface WorkStatisticsResponse { + columns: WorkStatisticsColumn[] + items: Array> +} + +export const ProduceWorkStatisticsService = { + monthly(params: WorkStatisticsQuery = {}) { + return request.get({ url: '/produce/statistics/monthly', params }) + }, + daily(params: WorkStatisticsQuery = {}) { + return request.get({ url: '/produce/statistics/daily', params }) + }, + getProcessOptions() { + return request.get({ + url: '/produce/process', + params: { format: 'select' } + }) + }, + getStepOptions(processId?: string | number) { + return request.get({ + url: '/produce/process-step', + params: { format: 'select', process_id: processId || undefined } + }) + } +} diff --git a/backend/plugin/produce/resource/template/admin/lang/en/en.json b/backend/plugin/produce/resource/template/admin/lang/en/en.json new file mode 100644 index 0000000..c3442b5 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/lang/en/en.json @@ -0,0 +1,217 @@ +{ + "produce": { + "common": { + "columns": { + "index": "No." + } + }, + "modules": { + "process": { + "title": "Process Management", + "search": { + "process_code": "Process Code", + "process_code_placeholder": "Please enter process code", + "process_name": "Process Name", + "process_name_placeholder": "Please enter process name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "process_code_placeholder": "Please enter process code", + "process_name_placeholder": "Please enter process name", + "description": "Process Description", + "description_placeholder": "Please enter process description" + }, + "columns": { + "process_code": "Process Code", + "process_name": "Process Name", + "step_count": "Step Count", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "process_step": { + "title": "Process Steps", + "search": { + "process_id": "Process", + "process_id_placeholder": "Please select process", + "step_code": "Step Code", + "step_code_placeholder": "Please enter step code", + "step_name": "Step Name", + "step_name_placeholder": "Please enter step name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "process_id": "Process", + "process_id_placeholder": "Please select process", + "step_code_placeholder": "Please enter step code", + "step_name_placeholder": "Please enter step name", + "description": "Step Description", + "description_placeholder": "Please enter step description" + }, + "columns": { + "process_name": "Process", + "step_code": "Step Code", + "step_name": "Step Name", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "step_status": { + "title": "Step Status", + "search": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "status_code": "Status Code", + "status_code_placeholder": "Please enter status code", + "status_name": "Status Name", + "status_name_placeholder": "Please enter status name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "status_code_placeholder": "Please enter status code", + "status_name_placeholder": "Please enter status name", + "description": "Status Description", + "description_placeholder": "Please enter status description" + }, + "columns": { + "step_name": "Step", + "status_code": "Status Code", + "status_name": "Status Name", + "is_initial": "Initial", + "is_final": "Final", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "step_relation": { + "title": "Step Relation", + "search": { + "from_step_id": "From Step", + "from_step_id_placeholder": "Please select from step", + "to_step_id": "To Step", + "to_step_id_placeholder": "Please select to step", + "relation_type": "Relation Type", + "relation_type_placeholder": "Please select relation type", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "from_step_id": "From Step", + "from_step_id_placeholder": "Please select from step", + "to_step_id": "To Step", + "to_step_id_placeholder": "Please select to step", + "flow_condition": "Flow Condition", + "flow_condition_placeholder": "Please enter JSON flow condition", + "description": "Relation Description", + "description_placeholder": "Please enter relation description" + }, + "columns": { + "from_step_name": "From Step", + "to_step_name": "To Step", + "relation_type": "Relation Type", + "priority": "Priority", + "flow_condition": "Flow Condition", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "status_transition": { + "title": "Status Transition", + "search": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "trigger_event": "Trigger Event", + "trigger_event_placeholder": "Please select trigger event", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "from_status_id": "From Status", + "from_status_id_placeholder": "Please select from status", + "to_status_id": "To Status", + "to_status_id_placeholder": "Please select to status", + "trigger_event_placeholder": "Please select trigger event", + "side_effect_type": "Side Effect Type", + "side_effect_type_placeholder": "Please select side effect type", + "event_config": "Event Config", + "event_config_placeholder": "Please enter JSON event config", + "side_effect_config": "Side Effect Config", + "side_effect_config_placeholder": "Please enter JSON side effect config", + "description": "Transition Description", + "description_placeholder": "Please enter transition description" + }, + "columns": { + "step_name": "Step", + "from_status_name": "From Status", + "to_status_name": "To Status", + "trigger_event": "Trigger Event", + "transition_type": "Transition Type", + "auto_trigger": "Auto Trigger", + "priority": "Priority", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "transition_log": { + "title": "Transition Log", + "search": { + "work_order_no": "Work Order No.", + "work_order_no_placeholder": "Please enter work order no", + "step_id": "Step", + "step_id_placeholder": "Please select step", + "trigger_event": "Trigger Event", + "trigger_event_placeholder": "Please select trigger event", + "trigger_user": "Trigger User", + "trigger_user_placeholder": "Please enter trigger user", + "is_success": "Result", + "is_success_placeholder": "Please select result" + }, + "columns": { + "work_order_no": "Work Order No", + "step_name": "Step", + "from_status_name": "From Status", + "to_status_name": "To Status", + "transition_event": "Transition Rule", + "trigger_event": "Trigger Event", + "trigger_user": "Trigger User", + "trigger_ip": "Trigger IP", + "is_auto": "Auto Trigger", + "is_success": "Result", + "duration": "Duration(ms)", + "trigger_data": "Trigger Data", + "error_message": "Error Message", + "remark": "Remark", + "created_name": "Created By", + "created_at": "Created At" + } + }, + "config": { + "title": "Production Config" + }, + "scan_data": { + "title": "Scan Data" + }, + "exception_data": { + "title": "Exception Records" + }, + "exception_setting": { + "title": "Exception Settings" + }, + "exception_analysis": { + "title": "Exception Analysis" + } + } + } +} diff --git a/backend/plugin/produce/resource/template/admin/lang/zh-cn/zh.json b/backend/plugin/produce/resource/template/admin/lang/zh-cn/zh.json new file mode 100644 index 0000000..b3cf285 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/lang/zh-cn/zh.json @@ -0,0 +1,217 @@ +{ + "produce": { + "common": { + "columns": { + "index": "序号" + } + }, + "modules": { + "process": { + "title": "工序管理", + "search": { + "process_code": "工序编码", + "process_code_placeholder": "请输入工序编码", + "process_name": "工序名称", + "process_name_placeholder": "请输入工序名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "process_code_placeholder": "请输入工序编码", + "process_name_placeholder": "请输入工序名称", + "description": "工序描述", + "description_placeholder": "请输入工序描述" + }, + "columns": { + "process_code": "工序编码", + "process_name": "工序名称", + "step_count": "步骤数", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "process_step": { + "title": "工序步骤", + "search": { + "process_id": "所属工序", + "process_id_placeholder": "请选择所属工序", + "step_code": "步骤编码", + "step_code_placeholder": "请输入步骤编码", + "step_name": "步骤名称", + "step_name_placeholder": "请输入步骤名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "process_id": "所属工序", + "process_id_placeholder": "请选择所属工序", + "step_code_placeholder": "请输入步骤编码", + "step_name_placeholder": "请输入步骤名称", + "description": "步骤描述", + "description_placeholder": "请输入步骤描述" + }, + "columns": { + "process_name": "所属工序", + "step_code": "步骤编码", + "step_name": "步骤名称", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "step_status": { + "title": "步骤状态", + "search": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "status_code": "状态编码", + "status_code_placeholder": "请输入状态编码", + "status_name": "状态名称", + "status_name_placeholder": "请输入状态名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "status_code_placeholder": "请输入状态编码", + "status_name_placeholder": "请输入状态名称", + "description": "状态描述", + "description_placeholder": "请输入状态描述" + }, + "columns": { + "step_name": "所属步骤", + "status_code": "状态编码", + "status_name": "状态名称", + "is_initial": "初始状态", + "is_final": "终态", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "step_relation": { + "title": "步骤关系", + "search": { + "from_step_id": "源步骤", + "from_step_id_placeholder": "请选择源步骤", + "to_step_id": "目标步骤", + "to_step_id_placeholder": "请选择目标步骤", + "relation_type": "关系类型", + "relation_type_placeholder": "请选择关系类型", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "from_step_id": "源步骤", + "from_step_id_placeholder": "请选择源步骤", + "to_step_id": "目标步骤", + "to_step_id_placeholder": "请选择目标步骤", + "flow_condition": "流转条件", + "flow_condition_placeholder": "请输入 JSON 格式流转条件", + "description": "关系描述", + "description_placeholder": "请输入关系描述" + }, + "columns": { + "from_step_name": "源步骤", + "to_step_name": "目标步骤", + "relation_type": "关系类型", + "priority": "优先级", + "flow_condition": "流转条件", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "status_transition": { + "title": "状态转换", + "search": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "trigger_event": "触发事件", + "trigger_event_placeholder": "请选择触发事件", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "from_status_id": "源状态", + "from_status_id_placeholder": "请选择源状态", + "to_status_id": "目标状态", + "to_status_id_placeholder": "请选择目标状态", + "trigger_event_placeholder": "请选择触发事件", + "side_effect_type": "副作用类型", + "side_effect_type_placeholder": "请选择副作用类型", + "event_config": "事件配置", + "event_config_placeholder": "请输入 JSON 格式事件配置", + "side_effect_config": "副作用配置", + "side_effect_config_placeholder": "请输入 JSON 格式副作用配置", + "description": "转换描述", + "description_placeholder": "请输入转换描述" + }, + "columns": { + "step_name": "所属步骤", + "from_status_name": "源状态", + "to_status_name": "目标状态", + "trigger_event": "触发事件", + "transition_type": "转换类型", + "auto_trigger": "自动触发", + "priority": "优先级", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "transition_log": { + "title": "转换日志", + "search": { + "work_order_no": "工单编号", + "work_order_no_placeholder": "请输入工单编号", + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "trigger_event": "触发事件", + "trigger_event_placeholder": "请选择触发事件", + "trigger_user": "触发用户", + "trigger_user_placeholder": "请输入触发用户", + "is_success": "执行结果", + "is_success_placeholder": "请选择执行结果" + }, + "columns": { + "work_order_no": "工单编号", + "step_name": "所属步骤", + "from_status_name": "源状态", + "to_status_name": "目标状态", + "transition_event": "转换规则", + "trigger_event": "触发事件", + "trigger_user": "触发用户", + "trigger_ip": "触发IP", + "is_auto": "自动触发", + "is_success": "执行结果", + "duration": "耗时(ms)", + "trigger_data": "触发数据", + "error_message": "错误信息", + "remark": "备注", + "created_name": "创建人", + "created_at": "创建时间" + } + }, + "config": { + "title": "生产配置" + }, + "scan_data": { + "title": "扫码数据" + }, + "exception_data": { + "title": "异常数据" + }, + "exception_setting": { + "title": "异常设置" + }, + "exception_analysis": { + "title": "异常数据分析" + } + } + } +} diff --git a/backend/plugin/produce/resource/template/admin/menu/admin.php b/backend/plugin/produce/resource/template/admin/menu/admin.php new file mode 100644 index 0000000..02598e3 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/menu/admin.php @@ -0,0 +1,134 @@ + '生产管理', + 'code' => 'produce', + 'app' => 'produce', + 'path' => '/produce', + 'icon' => 'plugin', + 'children' => [ + [ + 'name' => '生产配置', + 'code' => 'produce:config:index', + 'app' => 'produce', + 'path' => '/produce/config', + 'component' => '/produce/config/index', + ], + [ + 'name' => '工序管理', + 'code' => 'produce:process:index', + 'app' => 'produce', + 'path' => '/produce/process', + 'component' => '/produce/process/index', + ], + [ + 'name' => '工序步骤', + 'code' => 'produce:process-step:index', + 'app' => 'produce', + 'path' => '/produce/process-step', + 'component' => '/produce/process-step/index', + ], + [ + 'name' => '步骤状态', + 'code' => 'produce:step-status:index', + 'app' => 'produce', + 'path' => '/produce/step-status', + 'component' => '/produce/step-status/index', + ], + [ + 'name' => '步骤关系', + 'code' => 'produce:step-relation:index', + 'app' => 'produce', + 'path' => '/produce/step-relation', + 'component' => '/produce/step-relation/index', + ], + [ + 'name' => '状态转换', + 'code' => 'produce:status-transition:index', + 'app' => 'produce', + 'path' => '/produce/status-transition', + 'component' => '/produce/status-transition/index', + ], + [ + 'name' => '整机数据', + 'code' => 'produce:product-record:index', + 'app' => 'produce', + 'path' => '/produce/product-record', + 'component' => '/produce/product-record/index', + ], + [ + 'name' => '扫码数据', + 'code' => 'produce:scan-data:index', + 'app' => 'produce', + 'path' => '/produce/scan-data', + 'component' => '/produce/scan-data/index', + ], + [ + 'name' => '转换日志', + 'code' => 'produce:transition-log:index', + 'app' => 'produce', + 'path' => '/produce/transition-log', + 'component' => '/produce/transition-log/index', + ], + [ + 'name' => '异常管理', + 'code' => 'produce:exception:index', + 'app' => 'produce', + 'path' => '/produce/exception', + 'icon' => 'warning', + 'children' => [ + [ + 'name' => '异常数据', + 'code' => 'produce:exception-data:list', + 'app' => 'produce', + 'path' => '/produce/exception-data', + 'component' => '/produce/exception/data/index', + ], + [ + 'name' => '异常设置', + 'code' => 'produce:exception-setting:list', + 'app' => 'produce', + 'path' => '/produce/exception-setting', + 'component' => '/produce/exception/setting/index', + ], + [ + 'name' => '数据分析', + 'code' => 'produce:exception-analysis:read', + 'app' => 'produce', + 'path' => '/produce/exception-analysis', + 'component' => '/produce/exception/analysis/index', + ], + ], + ], + [ + 'name' => '数据统计', + 'code' => 'produce:statistics:index', + 'app' => 'produce', + 'path' => '/produce/statistics', + 'icon' => 'bar-chart-2', + 'children' => [ + [ + 'name' => '月工作统计', + 'code' => 'produce:statistics:monthly', + 'app' => 'produce', + 'path' => '/produce/statistics/monthly', + 'component' => '/produce/statistics/monthly/index', + ], + [ + 'name' => '日工作统计', + 'code' => 'produce:statistics:daily', + 'app' => 'produce', + 'path' => '/produce/statistics/daily', + 'component' => '/produce/statistics/daily/index', + ], + ], + ], + ], + ], +]; diff --git a/backend/plugin/produce/resource/template/admin/routes/index.ts b/backend/plugin/produce/resource/template/admin/routes/index.ts new file mode 100644 index 0000000..135eafe --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/routes/index.ts @@ -0,0 +1,215 @@ +import { AppRouteRecord } from "@/types/router"; +import { RoutesAlias } from "@/router/routesAlias"; + +export default { + path: "/produce", + name: "Produce", + component: RoutesAlias.Layout, + meta: { + title: "生产管理", + icon: "ri:plugin-line", + module: "produce", + }, + children: [ + { + path: "material-category", + name: "ProduceMaterialCategory", + component: "/produce/material-category/index", + meta: { + title: "物料分类", + icon: "ri:folder-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "material", + name: "ProduceMaterial", + component: "/produce/material/index", + meta: { + title: "物料库", + icon: "ri:archive-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "config", + name: "ProduceConfig", + component: "/produce/config/index", + meta: { + title: "生产配置", + icon: "ri:settings-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "process", + name: "ProduceProcess", + component: "/produce/process/index", + meta: { + title: "工序管理", + icon: "ri:stack-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "process-step", + name: "ProduceProcessStep", + component: "/produce/process-step/index", + meta: { + title: "工序步骤", + icon: "ri:list-check-3", + keepAlive: false, + module: "produce", + }, + }, + { + path: "step-status", + name: "ProduceStepStatus", + component: "/produce/step-status/index", + meta: { + title: "步骤状态", + icon: "ri:flag-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "step-relation", + name: "ProduceStepRelation", + component: "/produce/step-relation/index", + meta: { + title: "步骤关系", + icon: "ri:git-branch-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "status-transition", + name: "ProduceStatusTransition", + component: "/produce/status-transition/index", + meta: { + title: "状态转换", + icon: "ri:share-forward-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "product-record", + name: "ProduceProductRecord", + component: "/produce/product-record/index", + meta: { + title: "整机数据", + icon: "ri-product-hunt-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "scan-data", + name: "ProduceScanData", + component: "/produce/scan-data/index", + meta: { + title: "扫码数据", + icon: "ri:qr-scan-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "transition-log", + name: "ProduceTransitionLog", + component: "/produce/transition-log/index", + meta: { + title: "转换日志", + icon: "ri:file-list-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "repair", + name: "ProduceRepair", + meta: { + title: "维修管理", + icon: "ri:tools-line", + keepAlive: false, + module: "produce", + }, + children: [ + { + path: "repair-data", + name: "ProduceRepairData", + component: "/produce/repair/repair-data/index", + meta: { + title: "维修数据", + icon: "ri:file-list-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "repair-setting", + name: "ProduceRepairSetting", + component: "/produce/repair/repair-setting/index", + meta: { + title: "维修设置", + icon: "ri:settings-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "hardware-fault-description", + name: "ProduceHardwareFaultDescription", + component: "/produce/repair/hardware-fault-description/index", + meta: { + title: "硬件故障描述", + icon: "ri:tools-line", + keepAlive: false, + module: "produce", + }, + }, + ], + }, + { + path: "statistics", + name: "ProduceStatistics", + meta: { + title: "数据统计", + icon: "ri:bar-chart-2-line", + keepAlive: false, + module: "produce", + }, + children: [ + { + path: "monthly", + name: "ProduceMonthlyWorkStatistics", + component: "/produce/statistics/monthly/index", + meta: { + title: "月工作统计", + icon: "ri:calendar-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "daily", + name: "ProduceDailyWorkStatistics", + component: "/produce/statistics/daily/index", + meta: { + title: "日工作统计", + icon: "ri:time-line", + keepAlive: false, + module: "produce", + }, + }, + ], + }, + ], +} as AppRouteRecord; diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/index.vue new file mode 100644 index 0000000..648d1cb --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/index.vue @@ -0,0 +1,52 @@ + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/modules/basic-setting.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/basic-setting.vue new file mode 100644 index 0000000..ea96816 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/basic-setting.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/modules/erp-integration.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/erp-integration.vue new file mode 100644 index 0000000..28aa92f --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/erp-integration.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/modules/specific-rule.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/specific-rule.vue new file mode 100644 index 0000000..34b58e6 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/specific-rule.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/modules/step-rule.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/step-rule.vue new file mode 100644 index 0000000..eb9d66d --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/step-rule.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/exception/analysis/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/exception/analysis/index.vue new file mode 100644 index 0000000..08326fd --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/exception/analysis/index.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/exception/data/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/exception/data/index.vue new file mode 100644 index 0000000..12bdc6d --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/exception/data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/exception/data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/exception/data/schemas/index.tsx new file mode 100644 index 0000000..3d4630e --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/exception/data/schemas/index.tsx @@ -0,0 +1,127 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionRecordService } from '@/apps/produce/api' + +const statusOptions = [ + { label: '待处理', value: 0 }, + { label: '处理中', value: 1 }, + { label: '已处理', value: 2 }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionRecordService.list, + dialogTitle: '异常数据', + crudApi: { + add: ProduceExceptionRecordService.create, + edit: ProduceExceptionRecordService.update, + remove: ProduceExceptionRecordService.remove, + view: ProduceExceptionRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-data:create', + editAuth: 'produce:exception-data:update', + removeAuth: 'produce:exception-data:delete', + viewAuth: 'produce:exception-data:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' }, + colSpan: 8, + }, + { + prop: 'type_code', + label: '处理类型', + component: 'Input', + componentProps: { clearable: true, placeholder: '请输入处理类型编码' }, + colSpan: 8, + }, + { + prop: 'status', + label: '处理状态', + component: 'Select', + componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions }, + colSpan: 8, + }, + { + prop: 'reported_at_range', + label: '提报时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'order_no', label: '单号/条码', minWidth: 160 }, + { prop: 'type_name', label: '处理类型', minWidth: 120 }, + { prop: 'reason_name', label: '异常原因', minWidth: 120 }, + { prop: 'issue_name', label: '具体问题', minWidth: 140 }, + { prop: 'reporter', label: '提报人', minWidth: 110 }, + { prop: 'status_name', label: '处理状态', minWidth: 100 }, + { prop: 'handler', label: '处理人', minWidth: 110 }, + { prop: 'reported_date', label: '提报时间', minWidth: 170 }, + { prop: 'handled_date', label: '处理时间', minWidth: 170 }, + ], + dialogProps: { width: '820px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Input', prop: 'order_no', label: '单号/条码', + componentProps: { placeholder: '请输入单号或条码', maxlength: 100 }, + rules: [{ required: true, message: '请输入单号或条码' }], colSpan: 12, + }, + { component: 'Input', prop: 'user_code', label: '提报人工号', componentProps: { placeholder: '请输入提报人工号', maxlength: 50 }, colSpan: 12 }, + { component: 'Input', prop: 'reporter', label: '提报人', componentProps: { placeholder: '请输入提报人', maxlength: 100 }, colSpan: 12 }, + { + component: 'Input', prop: 'type_code', label: '处理类型编码', + componentProps: { placeholder: '例如 defective_repair', maxlength: 64 }, + rules: [{ required: true, message: '请输入处理类型编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'type_name', label: '处理类型', + componentProps: { placeholder: '例如 不良维修', maxlength: 100 }, + rules: [{ required: true, message: '请输入处理类型' }], colSpan: 12, + }, + { + component: 'Input', prop: 'reason_code', label: '异常原因编码', + componentProps: { placeholder: '例如 test_defect', maxlength: 64 }, + rules: [{ required: true, message: '请输入异常原因编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'reason_name', label: '异常原因', + componentProps: { placeholder: '例如 测试不良', maxlength: 100 }, + rules: [{ required: true, message: '请输入异常原因' }], colSpan: 12, + }, + { component: 'Input', prop: 'issue_code', label: '具体问题编码', componentProps: { placeholder: '可选', maxlength: 64 }, colSpan: 12 }, + { component: 'Input', prop: 'issue_name', label: '具体问题', componentProps: { placeholder: '可选', maxlength: 100 }, colSpan: 12 }, + { component: 'Select', prop: 'status', label: '处理状态', defaultValue: 0, componentProps: { options: statusOptions }, colSpan: 12 }, + { component: 'Input', prop: 'handler', label: '处理人', componentProps: { placeholder: '请输入处理人', maxlength: 100 }, colSpan: 12 }, + { + component: 'Upload', prop: 'images', label: '现场图片', componentProps: { listType: 'picture-card' }, ifDetail: true, colSpan: 24, + }, + { + component: 'Input', prop: 'remark', label: '补充说明', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24, + }, + ], + }, +}) diff --git a/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/index.vue new file mode 100644 index 0000000..a4a5e48 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/schemas/index.tsx new file mode 100644 index 0000000..fac58e4 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/exception/setting/schemas/index.tsx @@ -0,0 +1,100 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionSettingService } from '@/apps/produce/api' + +const settingTypeOptions = [ + { label: '处理类型', value: 'type' }, + { label: '异常原因', value: 'reason' }, + { label: '具体问题', value: 'issue' }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionSettingService.list, + dialogTitle: '异常设置', + crudApi: { + add: ProduceExceptionSettingService.create, + edit: ProduceExceptionSettingService.update, + remove: ProduceExceptionSettingService.remove, + view: ProduceExceptionSettingService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-setting:create', + editAuth: 'produce:exception-setting:update', + removeAuth: 'produce:exception-setting:delete', + viewAuth: 'produce:exception-setting:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', label: '关键字', component: 'Input', + componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8, + }, + { + prop: 'setting_type', label: '设置层级', component: 'Select', + componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8, + }, + { + prop: 'is_enabled', label: '启用状态', component: 'Select', + componentProps: { + clearable: true, placeholder: '请选择启用状态', + options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }], + }, colSpan: 8, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' }, + { prop: 'code', label: '编码', minWidth: 150 }, + { prop: 'name', label: '名称', minWidth: 160 }, + { prop: 'parent_name', label: '所属上级', minWidth: 140 }, + { prop: 'sort', label: '排序', width: 90 }, + { prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' }, + { prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true }, + { prop: 'created_name', label: '创建人', minWidth: 110 }, + ], + dialogProps: { width: '700px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Select', prop: 'setting_type', label: '设置层级', + componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions }, + rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12, + }, + { + component: 'ApiSelect', prop: 'parent_id', label: '所属上级', + componentProps: { + api: '/produce/exception-setting', requestMethod: 'get', + params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级', + }, colSpan: 12, + }, + { + component: 'Input', prop: 'code', label: '设置编码', + componentProps: { placeholder: '请输入唯一编码', maxlength: 64 }, + rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'name', label: '设置名称', + componentProps: { placeholder: '请输入显示名称', maxlength: 100 }, + rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12, + }, + { component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 }, + { + component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1, + componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12, + }, + { + component: 'Input', prop: 'remark', label: '备注', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24, + }, + ], + }, +}) diff --git a/backend/plugin/produce/resource/template/admin/views/produce/material-category/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/material-category/index.vue new file mode 100644 index 0000000..e424302 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/material-category/index.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/material-category/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/material-category/schemas/index.tsx new file mode 100644 index 0000000..5282377 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/material-category/schemas/index.tsx @@ -0,0 +1,42 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceMaterialCategoryService } from '../../../../api' + +export const crudSchema = (): CrudSchema => ({ + api: ProduceMaterialCategoryService.list, + dialogTitle: '物料分类', + crudApi: { + add: ProduceMaterialCategoryService.create, + edit: ProduceMaterialCategoryService.update, + remove: ProduceMaterialCategoryService.remove, + view: ProduceMaterialCategoryService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:material-category:create', + editAuth: 'produce:material-category:update', + removeAuth: 'produce:material-category:delete', + viewAuth: 'produce:material-category:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [{ prop: 'LIKE_category_name', label: '分类名称', component: 'Input', componentProps: { clearable: true, placeholder: '请输入分类名称' }, colSpan: 8 }], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'category_name', label: '分类名称', minWidth: 220 }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + ], + dialogProps: { width: '520px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '90px', + schema: [ + { component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' }, + { component: 'Input', prop: 'category_name', label: '分类名称', componentProps: { maxlength: 100, placeholder: '请输入分类名称' }, rules: [{ required: true, message: '请输入分类名称' }], colSpan: 24 }, + ], + }, +}) diff --git a/backend/plugin/produce/resource/template/admin/views/produce/material/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/material/index.vue new file mode 100644 index 0000000..ef5768a --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/material/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/material/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/material/schemas/index.tsx new file mode 100644 index 0000000..71e2114 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/material/schemas/index.tsx @@ -0,0 +1,50 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceMaterialService } from '../../../../api' + +export const crudSchema = (): CrudSchema => ({ + api: ProduceMaterialService.list, + dialogTitle: '物料库', + crudApi: { + add: ProduceMaterialService.create, + edit: ProduceMaterialService.update, + remove: ProduceMaterialService.remove, + view: ProduceMaterialService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + hasExport: true, + addAuth: 'produce:material:create', + editAuth: 'produce:material:update', + removeAuth: 'produce:material:delete', + viewAuth: 'produce:material:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 }, + { prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, colSpan: 8 }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'category_name', label: '物料分类', minWidth: 160 }, + { prop: 'product_code', label: '商品编码', minWidth: 180 }, + { prop: 'product_name', label: '商品名称', minWidth: 220 }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + ], + dialogProps: { width: '640px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '90px', + schema: [ + { component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' }, + { component: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, rules: [{ required: true, message: '请选择物料分类' }], colSpan: 12 }, + { component: 'Input', prop: 'product_code', label: '商品编码', componentProps: { maxlength: 100, placeholder: '请输入或扫描商品编码' }, rules: [{ required: true, message: '请输入商品编码' }], colSpan: 12 }, + { component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 }, + ], + }, +}) diff --git a/backend/plugin/produce/resource/template/admin/views/produce/process-step/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/process-step/index.vue new file mode 100644 index 0000000..3305168 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/process-step/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/process-step/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/process-step/schemas/index.tsx new file mode 100644 index 0000000..487fe0a --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/process-step/schemas/index.tsx @@ -0,0 +1,185 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceProcessStepService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProcessStepService.list, + dialogTitle: $t('produce.modules.process_step.title'), + crudApi: { + add: ProduceProcessStepService.create, + edit: ProduceProcessStepService.update, + remove: ProduceProcessStepService.remove, + view: ProduceProcessStepService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:process-step:create', + editAuth: 'produce:process-step:update', + removeAuth: 'produce:process-step:delete', + viewAuth: 'produce:process-step:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'process_id', + label: $t('produce.modules.process_step.search.process_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.process_step.search.process_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_step_code', + label: $t('produce.modules.process_step.search.step_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process_step.search.step_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_step_name', + label: $t('produce.modules.process_step.search.step_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process_step.search.step_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.process_step.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.process_step.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { + prop: 'process.process_name', + label: $t('produce.modules.process_step.columns.process_name'), + minWidth: 180, + }, + { prop: 'step_code', label: $t('produce.modules.process_step.columns.step_code'), minWidth: 160 }, + { prop: 'step_name', label: $t('produce.modules.process_step.columns.step_name'), minWidth: 180 }, + { prop: 'sort', label: $t('produce.modules.process_step.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.process_step.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.process_step.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '680px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'process_id', + label: $t('produce.modules.process_step.form.process_id'), + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.process_step.form.process_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.process_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_code', + label: $t('produce.modules.process_step.columns.step_code'), + componentProps: { + placeholder: $t('produce.modules.process_step.form.step_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.step_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_name', + label: $t('produce.modules.process_step.columns.step_name'), + componentProps: { + placeholder: $t('produce.modules.process_step.form.step_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.step_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.process_step.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.process_step.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.process_step.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.process_step.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/process/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/process/index.vue new file mode 100644 index 0000000..c2d5b17 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/process/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/process/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/process/schemas/index.tsx new file mode 100644 index 0000000..c662f03 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/process/schemas/index.tsx @@ -0,0 +1,154 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceProcessService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProcessService.list, + dialogTitle: $t('produce.modules.process.title'), + crudApi: { + add: ProduceProcessService.create, + edit: ProduceProcessService.update, + remove: ProduceProcessService.remove, + view: ProduceProcessService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:process:create', + editAuth: 'produce:process:update', + removeAuth: 'produce:process:delete', + viewAuth: 'produce:process:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_process_code', + label: $t('produce.modules.process.search.process_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process.search.process_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_process_name', + label: $t('produce.modules.process.search.process_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process.search.process_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.process.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.process.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'process_code', label: $t('produce.modules.process.columns.process_code'), minWidth: 160 }, + { prop: 'process_name', label: $t('produce.modules.process.columns.process_name'), minWidth: 180 }, + { prop: 'step_count', label: $t('produce.modules.process.columns.step_count'), width: 120 }, + { prop: 'sort', label: $t('produce.modules.process.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.process.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.process.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '640px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'process_code', + label: $t('produce.modules.process.columns.process_code'), + componentProps: { + placeholder: $t('produce.modules.process.form.process_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.process.form.process_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'process_name', + label: $t('produce.modules.process.columns.process_name'), + componentProps: { + placeholder: $t('produce.modules.process.form.process_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.process.form.process_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.process.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.process.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.process.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.process.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/product-record/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/product-record/index.vue new file mode 100644 index 0000000..bdd4600 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/product-record/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/product-record/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/product-record/schemas/index.tsx new file mode 100644 index 0000000..ae2abe9 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/product-record/schemas/index.tsx @@ -0,0 +1,249 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceProductRecordService } from '@/apps/produce/api' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProductRecordService.list, + dialogTitle: '整机数据', + crudApi: { + add: ProduceProductRecordService.create, + edit: ProduceProductRecordService.update, + remove: ProduceProductRecordService.remove, + view: ProduceProductRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:product-record:create', + editAuth: 'produce:product-record:update', + removeAuth: 'produce:product-record:delete', + viewAuth: 'produce:product-record:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入加工单号/商品编码/操作人员/备注', + }, + colSpan: 6, + }, + { + prop: 'process_id', + label: '工序名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: '步骤名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + }, + colSpan: 6, + }, + { + prop: 'status_id', + label: '步骤状态', + component: 'ApiSelect', + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + }, + colSpan: 6, + }, + { + prop: 'scan_result', + label: '扫码结果', + component: 'Select', + componentProps: { + clearable: true, + placeholder: '请选择扫码结果', + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + colSpan: 6, + }, + { + prop: 'created_time_range', + label: '创建时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'process_order_no', label: '加工单号', minWidth: 160 }, + { prop: 'product_code', label: '商品编码', minWidth: 160 }, + { prop: 'quantity', label: '数量', width: 100 }, + { prop: 'process_name', label: '工序名称', minWidth: 140 }, + { prop: 'step_name', label: '步骤名称', minWidth: 140 }, + { prop: 'status_name', label: '步骤状态', minWidth: 120 }, + { prop: 'operator', label: '操作人员', minWidth: 120 }, + { + prop: 'scan_result', + label: '扫码结果', + width: 100, + formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'), + }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + { prop: 'created_name', label: '创建人', minWidth: 120 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'process_order_no', + label: '加工单号', + componentProps: { + placeholder: '请输入加工单号', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入加工单号' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'product_code', + label: '商品编码', + componentProps: { + placeholder: '请输入商品编码', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入商品编码' }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'quantity', + label: '数量', + defaultValue: 1, + componentProps: { + min: 1, + precision: 0, + }, + rules: [{ required: true, message: '请输入数量' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'user_code', + label: '用户编码', + componentProps: { + placeholder: '请输入用户编码', + maxlength: 50, + }, + rules: [{ required: true, message: '请输入用户编码' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'operator', + label: '操作人员', + componentProps: { + placeholder: '请输入操作人员', + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'process_name', + label: '工序名称', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_name', + label: '步骤名称', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_name', + label: '步骤状态', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'scan_result', + label: '扫码结果', + defaultValue: 1, + componentProps: { + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + rules: [{ required: true, message: '请选择扫码结果' }], + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: '备注', + componentProps: { + type: 'textarea', + rows: 3, + placeholder: '请输入备注', + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/index.vue new file mode 100644 index 0000000..ebd6782 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/schemas/index.tsx new file mode 100644 index 0000000..a3eeb0f --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/hardware-fault-description/schemas/index.tsx @@ -0,0 +1,135 @@ +import { CrudSchema } from "@/components/crud"; +import { ProduceHardwareFaultDescriptionService } from "@/apps/produce/api"; + +const enabledOptions = [ + { label: "启用", value: 1 }, + { label: "停用", value: 0 }, +]; + +export const crudSchema = (): CrudSchema => ({ + api: ProduceHardwareFaultDescriptionService.list, + dialogTitle: "硬件故障描述", + crudApi: { + add: ProduceHardwareFaultDescriptionService.create, + edit: ProduceHardwareFaultDescriptionService.update, + remove: ProduceHardwareFaultDescriptionService.remove, + view: ProduceHardwareFaultDescriptionService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: "produce:hardware-fault-description:create", + editAuth: "produce:hardware-fault-description:update", + removeAuth: "produce:hardware-fault-description:delete", + viewAuth: "produce:hardware-fault-description:read", + rowKey: "id", + searchFormSchema: { + labelWidth: "90px", + schema: [ + { + prop: "category_id", + label: "物料分类", + component: "ApiSelect", + componentProps: { + api: ProduceHardwareFaultDescriptionService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + }, + colSpan: 8, + }, + { + prop: "keyword", + label: "关键字", + component: "Input", + componentProps: { clearable: true, placeholder: "故障描述或备注" }, + colSpan: 8, + }, + { + prop: "is_enabled", + label: "启用状态", + component: "Select", + componentProps: { clearable: true, options: enabledOptions }, + colSpan: 8, + }, + ], + }, + columns: [ + { type: "selection" }, + { type: "index", label: "序号", width: 70 }, + { prop: "category_name", label: "物料分类", minWidth: 160 }, + { + prop: "description", + label: "硬件故障描述", + minWidth: 300, + showOverflowTooltip: true, + }, + { prop: "use_count", label: "使用次数", width: 100 }, + { + prop: "is_enabled", + label: "状态", + width: 90, + formatter: (row: { is_enabled?: number }) => + row.is_enabled === 0 ? "停用" : "启用", + }, + { prop: "remark", label: "备注", minWidth: 180, showOverflowTooltip: true }, + { prop: "created_date", label: "创建时间", minWidth: 180 }, + ], + dialogProps: { width: "720px" }, + formSchema: { + showDefaultActions: false, + labelWidth: "110px", + schema: [ + { + component: "Input", + prop: "id", + label: "ID", + show: false, + ifDetail: true, + colSpan: 24, + }, + { + component: "ApiSelect", + prop: "category_id", + label: "物料分类", + componentProps: { + api: ProduceHardwareFaultDescriptionService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + placeholder: "请选择物料分类", + }, + rules: [{ required: true, message: "请选择物料分类" }], + colSpan: 12, + }, + { + component: "RadioGroup", + prop: "is_enabled", + label: "是否启用", + defaultValue: 1, + componentProps: { options: enabledOptions }, + colSpan: 12, + }, + { + component: "Input", + prop: "description", + label: "故障描述", + componentProps: { + maxlength: 500, + placeholder: "请输入该物料分类的硬件故障描述", + }, + rules: [{ required: true, message: "请输入硬件故障描述" }], + colSpan: 24, + }, + { + component: "Input", + prop: "remark", + label: "备注", + componentProps: { type: "textarea", rows: 3, maxlength: 500 }, + colSpan: 24, + }, + ], + }, +}); diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/index.vue new file mode 100644 index 0000000..aef41a3 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx new file mode 100644 index 0000000..d2db915 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx @@ -0,0 +1,490 @@ +import { h } from "vue"; +import { CrudSchema } from "@/components/crud"; +import { ProduceRepairRecordService } from "@/apps/produce/api"; + +const formatPartSource = (source: unknown): string => { + const labels: Record = { + new: "新料", + old: "售后", + after_sale: "售后", + after_sales: "售后", + "after-sale": "售后", + repair: "返修", + refurbished: "返修", + }; + const value = String(source || ""); + return labels[value] || value || "-"; +}; + +const parseParts = (parts: Array> | string | null | undefined): Array> => { + if (Array.isArray(parts)) return parts; + if (typeof parts !== "string" || !parts) return []; + try { + const parsed = JSON.parse(parts); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +const formatPartValues = ( + parts: Array> | string | null | undefined, + getValue: (part: Record) => string, +): string => + parseParts(parts) + .map((part) => getValue(part)) + .filter(Boolean) + .join("\n"); + +const formatPartRows = ( + parts: Array> | string | null | undefined, + getValue: (part: Record) => string, + nowrap = true, +) => + h( + "div", + parseParts(parts).map((part) => + h( + "div", + { style: { lineHeight: "24px", minHeight: "24px", whiteSpace: nowrap ? "nowrap" : "normal" } }, + getValue(part) || "-", + ), + ), + ); + +const formatPartFaultDescriptions = (part: Record): string => + Array.isArray(part.fault_descriptions) + ? part.fault_descriptions + .map((fault: { description?: string }) => fault.description || "") + .filter(Boolean) + .join("、") + : ""; + +const formatCategoryNames = (row: { + category_names?: string[] | string | null; + category_name?: string | null; +}): string => { + const names = row.category_names; + if (Array.isArray(names)) return names.filter(Boolean).join("、"); + if (typeof names === "string" && names) { + try { + const parsed = JSON.parse(names); + if (Array.isArray(parsed)) return parsed.filter(Boolean).join("、"); + } catch { + return names; + } + return names; + } + return row.category_name || ""; +}; + +type ResponsibilitySnapshot = { + process_name?: string; + step_name?: string; + operator?: string; + user_code?: string; +}; + +const parseSnapshots = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): ResponsibilitySnapshot[] => { + if (Array.isArray(snapshots)) return snapshots; + if (typeof snapshots !== "string" || !snapshots) return []; + try { + const parsed = JSON.parse(snapshots); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +const formatRepairResult = (repairResult?: string | null): string => { + const labels: Record = { + material_defect: "物料不良", + operation_defect: "作业不良", + assembly_mismatch: "搭配错误", + }; + return labels[repairResult || ""] || repairResult || ""; +}; + +const formatOperationFaultSteps = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => [item.process_name, item.step_name].filter(Boolean).join(" · ")) + .filter(Boolean) + .join("\n"); + +const formatResponsibleOperators = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => { + const operator = item.operator || item.user_code || ""; + const process = [item.process_name, item.step_name].filter(Boolean).join(" · "); + return [operator, process].filter(Boolean).join("|"); + }) + .filter(Boolean) + .join("\n"); + +const formatResponsibleOperatorNames = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => { + const step = item.step_name || ""; + const operator = item.operator || item.user_code || ""; + return [step, operator].filter(Boolean).join("-"); + }) + .filter(Boolean) + .join("\n"); + +export const crudSchema = (): CrudSchema => ({ + api: ProduceRepairRecordService.list, + dialogTitle: "维修数据", + crudApi: { + add: ProduceRepairRecordService.create, + edit: ProduceRepairRecordService.update, + remove: ProduceRepairRecordService.remove, + view: ProduceRepairRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + hasExport: true, + exportFileName: "维修数据", + exportFields: [ + { field: "repair_no", title: "维修单号" }, + { field: "order_product", title: "订单号/商品编码" }, + { field: "exception_type_name", title: "异常分类" }, + { field: "exception_reason", title: "异常原因" }, + { field: "part_category", title: "物料分类" }, + { field: "part_code", title: "物料编码" }, + { field: "part_name", title: "更换备件" }, + { field: "part_fault", title: "故障描述" }, + { field: "part_quantity", title: "数量" }, + { field: "part_source", title: "来源" }, + { field: "repair_result_name", title: "维修判定分类" }, + { field: "responsible_operator_names", title: "责任人员" }, + { field: "operator", title: "维修人员" }, + { field: "repaired_date", title: "维修时间" }, + ], + addAuth: "produce:repair-record:create", + editAuth: "produce:repair-record:update", + removeAuth: "produce:repair-record:delete", + viewAuth: "produce:repair-record:read", + rowKey: "id", + searchFormSchema: { + labelWidth: "90px", + schema: [ + { + prop: "keyword", + label: "关键字", + component: "Input", + componentProps: { + clearable: true, + placeholder: "维修单、订单、商品或故障描述", + }, + colSpan: 8, + }, + { + prop: "category_id", + label: "物料分类", + component: "ApiSelect", + componentProps: { + api: ProduceRepairRecordService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + }, + colSpan: 8, + }, + { + prop: "fault_description_id", + label: "故障描述", + component: "ApiSelect", + componentProps: { + api: ProduceRepairRecordService.faultDescriptionOptions, + labelField: "description", + valueField: "id", + clearable: true, + }, + colSpan: 8, + }, + ], + }, + columns: [ + { type: "selection" }, + { type: "index", label: "序号", width: 70 }, + { prop: "repair_no", label: "维修单号", minWidth: 160 }, + { + prop: "order_no", + label: "订单号/商品编码", + minWidth: 180, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { order_no?: string | null; product_code?: string | null }) => + [row.order_no, row.product_code].filter(Boolean).join("/"), + }, + { prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } }, + { + prop: "part_category", + label: "物料分类", + minWidth: 140, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.category_name || ""), + }, + { + prop: "part_code", + label: "物料编码", + minWidth: 150, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.product_code || ""), + }, + { + prop: "part_name", + label: "更换备件", + minWidth: 240, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.product_name || "", true), + }, + { + prop: "part_fault", + label: "故障描述", + minWidth: 180, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, formatPartFaultDescriptions), + }, + { + prop: "part_quantity", + label: "数量", + width: 80, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => String(part.quantity ?? 1)), + }, + { + prop: "part_source", + label: "来源", + width: 90, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => formatPartSource(part.source)), + }, + { + prop: "repair_result", + label: "维修判定分类", + minWidth: 140, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { repair_result?: string | null }) => formatRepairResult(row.repair_result), + }, + { + prop: "responsible_operators", + label: "责任人员", + minWidth: 120, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => + formatResponsibleOperatorNames(row.responsible_operators), + }, + { prop: "operator", label: "维修人员", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "repaired_date", label: "维修时间", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } }, + ], + dialogProps: { width: "860px" }, + formSchema: { + showDefaultActions: false, + labelWidth: "105px", + schema: [ + { + component: "Input", + prop: "id", + label: "ID", + show: false, + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "repair_no", + label: "维修单号", + componentProps: { maxlength: 100, placeholder: "请输入维修单号" }, + rules: [{ required: true, message: "请输入维修单号" }], + colSpan: 12, + }, + { + component: "Input", + prop: "order_no", + label: "订单号", + componentProps: { maxlength: 100, placeholder: "请输入订单号" }, + colSpan: 12, + }, + { + component: "Input", + prop: "product_code", + label: "商品编码", + componentProps: { maxlength: 100, placeholder: "请输入商品编码" }, + colSpan: 12, + }, + { + component: "Input", + prop: "product_name", + label: "商品名称", + componentProps: { maxlength: 200, placeholder: "请输入商品名称" }, + colSpan: 12, + }, + { + component: "ApiSelect", + prop: "category_id", + label: "首选物料分类", + componentProps: { + api: ProduceRepairRecordService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + placeholder: "请选择首选物料分类", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "category_names", + label: "物料分类", + componentProps: (model: { category_names?: string[] | string | null; category_name?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatCategoryNames(model), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "ApiSelect", + prop: "fault_description_id", + label: "故障描述", + componentProps: { + api: ProduceRepairRecordService.faultDescriptionOptions, + labelField: "description", + valueField: "id", + clearable: true, + placeholder: "请选择硬件故障描述", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "exception_reason", + label: "异常原因", + componentProps: (model: { exception_reason?: string | null; exception_issue?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: [model.exception_reason, model.exception_issue].filter(Boolean).join(" · "), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "fault_descriptions", + label: "故障描述", + componentProps: (model: { fault_descriptions?: string[]; fault_description?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: (model.fault_descriptions || []).join("、") || model.fault_description || "", + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "PartsDetail", + prop: "parts", + label: "更换备件", + ifDetail: true, + colSpan: 24, + }, + { + component: "Select", + prop: "repair_result", + label: "维修判定", + componentProps: { + clearable: true, + options: [ + { label: "物料不良", value: "material_defect" }, + { label: "作业不良", value: "operation_defect" }, + { label: "搭配错误", value: "assembly_mismatch" }, + ], + }, + colSpan: 12, + }, + { + component: "Input", + prop: "repair_result_detail", + label: "维修判定", + componentProps: (model: { repair_result?: string | null }) => ({ + readonly: true, + value: formatRepairResult(model.repair_result), + }), + ifDetail: true, + colSpan: 12, + }, + { + component: "Input", + prop: "operation_fault_steps", + label: "责任步骤", + componentProps: (model: { operation_fault_steps?: ResponsibilitySnapshot[] | string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatOperationFaultSteps(model.operation_fault_steps), + }), + ifDetail: true, + colSpan: 12, + }, + { + component: "Input", + prop: "responsible_operators", + label: "责任人员", + componentProps: (model: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatResponsibleOperators(model.responsible_operators), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "Upload", + prop: "images", + label: "维修现场图片", + componentProps: { listType: "picture-card" }, + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "operator", + label: "维修人员", + componentProps: { maxlength: 100, placeholder: "请输入维修人员" }, + colSpan: 12, + }, + { + component: "DatePicker", + prop: "repaired_at", + label: "维修时间", + componentProps: { + type: "datetime", + valueFormat: "x", + placeholder: "请选择维修时间", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "remark", + label: "备注", + componentProps: { type: "textarea", rows: 3, maxlength: 1000 }, + colSpan: 24, + }, + ], + }, +}); diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/index.vue new file mode 100644 index 0000000..037165f --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/schemas/index.tsx new file mode 100644 index 0000000..831356e --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-setting/schemas/index.tsx @@ -0,0 +1,100 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceRepairSettingService } from '@/apps/produce/api' + +const settingTypeOptions = [ + { label: '处理类型', value: 'type' }, + { label: '维修原因', value: 'reason' }, + { label: '具体问题', value: 'issue' }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceRepairSettingService.list, + dialogTitle: '维修设置', + crudApi: { + add: ProduceRepairSettingService.create, + edit: ProduceRepairSettingService.update, + remove: ProduceRepairSettingService.remove, + view: ProduceRepairSettingService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:repair-setting:create', + editAuth: 'produce:repair-setting:update', + removeAuth: 'produce:repair-setting:delete', + viewAuth: 'produce:repair-setting:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', label: '关键字', component: 'Input', + componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8, + }, + { + prop: 'setting_type', label: '设置层级', component: 'Select', + componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8, + }, + { + prop: 'is_enabled', label: '启用状态', component: 'Select', + componentProps: { + clearable: true, placeholder: '请选择启用状态', + options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }], + }, colSpan: 8, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' }, + { prop: 'code', label: '编码', minWidth: 150 }, + { prop: 'name', label: '名称', minWidth: 160 }, + { prop: 'parent_name', label: '所属上级', minWidth: 140 }, + { prop: 'sort', label: '排序', width: 90 }, + { prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' }, + { prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true }, + { prop: 'created_name', label: '创建人', minWidth: 110 }, + ], + dialogProps: { width: '700px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Select', prop: 'setting_type', label: '设置层级', + componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions }, + rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12, + }, + { + component: 'ApiSelect', prop: 'parent_id', label: '所属上级', + componentProps: { + api: '/produce/repair-setting', requestMethod: 'get', + params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级', + }, colSpan: 12, + }, + { + component: 'Input', prop: 'code', label: '设置编码', + componentProps: { placeholder: '请输入唯一编码', maxlength: 64 }, + rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'name', label: '设置名称', + componentProps: { placeholder: '请输入显示名称', maxlength: 100 }, + rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12, + }, + { component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 }, + { + component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1, + componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12, + }, + { + component: 'Input', prop: 'remark', label: '备注', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24, + }, + ], + }, +}) diff --git a/backend/plugin/produce/resource/template/admin/views/produce/scan-data/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/scan-data/index.vue new file mode 100644 index 0000000..082d7b1 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/scan-data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/scan-data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/scan-data/schemas/index.tsx new file mode 100644 index 0000000..045b17b --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/scan-data/schemas/index.tsx @@ -0,0 +1,254 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceScanDataService } from '@/apps/produce/api' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceScanDataService.list, + dialogTitle: '扫码数据', + crudApi: { + add: ProduceScanDataService.create, + edit: ProduceScanDataService.update, + remove: ProduceScanDataService.remove, + view: ProduceScanDataService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:scan-data:create', + editAuth: 'produce:scan-data:update', + removeAuth: 'produce:scan-data:delete', + viewAuth: 'produce:scan-data:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入订单号/用户编码/操作人员/备注', + }, + colSpan: 6, + }, + { + prop: 'process_id', + label: '工序名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: '步骤名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + }, + colSpan: 6, + }, + { + prop: 'status_id', + label: '步骤状态', + component: 'ApiSelect', + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + }, + colSpan: 6, + }, + { + prop: 'LIKE_operator', + label: '操作人员', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入操作人员', + }, + colSpan: 6, + }, + { + prop: 'scan_result', + label: '扫码结果', + component: 'Select', + componentProps: { + clearable: true, + placeholder: '请选择扫码结果', + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + colSpan: 6, + }, + { + prop: 'scan_time_range', + label: '扫码时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'order_no', label: '订单号', minWidth: 160 }, + { prop: 'quantity', label: '数量', width: 90 }, + { prop: 'user_code', label: '用户编码', minWidth: 120 }, + { prop: 'process_name', label: '工序名称', minWidth: 140 }, + { prop: 'step_name', label: '步骤名称', minWidth: 140 }, + { prop: 'status_name', label: '步骤状态', minWidth: 120 }, + { prop: 'operator', label: '操作人员', minWidth: 120 }, + { + prop: 'scan_result', + label: '扫码结果', + width: 100, + formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'), + }, + { prop: 'scan_date', label: '扫码时间', minWidth: 180 }, + { prop: 'created_name', label: '创建人', minWidth: 120 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'order_no', + label: '订单号', + componentProps: { + placeholder: '请输入订单号', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入订单号' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'user_code', + label: '用户编码', + componentProps: { + placeholder: '请输入用户编码', + maxlength: 50, + }, + rules: [{ required: true, message: '请输入用户编码' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'operator', + label: '操作人员', + componentProps: { + placeholder: '请输入操作人员', + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'process_id', + label: '工序名称', + componentProps: (values: any) => ({ + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择工序' }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: '步骤名称', + componentProps: (values: any) => ({ + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择步骤' }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'status_id', + label: '步骤状态', + componentProps: (values: any) => ({ + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择状态' }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'scan_result', + label: '扫码结果', + defaultValue: 1, + componentProps: { + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + rules: [{ required: true, message: '请选择扫码结果' }], + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: '备注', + componentProps: { + type: 'textarea', + rows: 3, + placeholder: '请输入备注', + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/statistics/daily/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/statistics/daily/index.vue new file mode 100644 index 0000000..405009a --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/statistics/daily/index.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/statistics/monthly/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/statistics/monthly/index.vue new file mode 100644 index 0000000..8bc0a3d --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/statistics/monthly/index.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/status-transition/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/status-transition/index.vue new file mode 100644 index 0000000..3d3743c --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/status-transition/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/status-transition/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/status-transition/schemas/index.tsx new file mode 100644 index 0000000..8c386f9 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/status-transition/schemas/index.tsx @@ -0,0 +1,290 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStatusTransitionService } from '../../../../api/index' + +const triggerEventOptions = [ + { label: '下一工序扫码触发', value: 'next_step_scan' }, + { label: '扫描触发', value: 'scan_trigger' }, + { label: '异常触发', value: 'exception_trigger' }, + { label: '手动触发', value: 'manual_trigger' }, + { label: '定时触发', value: 'timer_trigger' }, + { label: '完成触发', value: 'complete_trigger' }, +] + +const transitionTypeOptions = [ + { label: '主转换', value: 'primary' }, + { label: '副作用', value: 'side_effect' }, +] + +const sideEffectTypeOptions = [ + { label: '记录日志', value: 'log' }, + { label: '发送通知', value: 'notify' }, + { label: '更新字段', value: 'update_field' }, + { label: '触发事件', value: 'trigger_event' }, +] + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStatusTransitionService.list, + dialogTitle: $t('produce.modules.status_transition.title'), + crudApi: { + add: ProduceStatusTransitionService.create, + edit: ProduceStatusTransitionService.update, + remove: ProduceStatusTransitionService.remove, + view: ProduceStatusTransitionService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:status-transition:create', + editAuth: 'produce:status-transition:update', + removeAuth: 'produce:status-transition:delete', + viewAuth: 'produce:status-transition:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'step_id', + label: $t('produce.modules.status_transition.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'trigger_event', + label: $t('produce.modules.status_transition.search.trigger_event'), + component: 'Select', + componentProps: { + options: triggerEventOptions, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.trigger_event_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.status_transition.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'step.step_name', label: $t('produce.modules.status_transition.columns.step_name'), minWidth: 160 }, + { prop: 'from_status.status_name', label: $t('produce.modules.status_transition.columns.from_status_name'), minWidth: 140 }, + { prop: 'to_status.status_name', label: $t('produce.modules.status_transition.columns.to_status_name'), minWidth: 140 }, + { prop: 'trigger_event', label: $t('produce.modules.status_transition.columns.trigger_event'), minWidth: 160 }, + { prop: 'transition_type', label: $t('produce.modules.status_transition.columns.transition_type'), width: 120 }, + { + prop: 'auto_trigger', + label: $t('produce.modules.status_transition.columns.auto_trigger'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'priority', label: $t('produce.modules.status_transition.columns.priority'), width: 100 }, + { prop: 'sort', label: $t('produce.modules.status_transition.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.status_transition.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.status_transition.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '860px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '120px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: $t('produce.modules.status_transition.form.step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'from_status_id', + label: $t('produce.modules.status_transition.form.from_status_id'), + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.from_status_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.from_status_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'to_status_id', + label: $t('produce.modules.status_transition.form.to_status_id'), + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.to_status_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.to_status_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'trigger_event', + label: $t('produce.modules.status_transition.columns.trigger_event'), + componentProps: { + options: triggerEventOptions, + placeholder: $t('produce.modules.status_transition.form.trigger_event_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.trigger_event_placeholder') }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'transition_type', + label: $t('produce.modules.status_transition.columns.transition_type'), + defaultValue: 'primary', + componentProps: { + options: transitionTypeOptions, + isBtn: true, + renderType: 'RadioButton', + }, + colSpan: 24, + }, + { + component: 'Select', + prop: 'side_effect_type', + label: $t('produce.modules.status_transition.form.side_effect_type'), + componentProps: { + options: sideEffectTypeOptions, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.side_effect_type_placeholder'), + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'auto_trigger', + label: $t('produce.modules.status_transition.columns.auto_trigger'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'priority', + label: $t('produce.modules.status_transition.columns.priority'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.status_transition.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'event_config', + label: $t('produce.modules.status_transition.form.event_config'), + componentProps: { + type: 'textarea', + rows: 5, + placeholder: $t('produce.modules.status_transition.form.event_config_placeholder'), + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'side_effect_config', + label: $t('produce.modules.status_transition.form.side_effect_config'), + componentProps: { + type: 'textarea', + rows: 5, + placeholder: $t('produce.modules.status_transition.form.side_effect_config_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.status_transition.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.status_transition.form.description'), + componentProps: { + type: 'textarea', + rows: 3, + placeholder: $t('produce.modules.status_transition.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/step-relation/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/step-relation/index.vue new file mode 100644 index 0000000..44e01f0 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/step-relation/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/step-relation/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/step-relation/schemas/index.tsx new file mode 100644 index 0000000..c0c8a1e --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/step-relation/schemas/index.tsx @@ -0,0 +1,218 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStepRelationService } from '../../../../api/index' + +const relationTypeOptions = [ + { label: '正常流转', value: 'normal' }, + { label: '异常流转', value: 'exception' }, +] + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStepRelationService.list, + dialogTitle: $t('produce.modules.step_relation.title'), + crudApi: { + add: ProduceStepRelationService.create, + edit: ProduceStepRelationService.update, + remove: ProduceStepRelationService.remove, + view: ProduceStepRelationService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:step-relation:create', + editAuth: 'produce:step-relation:update', + removeAuth: 'produce:step-relation:delete', + viewAuth: 'produce:step-relation:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'from_step_id', + label: $t('produce.modules.step_relation.search.from_step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.from_step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'to_step_id', + label: $t('produce.modules.step_relation.search.to_step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.to_step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'relation_type', + label: $t('produce.modules.step_relation.search.relation_type'), + component: 'Select', + componentProps: { + options: relationTypeOptions, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.relation_type_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.step_relation.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'from_step.step_name', label: $t('produce.modules.step_relation.columns.from_step_name'), minWidth: 180 }, + { prop: 'to_step.step_name', label: $t('produce.modules.step_relation.columns.to_step_name'), minWidth: 180 }, + { prop: 'relation_type', label: $t('produce.modules.step_relation.columns.relation_type'), width: 120 }, + { prop: 'priority', label: $t('produce.modules.step_relation.columns.priority'), width: 100 }, + { prop: 'flow_condition', label: $t('produce.modules.step_relation.columns.flow_condition'), minWidth: 220 }, + { prop: 'sort', label: $t('produce.modules.step_relation.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.step_relation.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.step_relation.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'from_step_id', + label: $t('produce.modules.step_relation.form.from_step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.form.from_step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_relation.form.from_step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'to_step_id', + label: $t('produce.modules.step_relation.form.to_step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.form.to_step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_relation.form.to_step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'relation_type', + label: $t('produce.modules.step_relation.columns.relation_type'), + defaultValue: 'normal', + componentProps: { + options: relationTypeOptions, + isBtn: true, + renderType: 'RadioButton', + }, + colSpan: 24, + }, + { + component: 'InputNumber', + prop: 'priority', + label: $t('produce.modules.step_relation.columns.priority'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.step_relation.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'flow_condition', + label: $t('produce.modules.step_relation.form.flow_condition'), + componentProps: { + type: 'textarea', + rows: 6, + placeholder: $t('produce.modules.step_relation.form.flow_condition_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.step_relation.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.step_relation.form.description'), + componentProps: { + type: 'textarea', + rows: 3, + placeholder: $t('produce.modules.step_relation.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/step-status/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/step-status/index.vue new file mode 100644 index 0000000..19cb79c --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/step-status/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/step-status/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/step-status/schemas/index.tsx new file mode 100644 index 0000000..0832468 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/step-status/schemas/index.tsx @@ -0,0 +1,227 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStepStatusService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStepStatusService.list, + dialogTitle: $t('produce.modules.step_status.title'), + crudApi: { + add: ProduceStepStatusService.create, + edit: ProduceStepStatusService.update, + remove: ProduceStepStatusService.remove, + view: ProduceStepStatusService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:step-status:create', + editAuth: 'produce:step-status:update', + removeAuth: 'produce:step-status:delete', + viewAuth: 'produce:step-status:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'step_id', + label: $t('produce.modules.step_status.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_status.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_status_code', + label: $t('produce.modules.step_status.search.status_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_status_name', + label: $t('produce.modules.step_status.search.status_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.step_status.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { + prop: 'step.step_name', + label: $t('produce.modules.step_status.columns.step_name'), + minWidth: 180, + }, + { prop: 'status_code', label: $t('produce.modules.step_status.columns.status_code'), minWidth: 160 }, + { prop: 'status_name', label: $t('produce.modules.step_status.columns.status_name'), minWidth: 180 }, + { + prop: 'is_initial', + label: $t('produce.modules.step_status.columns.is_initial'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { + prop: 'is_final', + label: $t('produce.modules.step_status.columns.is_final'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'sort', label: $t('produce.modules.step_status.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.step_status.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.step_status.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '720px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: $t('produce.modules.step_status.form.step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_status.form.step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_code', + label: $t('produce.modules.step_status.columns.status_code'), + componentProps: { + placeholder: $t('produce.modules.step_status.form.status_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.status_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_name', + label: $t('produce.modules.step_status.columns.status_name'), + componentProps: { + placeholder: $t('produce.modules.step_status.form.status_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.status_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.step_status.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_initial', + label: $t('produce.modules.step_status.columns.is_initial'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_final', + label: $t('produce.modules.step_status.columns.is_final'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.step_status.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.step_status.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.step_status.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/admin/views/produce/transition-log/index.vue b/backend/plugin/produce/resource/template/admin/views/produce/transition-log/index.vue new file mode 100644 index 0000000..1a84668 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/transition-log/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/admin/views/produce/transition-log/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/transition-log/schemas/index.tsx new file mode 100644 index 0000000..1ec65b3 --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/transition-log/schemas/index.tsx @@ -0,0 +1,277 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceTransitionLogService } from '../../../../api/index' + +const triggerEventOptions = [ + { label: '下一工序扫码触发', value: 'next_step_scan' }, + { label: '扫描触发', value: 'scan_trigger' }, + { label: '异常触发', value: 'exception_trigger' }, + { label: '手动触发', value: 'manual_trigger' }, + { label: '定时触发', value: 'timer_trigger' }, + { label: '完成触发', value: 'complete_trigger' }, +] + +const formatJsonText = (value: unknown) => { + if (value === null || value === undefined || value === '') { + return '' + } + if (typeof value === 'string') { + return value + } + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } +} + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceTransitionLogService.list, + dialogTitle: $t('produce.modules.transition_log.title'), + crudApi: { + view: async (id: string | number) => { + const res = await ProduceTransitionLogService.get(id) + return { + ...res, + trigger_data: formatJsonText(res?.trigger_data), + } + }, + }, + useCrud: true, + hasAdd: false, + hasEdit: false, + hasRemove: false, + hasView: true, + viewAuth: 'produce:transition-log:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_work_order_no', + label: $t('produce.modules.transition_log.search.work_order_no'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.transition_log.search.work_order_no_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: $t('produce.modules.transition_log.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'trigger_event', + label: $t('produce.modules.transition_log.search.trigger_event'), + component: 'Select', + componentProps: { + options: triggerEventOptions, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.trigger_event_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_trigger_user', + label: $t('produce.modules.transition_log.search.trigger_user'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.transition_log.search.trigger_user_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'is_success', + label: $t('produce.modules.transition_log.search.is_success'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.is_success_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'work_order_no', label: $t('produce.modules.transition_log.columns.work_order_no'), minWidth: 140 }, + { prop: 'step.step_name', label: $t('produce.modules.transition_log.columns.step_name'), minWidth: 160 }, + { prop: 'from_status.status_name', label: $t('produce.modules.transition_log.columns.from_status_name'), minWidth: 140 }, + { prop: 'to_status.status_name', label: $t('produce.modules.transition_log.columns.to_status_name'), minWidth: 140 }, + { + prop: 'trigger_event', + label: $t('produce.modules.transition_log.columns.trigger_event'), + minWidth: 160, + formatter: (row: { trigger_event?: string }) => + triggerEventOptions.find((option) => option.value === row.trigger_event)?.label || row.trigger_event || '' + }, + { prop: 'trigger_user', label: $t('produce.modules.transition_log.columns.trigger_user'), minWidth: 120 }, + { + prop: 'is_auto', + label: $t('produce.modules.transition_log.columns.is_auto'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { + prop: 'is_success', + label: $t('produce.modules.transition_log.columns.is_success'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'duration', label: $t('produce.modules.transition_log.columns.duration'), width: 120 }, + { prop: 'error_message', label: $t('produce.modules.transition_log.columns.error_message'), minWidth: 200 }, + { prop: 'created_date', label: $t('produce.modules.transition_log.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '960px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '120px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 12, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'work_order_no', + label: $t('produce.modules.transition_log.columns.work_order_no'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'step.step_name', + label: $t('produce.modules.transition_log.columns.step_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'transition.trigger_event', + label: $t('produce.modules.transition_log.columns.transition_event'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'from_status.status_name', + label: $t('produce.modules.transition_log.columns.from_status_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'to_status.status_name', + label: $t('produce.modules.transition_log.columns.to_status_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_event', + label: $t('produce.modules.transition_log.columns.trigger_event'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_user', + label: $t('produce.modules.transition_log.columns.trigger_user'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_ip', + label: $t('produce.modules.transition_log.columns.trigger_ip'), + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_auto', + label: $t('produce.modules.transition_log.columns.is_auto'), + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_success', + label: $t('produce.modules.transition_log.columns.is_success'), + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'duration', + label: $t('produce.modules.transition_log.columns.duration'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'created_name', + label: $t('produce.modules.transition_log.columns.created_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'created_date', + label: $t('produce.modules.transition_log.columns.created_at'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_data', + label: $t('produce.modules.transition_log.columns.trigger_data'), + componentProps: { + type: 'textarea', + rows: 6, + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'error_message', + label: $t('produce.modules.transition_log.columns.error_message'), + componentProps: { + type: 'textarea', + rows: 4, + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: $t('produce.modules.transition_log.columns.remark'), + componentProps: { + type: 'textarea', + rows: 4, + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/backend/plugin/produce/resource/template/web/.gitkeep b/backend/plugin/produce/resource/template/web/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/plugin/produce/resource/template/web/api/data-entry.ts b/backend/plugin/produce/resource/template/web/api/data-entry.ts new file mode 100644 index 0000000..7796e94 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/api/data-entry.ts @@ -0,0 +1,167 @@ +import request from '~/utils/request' + +export interface ProduceOption { + label: string + value: string | number +} + +export interface ProduceRecordItem { + id?: string | number + order_no?: string + process_order_no?: string + product_code?: string + quantity?: number + user_code?: string + operator?: string + scan_result?: number + scan_date?: string + created_date?: string + process?: { + process_name?: string + } + step?: { + step_name?: string + } + status?: { + status_name?: string + } +} + +export interface ProduceSubmitScanDataResult { + code: number + msg: string + data?: { + id?: string | number + } +} + +export interface ProduceStatistics { + today_total: number + success_total: number + failed_total: number +} + +export interface ProduceUserStatsItem { + user_code: string + operator?: string + count: number +} + +export interface ProduceMemberUserCheckResult { + id: number + username: string + nickname: string +} + +export interface ProduceDataEntryStepRule { + id?: string | number + process_id?: string | number + process_name?: string + step_code?: string + step_name?: string + prerequisite_steps?: Array + prerequisite_mode?: string + prerequisite_require_final?: number + allow_repeat_scan?: number + repeat_scan_statuses?: Array +} + +export interface ProduceDataEntrySpecificRule { + id?: string | number + rule_name?: string + rule_pattern?: string + rule_type?: number + apply_mode?: 'order' | 'machine' | 'both' + match_field?: 'order_no' | 'process_order_no' | 'product_code' | 'user_code' + step_ids?: Array + sort?: number + remark?: string +} + +export interface ProduceDataEntryConfig { + basic_setting: Record + step_rule: ProduceDataEntryStepRule | null + specific_rules: ProduceDataEntrySpecificRule[] + current?: { + process_id?: string | number + step_id?: string | number + order_no?: string + apply_mode?: 'order' | 'machine' | 'both' + } +} + +export function getProcessOptions() { + return request.get('/produce/process', { format: 'select' }) +} + +export function getStepOptions(processId?: string | number) { + return request.get('/produce/process-step', { + format: 'select', + ...(processId ? { process_id: processId } : {}), + }) +} + +export function getStatusOptions(stepId?: string | number) { + return request.get('/produce/step-status', { + format: 'select', + ...(stepId ? { step_id: stepId } : {}), + }) +} + +export function getProduceConfig(groupCode: string) { + return request.get>('/produce/config/info', { group_code: groupCode }) +} + +export function getDataEntryConfig(params: Record) { + return request.get('/produce/config/data-entry', params) +} + +export function getRecentRecords(params: Record) { + return request.get<{ items: ProduceRecordItem[]; total: number }>('/produce/scan-data', { + limit: 10, + page: 1, + ...params, + }) +} + +export function getProductRecentRecords(params: Record) { + return request.get<{ items: ProduceRecordItem[]; total: number }>('/produce/product-record/recent', { + limit: 10, + page: 1, + ...params, + }) +} + +export function getStatistics(params: Record) { + return request.get('/produce/scan-data/statistics', params, { showErrorMessage: false }) +} + +export function getProductStatistics(params: Record) { + return request.get('/produce/product-record/statistics', params, { showErrorMessage: false }) +} + +export function getUserCodeStats(params: Record) { + return request.get('/produce/scan-data/user-code-stats', params, { showErrorMessage: false }) +} + +export function getProductUserCodeStats(params: Record) { + return request.get('/produce/product-record/user-code-stats', params, { showErrorMessage: false }) +} + +export function checkMemberUser(userCode: string) { + return request.get('/produce/member-user/check', { user_code: userCode }, { showErrorMessage: false }) +} + +export function submitScanData(data: Record) { + return request.post('/produce/scan-data/record', data, { + showErrorMessage: false, + returnFullResponse: true, + }) +} + +export function submitProductRecord(data: Record) { + return request.post('/produce/product-record/record', data, { + showErrorMessage: false, + returnFullResponse: true, + }) +} diff --git a/backend/plugin/produce/resource/template/web/api/exception.ts b/backend/plugin/produce/resource/template/web/api/exception.ts new file mode 100644 index 0000000..292cb05 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/api/exception.ts @@ -0,0 +1,143 @@ +import request from '~/utils/request' + +export interface ProduceExceptionSetting { + id: string | number + parent_id?: string | number | null + setting_type: 'type' | 'reason' | 'issue' | string + code?: string + name: string + sort?: number + frequency?: number + remark?: string +} + +export interface ProduceExceptionRecord { + id: string | number + order_no?: string + user_code?: string + reporter?: string + type_id?: string | number + type_name?: string + reason_id?: string | number + reason_name?: string + issue_id?: string | number | null + issue_name?: string + images?: string[] + remark?: string + status?: number + status_name?: string + reported_date?: string + handled_date?: string + handler?: string +} + +export interface ProduceRepairTask { + id: string | number + device_no: string + type_name?: string + reason_name?: string + issue_name?: string + reporter?: string + user_code?: string + reported_date?: string + status?: number + status_name?: string +} + +export interface ProduceRepairPart { + product_code: string + product_name?: string + category_id?: string | number + category_name?: string + source?: 'new' | 'after_sales' | 'refurbished' | string + quantity?: number + fault_description_ids?: Array +} + +export interface ProduceScanRecord { + id: string | number + process_name?: string + step_name?: string + operator?: string + user_code?: string +} + +export function getExceptionSettings() { + return request.get('/produce/exception/settings', {}, { showErrorMessage: false }) +} + +export function getExceptionStatistics(params: Record = {}) { + return request.get<{ pending: number; review: number; done: number }>('/produce/exception/statistics', params, { showErrorMessage: false }) +} + +export function getRecentExceptions(params: Record = {}) { + return request.get('/produce/exception/recent', params, { showErrorMessage: false }) +} + +export function checkExceptionOrder(orderNo: string) { + return request.get>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false }) +} + +export function submitExceptionRecord(data: Record) { + return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, { + showErrorMessage: false, + returnFullResponse: true, + }) +} + +export function getRepairTasks(params: Record = {}) { + return request.get('/produce/repair-record/tasks', params, { showErrorMessage: false }) +} + +export function getRepairStatistics(params: Record = {}) { + return request.get<{ pending: number; processing: number; completed_today: number }>('/produce/repair-record/statistics', params, { showErrorMessage: false }) +} + +export function getRepairTask(deviceNo: string) { + return request.get('/produce/exception/repair-task', { device_no: deviceNo.trim() }, { showErrorMessage: false }) +} + +export function getScanRecords(params: Record = {}) { + return request.get<{ items: ProduceScanRecord[]; total: number }>('/produce/scan-data', { + limit: 50, + page: 1, + ...params, + }, { showErrorMessage: false }) +} + +export function getMaterialCategories() { + return request.get>('/produce/scanapp/material-category', {}, { showErrorMessage: false }) +} + +export function getFaultDescriptionOptions(categoryIds: Array) { + return request.get>( + '/produce/hardware-fault-description/options', + { category_ids: categoryIds.join(',') }, + { showErrorMessage: false }, + ) +} + +export function lookupMaterial(productCode: string) { + return request.get<{ + id: string | number + category_id: string | number + category_name: string + product_code: string + product_name: string + }>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false }) +} + +export function submitRepairRecord(data: Record) { + return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, { + showErrorMessage: false, + returnFullResponse: true, + }) +} + +export async function uploadProduceImage(file: File, subDir: 'exception' | 'repair') { + const formData = new FormData() + formData.append('file', file) + formData.append('sub_dir', subDir) + const result = await request.upload>('/file/image', formData) + return result?.url || result?.full_url || result?.path || '' +} diff --git a/backend/plugin/produce/resource/template/web/api/query.ts b/backend/plugin/produce/resource/template/web/api/query.ts new file mode 100644 index 0000000..58cb956 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/api/query.ts @@ -0,0 +1,54 @@ +import request from '~/utils/request' + +export type ProduceQueryRecordType = 'scan' | 'product' | 'exception' | 'repair' + +export interface ProduceQueryRecord { + id: string + record_type: ProduceQueryRecordType + record_type_name: string + order_no?: string + product_code?: string + process_name?: string + exception_reason?: string + exception_content?: string + material_category?: string + fault_description?: string + quantity?: number | null + event_at?: number + event_date?: string | null + remark?: string +} + +export interface ProduceQueryPage { + items: ProduceQueryRecord[] + total: number + page: number + limit: number +} + +export interface ProduceQueryParams { + record_type: ProduceQueryRecordType + order_no?: string + keyword?: string + user_code?: string + status?: string | number + scan_result?: string | number + start_date?: string + end_date?: string + page?: number + limit?: number +} + +export interface ProduceQueryTimeline { + order_no: string + total: number + items: ProduceQueryRecord[] +} + +export function queryProduceRecords(params: ProduceQueryParams) { + return request.get('/produce/query/records', params, { showErrorMessage: false }) +} + +export function queryProduceTimeline(orderNo: string) { + return request.get('/produce/query/timeline', { order_no: orderNo.trim() }, { showErrorMessage: false }) +} diff --git a/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue b/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue new file mode 100644 index 0000000..a436d8d --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/data-entry/components/OrderEntryPanel.vue b/backend/plugin/produce/resource/template/web/pages/data-entry/components/OrderEntryPanel.vue new file mode 100644 index 0000000..3c29066 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/data-entry/components/OrderEntryPanel.vue @@ -0,0 +1,610 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/data-entry/components/entry-panel.css b/backend/plugin/produce/resource/template/web/pages/data-entry/components/entry-panel.css new file mode 100644 index 0000000..ecdb41f --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/data-entry/components/entry-panel.css @@ -0,0 +1,340 @@ +.main-top-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(340px, 1fr); + gap: 20px; + align-items: stretch; +} + +.workbench-panel, +.records-panel { + background: #ffffff; + border-radius: 20px; + border: 1px solid #e5e7eb; + padding: 28px; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 24px; +} + +.column-header { + padding-bottom: 18px; + border-bottom: 1px solid #eef2f7; + margin-bottom: 22px; +} + +.panel-metric-card { + min-width: 240px; + min-height: 56px; + padding: 12px 16px; + border-radius: 16px; + background: #f8fbff; + border: 1px solid #e3ebf7; + display: flex; + align-items: center; + gap: 12px; +} + +.panel-metric-label { + font-size: 13px; + color: #64748b; +} + +.panel-metric-value { + font-size: 28px; + line-height: 1; + font-weight: 700; + color: #2563eb; + margin-left: auto; +} + +.metric-eye-button { + width: 32px; + height: 32px; + border: 0; + border-radius: 999px; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + color: #94a3b8; + cursor: pointer; + transition: all 0.2s ease; +} + +.metric-eye-button svg { + width: 18px; + height: 18px; +} + +.metric-eye-button.is-hidden { + color: #c0cad8; +} + +.metric-eye-button:hover { + background: rgba(37, 99, 235, 0.08); + color: #2563eb; +} + +.panel-title { + font-size: 18px; + font-weight: 600; + color: #2563eb; +} + +.panel-subtitle { + margin-top: 8px; + font-size: 14px; + color: #6b7280; +} + +.workbench-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.field-row { + display: grid; + grid-template-columns: 1fr; + gap: 16px; +} + +.field-row-split { + grid-template-columns: minmax(0, 1fr) 180px; +} + +.field-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.field-group-quantity { + min-width: 0; +} + +.field-group label { + font-size: 14px; + font-weight: 600; + color: #334155; +} + +.field-error-tip { + font-size: 12px; + color: #ef4444; + margin-top: 6px; +} + +.field-primary :deep(.el-input__wrapper), +.field-row :deep(.el-input__wrapper), +.field-row :deep(.el-input-number .el-input__wrapper) { + min-height: 58px; + border-radius: 14px; + box-shadow: 0 0 0 1px #dbe4f0 inset; +} + +.field-primary :deep(.el-input__inner), +.field-row :deep(.el-input__inner), +.field-row :deep(.el-input-number .el-input__inner) { + font-size: 18px; +} + +.field-primary :deep(.el-input__wrapper.is-focus), +.field-row :deep(.el-input__wrapper.is-focus), +.field-row :deep(.el-input-number .el-input__wrapper.is-focus) { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +.quantity-input { + width: 100%; +} + +.quantity-input :deep(.el-input__wrapper) { + padding-right: 40px; + overflow: hidden; +} + +.quantity-input :deep(.el-input-number__increase), +.quantity-input :deep(.el-input-number__decrease) { + right: 1px; + width: 36px; + margin: 0; + background: #f8fafc; + color: #94a3b8; + border-left: 1px solid #dbe4f0; + box-shadow: none; +} + +.quantity-input :deep(.el-input-number__increase) { + top: 1px; + height: calc(50% - 0.5px); + border-top-right-radius: 13px; + border-bottom: 1px solid #e5edf6; +} + +.quantity-input :deep(.el-input-number__decrease) { + bottom: 1px; + height: calc(50% - 0.5px); + border-bottom-right-radius: 13px; +} + +.quantity-input :deep(.el-input-number__increase:hover), +.quantity-input :deep(.el-input-number__decrease:hover) { + color: var(--el-color-primary); + background: #f1f5f9; +} + +.field-label-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.lock-switch { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 13px; + color: #64748b; +} + +.action-row { + display: flex; + gap: 12px; +} + +.action-row :deep(.el-button) { + min-width: 160px; + border-radius: 12px; +} + +.dashboard-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.dashboard-title { + font-size: 18px; + font-weight: 600; + color: #111827; +} + +.dashboard-caption { + font-size: 13px; + color: #94a3b8; +} + +.station-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.station-card { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: 14px; + background: #ffffff; + border: 1px solid #e6edf6; +} + +.station-icon { + width: 34px; + height: 34px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #e8f1ff; + color: #3b82f6; + font-size: 13px; + font-weight: 700; + flex-shrink: 0; +} + +.station-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.station-info strong { + font-size: 16px; + color: #1f2937; +} + +.station-card b { + margin-left: auto; + font-size: 32px; + line-height: 1; + color: #2563eb; +} + +.records-header { + margin-bottom: 20px; +} + +.records-panel :deep(.el-table) { + --el-table-border-color: #eef2f7; + --el-table-header-bg-color: #f8fafc; +} + +.records-panel :deep(.el-table th.el-table__cell) { + font-weight: 600; + color: #475569; +} + +.records-panel :deep(.el-table td.el-table__cell) { + padding-top: 14px; + padding-bottom: 14px; +} + +@media (max-width: 1024px) { + .panel-header, + .field-label-row, + .dashboard-header { + flex-direction: column; + align-items: flex-start; + } + + .panel-metric-card { + width: 100%; + min-width: 0; + } + + .main-top-grid, + .station-grid { + grid-template-columns: 1fr; + } + + .field-row-split { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .workbench-panel, + .records-panel { + padding: 18px; + border-radius: 16px; + } + + .action-row { + flex-wrap: wrap; + } + + .action-row :deep(.el-button) { + width: 100%; + } +} diff --git a/backend/plugin/produce/resource/template/web/pages/data-entry/index.vue b/backend/plugin/produce/resource/template/web/pages/data-entry/index.vue new file mode 100644 index 0000000..fd4c107 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/data-entry/index.vue @@ -0,0 +1,1026 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/exception/index.vue b/backend/plugin/produce/resource/template/web/pages/exception/index.vue new file mode 100644 index 0000000..4aa024a --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/exception/index.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/query/index.vue b/backend/plugin/produce/resource/template/web/pages/query/index.vue new file mode 100644 index 0000000..cbf483c --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/query/index.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/repair/index.vue b/backend/plugin/produce/resource/template/web/pages/repair/index.vue new file mode 100644 index 0000000..27192a0 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/repair/index.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/repair/tasks.vue b/backend/plugin/produce/resource/template/web/pages/repair/tasks.vue new file mode 100644 index 0000000..a6a201a --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/repair/tasks.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/backend/plugin/produce/resource/template/web/pages/routes.ts b/backend/plugin/produce/resource/template/web/pages/routes.ts new file mode 100644 index 0000000..db00be5 --- /dev/null +++ b/backend/plugin/produce/resource/template/web/pages/routes.ts @@ -0,0 +1,42 @@ +export default [ + { + path: '/produce/data-entry', + component: () => import('~/apps/produce/pages/data-entry/index.vue'), + meta: { + title: '数据录入', + layout: 'default', + }, + }, + { + path: '/produce/exception', + component: () => import('~/apps/produce/pages/exception/index.vue'), + meta: { + title: '异常处理', + layout: 'default', + }, + }, + { + path: '/produce/repair', + component: () => import('~/apps/produce/pages/repair/index.vue'), + meta: { + title: '维修处理', + layout: 'default', + }, + }, + { + path: '/produce/repair/tasks', + component: () => import('~/apps/produce/pages/repair/tasks.vue'), + meta: { + title: '待维修订单', + layout: 'default', + }, + }, + { + path: '/produce/query', + component: () => import('~/apps/produce/pages/query/index.vue'), + meta: { + title: '综合查询', + layout: 'default', + }, + }, +] diff --git a/frontend/admin/.gitignore b/frontend/admin/.gitignore index 8ef10a2..430c718 100644 --- a/frontend/admin/.gitignore +++ b/frontend/admin/.gitignore @@ -8,7 +8,6 @@ dist-ssr # 忽略 src/apps 目录下除了 demo 之外的内容 -src/apps/* !src/apps/demo/ diff --git a/frontend/admin/src/apps/article - 副本/.gitkeep b/frontend/admin/src/apps/article - 副本/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/admin/src/apps/article - 副本/api/index.ts b/frontend/admin/src/apps/article - 副本/api/index.ts new file mode 100644 index 0000000..0dc5ccf --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/api/index.ts @@ -0,0 +1,88 @@ +import BaseService from '@/api/core/base' +import request from '@/utils/http' + +export interface ArticleCategoryRow { + id?: string | number + name: string + slug?: string + parent_id?: string | number + sort?: number + enabled?: number + description?: string + created_date?: string + updated_date?: string +} + +export interface ArticleTagRow { + id?: string | number + name: string + slug?: string + sort?: number + heat?: number + enabled?: number + description?: string + created_date?: string + updated_date?: string +} + +export interface ArticleContentRow { + id?: string | number + title: string + slug?: string + category_id?: string | number + category_name?: string + cover?: string + summary?: string + content?: string + status?: number + is_top?: number + sort?: number + view_count?: number + published_at?: number | string + tag_ids?: Array + tag_names?: string + created_date?: string + updated_date?: string +} + +export const ArticleContentService = { + ...BaseService({ baseUrl: '/article/index' }) +} + +export const ArticleCategoryService = { + ...BaseService({ baseUrl: '/article/category' }) +} + +export const ArticleTagService = { + ...BaseService({ baseUrl: '/article/tag' }) +} + +const pluginApi = { + async test(params = {}) { + return request.get<{ msg?: string; data?: unknown }>({ + url: '/article/index', + params + }) + }, + + contentList(params = {}) { + return ArticleContentService.list(params as Record) + }, + + categoryList(params = {}) { + return ArticleCategoryService.list(params as Record) + }, + + tagList(params = {}) { + return ArticleTagService.list(params as Record) + }, + + async recycleList(params = {}) { + return request.get<{ code: number; msg: string; data: Record[] }>({ + url: '/article/recycle', + params + }) + } +} + +export default pluginApi diff --git a/frontend/admin/src/apps/article - 副本/lang/en/en.json b/frontend/admin/src/apps/article - 副本/lang/en/en.json new file mode 100644 index 0000000..1cbdbc3 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/lang/en/en.json @@ -0,0 +1,60 @@ +{ + "article": { + "title": "Article Management", + "description": "Article content operation and management plugin", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh", + "common": { + "list_mode": "List Mode", + "columns": { + "index": "No." + } + }, + "modules": { + "content": { + "title": "Content Center", + "description": "Manage articles, publish states, and content archives.", + "columns": { + "title": "Article Title", + "category": "Category", + "tags": "Tags", + "status": "Status", + "updated_at": "Last Updated" + } + }, + "category": { + "title": "Content Category", + "description": "Manage article category structure and ordering.", + "columns": { + "name": "Category Name", + "slug": "Category Slug", + "article_count": "Article Count", + "sort": "Sort", + "updated_at": "Last Updated" + } + }, + "tag": { + "title": "Tag Management", + "description": "Maintain article tags, popularity, and related article counts.", + "columns": { + "name": "Tag Name", + "slug": "Tag Slug", + "article_count": "Article Count", + "heat": "Heat", + "updated_at": "Last Updated" + } + }, + "recycle": { + "title": "Content Recycle Bin", + "description": "Manage deleted articles for later recovery or cleanup.", + "columns": { + "title": "Article Title", + "category": "Original Category", + "deleted_by": "Deleted By", + "deleted_at": "Deleted At", + "expire_at": "Expire At" + } + } + } + } +} diff --git a/frontend/admin/src/apps/article - 副本/lang/en/index.json b/frontend/admin/src/apps/article - 副本/lang/en/index.json new file mode 100644 index 0000000..a11b9a1 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/lang/en/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "Article Management", + "description": "Article plugin demo page", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh" + } +} \ No newline at end of file diff --git a/frontend/admin/src/apps/article - 副本/lang/zh-cn/index.json b/frontend/admin/src/apps/article - 副本/lang/zh-cn/index.json new file mode 100644 index 0000000..582d445 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/lang/zh-cn/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "文章管理", + "description": "文章插件示例页面", + "card_title": "文章插件数据", + "refresh_btn": "刷新" + } +} \ No newline at end of file diff --git a/frontend/admin/src/apps/article - 副本/lang/zh-cn/zh.json b/frontend/admin/src/apps/article - 副本/lang/zh-cn/zh.json new file mode 100644 index 0000000..82765df --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/lang/zh-cn/zh.json @@ -0,0 +1,108 @@ +{ + "article": { + "title": "文章管理", + "description": "文章内容运营与管理插件", + "card_title": "文章插件数据", + "refresh_btn": "刷新", + "common": { + "list_mode": "列表模式", + "columns": { + "index": "序号" + } + }, + "modules": { + "content": { + "title": "内容中心", + "description": "管理文章内容、发布状态与内容归档。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题、分类或标签" + }, + "form": { + "title_placeholder": "请输入文章标题", + "slug_placeholder": "请输入文章标识", + "category_id": "内容分类ID", + "cover": "封面图", + "cover_placeholder": "请输入封面图地址", + "summary": "摘要", + "summary_placeholder": "请输入摘要", + "content": "文章内容", + "content_placeholder": "请输入文章内容", + "status": "状态", + "is_top": "是否置顶", + "sort_placeholder": "请输入排序", + "published_at": "发布时间", + "tag_ids": "标签ID" + }, + "columns": { + "title": "文章标题", + "category": "内容分类", + "tags": "标签", + "status": "状态", + "is_top": "置顶", + "updated_at": "最后更新" + } + }, + "category": { + "title": "内容分类", + "description": "管理文章分类结构与分类排序。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入分类名称或分类标识" + }, + "form": { + "name_placeholder": "请输入分类名称", + "slug_placeholder": "请输入分类标识", + "parent_id": "父级分类ID", + "description": "分类描述", + "description_placeholder": "请输入分类描述" + }, + "columns": { + "name": "分类名称", + "slug": "分类标识", + "article_count": "内容数量", + "enabled": "状态", + "sort": "排序", + "updated_at": "最后更新" + } + }, + "tag": { + "title": "标签管理", + "description": "维护文章标签、热度与关联内容数量。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入标签名称或标签标识" + }, + "form": { + "name_placeholder": "请输入标签名称", + "slug_placeholder": "请输入标签标识", + "description": "标签描述", + "description_placeholder": "请输入标签描述" + }, + "columns": { + "name": "标签名称", + "slug": "标签标识", + "article_count": "内容数量", + "enabled": "状态", + "heat": "热度", + "updated_at": "最后更新" + } + }, + "recycle": { + "title": "内容回收站", + "description": "管理已删除文章,支持后续恢复或清理。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题或删除人" + }, + "columns": { + "title": "文章标题", + "category": "原分类", + "deleted_by": "删除人", + "deleted_at": "删除时间", + "expire_at": "过期清理时间" + } + } + } + } +} diff --git a/frontend/admin/src/apps/article - 副本/routes/index.ts b/frontend/admin/src/apps/article - 副本/routes/index.ts new file mode 100644 index 0000000..6d04b32 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/routes/index.ts @@ -0,0 +1,59 @@ +import { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '@/router/routesAlias' + +export default { + path: '/article', + name: 'Article', + component: RoutesAlias.Layout, + meta: { + title: '文章管理', + icon: 'ri:plugin-line', + module: 'article' + }, + children: [ + { + path: 'index', + name: 'ArticleIndex', + component: '/article/content/index', + meta: { + title: '内容中心', + icon: 'ri:article-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'category', + name: 'ArticleCategory', + component: '/article/category/index', + meta: { + title: '内容分类', + icon: 'ri:folder-2-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'tag', + name: 'ArticleTag', + component: '/article/tag/index', + meta: { + title: '标签管理', + icon: 'ri:price-tag-3-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'recycle', + name: 'ArticleRecycle', + component: '/article/recycle/index', + meta: { + title: '内容回收站', + icon: 'ri:delete-bin-6-line', + keepAlive: false, + module: 'article' + } + } + ] +} as AppRouteRecord diff --git a/frontend/admin/src/apps/article - 副本/views/article/category/index.vue b/frontend/admin/src/apps/article - 副本/views/article/category/index.vue new file mode 100644 index 0000000..d835801 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/category/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article - 副本/views/article/category/schemas/index.tsx b/frontend/admin/src/apps/article - 副本/views/article/category/schemas/index.tsx new file mode 100644 index 0000000..54d659a --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/category/schemas/index.tsx @@ -0,0 +1,164 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleCategoryService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleCategoryService.list, + dialogTitle: $t('article.modules.category.title'), + crudApi: { + add: ArticleCategoryService.create, + edit: ArticleCategoryService.update, + remove: ArticleCategoryService.delete, + view: ArticleCategoryService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:category:create', + editAuth: 'article:category:update', + removeAuth: 'article:category:delete', + viewAuth: 'article:category:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.category.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.category.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.category.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + minWidth: 180, + }, + { + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.category.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.category.columns.name'), + componentProps: { + placeholder: $t('article.modules.category.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + componentProps: { + placeholder: $t('article.modules.category.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'parent_id', + label: $t('article.modules.category.form.parent_id'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.category.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.category.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article - 副本/views/article/content/index.vue b/frontend/admin/src/apps/article - 副本/views/article/content/index.vue new file mode 100644 index 0000000..c375ed5 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/content/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article - 副本/views/article/content/schemas/index.tsx b/frontend/admin/src/apps/article - 副本/views/article/content/schemas/index.tsx new file mode 100644 index 0000000..736c216 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/content/schemas/index.tsx @@ -0,0 +1,212 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleContentService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleContentService.list, + dialogTitle: $t('article.modules.content.title'), + crudApi: { + add: ArticleContentService.create, + edit: ArticleContentService.update, + remove: ArticleContentService.delete, + view: ArticleContentService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:create', + editAuth: 'article:update', + removeAuth: 'article:delete', + viewAuth: 'article:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.content.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.content.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'title', + label: $t('article.modules.content.columns.title'), + minWidth: 220, + }, + { + prop: 'category_name', + label: $t('article.modules.content.columns.category'), + minWidth: 160, + }, + { + prop: 'tag_names', + label: $t('article.modules.content.columns.tags'), + minWidth: 200, + }, + { + prop: 'status', + label: $t('article.modules.content.columns.status'), + width: 120, + }, + { + prop: 'is_top', + label: $t('article.modules.content.columns.is_top'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'updated_date', + label: $t('article.modules.content.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '70%', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'title', + label: $t('article.modules.content.columns.title'), + componentProps: { + placeholder: $t('article.modules.content.form.title_placeholder'), + maxlength: 200, + }, + rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: 'Slug', + componentProps: { + placeholder: $t('article.modules.content.form.slug_placeholder'), + maxlength: 200, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'category_id', + label: $t('article.modules.content.form.category_id'), + componentProps: { + min: 0, + class: 'w-full', + }, + rules: [{ required: true, message: $t('article.modules.content.form.category_id') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'cover', + label: $t('article.modules.content.form.cover'), + componentProps: { + placeholder: $t('article.modules.content.form.cover_placeholder'), + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'summary', + label: $t('article.modules.content.form.summary'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.content.form.summary_placeholder'), + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'content', + label: $t('article.modules.content.form.content'), + componentProps: { + type: 'textarea', + rows: 10, + placeholder: $t('article.modules.content.form.content_placeholder'), + }, + colSpan: 24, + }, + { + component: 'InputNumber', + prop: 'status', + label: $t('article.modules.content.form.status'), + defaultValue: 1, + componentProps: { + min: 0, + max: 2, + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'ApiDict', + prop: 'is_top', + label: $t('article.modules.content.form.is_top'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'InputNumber', + prop: 'sort', + label: 'Sort', + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'DatePicker', + prop: 'published_at', + label: $t('article.modules.content.form.published_at'), + componentProps: { + type: 'datetime', + valueFormat: 'X', + class: 'w-full', + }, + colSpan: 12, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article - 副本/views/article/index/index.vue b/frontend/admin/src/apps/article - 副本/views/article/index/index.vue new file mode 100644 index 0000000..2617d29 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/admin/src/apps/article - 副本/views/article/recycle/index.vue b/frontend/admin/src/apps/article - 副本/views/article/recycle/index.vue new file mode 100644 index 0000000..fa6aad5 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/recycle/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article - 副本/views/article/recycle/schemas/index.tsx b/frontend/admin/src/apps/article - 副本/views/article/recycle/schemas/index.tsx new file mode 100644 index 0000000..bd35770 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/recycle/schemas/index.tsx @@ -0,0 +1,65 @@ +import { CrudSchema } from '@/components/crud' +import { $t } from '@/locales' +import pluginApi from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: async (params: any) => { + const response = await pluginApi.recycleList(params) + const rows = Array.isArray(response?.data) ? response.data : [] + return { + recordCount: rows.length, + totalPage: 1, + rows, + } + }, + useCrud: false, + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.recycle.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.recycle.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'title', + label: $t('article.modules.recycle.columns.title'), + minWidth: 220, + }, + { + prop: 'category', + label: $t('article.modules.recycle.columns.category'), + minWidth: 160, + }, + { + prop: 'deleted_by', + label: $t('article.modules.recycle.columns.deleted_by'), + width: 140, + }, + { + prop: 'deleted_at', + label: $t('article.modules.recycle.columns.deleted_at'), + minWidth: 180, + }, + { + prop: 'expire_at', + label: $t('article.modules.recycle.columns.expire_at'), + minWidth: 180, + }, + ], + } +} diff --git a/frontend/admin/src/apps/article - 副本/views/article/tag/index.vue b/frontend/admin/src/apps/article - 副本/views/article/tag/index.vue new file mode 100644 index 0000000..c763b60 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/tag/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article - 副本/views/article/tag/schemas/index.tsx b/frontend/admin/src/apps/article - 副本/views/article/tag/schemas/index.tsx new file mode 100644 index 0000000..5ade820 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/article/tag/schemas/index.tsx @@ -0,0 +1,164 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleTagService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleTagService.list, + dialogTitle: $t('article.modules.tag.title'), + crudApi: { + add: ArticleTagService.create, + edit: ArticleTagService.update, + remove: ArticleTagService.delete, + view: ArticleTagService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:tag:create', + editAuth: 'article:tag:update', + removeAuth: 'article:tag:delete', + viewAuth: 'article:tag:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.tag.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.tag.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.tag.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + minWidth: 180, + }, + { + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.tag.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.tag.columns.name'), + componentProps: { + placeholder: $t('article.modules.tag.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + componentProps: { + placeholder: $t('article.modules.tag.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.tag.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.tag.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article - 副本/views/index/index.vue b/frontend/admin/src/apps/article - 副本/views/index/index.vue new file mode 100644 index 0000000..1d702c1 --- /dev/null +++ b/frontend/admin/src/apps/article - 副本/views/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/admin/src/apps/article/.gitkeep b/frontend/admin/src/apps/article/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/admin/src/apps/article/api/index.ts b/frontend/admin/src/apps/article/api/index.ts new file mode 100644 index 0000000..5c8087b --- /dev/null +++ b/frontend/admin/src/apps/article/api/index.ts @@ -0,0 +1,118 @@ +import BaseService from '@/api/core/base' +import request from '@/utils/http' + +export interface ArticleCategoryRow { + id?: string | number + name: string + slug?: string + parent_id?: string | number + article_count?: number + sort?: number + enabled?: number + description?: string + created_date?: string + updated_date?: string +} + +export interface ArticleTagRow { + id?: string | number + name: string + slug?: string + article_count?: number + sort?: number + heat?: number + enabled?: number + description?: string + created_date?: string + updated_date?: string +} + +export interface ArticleContentRow { + id?: string | number + title: string + content_type?: string + slug?: string + category_id?: string | number + category_name?: string + channel?: string + visibility?: number + author?: string + source?: string + cover?: string + seo_keywords?: string + seo_title?: string + seo_description?: string + summary?: string + content?: string + status?: number + is_top?: number + is_recommend?: number + allow_comment?: number + sort?: number + view_count?: number + like_count?: number + comment_count?: number + favorite_count?: number + published_at?: number | string + offline_at?: number | string + tag_ids?: Array + tag_names?: string + created_date?: string + updated_date?: string +} + +export const ArticleContentService = { + ...BaseService({ baseUrl: '/article/index' }) +} + +export const ArticleCategoryService = { + ...BaseService({ baseUrl: '/article/category' }) +} + +export const ArticleTagService = { + ...BaseService({ baseUrl: '/article/tag' }) +} + +const pluginApi = { + async test(params = {}) { + return request.get<{ msg?: string; data?: unknown }>({ + url: '/article/index', + params + }) + }, + + contentList(params = {}) { + return ArticleContentService.list(params as Record) + }, + + categoryList(params = {}) { + return ArticleCategoryService.list(params as Record) + }, + + tagList(params = {}) { + return ArticleTagService.list(params as Record) + }, + + async recycleList(params = {}) { + return request.get<{ code: number; msg: string; data: Record[] }>({ + url: '/article/recycle', + params + }) + }, + + async recycleRestore(id: string | number) { + return request.put({ + url: `/article/recycle/${id}/restore`, + params: { id } + }) + }, + + async recycleDelete(id: string | number) { + return request.del({ + url: `/article/recycle/${id}`, + params: { id } + }) + } +} + +export default pluginApi diff --git a/frontend/admin/src/apps/article/lang/en/en.json b/frontend/admin/src/apps/article/lang/en/en.json new file mode 100644 index 0000000..1ab8908 --- /dev/null +++ b/frontend/admin/src/apps/article/lang/en/en.json @@ -0,0 +1,136 @@ +{ + "article": { + "title": "Article Management", + "description": "Article content operation and management plugin", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh", + "common": { + "list_mode": "List Mode", + "columns": { + "index": "No." + } + }, + "modules": { + "content": { + "title": "Content Center", + "description": "Manage articles, publish states, and content archives.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter article title, category, or tags" + }, + "form": { + "title": "Content Title", + "title_placeholder": "Please enter article title", + "slug": "Content Slug", + "slug_placeholder": "Please enter article slug", + "content_type": "Content Type", + "category_id": "Category", + "category_id_placeholder": "Please select category", + "channel": "Channel", + "visibility": "Visibility", + "author": "Author", + "author_placeholder": "Please enter author", + "source": "Source", + "cover": "Cover", + "cover_placeholder": "Please enter cover URL", + "summary": "Summary", + "summary_placeholder": "Please enter summary", + "content": "Content", + "content_placeholder": "Please enter content", + "status": "Status", + "is_top": "Pinned", + "is_recommend": "Recommend", + "allow_comment": "Comment Setting", + "sort": "Sort Order", + "sort_placeholder": "Please enter sort order", + "view_count": "View Count", + "like_count": "Like Count", + "comment_count": "Comment Count", + "favorite_count": "Favorite Count", + "published_at": "Published At", + "offline_at": "Offline At", + "tag_ids": "Content Tags", + "tag_ids_placeholder": "Please select content tags", + "seo_keywords": "SEO Keywords", + "seo_keywords_placeholder": "For search engine optimization", + "seo_title": "SEO Title", + "seo_title_placeholder": "Optional, defaults to content title", + "seo_description": "SEO Description", + "seo_description_placeholder": "Optional, highlight value and search keywords" + }, + "columns": { + "title": "Article Title", + "content_info": "Content Info", + "category": "Category", + "tags": "Tags", + "status": "Status", + "business_data": "Business Data", + "time_info": "Time", + "is_top": "Pinned", + "updated_at": "Last Updated" + } + }, + "category": { + "title": "Content Category", + "description": "Manage article category structure and ordering.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter category name or slug" + }, + "form": { + "name_placeholder": "Please enter category name", + "slug_placeholder": "Please enter category slug", + "parent_id": "Parent Category", + "parent_top": "Top Category", + "description": "Category Description", + "description_placeholder": "Please enter category description" + }, + "columns": { + "name": "Category Name", + "slug": "Category Slug", + "article_count": "Content Count", + "enabled": "Status", + "sort": "Sort", + "updated_at": "Last Updated" + } + }, + "tag": { + "title": "Tag Management", + "description": "Maintain article tags, popularity, and related article counts.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter tag name or slug" + }, + "form": { + "name_placeholder": "Please enter tag name", + "slug_placeholder": "Please enter tag slug", + "description": "Tag Description", + "description_placeholder": "Please enter tag description" + }, + "columns": { + "name": "Tag Name", + "slug": "Tag Slug", + "article_count": "Usage Count", + "enabled": "Status", + "heat": "Heat", + "updated_at": "Last Updated" + } + }, + "recycle": { + "title": "Content Recycle Bin", + "description": "Manage deleted articles for later recovery or cleanup.", + "search": { + "keywords": "Keywords", + "keywords_placeholder": "Please enter article title or deleter" + }, + "columns": { + "title": "Article Title", + "category": "Original Category", + "deleted_by": "Deleted By", + "deleted_at": "Deleted At", + "expire_at": "Expire At" + } + } + } + } +} diff --git a/frontend/admin/src/apps/article/lang/en/index.json b/frontend/admin/src/apps/article/lang/en/index.json new file mode 100644 index 0000000..a11b9a1 --- /dev/null +++ b/frontend/admin/src/apps/article/lang/en/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "Article Management", + "description": "Article plugin demo page", + "card_title": "Article Plugin Data", + "refresh_btn": "Refresh" + } +} \ No newline at end of file diff --git a/frontend/admin/src/apps/article/lang/zh-cn/index.json b/frontend/admin/src/apps/article/lang/zh-cn/index.json new file mode 100644 index 0000000..582d445 --- /dev/null +++ b/frontend/admin/src/apps/article/lang/zh-cn/index.json @@ -0,0 +1,8 @@ +{ + "article": { + "title": "文章管理", + "description": "文章插件示例页面", + "card_title": "文章插件数据", + "refresh_btn": "刷新" + } +} \ No newline at end of file diff --git a/frontend/admin/src/apps/article/lang/zh-cn/zh.json b/frontend/admin/src/apps/article/lang/zh-cn/zh.json new file mode 100644 index 0000000..1fcdb6e --- /dev/null +++ b/frontend/admin/src/apps/article/lang/zh-cn/zh.json @@ -0,0 +1,136 @@ +{ + "article": { + "title": "文章管理", + "description": "文章内容运营与管理插件", + "card_title": "文章插件数据", + "refresh_btn": "刷新", + "common": { + "list_mode": "列表模式", + "columns": { + "index": "序号" + } + }, + "modules": { + "content": { + "title": "内容中心", + "description": "管理文章内容、发布状态与内容归档。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题、分类或标签" + }, + "form": { + "title": "内容标题", + "title_placeholder": "请输入文章标题", + "slug": "内容别名", + "slug_placeholder": "请输入文章标识", + "content_type": "内容类型", + "category_id": "内容分类", + "category_id_placeholder": "请选择内容分类", + "channel": "所属频道", + "visibility": "可见范围", + "author": "作者", + "author_placeholder": "请输入作者", + "source": "内容来源", + "cover": "封面图", + "cover_placeholder": "请输入封面图地址", + "summary": "摘要", + "summary_placeholder": "请输入摘要", + "content": "文章内容", + "content_placeholder": "请输入文章内容", + "status": "状态", + "is_top": "是否置顶", + "is_recommend": "推荐", + "allow_comment": "评论设置", + "sort": "内容排序", + "sort_placeholder": "请输入排序", + "view_count": "浏览量", + "like_count": "点赞量", + "comment_count": "评论量", + "favorite_count": "收藏量", + "published_at": "发布时间", + "offline_at": "下线时间", + "tag_ids": "内容标签", + "tag_ids_placeholder": "请选择内容标签", + "seo_keywords": "SEO 关键词", + "seo_keywords_placeholder": "用于搜索引擎优化", + "seo_title": "SEO 标题", + "seo_title_placeholder": "可选,默认继承内容标题", + "seo_description": "SEO 描述", + "seo_description_placeholder": "可选,建议突出商业价值与检索关键词" + }, + "columns": { + "title": "文章标题", + "content_info": "内容信息", + "category": "内容分类", + "tags": "标签", + "status": "状态", + "business_data": "经营数据", + "time_info": "时间", + "is_top": "置顶", + "updated_at": "最后更新" + } + }, + "category": { + "title": "内容分类", + "description": "管理文章分类结构与分类排序。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入分类名称或分类标识" + }, + "form": { + "name_placeholder": "请输入分类名称", + "slug_placeholder": "请输入分类标识", + "parent_id": "父级分类", + "parent_top": "顶级分类", + "description": "分类描述", + "description_placeholder": "请输入分类描述" + }, + "columns": { + "name": "分类名称", + "slug": "分类标识", + "article_count": "内容数", + "enabled": "状态", + "sort": "排序", + "updated_at": "最后更新" + } + }, + "tag": { + "title": "标签管理", + "description": "维护文章标签、热度与关联内容数量。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入标签名称或标签标识" + }, + "form": { + "name_placeholder": "请输入标签名称", + "slug_placeholder": "请输入标签标识", + "description": "标签描述", + "description_placeholder": "请输入标签描述" + }, + "columns": { + "name": "标签名称", + "slug": "标签标识", + "article_count": "使用次数", + "enabled": "状态", + "heat": "热度", + "updated_at": "最后更新" + } + }, + "recycle": { + "title": "内容回收站", + "description": "管理已删除文章,支持后续恢复或清理。", + "search": { + "keywords": "关键字", + "keywords_placeholder": "请输入文章标题或删除人" + }, + "columns": { + "title": "文章标题", + "category": "原分类", + "deleted_by": "删除人", + "deleted_at": "删除时间", + "expire_at": "过期清理时间" + } + } + } + } +} diff --git a/frontend/admin/src/apps/article/routes/index.ts b/frontend/admin/src/apps/article/routes/index.ts new file mode 100644 index 0000000..6d04b32 --- /dev/null +++ b/frontend/admin/src/apps/article/routes/index.ts @@ -0,0 +1,59 @@ +import { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '@/router/routesAlias' + +export default { + path: '/article', + name: 'Article', + component: RoutesAlias.Layout, + meta: { + title: '文章管理', + icon: 'ri:plugin-line', + module: 'article' + }, + children: [ + { + path: 'index', + name: 'ArticleIndex', + component: '/article/content/index', + meta: { + title: '内容中心', + icon: 'ri:article-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'category', + name: 'ArticleCategory', + component: '/article/category/index', + meta: { + title: '内容分类', + icon: 'ri:folder-2-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'tag', + name: 'ArticleTag', + component: '/article/tag/index', + meta: { + title: '标签管理', + icon: 'ri:price-tag-3-line', + keepAlive: false, + module: 'article' + } + }, + { + path: 'recycle', + name: 'ArticleRecycle', + component: '/article/recycle/index', + meta: { + title: '内容回收站', + icon: 'ri:delete-bin-6-line', + keepAlive: false, + module: 'article' + } + } + ] +} as AppRouteRecord diff --git a/frontend/admin/src/apps/article/views/article/category/index.vue b/frontend/admin/src/apps/article/views/article/category/index.vue new file mode 100644 index 0000000..d835801 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/category/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article/views/article/category/schemas/index.tsx b/frontend/admin/src/apps/article/views/article/category/schemas/index.tsx new file mode 100644 index 0000000..0d64db7 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/category/schemas/index.tsx @@ -0,0 +1,189 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleCategoryService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleCategoryService.list, + dialogTitle: $t('article.modules.category.title'), + crudApi: { + add: ArticleCategoryService.create, + edit: ArticleCategoryService.update, + remove: ArticleCategoryService.delete, + view: ArticleCategoryService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:category:create', + editAuth: 'article:category:update', + removeAuth: 'article:category:delete', + viewAuth: 'article:category:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.category.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.category.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.category.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + minWidth: 180, + }, + { + prop: 'article_count', + label: $t('article.modules.category.columns.article_count'), + width: 120, + }, + { + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.category.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.category.columns.name'), + componentProps: { + placeholder: $t('article.modules.category.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.category.columns.slug'), + componentProps: { + placeholder: $t('article.modules.category.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'ApiTreeSelect', + prop: 'parent_id', + label: $t('article.modules.category.form.parent_id'), + defaultValue: 0, + componentProps: { + api: '/article/category', + requestMethod: 'get', + params: { + format: 'tree', + }, + afterFetch: async (res: any) => { + if (!Array.isArray(res)) { + return res + } + return [ + { + id: 0, + name: $t('article.modules.category.form.parent_top'), + }, + ...res, + ] + }, + clearable: true, + checkStrictly: true, + class: 'w-full', + labelField: 'name', + valueField: 'id', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.category.columns.sort'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.category.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.category.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.category.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article/views/article/content/index.vue b/frontend/admin/src/apps/article/views/article/content/index.vue new file mode 100644 index 0000000..c375ed5 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/content/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article/views/article/content/schemas/index.tsx b/frontend/admin/src/apps/article/views/article/content/schemas/index.tsx new file mode 100644 index 0000000..fde1498 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/content/schemas/index.tsx @@ -0,0 +1,544 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleContentService } from '../../../../api/index' +import { h } from 'vue' + +const contentTypeOptions = [ + { label: '文章', value: 'article' }, + { label: '图文', value: 'news' }, + { label: '公告', value: 'notice' }, +] + +const channelOptions = [ + { label: '官网内容', value: 'official' }, + { label: '专题内容', value: 'topic' }, + { label: '活动内容', value: 'activity' }, +] + +const visibilityOptions = [ + { label: '公开', value: 1 }, + { label: '登录可见', value: 2 }, + { label: '私密', value: 3 }, +] + +const sourceOptions = [ + { label: '原创', value: 'original' }, + { label: '转载', value: 'repost' }, + { label: '投稿', value: 'contribution' }, +] + +const statusOptions = [ + { label: '草稿', value: 0 }, + { label: '发布', value: 1 }, + { label: '下线', value: 2 }, +] + +const getOptionLabel = (options: Array<{ label: string; value: string | number }>, value: string | number) => { + return options.find((item) => item.value === value)?.label || '-' +} + +const getStatusTagClass = (status: number) => { + if (status === 1) { + return 'inline-flex rounded-full bg-[#ecfdf5] px-2 py-[2px] text-xs text-[#10b981]' + } + if (status === 2) { + return 'inline-flex rounded-full bg-[#fef2f2] px-2 py-[2px] text-xs text-[#ef4444]' + } + return 'inline-flex rounded-full bg-[#f4f4f5] px-2 py-[2px] text-xs text-[#909399]' +} + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleContentService.list, + dialogTitle: $t('article.modules.content.title'), + exportFileName: '内容中心', + exportFields: [ + { field: 'title', title: '标题' }, + { field: 'content_type', title: '内容类型' }, + { field: 'category_name', title: '分类' }, + { field: 'tag_names', title: '标签' }, + { field: 'author', title: '作者' }, + { field: 'channel', title: '频道' }, + { field: 'visibility', title: '可见范围' }, + { field: 'source', title: '来源' }, + { field: 'status', title: '状态' }, + { field: 'is_top', title: '置顶' }, + { field: 'is_recommend', title: '推荐' }, + { field: 'view_count', title: '浏览数' }, + { field: 'like_count', title: '点赞数' }, + { field: 'comment_count', title: '评论数' }, + { field: 'favorite_count', title: '收藏数' }, + { field: 'published_at', title: '发布时间' }, + { field: 'updated_date', title: '更新时间' }, + { field: 'summary', title: '摘要' } + ], + crudApi: { + add: ArticleContentService.create, + edit: ArticleContentService.update, + remove: ArticleContentService.remove, + view: ArticleContentService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:create', + editAuth: 'article:update', + removeAuth: 'article:delete', + viewAuth: 'article:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.content.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.content.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'content_info', + label: $t('article.modules.content.columns.content_info'), + minWidth: 380, + formatter: (row: any) => { + const metaItems = [ + getOptionLabel(contentTypeOptions, row.content_type), + row.author || '管理员', + row.category_name, + row.tag_names, + ].filter(Boolean) + + const badgeNodes = [ + Number(row.is_top) === 1 + ? h('span', { class: 'inline-flex items-center rounded bg-[#fff7e6] px-1.5 py-[1px] text-[10px] text-[#fa8c16]' }, '置顶') + : null, + Number(row.is_recommend) === 1 + ? h('span', { class: 'inline-flex items-center rounded bg-[#ecfdf5] px-1.5 py-[1px] text-[10px] text-[#10b981]' }, '推荐') + : null, + ].filter(Boolean) + + return h('div', { class: 'flex gap-3 py-2' }, [ + row.cover + ? h('img', { + src: row.cover, + class: 'mt-1 h-14 w-20 flex-none rounded bg-[#f5f7fa] object-cover border border-[#f0f0f0]' + }) + : h( + 'div', + { + class: 'mt-1 h-14 w-20 flex-none rounded border border-[#f0f0f0] bg-[#fafafa] text-[10px] text-[#c0c4cc] flex items-center justify-center' + }, + '无上传封面' + ), + h('div', { class: 'min-w-0 flex-1 leading-6' }, [ + h('div', { class: 'flex items-center gap-2 text-sm font-medium text-[#1f2937]' }, [ + h('span', { class: 'line-clamp-1' }, row.title || '-'), + ...badgeNodes + ]), + h( + 'div', + { class: 'mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-[#909399]' }, + metaItems.length > 0 ? metaItems.map((item) => h('span', item)) : [h('span', '-')] + ), + h('div', { class: 'mt-1 line-clamp-2 text-xs text-[#606266]' }, row.summary || row.slug || '-') + ]) + ]) + } + }, + { + prop: 'status_info', + label: $t('article.modules.content.columns.status'), + width: 140, + align: 'center', + formatter: (row: any) => { + const statusText = getOptionLabel(statusOptions, row.status) + const visibilityText = getOptionLabel(visibilityOptions, row.visibility) + const sourceText = getOptionLabel(sourceOptions, row.source) + return h('div', { class: 'flex flex-col items-center py-2 text-sm leading-6' }, [ + h('div', [ + h('span', { class: getStatusTagClass(Number(row.status)) }, statusText) + ]), + h('div', { class: 'mt-1 text-xs text-[#606266]' }, visibilityText), + h('div', { class: 'mt-1 text-xs text-[#909399]' }, sourceText) + ]) + } + }, + { + prop: 'business_data', + label: $t('article.modules.content.columns.business_data'), + minWidth: 250, + formatter: (row: any) => { + const metricItems = [ + { label: '浏览', value: row.view_count ?? 0 }, + { label: '点赞', value: row.like_count ?? 0 }, + { label: '评论', value: row.comment_count ?? 0 }, + { label: '收藏', value: row.favorite_count ?? 0 }, + ] + + return h('div', { class: 'grid grid-cols-2 gap-2 py-2 text-xs text-[#606266]' }, [ + ...metricItems.map((item) => + h('div', { class: 'flex items-center justify-between rounded border border-[#e5e7eb] bg-[#eef2f6] px-3 py-1.5 transition-colors hover:bg-[#e5e7eb]' }, [ + h('span', { class: 'text-[#909399]' }, item.label), + h('span', item.value) + ]) + ) + ]) + } + }, + { + prop: 'time_info', + label: $t('article.modules.content.columns.time_info'), + minWidth: 190, + formatter: (row: any) => { + return h('div', { class: 'py-2 text-xs leading-6 text-[#606266]' }, [ + h('div', [ + h('span', { class: 'text-[#909399]' }, '更新 '), + h('span', row.updated_date || '-') + ]), + h('div', { class: 'mt-1' }, [ + h('span', { class: 'text-[#909399]' }, '发布 '), + h('span', row.created_date || '-') + ]) + ]) + } + }, + ], + tableActionColumn: { + width: 170, + }, + dialogProps: { + width: '70%', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'title', + label: $t('article.modules.content.columns.title'), + componentProps: { + placeholder: $t('article.modules.content.form.title_placeholder'), + maxlength: 200, + }, + rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.content.form.slug'), + componentProps: { + placeholder: $t('article.modules.content.form.slug_placeholder'), + maxlength: 200, + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'content_type', + label: $t('article.modules.content.form.content_type'), + defaultValue: 'article', + componentProps: { + options: contentTypeOptions, + class: 'w-full', + }, + rules: [{ required: true, message: $t('article.modules.content.form.content_type') }], + colSpan: 12, + }, + { + component: 'ApiTreeSelect', + prop: 'category_id', + label: $t('article.modules.content.form.category_id'), + componentProps: { + api: '/article/category', + requestMethod: 'get', + params: { + format: 'tree', + }, + teleported: true, + clearable: true, + checkStrictly: true, + class: 'w-full', + labelField: 'name', + valueField: 'id', + placeholder: $t('article.modules.content.form.category_id_placeholder'), + }, + rules: [{ required: true, message: $t('article.modules.content.form.category_id') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'channel', + label: $t('article.modules.content.form.channel'), + defaultValue: 'official', + componentProps: { + options: channelOptions, + class: 'w-full', + }, + rules: [{ required: true, message: $t('article.modules.content.form.channel') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'author', + label: $t('article.modules.content.form.author'), + componentProps: { + placeholder: $t('article.modules.content.form.author_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.content.form.author_placeholder') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'visibility', + label: $t('article.modules.content.form.visibility'), + defaultValue: 1, + componentProps: { + options: visibilityOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'status', + label: $t('article.modules.content.form.status'), + defaultValue: 0, + componentProps: { + options: statusOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Select', + prop: 'source', + label: $t('article.modules.content.form.source'), + defaultValue: 'original', + componentProps: { + options: sourceOptions, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Avatar', + prop: 'cover', + label: $t('article.modules.content.form.cover'), + componentProps: { + shape: 'square', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'summary', + label: $t('article.modules.content.form.summary'), + componentProps: { + type: 'textarea', + rows: 4, + maxlength: 500, + placeholder: $t('article.modules.content.form.summary_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiSelect', + prop: 'tag_ids', + label: $t('article.modules.content.form.tag_ids'), + componentProps: { + api: '/article/tag', + requestMethod: 'get', + params: { + format: 'select', + }, + labelField: 'label', + valueField: 'value', + multiple: true, + clearable: true, + filterable: true, + class: 'w-full', + placeholder: $t('article.modules.content.form.tag_ids_placeholder'), + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_keywords', + label: $t('article.modules.content.form.seo_keywords'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_keywords_placeholder'), + maxlength: 255, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_title', + label: $t('article.modules.content.form.seo_title'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_title_placeholder'), + maxlength: 200, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'seo_description', + label: $t('article.modules.content.form.seo_description'), + componentProps: { + placeholder: $t('article.modules.content.form.seo_description_placeholder'), + maxlength: 500, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.content.form.sort'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'DatePicker', + prop: 'published_at', + label: $t('article.modules.content.form.published_at'), + componentProps: { + type: 'datetime', + valueFormat: 'X', + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'DatePicker', + prop: 'offline_at', + label: $t('article.modules.content.form.offline_at'), + componentProps: { + type: 'datetime', + valueFormat: 'X', + class: 'w-full', + }, + colSpan: 8, + }, + { + component: 'InputNumber', + prop: 'view_count', + label: $t('article.modules.content.form.view_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'like_count', + label: $t('article.modules.content.form.like_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'comment_count', + label: $t('article.modules.content.form.comment_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'InputNumber', + prop: 'favorite_count', + label: $t('article.modules.content.form.favorite_count'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 6, + }, + { + component: 'ApiDict', + prop: 'is_top', + label: $t('article.modules.content.form.is_top'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'ApiDict', + prop: 'is_recommend', + label: $t('article.modules.content.form.is_recommend'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'ApiDict', + prop: 'allow_comment', + label: $t('article.modules.content.form.allow_comment'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 8, + }, + { + component: 'Editor', + prop: 'content', + label: $t('article.modules.content.form.content'), + componentProps: { + placeholder: $t('article.modules.content.form.content_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article/views/article/index/index.vue b/frontend/admin/src/apps/article/views/article/index/index.vue new file mode 100644 index 0000000..2617d29 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/admin/src/apps/article/views/article/recycle/index.vue b/frontend/admin/src/apps/article/views/article/recycle/index.vue new file mode 100644 index 0000000..68ba3c5 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/recycle/index.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/frontend/admin/src/apps/article/views/article/recycle/schemas/index.tsx b/frontend/admin/src/apps/article/views/article/recycle/schemas/index.tsx new file mode 100644 index 0000000..9d6c771 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/recycle/schemas/index.tsx @@ -0,0 +1,65 @@ +import { CrudSchema } from '@/components/crud' +import { $t } from '@/locales' +import pluginApi from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: async (params: any) => { + const response = await pluginApi.recycleList(params) + const rows = Array.isArray(response) ? response : [] + return { + recordCount: rows.length, + totalPage: 1, + rows, + } + }, + useCrud: false, + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_title', + label: $t('article.modules.recycle.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.recycle.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'title', + label: $t('article.modules.recycle.columns.title'), + minWidth: 220, + }, + { + prop: 'category', + label: $t('article.modules.recycle.columns.category'), + minWidth: 160, + }, + { + prop: 'deleted_by', + label: $t('article.modules.recycle.columns.deleted_by'), + width: 140, + }, + { + prop: 'deleted_at', + label: $t('article.modules.recycle.columns.deleted_at'), + minWidth: 180, + }, + { + prop: 'expire_at', + label: $t('article.modules.recycle.columns.expire_at'), + minWidth: 180, + }, + ], + } +} diff --git a/frontend/admin/src/apps/article/views/article/tag/index.vue b/frontend/admin/src/apps/article/views/article/tag/index.vue new file mode 100644 index 0000000..c763b60 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/tag/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/article/views/article/tag/schemas/index.tsx b/frontend/admin/src/apps/article/views/article/tag/schemas/index.tsx new file mode 100644 index 0000000..3031030 --- /dev/null +++ b/frontend/admin/src/apps/article/views/article/tag/schemas/index.tsx @@ -0,0 +1,169 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ArticleTagService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ArticleTagService.list, + dialogTitle: $t('article.modules.tag.title'), + crudApi: { + add: ArticleTagService.create, + edit: ArticleTagService.update, + remove: ArticleTagService.delete, + view: ArticleTagService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'article:tag:create', + editAuth: 'article:tag:update', + removeAuth: 'article:tag:delete', + viewAuth: 'article:tag:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_name', + label: $t('article.modules.tag.search.keywords'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('article.modules.tag.search.keywords_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { + type: 'selection', + }, + { + type: 'index', + label: $t('article.common.columns.index'), + width: 70, + }, + { + prop: 'name', + label: $t('article.modules.tag.columns.name'), + minWidth: 220, + }, + { + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + minWidth: 180, + }, + { + prop: 'article_count', + label: $t('article.modules.tag.columns.article_count'), + width: 120, + }, + { + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + }, + }, + { + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + width: 120, + }, + { + prop: 'updated_date', + label: $t('article.modules.tag.columns.updated_at'), + minWidth: 180, + }, + ], + dialogProps: { + width: '600px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'name', + label: $t('article.modules.tag.columns.name'), + componentProps: { + placeholder: $t('article.modules.tag.form.name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'slug', + label: $t('article.modules.tag.columns.slug'), + componentProps: { + placeholder: $t('article.modules.tag.form.slug_placeholder'), + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 100, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'heat', + label: $t('article.modules.tag.columns.heat'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'enabled', + label: $t('article.modules.tag.columns.enabled'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_ENABLED_STATUS, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('article.modules.tag.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('article.modules.tag.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/article/views/index/index.vue b/frontend/admin/src/apps/article/views/index/index.vue new file mode 100644 index 0000000..1d702c1 --- /dev/null +++ b/frontend/admin/src/apps/article/views/index/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/.gitkeep b/frontend/admin/src/apps/produce/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/admin/src/apps/produce/api/index.ts b/frontend/admin/src/apps/produce/api/index.ts new file mode 100644 index 0000000..a182241 --- /dev/null +++ b/frontend/admin/src/apps/produce/api/index.ts @@ -0,0 +1,519 @@ +import request from "@/utils/http"; + +/** + * produce 插件 API + */ +import BaseService from "@/api/core/base"; + +export interface ProduceProcessRow { + id?: string | number; + process_code: string; + process_name: string; + description?: string; + step_count?: number; + sort?: number; + status?: number; + created_date?: string; + updated_date?: string; +} + +export interface ProduceProcessStepRow { + id?: string | number; + process_id: string | number; + step_code: string; + step_name: string; + description?: string; + sort?: number; + status?: number; + prerequisite_steps?: Array | string | null; + prerequisite_mode?: "all" | "any" | "none"; + prerequisite_require_final?: number; + allow_repeat_scan?: number; + repeat_scan_statuses?: Array | string | null; + process?: { + id?: string | number; + process_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStepStatusRow { + id?: string | number; + step_id: string | number; + status_code: string; + status_name: string; + is_initial?: number; + is_final?: number; + description?: string; + sort?: number; + status?: number; + step?: { + id?: string | number; + step_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStepRelationRow { + id?: string | number; + from_step_id: string | number; + to_step_id: string | number; + relation_type: "normal" | "exception"; + flow_condition?: string | Record | null; + priority?: number; + description?: string; + sort?: number; + status?: number; + from_step?: { + id?: string | number; + step_name?: string; + }; + to_step?: { + id?: string | number; + step_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceStatusTransitionRow { + id?: string | number; + step_id: string | number; + from_status_id: string | number; + to_status_id: string | number; + trigger_event: string; + event_config?: string | Record | null; + auto_trigger?: number; + priority?: number; + transition_type?: "primary" | "side_effect"; + side_effect_type?: string; + side_effect_config?: string | Record | null; + description?: string; + sort?: number; + status?: number; + step?: { + id?: string | number; + step_name?: string; + }; + from_status?: { + id?: string | number; + status_name?: string; + }; + to_status?: { + id?: string | number; + status_name?: string; + }; + created_date?: string; + updated_date?: string; +} + +export interface ProduceMaterialCategoryRow { + id?: string | number; + category_name: string; + created_date?: string; + updated_date?: string; +} + +export interface ProduceMaterialRow { + id?: string | number; + category_id: string | number; + category_name?: string; + product_code: string; + product_name: string; + created_date?: string; + updated_date?: string; +} + +export interface ProduceHardwareFaultDescriptionRow { + id?: string | number; + // ... other properties +} + +export const ProduceMaterialCategoryService = { + ...BaseService({ + baseUrl: "/produce/material-category", + }), +}; + +export const ProduceMaterialService = { + ...BaseService({ baseUrl: "/produce/material" }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + importExcel(data: FormData) { + return request.post<{ created: number; updated: number }>({ + url: "/produce/material/import", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, +}; + +export const ProduceProcessService = { + ...BaseService({ baseUrl: "/produce/process" }), +}; + +export const ProduceProcessStepService = { + ...BaseService({ baseUrl: "/produce/process-step" }), +}; + +export const ProduceStepStatusService = { + ...BaseService({ baseUrl: "/produce/step-status" }), +}; + +export const ProduceStepRelationService = { + ...BaseService({ baseUrl: "/produce/step-relation" }), +}; + +export const ProduceStatusTransitionService = { + ...BaseService({ + baseUrl: "/produce/status-transition", + }), +}; + +export interface ProduceTransitionLogRow { + id?: string | number; + work_order_id?: string | number | null; + work_order_no?: string; + step_id: string | number; + transition_id?: string | number | null; + from_status_id: string | number; + to_status_id: string | number; + trigger_event: string; + trigger_data?: Record | string | null; + trigger_user?: string; + trigger_ip?: string; + is_auto?: number; + is_success?: number; + error_message?: string | null; + duration?: number; + remark?: string | null; + created_by?: string | number | null; + created_name?: string; + created_date?: string; + step?: { + id?: string | number; + step_name?: string; + }; + transition?: { + id?: string | number; + trigger_event?: string; + }; + from_status?: { + id?: string | number; + status_name?: string; + }; + to_status?: { + id?: string | number; + status_name?: string; + }; +} + +export const ProduceTransitionLogService = { + ...BaseService({ + baseUrl: "/produce/transition-log", + allowedMethods: ["list", "get", "export"], + }), +}; + +export interface ProduceSpecificRuleRow { + id?: string | number; + rule_name: string; + rule_pattern: string; + rule_type: number; + apply_mode?: "order" | "machine" | "both"; + match_field?: "order_no" | "process_order_no" | "product_code" | "user_code"; + is_enabled?: number; + step_ids?: Array | string | null; + sort?: number; + remark?: string; + created_name?: string; + created_date?: string; +} + +export interface ProduceProductRecordRow { + id?: string | number; + process_order_no: string; + product_code: string; + quantity: number; + process_id?: string | number | null; + step_id?: string | number | null; + status_id?: string | number | null; + user_code?: string; + operator?: string; + operator_id?: string | number | null; + scan_result?: number; + remark?: string | null; + process?: { + id?: string | number; + process_name?: string; + }; + step?: { + id?: string | number; + step_name?: string; + }; + status?: { + id?: string | number; + status_name?: string; + }; + created_name?: string; + created_date?: string; +} + +export interface ProduceScanDataRow { + id?: string | number; + order_no: string; + quantity?: number; + user_code: string; + operator?: string; + operator_id?: string | number | null; + process_id: string | number; + step_id: string | number; + status_id?: string | number | null; + scan_result: number; + scan_time?: number; + scan_date?: string; + remark?: string | null; + process?: { + id?: string | number; + process_name?: string; + }; + step?: { + id?: string | number; + step_name?: string; + }; + status?: { + id?: string | number; + status_name?: string; + }; + created_name?: string; + created_date?: string; +} + +export const ProduceSpecificRuleService = { + ...BaseService({ baseUrl: "/produce/specific-rule" }), +}; + +export const ProduceProductRecordService = { + ...BaseService({ + baseUrl: "/produce/product-record", + }), +}; + +export const ProduceScanDataService = { + ...BaseService({ baseUrl: "/produce/scan-data" }), +}; + +export interface ProduceExceptionSettingRow { + id?: string | number; + parent_id?: string | number | null; + parent_name?: string; + setting_type: "type" | "reason" | "issue"; + code: string; + name: string; + sort?: number; + is_enabled?: number; + remark?: string | null; + created_name?: string; + created_date?: string; +} + +export interface ProduceExceptionRecordRow { + id?: string | number; + order_no: string; + user_code?: string | null; + reporter?: string | null; + type_id?: string | number | null; + type_code: string; + type_name: string; + reason_id?: string | number | null; + reason_code: string; + reason_name: string; + issue_id?: string | number | null; + issue_code?: string | null; + issue_name?: string | null; + images?: string[] | null; + remark?: string | null; + status?: number; + status_name?: string; + handler?: string | null; + handler_id?: string | number | null; + handled_at?: number | null; + handled_date?: string | null; + reported_at?: number | null; + reported_date?: string | null; + created_name?: string; + created_date?: string; +} + +export const ProduceExceptionSettingService = { + ...BaseService({ + baseUrl: "/produce/exception-setting", + }), +}; + +export interface ProduceRepairSettingRow { + id?: string | number; + parent_id?: string | number | null; + parent_name?: string; + setting_type: "type" | "reason" | "issue"; + code: string; + name: string; + sort?: number; + is_enabled?: number; + remark?: string | null; + created_name?: string; + created_date?: string; +} + +export interface ProduceHardwareFaultDescriptionRow { + id?: string | number; + category_id: string | number; + category_name?: string; + description: string; + use_count?: number; + is_enabled?: number; + remark?: string | null; + created_date?: string; +} + +export interface ProduceRepairRecordRow { + id?: string | number; + repair_no: string; + exception_id?: string | number | null; + exception_reason?: string | null; + exception_issue?: string | null; + exception_type_name?: string | null; + order_no?: string | null; + product_code?: string | null; + product_name?: string | null; + category_id?: string | number | null; + category_name?: string | null; + fault_description_id?: string | number | null; + fault_description_ids?: Array; + fault_description?: string | null; + fault_descriptions?: string[]; + repair_result?: string | null; + parts?: Array> | string | null; + responsible_operators?: Array> | string | null; + operator?: string | null; + repaired_at?: number | null; + repaired_date?: string | null; + remark?: string | null; + created_date?: string; +} + +export const ProduceRepairSettingService = { + ...BaseService({ + baseUrl: "/produce/repair-setting", + }), +}; + +export const ProduceHardwareFaultDescriptionService = { + ...BaseService({ + baseUrl: "/produce/hardware-fault-description", + }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + importExcel(data: FormData) { + return request.post<{ created: number; updated: number }>({ + url: "/produce/hardware-fault-description/import", + data, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, +}; + +export const ProduceRepairRecordService = { + ...BaseService({ baseUrl: "/produce/repair-record" }), + categoryOptions(params: Record = {}) { + return request.get({ + url: "/produce/material-category", + params: { format: "select", ...params }, + }); + }, + faultDescriptionOptions(params: Record = {}) { + return ProduceHardwareFaultDescriptionService.list({ + limit: 999, + is_enabled: 1, + ...params, + }); + }, +}; + +export const ProduceExceptionRecordService = { + ...BaseService({ + baseUrl: "/produce/exception-record", + }), +}; + +export const ProduceConfigApi = { + info(params: Record) { + return request.get({ url: "/produce/config/info", params }); + }, + save(data: Record) { + return request.post({ url: "/produce/config/save", data }); + }, +}; + +export default { + materialCategoryList(params = {}) { + return ProduceMaterialCategoryService.list(params as Record); + }, + materialList(params = {}) { + return ProduceMaterialService.list(params as Record); + }, + processList(params = {}) { + return ProduceProcessService.list(params as Record); + }, + processStepList(params = {}) { + return ProduceProcessStepService.list(params as Record); + }, + stepStatusList(params = {}) { + return ProduceStepStatusService.list(params as Record); + }, + stepRelationList(params = {}) { + return ProduceStepRelationService.list(params as Record); + }, + statusTransitionList(params = {}) { + return ProduceStatusTransitionService.list(params as Record); + }, + transitionLogList(params = {}) { + return ProduceTransitionLogService.list(params as Record); + }, + specificRuleList(params = {}) { + return ProduceSpecificRuleService.list(params as Record); + }, + productRecordList(params = {}) { + return ProduceProductRecordService.list(params as Record); + }, + scanDataList(params = {}) { + return ProduceScanDataService.list(params as Record); + }, + exceptionRecordList(params = {}) { + return ProduceExceptionRecordService.list(params as Record); + }, + exceptionSettingList(params = {}) { + return ProduceExceptionSettingService.list(params as Record); + }, + repairSettingList(params = {}) { + return ProduceRepairSettingService.list(params as Record); + }, + hardwareFaultDescriptionList(params = {}) { + return ProduceHardwareFaultDescriptionService.list( + params as Record, + ); + }, + repairRecordList(params = {}) { + return ProduceRepairRecordService.list(params as Record); + }, +}; diff --git a/frontend/admin/src/apps/produce/api/statistics.ts b/frontend/admin/src/apps/produce/api/statistics.ts new file mode 100644 index 0000000..fd84d75 --- /dev/null +++ b/frontend/admin/src/apps/produce/api/statistics.ts @@ -0,0 +1,50 @@ +import request from '@/utils/http' + +export interface WorkStatisticsQuery { + month?: string + date?: string + data_type?: string + process_id?: string | number + step_id?: string | number + scan_result?: string | number + user_code?: string + operator?: string + process_name?: string + step_name?: string +} + +export interface WorkStatisticsColumn { + key: string + label: string +} + +export interface WorkStatisticsOption { + label: string + value: string | number +} + +export interface WorkStatisticsResponse { + columns: WorkStatisticsColumn[] + items: Array> +} + +export const ProduceWorkStatisticsService = { + monthly(params: WorkStatisticsQuery = {}) { + return request.get({ url: '/produce/statistics/monthly', params }) + }, + daily(params: WorkStatisticsQuery = {}) { + return request.get({ url: '/produce/statistics/daily', params }) + }, + getProcessOptions() { + return request.get({ + url: '/produce/process', + params: { format: 'select' } + }) + }, + getStepOptions(processId?: string | number) { + return request.get({ + url: '/produce/process-step', + params: { format: 'select', process_id: processId || undefined } + }) + } +} diff --git a/frontend/admin/src/apps/produce/lang/en/en.json b/frontend/admin/src/apps/produce/lang/en/en.json new file mode 100644 index 0000000..c3442b5 --- /dev/null +++ b/frontend/admin/src/apps/produce/lang/en/en.json @@ -0,0 +1,217 @@ +{ + "produce": { + "common": { + "columns": { + "index": "No." + } + }, + "modules": { + "process": { + "title": "Process Management", + "search": { + "process_code": "Process Code", + "process_code_placeholder": "Please enter process code", + "process_name": "Process Name", + "process_name_placeholder": "Please enter process name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "process_code_placeholder": "Please enter process code", + "process_name_placeholder": "Please enter process name", + "description": "Process Description", + "description_placeholder": "Please enter process description" + }, + "columns": { + "process_code": "Process Code", + "process_name": "Process Name", + "step_count": "Step Count", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "process_step": { + "title": "Process Steps", + "search": { + "process_id": "Process", + "process_id_placeholder": "Please select process", + "step_code": "Step Code", + "step_code_placeholder": "Please enter step code", + "step_name": "Step Name", + "step_name_placeholder": "Please enter step name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "process_id": "Process", + "process_id_placeholder": "Please select process", + "step_code_placeholder": "Please enter step code", + "step_name_placeholder": "Please enter step name", + "description": "Step Description", + "description_placeholder": "Please enter step description" + }, + "columns": { + "process_name": "Process", + "step_code": "Step Code", + "step_name": "Step Name", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "step_status": { + "title": "Step Status", + "search": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "status_code": "Status Code", + "status_code_placeholder": "Please enter status code", + "status_name": "Status Name", + "status_name_placeholder": "Please enter status name", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "status_code_placeholder": "Please enter status code", + "status_name_placeholder": "Please enter status name", + "description": "Status Description", + "description_placeholder": "Please enter status description" + }, + "columns": { + "step_name": "Step", + "status_code": "Status Code", + "status_name": "Status Name", + "is_initial": "Initial", + "is_final": "Final", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "step_relation": { + "title": "Step Relation", + "search": { + "from_step_id": "From Step", + "from_step_id_placeholder": "Please select from step", + "to_step_id": "To Step", + "to_step_id_placeholder": "Please select to step", + "relation_type": "Relation Type", + "relation_type_placeholder": "Please select relation type", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "from_step_id": "From Step", + "from_step_id_placeholder": "Please select from step", + "to_step_id": "To Step", + "to_step_id_placeholder": "Please select to step", + "flow_condition": "Flow Condition", + "flow_condition_placeholder": "Please enter JSON flow condition", + "description": "Relation Description", + "description_placeholder": "Please enter relation description" + }, + "columns": { + "from_step_name": "From Step", + "to_step_name": "To Step", + "relation_type": "Relation Type", + "priority": "Priority", + "flow_condition": "Flow Condition", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "status_transition": { + "title": "Status Transition", + "search": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "trigger_event": "Trigger Event", + "trigger_event_placeholder": "Please select trigger event", + "status": "Status", + "status_placeholder": "Please select status" + }, + "form": { + "step_id": "Step", + "step_id_placeholder": "Please select step", + "from_status_id": "From Status", + "from_status_id_placeholder": "Please select from status", + "to_status_id": "To Status", + "to_status_id_placeholder": "Please select to status", + "trigger_event_placeholder": "Please select trigger event", + "side_effect_type": "Side Effect Type", + "side_effect_type_placeholder": "Please select side effect type", + "event_config": "Event Config", + "event_config_placeholder": "Please enter JSON event config", + "side_effect_config": "Side Effect Config", + "side_effect_config_placeholder": "Please enter JSON side effect config", + "description": "Transition Description", + "description_placeholder": "Please enter transition description" + }, + "columns": { + "step_name": "Step", + "from_status_name": "From Status", + "to_status_name": "To Status", + "trigger_event": "Trigger Event", + "transition_type": "Transition Type", + "auto_trigger": "Auto Trigger", + "priority": "Priority", + "sort": "Sort", + "status": "Status", + "created_at": "Created At" + } + }, + "transition_log": { + "title": "Transition Log", + "search": { + "work_order_no": "Work Order No.", + "work_order_no_placeholder": "Please enter work order no", + "step_id": "Step", + "step_id_placeholder": "Please select step", + "trigger_event": "Trigger Event", + "trigger_event_placeholder": "Please select trigger event", + "trigger_user": "Trigger User", + "trigger_user_placeholder": "Please enter trigger user", + "is_success": "Result", + "is_success_placeholder": "Please select result" + }, + "columns": { + "work_order_no": "Work Order No", + "step_name": "Step", + "from_status_name": "From Status", + "to_status_name": "To Status", + "transition_event": "Transition Rule", + "trigger_event": "Trigger Event", + "trigger_user": "Trigger User", + "trigger_ip": "Trigger IP", + "is_auto": "Auto Trigger", + "is_success": "Result", + "duration": "Duration(ms)", + "trigger_data": "Trigger Data", + "error_message": "Error Message", + "remark": "Remark", + "created_name": "Created By", + "created_at": "Created At" + } + }, + "config": { + "title": "Production Config" + }, + "scan_data": { + "title": "Scan Data" + }, + "exception_data": { + "title": "Exception Records" + }, + "exception_setting": { + "title": "Exception Settings" + }, + "exception_analysis": { + "title": "Exception Analysis" + } + } + } +} diff --git a/frontend/admin/src/apps/produce/lang/zh-cn/zh.json b/frontend/admin/src/apps/produce/lang/zh-cn/zh.json new file mode 100644 index 0000000..b3cf285 --- /dev/null +++ b/frontend/admin/src/apps/produce/lang/zh-cn/zh.json @@ -0,0 +1,217 @@ +{ + "produce": { + "common": { + "columns": { + "index": "序号" + } + }, + "modules": { + "process": { + "title": "工序管理", + "search": { + "process_code": "工序编码", + "process_code_placeholder": "请输入工序编码", + "process_name": "工序名称", + "process_name_placeholder": "请输入工序名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "process_code_placeholder": "请输入工序编码", + "process_name_placeholder": "请输入工序名称", + "description": "工序描述", + "description_placeholder": "请输入工序描述" + }, + "columns": { + "process_code": "工序编码", + "process_name": "工序名称", + "step_count": "步骤数", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "process_step": { + "title": "工序步骤", + "search": { + "process_id": "所属工序", + "process_id_placeholder": "请选择所属工序", + "step_code": "步骤编码", + "step_code_placeholder": "请输入步骤编码", + "step_name": "步骤名称", + "step_name_placeholder": "请输入步骤名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "process_id": "所属工序", + "process_id_placeholder": "请选择所属工序", + "step_code_placeholder": "请输入步骤编码", + "step_name_placeholder": "请输入步骤名称", + "description": "步骤描述", + "description_placeholder": "请输入步骤描述" + }, + "columns": { + "process_name": "所属工序", + "step_code": "步骤编码", + "step_name": "步骤名称", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "step_status": { + "title": "步骤状态", + "search": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "status_code": "状态编码", + "status_code_placeholder": "请输入状态编码", + "status_name": "状态名称", + "status_name_placeholder": "请输入状态名称", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "status_code_placeholder": "请输入状态编码", + "status_name_placeholder": "请输入状态名称", + "description": "状态描述", + "description_placeholder": "请输入状态描述" + }, + "columns": { + "step_name": "所属步骤", + "status_code": "状态编码", + "status_name": "状态名称", + "is_initial": "初始状态", + "is_final": "终态", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "step_relation": { + "title": "步骤关系", + "search": { + "from_step_id": "源步骤", + "from_step_id_placeholder": "请选择源步骤", + "to_step_id": "目标步骤", + "to_step_id_placeholder": "请选择目标步骤", + "relation_type": "关系类型", + "relation_type_placeholder": "请选择关系类型", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "from_step_id": "源步骤", + "from_step_id_placeholder": "请选择源步骤", + "to_step_id": "目标步骤", + "to_step_id_placeholder": "请选择目标步骤", + "flow_condition": "流转条件", + "flow_condition_placeholder": "请输入 JSON 格式流转条件", + "description": "关系描述", + "description_placeholder": "请输入关系描述" + }, + "columns": { + "from_step_name": "源步骤", + "to_step_name": "目标步骤", + "relation_type": "关系类型", + "priority": "优先级", + "flow_condition": "流转条件", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "status_transition": { + "title": "状态转换", + "search": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "trigger_event": "触发事件", + "trigger_event_placeholder": "请选择触发事件", + "status": "状态", + "status_placeholder": "请选择状态" + }, + "form": { + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "from_status_id": "源状态", + "from_status_id_placeholder": "请选择源状态", + "to_status_id": "目标状态", + "to_status_id_placeholder": "请选择目标状态", + "trigger_event_placeholder": "请选择触发事件", + "side_effect_type": "副作用类型", + "side_effect_type_placeholder": "请选择副作用类型", + "event_config": "事件配置", + "event_config_placeholder": "请输入 JSON 格式事件配置", + "side_effect_config": "副作用配置", + "side_effect_config_placeholder": "请输入 JSON 格式副作用配置", + "description": "转换描述", + "description_placeholder": "请输入转换描述" + }, + "columns": { + "step_name": "所属步骤", + "from_status_name": "源状态", + "to_status_name": "目标状态", + "trigger_event": "触发事件", + "transition_type": "转换类型", + "auto_trigger": "自动触发", + "priority": "优先级", + "sort": "排序", + "status": "状态", + "created_at": "创建时间" + } + }, + "transition_log": { + "title": "转换日志", + "search": { + "work_order_no": "工单编号", + "work_order_no_placeholder": "请输入工单编号", + "step_id": "所属步骤", + "step_id_placeholder": "请选择所属步骤", + "trigger_event": "触发事件", + "trigger_event_placeholder": "请选择触发事件", + "trigger_user": "触发用户", + "trigger_user_placeholder": "请输入触发用户", + "is_success": "执行结果", + "is_success_placeholder": "请选择执行结果" + }, + "columns": { + "work_order_no": "工单编号", + "step_name": "所属步骤", + "from_status_name": "源状态", + "to_status_name": "目标状态", + "transition_event": "转换规则", + "trigger_event": "触发事件", + "trigger_user": "触发用户", + "trigger_ip": "触发IP", + "is_auto": "自动触发", + "is_success": "执行结果", + "duration": "耗时(ms)", + "trigger_data": "触发数据", + "error_message": "错误信息", + "remark": "备注", + "created_name": "创建人", + "created_at": "创建时间" + } + }, + "config": { + "title": "生产配置" + }, + "scan_data": { + "title": "扫码数据" + }, + "exception_data": { + "title": "异常数据" + }, + "exception_setting": { + "title": "异常设置" + }, + "exception_analysis": { + "title": "异常数据分析" + } + } + } +} diff --git a/frontend/admin/src/apps/produce/menu/admin.php b/frontend/admin/src/apps/produce/menu/admin.php new file mode 100644 index 0000000..02598e3 --- /dev/null +++ b/frontend/admin/src/apps/produce/menu/admin.php @@ -0,0 +1,134 @@ + '生产管理', + 'code' => 'produce', + 'app' => 'produce', + 'path' => '/produce', + 'icon' => 'plugin', + 'children' => [ + [ + 'name' => '生产配置', + 'code' => 'produce:config:index', + 'app' => 'produce', + 'path' => '/produce/config', + 'component' => '/produce/config/index', + ], + [ + 'name' => '工序管理', + 'code' => 'produce:process:index', + 'app' => 'produce', + 'path' => '/produce/process', + 'component' => '/produce/process/index', + ], + [ + 'name' => '工序步骤', + 'code' => 'produce:process-step:index', + 'app' => 'produce', + 'path' => '/produce/process-step', + 'component' => '/produce/process-step/index', + ], + [ + 'name' => '步骤状态', + 'code' => 'produce:step-status:index', + 'app' => 'produce', + 'path' => '/produce/step-status', + 'component' => '/produce/step-status/index', + ], + [ + 'name' => '步骤关系', + 'code' => 'produce:step-relation:index', + 'app' => 'produce', + 'path' => '/produce/step-relation', + 'component' => '/produce/step-relation/index', + ], + [ + 'name' => '状态转换', + 'code' => 'produce:status-transition:index', + 'app' => 'produce', + 'path' => '/produce/status-transition', + 'component' => '/produce/status-transition/index', + ], + [ + 'name' => '整机数据', + 'code' => 'produce:product-record:index', + 'app' => 'produce', + 'path' => '/produce/product-record', + 'component' => '/produce/product-record/index', + ], + [ + 'name' => '扫码数据', + 'code' => 'produce:scan-data:index', + 'app' => 'produce', + 'path' => '/produce/scan-data', + 'component' => '/produce/scan-data/index', + ], + [ + 'name' => '转换日志', + 'code' => 'produce:transition-log:index', + 'app' => 'produce', + 'path' => '/produce/transition-log', + 'component' => '/produce/transition-log/index', + ], + [ + 'name' => '异常管理', + 'code' => 'produce:exception:index', + 'app' => 'produce', + 'path' => '/produce/exception', + 'icon' => 'warning', + 'children' => [ + [ + 'name' => '异常数据', + 'code' => 'produce:exception-data:list', + 'app' => 'produce', + 'path' => '/produce/exception-data', + 'component' => '/produce/exception/data/index', + ], + [ + 'name' => '异常设置', + 'code' => 'produce:exception-setting:list', + 'app' => 'produce', + 'path' => '/produce/exception-setting', + 'component' => '/produce/exception/setting/index', + ], + [ + 'name' => '数据分析', + 'code' => 'produce:exception-analysis:read', + 'app' => 'produce', + 'path' => '/produce/exception-analysis', + 'component' => '/produce/exception/analysis/index', + ], + ], + ], + [ + 'name' => '数据统计', + 'code' => 'produce:statistics:index', + 'app' => 'produce', + 'path' => '/produce/statistics', + 'icon' => 'bar-chart-2', + 'children' => [ + [ + 'name' => '月工作统计', + 'code' => 'produce:statistics:monthly', + 'app' => 'produce', + 'path' => '/produce/statistics/monthly', + 'component' => '/produce/statistics/monthly/index', + ], + [ + 'name' => '日工作统计', + 'code' => 'produce:statistics:daily', + 'app' => 'produce', + 'path' => '/produce/statistics/daily', + 'component' => '/produce/statistics/daily/index', + ], + ], + ], + ], + ], +]; diff --git a/frontend/admin/src/apps/produce/routes/index.ts b/frontend/admin/src/apps/produce/routes/index.ts new file mode 100644 index 0000000..135eafe --- /dev/null +++ b/frontend/admin/src/apps/produce/routes/index.ts @@ -0,0 +1,215 @@ +import { AppRouteRecord } from "@/types/router"; +import { RoutesAlias } from "@/router/routesAlias"; + +export default { + path: "/produce", + name: "Produce", + component: RoutesAlias.Layout, + meta: { + title: "生产管理", + icon: "ri:plugin-line", + module: "produce", + }, + children: [ + { + path: "material-category", + name: "ProduceMaterialCategory", + component: "/produce/material-category/index", + meta: { + title: "物料分类", + icon: "ri:folder-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "material", + name: "ProduceMaterial", + component: "/produce/material/index", + meta: { + title: "物料库", + icon: "ri:archive-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "config", + name: "ProduceConfig", + component: "/produce/config/index", + meta: { + title: "生产配置", + icon: "ri:settings-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "process", + name: "ProduceProcess", + component: "/produce/process/index", + meta: { + title: "工序管理", + icon: "ri:stack-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "process-step", + name: "ProduceProcessStep", + component: "/produce/process-step/index", + meta: { + title: "工序步骤", + icon: "ri:list-check-3", + keepAlive: false, + module: "produce", + }, + }, + { + path: "step-status", + name: "ProduceStepStatus", + component: "/produce/step-status/index", + meta: { + title: "步骤状态", + icon: "ri:flag-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "step-relation", + name: "ProduceStepRelation", + component: "/produce/step-relation/index", + meta: { + title: "步骤关系", + icon: "ri:git-branch-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "status-transition", + name: "ProduceStatusTransition", + component: "/produce/status-transition/index", + meta: { + title: "状态转换", + icon: "ri:share-forward-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "product-record", + name: "ProduceProductRecord", + component: "/produce/product-record/index", + meta: { + title: "整机数据", + icon: "ri-product-hunt-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "scan-data", + name: "ProduceScanData", + component: "/produce/scan-data/index", + meta: { + title: "扫码数据", + icon: "ri:qr-scan-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "transition-log", + name: "ProduceTransitionLog", + component: "/produce/transition-log/index", + meta: { + title: "转换日志", + icon: "ri:file-list-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "repair", + name: "ProduceRepair", + meta: { + title: "维修管理", + icon: "ri:tools-line", + keepAlive: false, + module: "produce", + }, + children: [ + { + path: "repair-data", + name: "ProduceRepairData", + component: "/produce/repair/repair-data/index", + meta: { + title: "维修数据", + icon: "ri:file-list-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "repair-setting", + name: "ProduceRepairSetting", + component: "/produce/repair/repair-setting/index", + meta: { + title: "维修设置", + icon: "ri:settings-3-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "hardware-fault-description", + name: "ProduceHardwareFaultDescription", + component: "/produce/repair/hardware-fault-description/index", + meta: { + title: "硬件故障描述", + icon: "ri:tools-line", + keepAlive: false, + module: "produce", + }, + }, + ], + }, + { + path: "statistics", + name: "ProduceStatistics", + meta: { + title: "数据统计", + icon: "ri:bar-chart-2-line", + keepAlive: false, + module: "produce", + }, + children: [ + { + path: "monthly", + name: "ProduceMonthlyWorkStatistics", + component: "/produce/statistics/monthly/index", + meta: { + title: "月工作统计", + icon: "ri:calendar-2-line", + keepAlive: false, + module: "produce", + }, + }, + { + path: "daily", + name: "ProduceDailyWorkStatistics", + component: "/produce/statistics/daily/index", + meta: { + title: "日工作统计", + icon: "ri:time-line", + keepAlive: false, + module: "produce", + }, + }, + ], + }, + ], +} as AppRouteRecord; diff --git a/frontend/admin/src/apps/produce/views/produce/config/index.vue b/frontend/admin/src/apps/produce/views/produce/config/index.vue new file mode 100644 index 0000000..648d1cb --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/index.vue @@ -0,0 +1,52 @@ + + + diff --git a/frontend/admin/src/apps/produce/views/produce/config/modules/basic-setting.vue b/frontend/admin/src/apps/produce/views/produce/config/modules/basic-setting.vue new file mode 100644 index 0000000..ea96816 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/modules/basic-setting.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/config/modules/erp-integration.vue b/frontend/admin/src/apps/produce/views/produce/config/modules/erp-integration.vue new file mode 100644 index 0000000..28aa92f --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/modules/erp-integration.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/config/modules/specific-rule.vue b/frontend/admin/src/apps/produce/views/produce/config/modules/specific-rule.vue new file mode 100644 index 0000000..34b58e6 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/modules/specific-rule.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/config/modules/step-rule.vue b/frontend/admin/src/apps/produce/views/produce/config/modules/step-rule.vue new file mode 100644 index 0000000..eb9d66d --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/modules/step-rule.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception-analysis/index.vue b/frontend/admin/src/apps/produce/views/produce/exception-analysis/index.vue new file mode 100644 index 0000000..08326fd --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception-analysis/index.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception-data/index.vue b/frontend/admin/src/apps/produce/views/produce/exception-data/index.vue new file mode 100644 index 0000000..12bdc6d --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception-data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception-data/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/exception-data/schemas/index.tsx new file mode 100644 index 0000000..17a46fd --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception-data/schemas/index.tsx @@ -0,0 +1,187 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionRecordService } from '@/apps/produce/api' + +const statusOptions = [ + { label: '待处理', value: 0 }, + { label: '处理中', value: 1 }, + { label: '已处理', value: 2 } +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionRecordService.list, + dialogTitle: '异常数据', + crudApi: { + add: ProduceExceptionRecordService.create, + edit: ProduceExceptionRecordService.update, + remove: ProduceExceptionRecordService.remove, + view: ProduceExceptionRecordService.get + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-data:create', + editAuth: 'produce:exception-data:update', + removeAuth: 'produce:exception-data:delete', + viewAuth: 'produce:exception-data:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' }, + colSpan: 8 + }, + { + prop: 'type_code', + label: '处理类型', + component: 'Input', + componentProps: { clearable: true, placeholder: '请输入处理类型编码' }, + colSpan: 8 + }, + { + prop: 'status', + label: '处理状态', + component: 'Select', + componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions }, + colSpan: 8 + }, + { + prop: 'reported_at_range', + label: '提报时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期' + }, + colSpan: 12 + } + ] + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'order_no', label: '单号/条码', minWidth: 160 }, + { prop: 'type_name', label: '处理类型', minWidth: 120 }, + { prop: 'reason_name', label: '异常原因', minWidth: 120 }, + { prop: 'issue_name', label: '具体问题', minWidth: 140 }, + { prop: 'reporter', label: '提报人', minWidth: 110 }, + { prop: 'status_name', label: '处理状态', minWidth: 100 }, + { prop: 'handler', label: '处理人', minWidth: 110 }, + { prop: 'reported_date', label: '提报时间', minWidth: 170 }, + { prop: 'handled_date', label: '处理时间', minWidth: 170 } + ], + dialogProps: { width: '820px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Input', + prop: 'order_no', + label: '单号/条码', + componentProps: { placeholder: '请输入单号或条码', maxlength: 100 }, + rules: [{ required: true, message: '请输入单号或条码' }], + colSpan: 12 + }, + { + component: 'Input', + prop: 'user_code', + label: '提报人工号', + componentProps: { placeholder: '请输入提报人工号', maxlength: 50 }, + colSpan: 12 + }, + { + component: 'Input', + prop: 'reporter', + label: '提报人', + componentProps: { placeholder: '请输入提报人', maxlength: 100 }, + colSpan: 12 + }, + { + component: 'Input', + prop: 'type_code', + label: '处理类型编码', + componentProps: { placeholder: '例如 defective_repair', maxlength: 64 }, + rules: [{ required: true, message: '请输入处理类型编码' }], + colSpan: 12 + }, + { + component: 'Input', + prop: 'type_name', + label: '处理类型', + componentProps: { placeholder: '例如 不良维修', maxlength: 100 }, + rules: [{ required: true, message: '请输入处理类型' }], + colSpan: 12 + }, + { + component: 'Input', + prop: 'reason_code', + label: '异常原因编码', + componentProps: { placeholder: '例如 test_defect', maxlength: 64 }, + rules: [{ required: true, message: '请输入异常原因编码' }], + colSpan: 12 + }, + { + component: 'Input', + prop: 'reason_name', + label: '异常原因', + componentProps: { placeholder: '例如 测试不良', maxlength: 100 }, + rules: [{ required: true, message: '请输入异常原因' }], + colSpan: 12 + }, + { + component: 'Input', + prop: 'issue_code', + label: '具体问题编码', + componentProps: { placeholder: '可选', maxlength: 64 }, + colSpan: 12 + }, + { + component: 'Input', + prop: 'issue_name', + label: '具体问题', + componentProps: { placeholder: '可选', maxlength: 100 }, + colSpan: 12 + }, + { + component: 'Select', + prop: 'status', + label: '处理状态', + defaultValue: 0, + componentProps: { options: statusOptions }, + colSpan: 12 + }, + { + component: 'Input', + prop: 'handler', + label: '处理人', + componentProps: { placeholder: '请输入处理人', maxlength: 100 }, + colSpan: 12 + }, + { + component: 'Upload', + prop: 'images', + label: '现场图片', + componentProps: { listType: 'picture-card' }, + ifDetail: true, + colSpan: 24 + }, + { + component: 'Input', + prop: 'remark', + label: '补充说明', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, + colSpan: 24 + } + ] + } +}) diff --git a/frontend/admin/src/apps/produce/views/produce/exception-setting/index.vue b/frontend/admin/src/apps/produce/views/produce/exception-setting/index.vue new file mode 100644 index 0000000..a4a5e48 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception-setting/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception-setting/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/exception-setting/schemas/index.tsx new file mode 100644 index 0000000..fac58e4 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception-setting/schemas/index.tsx @@ -0,0 +1,100 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionSettingService } from '@/apps/produce/api' + +const settingTypeOptions = [ + { label: '处理类型', value: 'type' }, + { label: '异常原因', value: 'reason' }, + { label: '具体问题', value: 'issue' }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionSettingService.list, + dialogTitle: '异常设置', + crudApi: { + add: ProduceExceptionSettingService.create, + edit: ProduceExceptionSettingService.update, + remove: ProduceExceptionSettingService.remove, + view: ProduceExceptionSettingService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-setting:create', + editAuth: 'produce:exception-setting:update', + removeAuth: 'produce:exception-setting:delete', + viewAuth: 'produce:exception-setting:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', label: '关键字', component: 'Input', + componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8, + }, + { + prop: 'setting_type', label: '设置层级', component: 'Select', + componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8, + }, + { + prop: 'is_enabled', label: '启用状态', component: 'Select', + componentProps: { + clearable: true, placeholder: '请选择启用状态', + options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }], + }, colSpan: 8, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' }, + { prop: 'code', label: '编码', minWidth: 150 }, + { prop: 'name', label: '名称', minWidth: 160 }, + { prop: 'parent_name', label: '所属上级', minWidth: 140 }, + { prop: 'sort', label: '排序', width: 90 }, + { prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' }, + { prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true }, + { prop: 'created_name', label: '创建人', minWidth: 110 }, + ], + dialogProps: { width: '700px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Select', prop: 'setting_type', label: '设置层级', + componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions }, + rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12, + }, + { + component: 'ApiSelect', prop: 'parent_id', label: '所属上级', + componentProps: { + api: '/produce/exception-setting', requestMethod: 'get', + params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级', + }, colSpan: 12, + }, + { + component: 'Input', prop: 'code', label: '设置编码', + componentProps: { placeholder: '请输入唯一编码', maxlength: 64 }, + rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'name', label: '设置名称', + componentProps: { placeholder: '请输入显示名称', maxlength: 100 }, + rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12, + }, + { component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 }, + { + component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1, + componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12, + }, + { + component: 'Input', prop: 'remark', label: '备注', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24, + }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/exception/analysis/index.vue b/frontend/admin/src/apps/produce/views/produce/exception/analysis/index.vue new file mode 100644 index 0000000..08326fd --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception/analysis/index.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception/data/index.vue b/frontend/admin/src/apps/produce/views/produce/exception/data/index.vue new file mode 100644 index 0000000..12bdc6d --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception/data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception/data/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/exception/data/schemas/index.tsx new file mode 100644 index 0000000..3d4630e --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception/data/schemas/index.tsx @@ -0,0 +1,127 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionRecordService } from '@/apps/produce/api' + +const statusOptions = [ + { label: '待处理', value: 0 }, + { label: '处理中', value: 1 }, + { label: '已处理', value: 2 }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionRecordService.list, + dialogTitle: '异常数据', + crudApi: { + add: ProduceExceptionRecordService.create, + edit: ProduceExceptionRecordService.update, + remove: ProduceExceptionRecordService.remove, + view: ProduceExceptionRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-data:create', + editAuth: 'produce:exception-data:update', + removeAuth: 'produce:exception-data:delete', + viewAuth: 'produce:exception-data:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' }, + colSpan: 8, + }, + { + prop: 'type_code', + label: '处理类型', + component: 'Input', + componentProps: { clearable: true, placeholder: '请输入处理类型编码' }, + colSpan: 8, + }, + { + prop: 'status', + label: '处理状态', + component: 'Select', + componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions }, + colSpan: 8, + }, + { + prop: 'reported_at_range', + label: '提报时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'order_no', label: '单号/条码', minWidth: 160 }, + { prop: 'type_name', label: '处理类型', minWidth: 120 }, + { prop: 'reason_name', label: '异常原因', minWidth: 120 }, + { prop: 'issue_name', label: '具体问题', minWidth: 140 }, + { prop: 'reporter', label: '提报人', minWidth: 110 }, + { prop: 'status_name', label: '处理状态', minWidth: 100 }, + { prop: 'handler', label: '处理人', minWidth: 110 }, + { prop: 'reported_date', label: '提报时间', minWidth: 170 }, + { prop: 'handled_date', label: '处理时间', minWidth: 170 }, + ], + dialogProps: { width: '820px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Input', prop: 'order_no', label: '单号/条码', + componentProps: { placeholder: '请输入单号或条码', maxlength: 100 }, + rules: [{ required: true, message: '请输入单号或条码' }], colSpan: 12, + }, + { component: 'Input', prop: 'user_code', label: '提报人工号', componentProps: { placeholder: '请输入提报人工号', maxlength: 50 }, colSpan: 12 }, + { component: 'Input', prop: 'reporter', label: '提报人', componentProps: { placeholder: '请输入提报人', maxlength: 100 }, colSpan: 12 }, + { + component: 'Input', prop: 'type_code', label: '处理类型编码', + componentProps: { placeholder: '例如 defective_repair', maxlength: 64 }, + rules: [{ required: true, message: '请输入处理类型编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'type_name', label: '处理类型', + componentProps: { placeholder: '例如 不良维修', maxlength: 100 }, + rules: [{ required: true, message: '请输入处理类型' }], colSpan: 12, + }, + { + component: 'Input', prop: 'reason_code', label: '异常原因编码', + componentProps: { placeholder: '例如 test_defect', maxlength: 64 }, + rules: [{ required: true, message: '请输入异常原因编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'reason_name', label: '异常原因', + componentProps: { placeholder: '例如 测试不良', maxlength: 100 }, + rules: [{ required: true, message: '请输入异常原因' }], colSpan: 12, + }, + { component: 'Input', prop: 'issue_code', label: '具体问题编码', componentProps: { placeholder: '可选', maxlength: 64 }, colSpan: 12 }, + { component: 'Input', prop: 'issue_name', label: '具体问题', componentProps: { placeholder: '可选', maxlength: 100 }, colSpan: 12 }, + { component: 'Select', prop: 'status', label: '处理状态', defaultValue: 0, componentProps: { options: statusOptions }, colSpan: 12 }, + { component: 'Input', prop: 'handler', label: '处理人', componentProps: { placeholder: '请输入处理人', maxlength: 100 }, colSpan: 12 }, + { + component: 'Upload', prop: 'images', label: '现场图片', componentProps: { listType: 'picture-card' }, ifDetail: true, colSpan: 24, + }, + { + component: 'Input', prop: 'remark', label: '补充说明', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24, + }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/exception/setting/index.vue b/frontend/admin/src/apps/produce/views/produce/exception/setting/index.vue new file mode 100644 index 0000000..a4a5e48 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception/setting/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/exception/setting/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/exception/setting/schemas/index.tsx new file mode 100644 index 0000000..fac58e4 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/exception/setting/schemas/index.tsx @@ -0,0 +1,100 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceExceptionSettingService } from '@/apps/produce/api' + +const settingTypeOptions = [ + { label: '处理类型', value: 'type' }, + { label: '异常原因', value: 'reason' }, + { label: '具体问题', value: 'issue' }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceExceptionSettingService.list, + dialogTitle: '异常设置', + crudApi: { + add: ProduceExceptionSettingService.create, + edit: ProduceExceptionSettingService.update, + remove: ProduceExceptionSettingService.remove, + view: ProduceExceptionSettingService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:exception-setting:create', + editAuth: 'produce:exception-setting:update', + removeAuth: 'produce:exception-setting:delete', + viewAuth: 'produce:exception-setting:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', label: '关键字', component: 'Input', + componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8, + }, + { + prop: 'setting_type', label: '设置层级', component: 'Select', + componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8, + }, + { + prop: 'is_enabled', label: '启用状态', component: 'Select', + componentProps: { + clearable: true, placeholder: '请选择启用状态', + options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }], + }, colSpan: 8, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' }, + { prop: 'code', label: '编码', minWidth: 150 }, + { prop: 'name', label: '名称', minWidth: 160 }, + { prop: 'parent_name', label: '所属上级', minWidth: 140 }, + { prop: 'sort', label: '排序', width: 90 }, + { prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' }, + { prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true }, + { prop: 'created_name', label: '创建人', minWidth: 110 }, + ], + dialogProps: { width: '700px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Select', prop: 'setting_type', label: '设置层级', + componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions }, + rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12, + }, + { + component: 'ApiSelect', prop: 'parent_id', label: '所属上级', + componentProps: { + api: '/produce/exception-setting', requestMethod: 'get', + params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级', + }, colSpan: 12, + }, + { + component: 'Input', prop: 'code', label: '设置编码', + componentProps: { placeholder: '请输入唯一编码', maxlength: 64 }, + rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'name', label: '设置名称', + componentProps: { placeholder: '请输入显示名称', maxlength: 100 }, + rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12, + }, + { component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 }, + { + component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1, + componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12, + }, + { + component: 'Input', prop: 'remark', label: '备注', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24, + }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/material-category/index.vue b/frontend/admin/src/apps/produce/views/produce/material-category/index.vue new file mode 100644 index 0000000..e424302 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/material-category/index.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/material-category/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/material-category/schemas/index.tsx new file mode 100644 index 0000000..5282377 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/material-category/schemas/index.tsx @@ -0,0 +1,42 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceMaterialCategoryService } from '../../../../api' + +export const crudSchema = (): CrudSchema => ({ + api: ProduceMaterialCategoryService.list, + dialogTitle: '物料分类', + crudApi: { + add: ProduceMaterialCategoryService.create, + edit: ProduceMaterialCategoryService.update, + remove: ProduceMaterialCategoryService.remove, + view: ProduceMaterialCategoryService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:material-category:create', + editAuth: 'produce:material-category:update', + removeAuth: 'produce:material-category:delete', + viewAuth: 'produce:material-category:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [{ prop: 'LIKE_category_name', label: '分类名称', component: 'Input', componentProps: { clearable: true, placeholder: '请输入分类名称' }, colSpan: 8 }], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'category_name', label: '分类名称', minWidth: 220 }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + ], + dialogProps: { width: '520px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '90px', + schema: [ + { component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' }, + { component: 'Input', prop: 'category_name', label: '分类名称', componentProps: { maxlength: 100, placeholder: '请输入分类名称' }, rules: [{ required: true, message: '请输入分类名称' }], colSpan: 24 }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/material/index.vue b/frontend/admin/src/apps/produce/views/produce/material/index.vue new file mode 100644 index 0000000..ef5768a --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/material/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/material/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/material/schemas/index.tsx new file mode 100644 index 0000000..71e2114 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/material/schemas/index.tsx @@ -0,0 +1,50 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceMaterialService } from '../../../../api' + +export const crudSchema = (): CrudSchema => ({ + api: ProduceMaterialService.list, + dialogTitle: '物料库', + crudApi: { + add: ProduceMaterialService.create, + edit: ProduceMaterialService.update, + remove: ProduceMaterialService.remove, + view: ProduceMaterialService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + hasExport: true, + addAuth: 'produce:material:create', + editAuth: 'produce:material:update', + removeAuth: 'produce:material:delete', + viewAuth: 'produce:material:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 }, + { prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, colSpan: 8 }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'category_name', label: '物料分类', minWidth: 160 }, + { prop: 'product_code', label: '商品编码', minWidth: 180 }, + { prop: 'product_name', label: '商品名称', minWidth: 220 }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + ], + dialogProps: { width: '640px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '90px', + schema: [ + { component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' }, + { component: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'category_name', valueField: 'id', clearable: true, placeholder: '请选择分类' }, rules: [{ required: true, message: '请选择物料分类' }], colSpan: 12 }, + { component: 'Input', prop: 'product_code', label: '商品编码', componentProps: { maxlength: 100, placeholder: '请输入或扫描商品编码' }, rules: [{ required: true, message: '请输入商品编码' }], colSpan: 12 }, + { component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/process-step/index.vue b/frontend/admin/src/apps/produce/views/produce/process-step/index.vue new file mode 100644 index 0000000..3305168 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/process-step/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/process-step/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/process-step/schemas/index.tsx new file mode 100644 index 0000000..487fe0a --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/process-step/schemas/index.tsx @@ -0,0 +1,185 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceProcessStepService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProcessStepService.list, + dialogTitle: $t('produce.modules.process_step.title'), + crudApi: { + add: ProduceProcessStepService.create, + edit: ProduceProcessStepService.update, + remove: ProduceProcessStepService.remove, + view: ProduceProcessStepService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:process-step:create', + editAuth: 'produce:process-step:update', + removeAuth: 'produce:process-step:delete', + viewAuth: 'produce:process-step:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'process_id', + label: $t('produce.modules.process_step.search.process_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.process_step.search.process_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_step_code', + label: $t('produce.modules.process_step.search.step_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process_step.search.step_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_step_name', + label: $t('produce.modules.process_step.search.step_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process_step.search.step_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.process_step.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.process_step.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { + prop: 'process.process_name', + label: $t('produce.modules.process_step.columns.process_name'), + minWidth: 180, + }, + { prop: 'step_code', label: $t('produce.modules.process_step.columns.step_code'), minWidth: 160 }, + { prop: 'step_name', label: $t('produce.modules.process_step.columns.step_name'), minWidth: 180 }, + { prop: 'sort', label: $t('produce.modules.process_step.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.process_step.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.process_step.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '680px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'process_id', + label: $t('produce.modules.process_step.form.process_id'), + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.process_step.form.process_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.process_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_code', + label: $t('produce.modules.process_step.columns.step_code'), + componentProps: { + placeholder: $t('produce.modules.process_step.form.step_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.step_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_name', + label: $t('produce.modules.process_step.columns.step_name'), + componentProps: { + placeholder: $t('produce.modules.process_step.form.step_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.process_step.form.step_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.process_step.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.process_step.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.process_step.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.process_step.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/process/index.vue b/frontend/admin/src/apps/produce/views/produce/process/index.vue new file mode 100644 index 0000000..c2d5b17 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/process/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/process/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/process/schemas/index.tsx new file mode 100644 index 0000000..c662f03 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/process/schemas/index.tsx @@ -0,0 +1,154 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceProcessService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProcessService.list, + dialogTitle: $t('produce.modules.process.title'), + crudApi: { + add: ProduceProcessService.create, + edit: ProduceProcessService.update, + remove: ProduceProcessService.remove, + view: ProduceProcessService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:process:create', + editAuth: 'produce:process:update', + removeAuth: 'produce:process:delete', + viewAuth: 'produce:process:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_process_code', + label: $t('produce.modules.process.search.process_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process.search.process_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_process_name', + label: $t('produce.modules.process.search.process_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.process.search.process_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.process.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.process.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'process_code', label: $t('produce.modules.process.columns.process_code'), minWidth: 160 }, + { prop: 'process_name', label: $t('produce.modules.process.columns.process_name'), minWidth: 180 }, + { prop: 'step_count', label: $t('produce.modules.process.columns.step_count'), width: 120 }, + { prop: 'sort', label: $t('produce.modules.process.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.process.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.process.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '640px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '100px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'process_code', + label: $t('produce.modules.process.columns.process_code'), + componentProps: { + placeholder: $t('produce.modules.process.form.process_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.process.form.process_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'process_name', + label: $t('produce.modules.process.columns.process_name'), + componentProps: { + placeholder: $t('produce.modules.process.form.process_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.process.form.process_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.process.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.process.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.process.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.process.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/product-record/index.vue b/frontend/admin/src/apps/produce/views/produce/product-record/index.vue new file mode 100644 index 0000000..bdd4600 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/product-record/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/product-record/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/product-record/schemas/index.tsx new file mode 100644 index 0000000..ae2abe9 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/product-record/schemas/index.tsx @@ -0,0 +1,249 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceProductRecordService } from '@/apps/produce/api' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceProductRecordService.list, + dialogTitle: '整机数据', + crudApi: { + add: ProduceProductRecordService.create, + edit: ProduceProductRecordService.update, + remove: ProduceProductRecordService.remove, + view: ProduceProductRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:product-record:create', + editAuth: 'produce:product-record:update', + removeAuth: 'produce:product-record:delete', + viewAuth: 'produce:product-record:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入加工单号/商品编码/操作人员/备注', + }, + colSpan: 6, + }, + { + prop: 'process_id', + label: '工序名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: '步骤名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + }, + colSpan: 6, + }, + { + prop: 'status_id', + label: '步骤状态', + component: 'ApiSelect', + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + }, + colSpan: 6, + }, + { + prop: 'scan_result', + label: '扫码结果', + component: 'Select', + componentProps: { + clearable: true, + placeholder: '请选择扫码结果', + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + colSpan: 6, + }, + { + prop: 'created_time_range', + label: '创建时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'process_order_no', label: '加工单号', minWidth: 160 }, + { prop: 'product_code', label: '商品编码', minWidth: 160 }, + { prop: 'quantity', label: '数量', width: 100 }, + { prop: 'process_name', label: '工序名称', minWidth: 140 }, + { prop: 'step_name', label: '步骤名称', minWidth: 140 }, + { prop: 'status_name', label: '步骤状态', minWidth: 120 }, + { prop: 'operator', label: '操作人员', minWidth: 120 }, + { + prop: 'scan_result', + label: '扫码结果', + width: 100, + formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'), + }, + { prop: 'created_date', label: '创建时间', minWidth: 180 }, + { prop: 'created_name', label: '创建人', minWidth: 120 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'process_order_no', + label: '加工单号', + componentProps: { + placeholder: '请输入加工单号', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入加工单号' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'product_code', + label: '商品编码', + componentProps: { + placeholder: '请输入商品编码', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入商品编码' }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'quantity', + label: '数量', + defaultValue: 1, + componentProps: { + min: 1, + precision: 0, + }, + rules: [{ required: true, message: '请输入数量' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'user_code', + label: '用户编码', + componentProps: { + placeholder: '请输入用户编码', + maxlength: 50, + }, + rules: [{ required: true, message: '请输入用户编码' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'operator', + label: '操作人员', + componentProps: { + placeholder: '请输入操作人员', + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'process_name', + label: '工序名称', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'step_name', + label: '步骤名称', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_name', + label: '步骤状态', + componentProps: { + disabled: true, + }, + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'scan_result', + label: '扫码结果', + defaultValue: 1, + componentProps: { + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + rules: [{ required: true, message: '请选择扫码结果' }], + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: '备注', + componentProps: { + type: 'textarea', + rows: 3, + placeholder: '请输入备注', + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/index.vue b/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/index.vue new file mode 100644 index 0000000..ebd6782 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/schemas/index.tsx new file mode 100644 index 0000000..a3eeb0f --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/hardware-fault-description/schemas/index.tsx @@ -0,0 +1,135 @@ +import { CrudSchema } from "@/components/crud"; +import { ProduceHardwareFaultDescriptionService } from "@/apps/produce/api"; + +const enabledOptions = [ + { label: "启用", value: 1 }, + { label: "停用", value: 0 }, +]; + +export const crudSchema = (): CrudSchema => ({ + api: ProduceHardwareFaultDescriptionService.list, + dialogTitle: "硬件故障描述", + crudApi: { + add: ProduceHardwareFaultDescriptionService.create, + edit: ProduceHardwareFaultDescriptionService.update, + remove: ProduceHardwareFaultDescriptionService.remove, + view: ProduceHardwareFaultDescriptionService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: "produce:hardware-fault-description:create", + editAuth: "produce:hardware-fault-description:update", + removeAuth: "produce:hardware-fault-description:delete", + viewAuth: "produce:hardware-fault-description:read", + rowKey: "id", + searchFormSchema: { + labelWidth: "90px", + schema: [ + { + prop: "category_id", + label: "物料分类", + component: "ApiSelect", + componentProps: { + api: ProduceHardwareFaultDescriptionService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + }, + colSpan: 8, + }, + { + prop: "keyword", + label: "关键字", + component: "Input", + componentProps: { clearable: true, placeholder: "故障描述或备注" }, + colSpan: 8, + }, + { + prop: "is_enabled", + label: "启用状态", + component: "Select", + componentProps: { clearable: true, options: enabledOptions }, + colSpan: 8, + }, + ], + }, + columns: [ + { type: "selection" }, + { type: "index", label: "序号", width: 70 }, + { prop: "category_name", label: "物料分类", minWidth: 160 }, + { + prop: "description", + label: "硬件故障描述", + minWidth: 300, + showOverflowTooltip: true, + }, + { prop: "use_count", label: "使用次数", width: 100 }, + { + prop: "is_enabled", + label: "状态", + width: 90, + formatter: (row: { is_enabled?: number }) => + row.is_enabled === 0 ? "停用" : "启用", + }, + { prop: "remark", label: "备注", minWidth: 180, showOverflowTooltip: true }, + { prop: "created_date", label: "创建时间", minWidth: 180 }, + ], + dialogProps: { width: "720px" }, + formSchema: { + showDefaultActions: false, + labelWidth: "110px", + schema: [ + { + component: "Input", + prop: "id", + label: "ID", + show: false, + ifDetail: true, + colSpan: 24, + }, + { + component: "ApiSelect", + prop: "category_id", + label: "物料分类", + componentProps: { + api: ProduceHardwareFaultDescriptionService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + placeholder: "请选择物料分类", + }, + rules: [{ required: true, message: "请选择物料分类" }], + colSpan: 12, + }, + { + component: "RadioGroup", + prop: "is_enabled", + label: "是否启用", + defaultValue: 1, + componentProps: { options: enabledOptions }, + colSpan: 12, + }, + { + component: "Input", + prop: "description", + label: "故障描述", + componentProps: { + maxlength: 500, + placeholder: "请输入该物料分类的硬件故障描述", + }, + rules: [{ required: true, message: "请输入硬件故障描述" }], + colSpan: 24, + }, + { + component: "Input", + prop: "remark", + label: "备注", + componentProps: { type: "textarea", rows: 3, maxlength: 500 }, + colSpan: 24, + }, + ], + }, +}); diff --git a/frontend/admin/src/apps/produce/views/produce/repair/repair-data/index.vue b/frontend/admin/src/apps/produce/views/produce/repair/repair-data/index.vue new file mode 100644 index 0000000..aef41a3 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/repair-data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/repair/repair-data/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/repair/repair-data/schemas/index.tsx new file mode 100644 index 0000000..d2db915 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/repair-data/schemas/index.tsx @@ -0,0 +1,490 @@ +import { h } from "vue"; +import { CrudSchema } from "@/components/crud"; +import { ProduceRepairRecordService } from "@/apps/produce/api"; + +const formatPartSource = (source: unknown): string => { + const labels: Record = { + new: "新料", + old: "售后", + after_sale: "售后", + after_sales: "售后", + "after-sale": "售后", + repair: "返修", + refurbished: "返修", + }; + const value = String(source || ""); + return labels[value] || value || "-"; +}; + +const parseParts = (parts: Array> | string | null | undefined): Array> => { + if (Array.isArray(parts)) return parts; + if (typeof parts !== "string" || !parts) return []; + try { + const parsed = JSON.parse(parts); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +const formatPartValues = ( + parts: Array> | string | null | undefined, + getValue: (part: Record) => string, +): string => + parseParts(parts) + .map((part) => getValue(part)) + .filter(Boolean) + .join("\n"); + +const formatPartRows = ( + parts: Array> | string | null | undefined, + getValue: (part: Record) => string, + nowrap = true, +) => + h( + "div", + parseParts(parts).map((part) => + h( + "div", + { style: { lineHeight: "24px", minHeight: "24px", whiteSpace: nowrap ? "nowrap" : "normal" } }, + getValue(part) || "-", + ), + ), + ); + +const formatPartFaultDescriptions = (part: Record): string => + Array.isArray(part.fault_descriptions) + ? part.fault_descriptions + .map((fault: { description?: string }) => fault.description || "") + .filter(Boolean) + .join("、") + : ""; + +const formatCategoryNames = (row: { + category_names?: string[] | string | null; + category_name?: string | null; +}): string => { + const names = row.category_names; + if (Array.isArray(names)) return names.filter(Boolean).join("、"); + if (typeof names === "string" && names) { + try { + const parsed = JSON.parse(names); + if (Array.isArray(parsed)) return parsed.filter(Boolean).join("、"); + } catch { + return names; + } + return names; + } + return row.category_name || ""; +}; + +type ResponsibilitySnapshot = { + process_name?: string; + step_name?: string; + operator?: string; + user_code?: string; +}; + +const parseSnapshots = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): ResponsibilitySnapshot[] => { + if (Array.isArray(snapshots)) return snapshots; + if (typeof snapshots !== "string" || !snapshots) return []; + try { + const parsed = JSON.parse(snapshots); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +const formatRepairResult = (repairResult?: string | null): string => { + const labels: Record = { + material_defect: "物料不良", + operation_defect: "作业不良", + assembly_mismatch: "搭配错误", + }; + return labels[repairResult || ""] || repairResult || ""; +}; + +const formatOperationFaultSteps = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => [item.process_name, item.step_name].filter(Boolean).join(" · ")) + .filter(Boolean) + .join("\n"); + +const formatResponsibleOperators = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => { + const operator = item.operator || item.user_code || ""; + const process = [item.process_name, item.step_name].filter(Boolean).join(" · "); + return [operator, process].filter(Boolean).join("|"); + }) + .filter(Boolean) + .join("\n"); + +const formatResponsibleOperatorNames = (snapshots: ResponsibilitySnapshot[] | string | null | undefined): string => + parseSnapshots(snapshots) + .map((item) => { + const step = item.step_name || ""; + const operator = item.operator || item.user_code || ""; + return [step, operator].filter(Boolean).join("-"); + }) + .filter(Boolean) + .join("\n"); + +export const crudSchema = (): CrudSchema => ({ + api: ProduceRepairRecordService.list, + dialogTitle: "维修数据", + crudApi: { + add: ProduceRepairRecordService.create, + edit: ProduceRepairRecordService.update, + remove: ProduceRepairRecordService.remove, + view: ProduceRepairRecordService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + hasExport: true, + exportFileName: "维修数据", + exportFields: [ + { field: "repair_no", title: "维修单号" }, + { field: "order_product", title: "订单号/商品编码" }, + { field: "exception_type_name", title: "异常分类" }, + { field: "exception_reason", title: "异常原因" }, + { field: "part_category", title: "物料分类" }, + { field: "part_code", title: "物料编码" }, + { field: "part_name", title: "更换备件" }, + { field: "part_fault", title: "故障描述" }, + { field: "part_quantity", title: "数量" }, + { field: "part_source", title: "来源" }, + { field: "repair_result_name", title: "维修判定分类" }, + { field: "responsible_operator_names", title: "责任人员" }, + { field: "operator", title: "维修人员" }, + { field: "repaired_date", title: "维修时间" }, + ], + addAuth: "produce:repair-record:create", + editAuth: "produce:repair-record:update", + removeAuth: "produce:repair-record:delete", + viewAuth: "produce:repair-record:read", + rowKey: "id", + searchFormSchema: { + labelWidth: "90px", + schema: [ + { + prop: "keyword", + label: "关键字", + component: "Input", + componentProps: { + clearable: true, + placeholder: "维修单、订单、商品或故障描述", + }, + colSpan: 8, + }, + { + prop: "category_id", + label: "物料分类", + component: "ApiSelect", + componentProps: { + api: ProduceRepairRecordService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + }, + colSpan: 8, + }, + { + prop: "fault_description_id", + label: "故障描述", + component: "ApiSelect", + componentProps: { + api: ProduceRepairRecordService.faultDescriptionOptions, + labelField: "description", + valueField: "id", + clearable: true, + }, + colSpan: 8, + }, + ], + }, + columns: [ + { type: "selection" }, + { type: "index", label: "序号", width: 70 }, + { prop: "repair_no", label: "维修单号", minWidth: 160 }, + { + prop: "order_no", + label: "订单号/商品编码", + minWidth: 180, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { order_no?: string | null; product_code?: string | null }) => + [row.order_no, row.product_code].filter(Boolean).join("/"), + }, + { prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } }, + { + prop: "part_category", + label: "物料分类", + minWidth: 140, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.category_name || ""), + }, + { + prop: "part_code", + label: "物料编码", + minWidth: 150, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.product_code || ""), + }, + { + prop: "part_name", + label: "更换备件", + minWidth: 240, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => part.product_name || "", true), + }, + { + prop: "part_fault", + label: "故障描述", + minWidth: 180, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, formatPartFaultDescriptions), + }, + { + prop: "part_quantity", + label: "数量", + width: 80, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => String(part.quantity ?? 1)), + }, + { + prop: "part_source", + label: "来源", + width: 90, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { parts?: Array> | string | null }) => + formatPartRows(row.parts, (part) => formatPartSource(part.source)), + }, + { + prop: "repair_result", + label: "维修判定分类", + minWidth: 140, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { repair_result?: string | null }) => formatRepairResult(row.repair_result), + }, + { + prop: "responsible_operators", + label: "责任人员", + minWidth: 120, + cellStyle: { whiteSpace: "nowrap" }, + formatter: (row: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => + formatResponsibleOperatorNames(row.responsible_operators), + }, + { prop: "operator", label: "维修人员", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "repaired_date", label: "维修时间", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } }, + ], + dialogProps: { width: "860px" }, + formSchema: { + showDefaultActions: false, + labelWidth: "105px", + schema: [ + { + component: "Input", + prop: "id", + label: "ID", + show: false, + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "repair_no", + label: "维修单号", + componentProps: { maxlength: 100, placeholder: "请输入维修单号" }, + rules: [{ required: true, message: "请输入维修单号" }], + colSpan: 12, + }, + { + component: "Input", + prop: "order_no", + label: "订单号", + componentProps: { maxlength: 100, placeholder: "请输入订单号" }, + colSpan: 12, + }, + { + component: "Input", + prop: "product_code", + label: "商品编码", + componentProps: { maxlength: 100, placeholder: "请输入商品编码" }, + colSpan: 12, + }, + { + component: "Input", + prop: "product_name", + label: "商品名称", + componentProps: { maxlength: 200, placeholder: "请输入商品名称" }, + colSpan: 12, + }, + { + component: "ApiSelect", + prop: "category_id", + label: "首选物料分类", + componentProps: { + api: ProduceRepairRecordService.categoryOptions, + labelField: "label", + valueField: "value", + clearable: true, + placeholder: "请选择首选物料分类", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "category_names", + label: "物料分类", + componentProps: (model: { category_names?: string[] | string | null; category_name?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatCategoryNames(model), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "ApiSelect", + prop: "fault_description_id", + label: "故障描述", + componentProps: { + api: ProduceRepairRecordService.faultDescriptionOptions, + labelField: "description", + valueField: "id", + clearable: true, + placeholder: "请选择硬件故障描述", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "exception_reason", + label: "异常原因", + componentProps: (model: { exception_reason?: string | null; exception_issue?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: [model.exception_reason, model.exception_issue].filter(Boolean).join(" · "), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "fault_descriptions", + label: "故障描述", + componentProps: (model: { fault_descriptions?: string[]; fault_description?: string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: (model.fault_descriptions || []).join("、") || model.fault_description || "", + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "PartsDetail", + prop: "parts", + label: "更换备件", + ifDetail: true, + colSpan: 24, + }, + { + component: "Select", + prop: "repair_result", + label: "维修判定", + componentProps: { + clearable: true, + options: [ + { label: "物料不良", value: "material_defect" }, + { label: "作业不良", value: "operation_defect" }, + { label: "搭配错误", value: "assembly_mismatch" }, + ], + }, + colSpan: 12, + }, + { + component: "Input", + prop: "repair_result_detail", + label: "维修判定", + componentProps: (model: { repair_result?: string | null }) => ({ + readonly: true, + value: formatRepairResult(model.repair_result), + }), + ifDetail: true, + colSpan: 12, + }, + { + component: "Input", + prop: "operation_fault_steps", + label: "责任步骤", + componentProps: (model: { operation_fault_steps?: ResponsibilitySnapshot[] | string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatOperationFaultSteps(model.operation_fault_steps), + }), + ifDetail: true, + colSpan: 12, + }, + { + component: "Input", + prop: "responsible_operators", + label: "责任人员", + componentProps: (model: { responsible_operators?: ResponsibilitySnapshot[] | string | null }) => ({ + type: "textarea", + rows: 2, + readonly: true, + value: formatResponsibleOperators(model.responsible_operators), + }), + ifDetail: true, + colSpan: 24, + }, + { + component: "Upload", + prop: "images", + label: "维修现场图片", + componentProps: { listType: "picture-card" }, + ifDetail: true, + colSpan: 24, + }, + { + component: "Input", + prop: "operator", + label: "维修人员", + componentProps: { maxlength: 100, placeholder: "请输入维修人员" }, + colSpan: 12, + }, + { + component: "DatePicker", + prop: "repaired_at", + label: "维修时间", + componentProps: { + type: "datetime", + valueFormat: "x", + placeholder: "请选择维修时间", + }, + colSpan: 12, + }, + { + component: "Input", + prop: "remark", + label: "备注", + componentProps: { type: "textarea", rows: 3, maxlength: 1000 }, + colSpan: 24, + }, + ], + }, +}); diff --git a/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/index.vue b/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/index.vue new file mode 100644 index 0000000..037165f --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/schemas/index.tsx new file mode 100644 index 0000000..831356e --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/repair/repair-setting/schemas/index.tsx @@ -0,0 +1,100 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceRepairSettingService } from '@/apps/produce/api' + +const settingTypeOptions = [ + { label: '处理类型', value: 'type' }, + { label: '维修原因', value: 'reason' }, + { label: '具体问题', value: 'issue' }, +] + +export const crudSchema = (): CrudSchema => ({ + api: ProduceRepairSettingService.list, + dialogTitle: '维修设置', + crudApi: { + add: ProduceRepairSettingService.create, + edit: ProduceRepairSettingService.update, + remove: ProduceRepairSettingService.remove, + view: ProduceRepairSettingService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:repair-setting:create', + editAuth: 'produce:repair-setting:update', + removeAuth: 'produce:repair-setting:delete', + viewAuth: 'produce:repair-setting:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '90px', + schema: [ + { + prop: 'keyword', label: '关键字', component: 'Input', + componentProps: { clearable: true, placeholder: '编码、名称或备注' }, colSpan: 8, + }, + { + prop: 'setting_type', label: '设置层级', component: 'Select', + componentProps: { clearable: true, placeholder: '请选择设置层级', options: settingTypeOptions }, colSpan: 8, + }, + { + prop: 'is_enabled', label: '启用状态', component: 'Select', + componentProps: { + clearable: true, placeholder: '请选择启用状态', + options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }], + }, colSpan: 8, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'setting_type', label: '设置层级', minWidth: 100, formatter: (row: { setting_type?: string }) => settingTypeOptions.find((item) => item.value === row.setting_type)?.label || row.setting_type || '' }, + { prop: 'code', label: '编码', minWidth: 150 }, + { prop: 'name', label: '名称', minWidth: 160 }, + { prop: 'parent_name', label: '所属上级', minWidth: 140 }, + { prop: 'sort', label: '排序', width: 90 }, + { prop: 'is_enabled', label: '状态', width: 90, formatter: (row: any) => row.is_enabled === 0 ? '停用' : '启用' }, + { prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true }, + { prop: 'created_name', label: '创建人', minWidth: 110 }, + ], + dialogProps: { width: '700px' }, + formSchema: { + showDefaultActions: false, + labelWidth: '105px', + schema: [ + { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, + { + component: 'Select', prop: 'setting_type', label: '设置层级', + componentProps: { placeholder: '请选择设置层级', options: settingTypeOptions }, + rules: [{ required: true, message: '请选择设置层级' }], colSpan: 12, + }, + { + component: 'ApiSelect', prop: 'parent_id', label: '所属上级', + componentProps: { + api: '/produce/repair-setting', requestMethod: 'get', + params: { format: 'select', NE_setting_type: 'issue' }, clearable: true, placeholder: '处理类型无需选择上级', + }, colSpan: 12, + }, + { + component: 'Input', prop: 'code', label: '设置编码', + componentProps: { placeholder: '请输入唯一编码', maxlength: 64 }, + rules: [{ required: true, message: '请输入设置编码' }], colSpan: 12, + }, + { + component: 'Input', prop: 'name', label: '设置名称', + componentProps: { placeholder: '请输入显示名称', maxlength: 100 }, + rules: [{ required: true, message: '请输入设置名称' }], colSpan: 12, + }, + { component: 'InputNumber', prop: 'sort', label: '排序', defaultValue: 0, componentProps: { min: 0 }, colSpan: 12 }, + { + component: 'RadioGroup', prop: 'is_enabled', label: '是否启用', defaultValue: 1, + componentProps: { options: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] }, colSpan: 12, + }, + { + component: 'Input', prop: 'remark', label: '备注', + componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24, + }, + ], + }, +}) diff --git a/frontend/admin/src/apps/produce/views/produce/scan-data/index.vue b/frontend/admin/src/apps/produce/views/produce/scan-data/index.vue new file mode 100644 index 0000000..082d7b1 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/scan-data/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/scan-data/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/scan-data/schemas/index.tsx new file mode 100644 index 0000000..045b17b --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/scan-data/schemas/index.tsx @@ -0,0 +1,254 @@ +import { CrudSchema } from '@/components/crud' +import { ProduceScanDataService } from '@/apps/produce/api' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceScanDataService.list, + dialogTitle: '扫码数据', + crudApi: { + add: ProduceScanDataService.create, + edit: ProduceScanDataService.update, + remove: ProduceScanDataService.remove, + view: ProduceScanDataService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:scan-data:create', + editAuth: 'produce:scan-data:update', + removeAuth: 'produce:scan-data:delete', + viewAuth: 'produce:scan-data:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'keyword', + label: '关键字', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入订单号/用户编码/操作人员/备注', + }, + colSpan: 6, + }, + { + prop: 'process_id', + label: '工序名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: '步骤名称', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + }, + colSpan: 6, + }, + { + prop: 'status_id', + label: '步骤状态', + component: 'ApiSelect', + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + }, + colSpan: 6, + }, + { + prop: 'LIKE_operator', + label: '操作人员', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入操作人员', + }, + colSpan: 6, + }, + { + prop: 'scan_result', + label: '扫码结果', + component: 'Select', + componentProps: { + clearable: true, + placeholder: '请选择扫码结果', + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + colSpan: 6, + }, + { + prop: 'scan_time_range', + label: '扫码时间', + component: 'DatePicker', + componentProps: { + type: 'daterange', + clearable: true, + valueFormat: 'YYYY-MM-DD', + startPlaceholder: '开始日期', + endPlaceholder: '结束日期', + }, + colSpan: 12, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: '序号', width: 70 }, + { prop: 'order_no', label: '订单号', minWidth: 160 }, + { prop: 'quantity', label: '数量', width: 90 }, + { prop: 'user_code', label: '用户编码', minWidth: 120 }, + { prop: 'process_name', label: '工序名称', minWidth: 140 }, + { prop: 'step_name', label: '步骤名称', minWidth: 140 }, + { prop: 'status_name', label: '步骤状态', minWidth: 120 }, + { prop: 'operator', label: '操作人员', minWidth: 120 }, + { + prop: 'scan_result', + label: '扫码结果', + width: 100, + formatter: (row: any) => (row.scan_result === 0 ? '失败' : '成功'), + }, + { prop: 'scan_date', label: '扫码时间', minWidth: 180 }, + { prop: 'created_name', label: '创建人', minWidth: 120 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'order_no', + label: '订单号', + componentProps: { + placeholder: '请输入订单号', + maxlength: 100, + }, + rules: [{ required: true, message: '请输入订单号' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'user_code', + label: '用户编码', + componentProps: { + placeholder: '请输入用户编码', + maxlength: 50, + }, + rules: [{ required: true, message: '请输入用户编码' }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'operator', + label: '操作人员', + componentProps: { + placeholder: '请输入操作人员', + maxlength: 100, + }, + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'process_id', + label: '工序名称', + componentProps: (values: any) => ({ + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择工序', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择工序' }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: '步骤名称', + componentProps: (values: any) => ({ + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择步骤', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择步骤' }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'status_id', + label: '步骤状态', + componentProps: (values: any) => ({ + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择状态', + disabled: values?.id !== undefined && values?.id !== null && values?.id !== '', + }), + rules: [{ required: true, message: '请选择状态' }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'scan_result', + label: '扫码结果', + defaultValue: 1, + componentProps: { + options: [ + { label: '成功', value: 1 }, + { label: '失败', value: 0 }, + ], + }, + rules: [{ required: true, message: '请选择扫码结果' }], + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: '备注', + componentProps: { + type: 'textarea', + rows: 3, + placeholder: '请输入备注', + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/statistics/daily/index.vue b/frontend/admin/src/apps/produce/views/produce/statistics/daily/index.vue new file mode 100644 index 0000000..405009a --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/statistics/daily/index.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/statistics/monthly/index.vue b/frontend/admin/src/apps/produce/views/produce/statistics/monthly/index.vue new file mode 100644 index 0000000..8bc0a3d --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/statistics/monthly/index.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/status-transition/index.vue b/frontend/admin/src/apps/produce/views/produce/status-transition/index.vue new file mode 100644 index 0000000..3d3743c --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/status-transition/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/status-transition/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/status-transition/schemas/index.tsx new file mode 100644 index 0000000..8c386f9 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/status-transition/schemas/index.tsx @@ -0,0 +1,290 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStatusTransitionService } from '../../../../api/index' + +const triggerEventOptions = [ + { label: '下一工序扫码触发', value: 'next_step_scan' }, + { label: '扫描触发', value: 'scan_trigger' }, + { label: '异常触发', value: 'exception_trigger' }, + { label: '手动触发', value: 'manual_trigger' }, + { label: '定时触发', value: 'timer_trigger' }, + { label: '完成触发', value: 'complete_trigger' }, +] + +const transitionTypeOptions = [ + { label: '主转换', value: 'primary' }, + { label: '副作用', value: 'side_effect' }, +] + +const sideEffectTypeOptions = [ + { label: '记录日志', value: 'log' }, + { label: '发送通知', value: 'notify' }, + { label: '更新字段', value: 'update_field' }, + { label: '触发事件', value: 'trigger_event' }, +] + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStatusTransitionService.list, + dialogTitle: $t('produce.modules.status_transition.title'), + crudApi: { + add: ProduceStatusTransitionService.create, + edit: ProduceStatusTransitionService.update, + remove: ProduceStatusTransitionService.remove, + view: ProduceStatusTransitionService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:status-transition:create', + editAuth: 'produce:status-transition:update', + removeAuth: 'produce:status-transition:delete', + viewAuth: 'produce:status-transition:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'step_id', + label: $t('produce.modules.status_transition.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'trigger_event', + label: $t('produce.modules.status_transition.search.trigger_event'), + component: 'Select', + componentProps: { + options: triggerEventOptions, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.trigger_event_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.status_transition.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.status_transition.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'step.step_name', label: $t('produce.modules.status_transition.columns.step_name'), minWidth: 160 }, + { prop: 'from_status.status_name', label: $t('produce.modules.status_transition.columns.from_status_name'), minWidth: 140 }, + { prop: 'to_status.status_name', label: $t('produce.modules.status_transition.columns.to_status_name'), minWidth: 140 }, + { prop: 'trigger_event', label: $t('produce.modules.status_transition.columns.trigger_event'), minWidth: 160 }, + { prop: 'transition_type', label: $t('produce.modules.status_transition.columns.transition_type'), width: 120 }, + { + prop: 'auto_trigger', + label: $t('produce.modules.status_transition.columns.auto_trigger'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'priority', label: $t('produce.modules.status_transition.columns.priority'), width: 100 }, + { prop: 'sort', label: $t('produce.modules.status_transition.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.status_transition.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.status_transition.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '860px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '120px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: $t('produce.modules.status_transition.form.step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'from_status_id', + label: $t('produce.modules.status_transition.form.from_status_id'), + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.from_status_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.from_status_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'to_status_id', + label: $t('produce.modules.status_transition.form.to_status_id'), + componentProps: { + api: '/produce/step-status', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.to_status_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.to_status_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Select', + prop: 'trigger_event', + label: $t('produce.modules.status_transition.columns.trigger_event'), + componentProps: { + options: triggerEventOptions, + placeholder: $t('produce.modules.status_transition.form.trigger_event_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.status_transition.form.trigger_event_placeholder') }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'transition_type', + label: $t('produce.modules.status_transition.columns.transition_type'), + defaultValue: 'primary', + componentProps: { + options: transitionTypeOptions, + isBtn: true, + renderType: 'RadioButton', + }, + colSpan: 24, + }, + { + component: 'Select', + prop: 'side_effect_type', + label: $t('produce.modules.status_transition.form.side_effect_type'), + componentProps: { + options: sideEffectTypeOptions, + clearable: true, + placeholder: $t('produce.modules.status_transition.form.side_effect_type_placeholder'), + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'auto_trigger', + label: $t('produce.modules.status_transition.columns.auto_trigger'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'priority', + label: $t('produce.modules.status_transition.columns.priority'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.status_transition.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'event_config', + label: $t('produce.modules.status_transition.form.event_config'), + componentProps: { + type: 'textarea', + rows: 5, + placeholder: $t('produce.modules.status_transition.form.event_config_placeholder'), + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'side_effect_config', + label: $t('produce.modules.status_transition.form.side_effect_config'), + componentProps: { + type: 'textarea', + rows: 5, + placeholder: $t('produce.modules.status_transition.form.side_effect_config_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.status_transition.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.status_transition.form.description'), + componentProps: { + type: 'textarea', + rows: 3, + placeholder: $t('produce.modules.status_transition.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/step-relation/index.vue b/frontend/admin/src/apps/produce/views/produce/step-relation/index.vue new file mode 100644 index 0000000..44e01f0 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/step-relation/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/step-relation/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/step-relation/schemas/index.tsx new file mode 100644 index 0000000..c0c8a1e --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/step-relation/schemas/index.tsx @@ -0,0 +1,218 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStepRelationService } from '../../../../api/index' + +const relationTypeOptions = [ + { label: '正常流转', value: 'normal' }, + { label: '异常流转', value: 'exception' }, +] + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStepRelationService.list, + dialogTitle: $t('produce.modules.step_relation.title'), + crudApi: { + add: ProduceStepRelationService.create, + edit: ProduceStepRelationService.update, + remove: ProduceStepRelationService.remove, + view: ProduceStepRelationService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:step-relation:create', + editAuth: 'produce:step-relation:update', + removeAuth: 'produce:step-relation:delete', + viewAuth: 'produce:step-relation:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'from_step_id', + label: $t('produce.modules.step_relation.search.from_step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.from_step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'to_step_id', + label: $t('produce.modules.step_relation.search.to_step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.to_step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'relation_type', + label: $t('produce.modules.step_relation.search.relation_type'), + component: 'Select', + componentProps: { + options: relationTypeOptions, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.relation_type_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.step_relation.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.step_relation.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'from_step.step_name', label: $t('produce.modules.step_relation.columns.from_step_name'), minWidth: 180 }, + { prop: 'to_step.step_name', label: $t('produce.modules.step_relation.columns.to_step_name'), minWidth: 180 }, + { prop: 'relation_type', label: $t('produce.modules.step_relation.columns.relation_type'), width: 120 }, + { prop: 'priority', label: $t('produce.modules.step_relation.columns.priority'), width: 100 }, + { prop: 'flow_condition', label: $t('produce.modules.step_relation.columns.flow_condition'), minWidth: 220 }, + { prop: 'sort', label: $t('produce.modules.step_relation.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.step_relation.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.step_relation.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '760px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'from_step_id', + label: $t('produce.modules.step_relation.form.from_step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.form.from_step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_relation.form.from_step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'ApiSelect', + prop: 'to_step_id', + label: $t('produce.modules.step_relation.form.to_step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_relation.form.to_step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_relation.form.to_step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'RadioGroup', + prop: 'relation_type', + label: $t('produce.modules.step_relation.columns.relation_type'), + defaultValue: 'normal', + componentProps: { + options: relationTypeOptions, + isBtn: true, + renderType: 'RadioButton', + }, + colSpan: 24, + }, + { + component: 'InputNumber', + prop: 'priority', + label: $t('produce.modules.step_relation.columns.priority'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.step_relation.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'flow_condition', + label: $t('produce.modules.step_relation.form.flow_condition'), + componentProps: { + type: 'textarea', + rows: 6, + placeholder: $t('produce.modules.step_relation.form.flow_condition_placeholder'), + }, + colSpan: 24, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.step_relation.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.step_relation.form.description'), + componentProps: { + type: 'textarea', + rows: 3, + placeholder: $t('produce.modules.step_relation.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/step-status/index.vue b/frontend/admin/src/apps/produce/views/produce/step-status/index.vue new file mode 100644 index 0000000..19cb79c --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/step-status/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/step-status/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/step-status/schemas/index.tsx new file mode 100644 index 0000000..0832468 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/step-status/schemas/index.tsx @@ -0,0 +1,227 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceStepStatusService } from '../../../../api/index' + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceStepStatusService.list, + dialogTitle: $t('produce.modules.step_status.title'), + crudApi: { + add: ProduceStepStatusService.create, + edit: ProduceStepStatusService.update, + remove: ProduceStepStatusService.remove, + view: ProduceStepStatusService.get, + }, + useCrud: true, + hasAdd: true, + hasEdit: true, + hasRemove: true, + hasView: true, + addAuth: 'produce:step-status:create', + editAuth: 'produce:step-status:update', + removeAuth: 'produce:step-status:delete', + viewAuth: 'produce:step-status:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'step_id', + label: $t('produce.modules.step_status.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_status.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_status_code', + label: $t('produce.modules.step_status.search.status_code'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_code_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_status_name', + label: $t('produce.modules.step_status.search.status_name'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_name_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'status', + label: $t('produce.modules.step_status.search.status'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.step_status.search.status_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'selection' }, + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { + prop: 'step.step_name', + label: $t('produce.modules.step_status.columns.step_name'), + minWidth: 180, + }, + { prop: 'status_code', label: $t('produce.modules.step_status.columns.status_code'), minWidth: 160 }, + { prop: 'status_name', label: $t('produce.modules.step_status.columns.status_name'), minWidth: 180 }, + { + prop: 'is_initial', + label: $t('produce.modules.step_status.columns.is_initial'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { + prop: 'is_final', + label: $t('produce.modules.step_status.columns.is_final'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'sort', label: $t('produce.modules.step_status.columns.sort'), width: 100 }, + { + prop: 'status', + label: $t('produce.modules.step_status.columns.status'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'created_date', label: $t('produce.modules.step_status.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '720px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '110px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 24, + prop: 'id', + label: 'ID', + }, + { + component: 'ApiSelect', + prop: 'step_id', + label: $t('produce.modules.step_status.form.step_id'), + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.step_status.form.step_id_placeholder'), + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.step_id_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_code', + label: $t('produce.modules.step_status.columns.status_code'), + componentProps: { + placeholder: $t('produce.modules.step_status.form.status_code_placeholder'), + maxlength: 50, + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.status_code_placeholder') }], + colSpan: 12, + }, + { + component: 'Input', + prop: 'status_name', + label: $t('produce.modules.step_status.columns.status_name'), + componentProps: { + placeholder: $t('produce.modules.step_status.form.status_name_placeholder'), + maxlength: 100, + }, + rules: [{ required: true, message: $t('produce.modules.step_status.form.status_name_placeholder') }], + colSpan: 12, + }, + { + component: 'InputNumber', + prop: 'sort', + label: $t('produce.modules.step_status.columns.sort'), + defaultValue: 0, + componentProps: { + min: 0, + class: 'w-full', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_initial', + label: $t('produce.modules.step_status.columns.is_initial'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_final', + label: $t('produce.modules.step_status.columns.is_final'), + defaultValue: 0, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'status', + label: $t('produce.modules.step_status.columns.status'), + defaultValue: 1, + componentProps: { + code: DictEnum.SYS_YES_NO, + isBtn: true, + renderType: 'RadioGroup', + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'description', + label: $t('produce.modules.step_status.form.description'), + componentProps: { + type: 'textarea', + rows: 4, + placeholder: $t('produce.modules.step_status.form.description_placeholder'), + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/produce/views/produce/transition-log/index.vue b/frontend/admin/src/apps/produce/views/produce/transition-log/index.vue new file mode 100644 index 0000000..1a84668 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/transition-log/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/apps/produce/views/produce/transition-log/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/transition-log/schemas/index.tsx new file mode 100644 index 0000000..1ec65b3 --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/transition-log/schemas/index.tsx @@ -0,0 +1,277 @@ +import { CrudSchema } from '@/components/crud' +import { DictEnum } from '@/enums' +import { $t } from '@/locales' +import { ProduceTransitionLogService } from '../../../../api/index' + +const triggerEventOptions = [ + { label: '下一工序扫码触发', value: 'next_step_scan' }, + { label: '扫描触发', value: 'scan_trigger' }, + { label: '异常触发', value: 'exception_trigger' }, + { label: '手动触发', value: 'manual_trigger' }, + { label: '定时触发', value: 'timer_trigger' }, + { label: '完成触发', value: 'complete_trigger' }, +] + +const formatJsonText = (value: unknown) => { + if (value === null || value === undefined || value === '') { + return '' + } + if (typeof value === 'string') { + return value + } + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } +} + +export const crudSchema = (): CrudSchema => { + return { + api: ProduceTransitionLogService.list, + dialogTitle: $t('produce.modules.transition_log.title'), + crudApi: { + view: async (id: string | number) => { + const res = await ProduceTransitionLogService.get(id) + return { + ...res, + trigger_data: formatJsonText(res?.trigger_data), + } + }, + }, + useCrud: true, + hasAdd: false, + hasEdit: false, + hasRemove: false, + hasView: true, + viewAuth: 'produce:transition-log:read', + rowKey: 'id', + searchFormSchema: { + labelWidth: '100px', + schema: [ + { + prop: 'LIKE_work_order_no', + label: $t('produce.modules.transition_log.search.work_order_no'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.transition_log.search.work_order_no_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'step_id', + label: $t('produce.modules.transition_log.search.step_id'), + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.step_id_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'trigger_event', + label: $t('produce.modules.transition_log.search.trigger_event'), + component: 'Select', + componentProps: { + options: triggerEventOptions, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.trigger_event_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'LIKE_trigger_user', + label: $t('produce.modules.transition_log.search.trigger_user'), + component: 'Input', + componentProps: { + clearable: true, + placeholder: $t('produce.modules.transition_log.search.trigger_user_placeholder'), + }, + colSpan: 6, + }, + { + prop: 'is_success', + label: $t('produce.modules.transition_log.search.is_success'), + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + clearable: true, + placeholder: $t('produce.modules.transition_log.search.is_success_placeholder'), + }, + colSpan: 6, + }, + ], + }, + columns: [ + { type: 'index', label: $t('produce.common.columns.index'), width: 70 }, + { prop: 'work_order_no', label: $t('produce.modules.transition_log.columns.work_order_no'), minWidth: 140 }, + { prop: 'step.step_name', label: $t('produce.modules.transition_log.columns.step_name'), minWidth: 160 }, + { prop: 'from_status.status_name', label: $t('produce.modules.transition_log.columns.from_status_name'), minWidth: 140 }, + { prop: 'to_status.status_name', label: $t('produce.modules.transition_log.columns.to_status_name'), minWidth: 140 }, + { + prop: 'trigger_event', + label: $t('produce.modules.transition_log.columns.trigger_event'), + minWidth: 160, + formatter: (row: { trigger_event?: string }) => + triggerEventOptions.find((option) => option.value === row.trigger_event)?.label || row.trigger_event || '' + }, + { prop: 'trigger_user', label: $t('produce.modules.transition_log.columns.trigger_user'), minWidth: 120 }, + { + prop: 'is_auto', + label: $t('produce.modules.transition_log.columns.is_auto'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { + prop: 'is_success', + label: $t('produce.modules.transition_log.columns.is_success'), + width: 120, + component: 'ApiDict', + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + }, + { prop: 'duration', label: $t('produce.modules.transition_log.columns.duration'), width: 120 }, + { prop: 'error_message', label: $t('produce.modules.transition_log.columns.error_message'), minWidth: 200 }, + { prop: 'created_date', label: $t('produce.modules.transition_log.columns.created_at'), minWidth: 180 }, + ], + dialogProps: { + width: '960px', + }, + formSchema: { + showDefaultActions: false, + labelWidth: '120px', + schema: [ + { + component: 'Input', + show: false, + ifDetail: true, + colSpan: 12, + prop: 'id', + label: 'ID', + }, + { + component: 'Input', + prop: 'work_order_no', + label: $t('produce.modules.transition_log.columns.work_order_no'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'step.step_name', + label: $t('produce.modules.transition_log.columns.step_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'transition.trigger_event', + label: $t('produce.modules.transition_log.columns.transition_event'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'from_status.status_name', + label: $t('produce.modules.transition_log.columns.from_status_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'to_status.status_name', + label: $t('produce.modules.transition_log.columns.to_status_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_event', + label: $t('produce.modules.transition_log.columns.trigger_event'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_user', + label: $t('produce.modules.transition_log.columns.trigger_user'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_ip', + label: $t('produce.modules.transition_log.columns.trigger_ip'), + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_auto', + label: $t('produce.modules.transition_log.columns.is_auto'), + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + colSpan: 12, + }, + { + component: 'ApiDict', + prop: 'is_success', + label: $t('produce.modules.transition_log.columns.is_success'), + componentProps: { + code: DictEnum.SYS_YES_NO, + }, + colSpan: 12, + }, + { + component: 'Input', + prop: 'duration', + label: $t('produce.modules.transition_log.columns.duration'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'created_name', + label: $t('produce.modules.transition_log.columns.created_name'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'created_date', + label: $t('produce.modules.transition_log.columns.created_at'), + colSpan: 12, + }, + { + component: 'Input', + prop: 'trigger_data', + label: $t('produce.modules.transition_log.columns.trigger_data'), + componentProps: { + type: 'textarea', + rows: 6, + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'error_message', + label: $t('produce.modules.transition_log.columns.error_message'), + componentProps: { + type: 'textarea', + rows: 4, + }, + colSpan: 24, + }, + { + component: 'Input', + prop: 'remark', + label: $t('produce.modules.transition_log.columns.remark'), + componentProps: { + type: 'textarea', + rows: 4, + }, + colSpan: 24, + }, + ], + }, + } +} diff --git a/frontend/admin/src/apps/test01/api/index.ts b/frontend/admin/src/apps/test01/api/index.ts new file mode 100644 index 0000000..e49ecda --- /dev/null +++ b/frontend/admin/src/apps/test01/api/index.ts @@ -0,0 +1,18 @@ +import request from '@/utils/http'; + +/** + * test01 插件 API + */ +export default { + /** + * 列表 API + * @param params 查询参数 + * @returns 响应数据 + */ + async test(params = {}) { + return request.get<{ msg?: string; data?: unknown }>({ + url: '/test01/index', + params + }); + } +}; diff --git a/frontend/admin/src/apps/test01/lang/en/en.json b/frontend/admin/src/apps/test01/lang/en/en.json new file mode 100644 index 0000000..a933478 --- /dev/null +++ b/frontend/admin/src/apps/test01/lang/en/en.json @@ -0,0 +1,8 @@ +{ + "test01": { + "title": "插件测试0", + "description": "", + "card_title": "插件测试0 Data", + "refresh_btn": "Refresh" + } +} diff --git a/frontend/admin/src/apps/test01/lang/zh-cn/zh.json b/frontend/admin/src/apps/test01/lang/zh-cn/zh.json new file mode 100644 index 0000000..1287c59 --- /dev/null +++ b/frontend/admin/src/apps/test01/lang/zh-cn/zh.json @@ -0,0 +1,8 @@ +{ + "test01": { + "title": "插件测试0", + "description": "", + "card_title": "插件测试0数据", + "refresh_btn": "刷新" + } +}