From 32f1486f5e0371889d5bfb59784302e26185c9ba Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Tue, 1 Sep 2026 19:17:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=94=9F=E4=BA=A7=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E7=BB=B4=E4=BF=AE=E4=B8=8E=E5=A4=87=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .windsurf/skills/git-submit/SKILL.md | 241 ++++++++++++++++++ .../controller/RepairRecordController.php | 3 +- .../api/controller/DataEntryController.php | 150 +++++++++-- .../produce/app/model/ExceptionRecord.php | 26 ++ .../app/service/admin/RepairRecordService.php | 27 +- .../app/validate/RepairRecordValidate.php | 31 ++- ...1_add_process_step_to_exception_record.php | 44 ++++ .../resource/template/admin/api/index.ts | 5 + .../admin/views/produce/config/index.vue | 6 + .../produce/config/modules/basic-setting.vue | 56 +++- .../produce/config/modules/repair-flow.vue | 87 +++++++ .../produce/exception-analysis/index.vue | 228 +++++++++++++++++ .../views/produce/exception-data/index.vue | 22 ++ .../produce/exception-data/schemas/index.tsx | 187 ++++++++++++++ .../views/produce/exception-setting/index.vue | 22 ++ .../exception-setting/schemas/index.tsx | 100 ++++++++ .../produce/exception/data/schemas/index.tsx | 126 ++++++--- .../views/produce/material/schemas/index.tsx | 4 +- .../repair/repair-data/schemas/index.tsx | 1 - .../resource/template/web/api/exception.ts | 39 ++- .../resource/template/web/api/query.ts | 1 + .../template/web/pages/exception/index.vue | 24 +- .../template/web/pages/repair/index.vue | 58 ++++- docs/2026-09-01/1-更新异常提报工序步骤.md | 11 + docs/2026-09-01/10-用户编码样式优化.md | 5 + docs/2026-09-01/12-责任人员文字间距优化.md | 5 + docs/2026-09-01/13-维修备件搜索弹窗.md | 22 ++ docs/2026-09-01/14-维修备件多条件筛选.md | 13 + docs/2026-09-01/15-维修备件按频率排序.md | 7 + docs/2026-09-01/16-物料分类下拉显示修复.md | 7 + docs/2026-09-01/2-更新异常记录展示工序步骤.md | 3 + docs/2026-09-01/3-维修流程配置页.md | 6 + docs/2026-09-01/4-基础设置字段调整.md | 5 + docs/2026-09-01/4-维修数据CRUD修复.md | 6 + docs/2026-09-01/5-维修记录工序步骤展示修复.md | 5 + docs/2026-09-01/6-查询记录数量显示.md | 5 + docs/2026-09-01/7-模板源运行目录一致性检查.md | 6 + docs/2026-09-01/8-维修记录工号显示修复.md | 5 + docs/2026-09-01/9-异常维修用户编码录入.md | 6 + frontend/admin/src/apps/produce/api/index.ts | 5 + .../produce/views/produce/config/index.vue | 6 + .../produce/config/modules/basic-setting.vue | 56 +++- .../produce/config/modules/repair-flow.vue | 87 +++++++ .../views/produce/material/schemas/index.tsx | 4 +- .../repair/repair-data/schemas/index.tsx | 1 - .../web/app/apps/produce/api/exception.ts | 39 ++- frontend/web/app/apps/produce/api/query.ts | 1 + .../apps/produce/pages/exception/index.vue | 24 +- .../app/apps/produce/pages/repair/index.vue | 58 ++++- 49 files changed, 1765 insertions(+), 121 deletions(-) create mode 100644 .windsurf/skills/git-submit/SKILL.md create mode 100644 backend/plugin/produce/resource/database/migrations/2026_09_01_000001_add_process_step_to_exception_record.php create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/config/modules/repair-flow.vue create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/exception-analysis/index.vue create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/exception-data/index.vue create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/exception-data/schemas/index.tsx create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/exception-setting/index.vue create mode 100644 backend/plugin/produce/resource/template/admin/views/produce/exception-setting/schemas/index.tsx create mode 100644 docs/2026-09-01/1-更新异常提报工序步骤.md create mode 100644 docs/2026-09-01/10-用户编码样式优化.md create mode 100644 docs/2026-09-01/12-责任人员文字间距优化.md create mode 100644 docs/2026-09-01/13-维修备件搜索弹窗.md create mode 100644 docs/2026-09-01/14-维修备件多条件筛选.md create mode 100644 docs/2026-09-01/15-维修备件按频率排序.md create mode 100644 docs/2026-09-01/16-物料分类下拉显示修复.md create mode 100644 docs/2026-09-01/2-更新异常记录展示工序步骤.md create mode 100644 docs/2026-09-01/3-维修流程配置页.md create mode 100644 docs/2026-09-01/4-基础设置字段调整.md create mode 100644 docs/2026-09-01/4-维修数据CRUD修复.md create mode 100644 docs/2026-09-01/5-维修记录工序步骤展示修复.md create mode 100644 docs/2026-09-01/6-查询记录数量显示.md create mode 100644 docs/2026-09-01/7-模板源运行目录一致性检查.md create mode 100644 docs/2026-09-01/8-维修记录工号显示修复.md create mode 100644 docs/2026-09-01/9-异常维修用户编码录入.md create mode 100644 frontend/admin/src/apps/produce/views/produce/config/modules/repair-flow.vue diff --git a/.windsurf/skills/git-submit/SKILL.md b/.windsurf/skills/git-submit/SKILL.md new file mode 100644 index 0000000..78c1a39 --- /dev/null +++ b/.windsurf/skills/git-submit/SKILL.md @@ -0,0 +1,241 @@ +--- +name: git-submit +description: 仓库代码提交规范,覆盖状态检查、提交、同步远程、推送及常见冲突处理 +--- + +## 适用范围 + +用于将当前仓库的本地代码安全提交到远程仓库。执行前必须确认用户明确要求提交或推送;仅查看代码、开发功能或运行测试时,不主动执行提交和推送。 + +当前项目约定: + +- 默认分支:`main` +- 远程名称:`origin` +- 远程地址:以 `git remote -v` 的实际输出为准,不重复添加已存在的 `origin` +- 提交信息:使用简洁、准确的中文描述,说明本次变更目的 + +## 标准提交流程 + +### 1. 确认仓库位置和当前状态 + +在项目根目录执行: + +```powershell +git rev-parse --show-toplevel +git status --short --branch +git branch --show-current +git remote -v +``` + +检查以下内容: + +- 是否位于目标仓库根目录 +- 当前分支是否为预期分支,默认应为 `main` +- 是否存在未提交修改、未跟踪文件或冲突文件 +- `origin` 是否已配置,以及拉取和推送地址是否正确 + +如果 `origin` 已存在,不要执行 `git remote add origin`。需要修正地址时使用: + +```powershell +git remote set-url origin <仓库地址> +``` + +### 2. 审阅变更内容 + +提交前查看文件变更和差异: + +```powershell +git status +git diff +git diff --stat +``` + +确认: + +- 没有误改、敏感信息、密钥、日志或临时文件 +- 修改范围与用户需求一致 +- 不应提交的文件已通过 `.gitignore` 排除 +- 若发现不属于本次任务的修改,不要擅自删除或覆盖;只暂存确认过的文件 + +如需查看某个文件的差异: + +```powershell +git diff -- <文件路径> +``` + +### 3. 暂存需要提交的文件 + +优先按文件或目录精确暂存: + +```powershell +git add <文件路径> +git add <目录路径> +``` + +仅当确认工作区所有修改都属于本次任务时,才使用: + +```powershell +git add . +``` + +暂存后复核: + +```powershell +git status + +git diff --cached +git diff --cached --stat +``` + +如果误暂存文件,可取消暂存,不会丢失文件修改: + +```powershell +git restore --staged <文件路径> +``` + +### 4. 提交本地变更 + +确保暂存区内容正确后提交: + +```powershell +git commit -m "<简洁明确的提交说明>" +``` + +提交信息建议: + +- 使用中文描述业务目的或技术变更 +- 避免使用“修改一下”“临时提交”等无信息内容 +- 一次提交尽量只包含一个完整主题 + +提交后检查: + +```powershell +git status --short --branch +git log -1 --oneline +``` + +如果提示 `nothing to commit`,说明当前没有新的已暂存变更;先检查状态和最近提交,不要重复创建空提交。 + +### 5. 推送到远程仓库 + +首次推送当前分支或需要建立跟踪关系时: + +```powershell +git push -u origin main +``` + +已有跟踪关系时: + +```powershell +git push origin main +``` + +不要在未确认目标分支的情况下直接推送其他分支。推送后核验: + +```powershell +git status --short --branch +git log -1 --oneline +``` + +状态应显示本地 `main` 与 `origin/main` 同步,且没有意外的未提交修改。 + +## 远程同步与冲突处理 + +### 推送被拒绝:远程包含本地没有的提交 + +不要直接强制推送。先获取并以变基方式同步: + +```powershell +git pull --rebase origin main +``` + +同步无冲突后再次推送: + +```powershell +git push origin main +``` + +### 变基过程中出现冲突 + +1. 查看冲突文件: + +```powershell +git status +``` + +2. 打开冲突文件,保留正确内容并移除冲突标记: + +```text +<<<<<<< +======= +>>>>>>> +``` + +3. 逐个标记已解决的文件: + +```powershell +git add <已解决的文件路径> +``` + +4. 继续变基: + +```powershell +git rebase --continue +``` + +5. 变基完成后推送: + +```powershell +git push origin main +``` + +如果无法安全解决,保留现场并中止变基: + +```powershell +git rebase --abort +``` + +中止后先向用户说明冲突文件和原因,不要擅自覆盖远程或本地内容。 + +### 工作区有本地未提交修改,无法同步 + +先查看状态和差异,确认这些修改是否属于当前任务。未经确认不要使用 `git reset --hard` 或其他会丢失修改的命令。必要时可在用户确认后暂存并同步,或先创建临时提交。 + +### 只有确认远程历史应被覆盖时才使用强制推送 + +默认禁止使用: + +```powershell +git push --force +``` + +如果用户明确确认覆盖远程历史,优先使用更安全的方式: + +```powershell +git push --force-with-lease origin main +``` + +执行前再次核对远程地址、目标分支和将被覆盖的提交。 + +## 禁止事项 + +- 不重复执行已存在远程名的 `git remote add origin` +- 不使用 `git push --force` 绕过推送拒绝 +- 不使用 `git reset --hard`、清理命令或覆盖操作来隐藏问题 +- 不提交密钥、密码、个人配置、构建产物和临时文件 +- 不跳过变更审阅直接执行 `git add .`、`git commit`、`git push` +- 不在未确认分支和远程地址时推送 + +## 提交检查清单 + +- [ ] 已确认项目根目录和当前分支 +- [ ] 已确认 `origin` 的实际远程地址 +- [ ] 已查看工作区状态和差异 +- [ ] 已排除无关修改及敏感文件 +- [ ] 已精确暂存目标文件 +- [ ] 已复核暂存区差异 +- [ ] 已使用清晰的提交信息 +- [ ] 已确认提交成功 +- [ ] 已同步远程历史或处理冲突 +- [ ] 已推送到预期的 `origin/main` +- [ ] 已确认本地分支与远程分支同步 diff --git a/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php b/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php index 6ef2cc5..0ed51c4 100644 --- a/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php +++ b/backend/plugin/produce/app/adminapi/controller/RepairRecordController.php @@ -49,7 +49,8 @@ class RepairRecordController extends Crud ]; $order = $order ?: 'repaired_at DESC, id DESC'; $total = $this->service->getCount($where); - $list = $this->service->selectList($where, $field, $page, $limit, $order, [], false); + // 与总数查询保持一致,启用关键词搜索字段。 + $list = $this->service->selectList($where, $field, $page, $limit, $order, [], true); return call_user_func([$this, $methods[$format] ?? 'formatNormal'], $list, $total); } catch (\Throwable $e) { return Json::fail($e->getMessage()); diff --git a/backend/plugin/produce/app/api/controller/DataEntryController.php b/backend/plugin/produce/app/api/controller/DataEntryController.php index 867ef5e..813a8bd 100644 --- a/backend/plugin/produce/app/api/controller/DataEntryController.php +++ b/backend/plugin/produce/app/api/controller/DataEntryController.php @@ -40,6 +40,7 @@ final class DataEntryController extends Base { protected ?BaseValidate $validate = null; protected ?BaseValidate $productRecordValidate = null; + private ?array $repairFlowCache = null; public function __construct( protected CurrentMember $currentMember, @@ -124,6 +125,61 @@ final class DataEntryController extends Base ]); } + #[OA\Get(path: '/produce/material/options', summary: '获取维修备件选项', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] + public function materialOptions(Request $request): Response + { + $productCode = trim((string) $request->input('product_code', '')); + $productName = trim((string) $request->input('product_name', '')); + $categoryId = (int) $request->input('category_id', 0); + $page = max(1, (int) $request->input('page', 1)); + $limit = min(50, max(1, (int) $request->input('limit', 10))); + + $query = Material::query()->with('category:id,category_name'); + $query->when($productCode !== '', static fn ($builder) => $builder->where('product_code', 'like', "%{$productCode}%")); + $query->when($productName !== '', static fn ($builder) => $builder->where('product_name', 'like', "%{$productName}%")); + $query->when($categoryId > 0, static fn ($builder) => $builder->where('category_id', $categoryId)); + + $usageCounts = $this->materialUsageCounts(); + $materials = $query->get(['id', 'category_id', 'product_code', 'product_name']) + ->sort(function (Material $left, Material $right) use ($usageCounts): int { + $leftCount = $usageCounts[(string) $left->product_code] ?? 0; + $rightCount = $usageCounts[(string) $right->product_code] ?? 0; + return $leftCount === $rightCount + ? strcmp((string) $left->product_code, (string) $right->product_code) + : $rightCount <=> $leftCount; + }); + $total = $materials->count(); + $items = $materials->forPage($page, $limit) + ->map(static fn (Material $material): array => [ + 'id' => (string) $material->id, + 'category_id' => (string) $material->category_id, + 'category_name' => (string) $material->category_name, + 'product_code' => (string) $material->product_code, + 'product_name' => (string) $material->product_name, + ]) + ->values() + ->all(); + + return Json::success('获取成功', compact('items', 'total', 'page', 'limit')); + } + + private function materialUsageCounts(): array + { + $counts = []; + RepairRecord::query()->without('exception')->whereNotNull('parts')->get(['parts'])->each(function (RepairRecord $record) use (&$counts): void { + foreach ((array) $record->parts as $part) { + $code = trim((string) ($part['product_code'] ?? '')); + if ($code === '') { + continue; + } + $counts[$code] = ($counts[$code] ?? 0) + max(1, (int) ($part['quantity'] ?? 1)); + } + }); + return $counts; + } + #[OA\Get(path: '/produce/scanapp/material-category', summary: '获取扫码端物料分类选项', tags: ['produce-web'])] #[DataResponse(description: '获取成功')] #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] @@ -644,7 +700,7 @@ final class DataEntryController extends Base $query = match ($recordType) { 'scan' => ScanData::query()->with(['process', 'step', 'status']), 'product' => ProductRecord::query()->with(['process', 'step', 'status']), - 'exception' => ExceptionRecord::query(), + 'exception' => ExceptionRecord::query()->with(['process', 'step']), 'repair' => RepairRecord::query()->with('exception'), }; @@ -731,7 +787,7 @@ final class DataEntryController extends Base 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 { + ExceptionRecord::query()->with(['process', 'step'])->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 { @@ -1063,6 +1119,22 @@ final class DataEntryController extends Base return Json::fail('用户编码不存在'); } + $config = $this->getConfigGroupData('basic_setting'); + $processId = (int) ($config['exception_process_id'] ?? 0); + $stepId = (int) ($config['exception_step_id'] ?? 0); + if ($processId <= 0 || $stepId <= 0) { + return Json::fail('异常提报工序步骤尚未配置,请联系管理员'); + } + + $step = ProcessStep::query() + ->whereKey($stepId) + ->where('process_id', $processId) + ->where('status', 1) + ->first(); + if (!$step) { + return Json::fail('异常提报工序步骤配置无效,请联系管理员'); + } + $type = ExceptionSetting::query() ->whereKey($typeId) ->where('setting_type', 'type') @@ -1099,24 +1171,26 @@ final class DataEntryController extends Base 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(), - ]); + 'order_no' => $orderNo, + 'process_id' => $processId, + 'step_id' => $stepId, + '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]); } @@ -1223,13 +1297,13 @@ final class DataEntryController extends Base 'record_type_name' => '异常提报', 'order_no' => (string) $item->order_no, 'product_code' => '', - 'process_name' => '', - 'step_name' => '', + 'process_name' => (string) $item->process_name, + 'step_name' => (string) $item->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, + 'quantity' => 1, 'exception_reason' => (string) $item->reason_name, 'exception_content' => (string) $item->issue_name, 'event_at' => (int) $item->reported_at, @@ -1242,13 +1316,13 @@ final class DataEntryController extends Base 'record_type_name' => '维修记录', 'order_no' => (string) $item->order_no, 'product_code' => (string) $item->product_code, - 'process_name' => '', - 'step_name' => '', + 'process_name' => $this->getRepairFlowNodeNames()['process_name'], + 'step_name' => $this->getRepairFlowNodeNames()['step_name'], 'status_name' => '已维修', 'result_name' => (string) $item->repair_result_name, 'operator' => (string) $item->operator, - 'user_code' => '', - 'quantity' => null, + 'user_code' => (string) ($item->exception?->user_code ?? ''), + 'quantity' => 1, '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), @@ -1258,6 +1332,28 @@ final class DataEntryController extends Base }; } + /** 获取维修记录配置的工序与步骤名称。 */ + private function getRepairFlowNodeNames(): array + { + if ($this->repairFlowCache !== null) { + return $this->repairFlowCache; + } + + $basic = $this->getConfigGroupData('basic_setting'); + $flow = $this->getConfigGroupData('repair_flow'); + $processId = (int) ($basic['repair_process_id'] ?? 0); + $stepId = (int) ($basic['repair_step_id'] ?? 0); + $processId = $processId > 0 ? $processId : (int) ($flow['repair_process_id'] ?? 0); + $stepId = $stepId > 0 ? $stepId : (int) ($flow['repair_step_id'] ?? 0); + $process = $processId > 0 ? Process::query()->withTrashed()->find($processId) : null; + $step = $stepId > 0 ? ProcessStep::query()->withTrashed()->find($stepId) : null; + + return $this->repairFlowCache = [ + 'process_name' => (string) ($process?->process_name ?? ''), + 'step_name' => (string) ($step?->step_name ?? ''), + ]; + } + private function formatTimelineRecord(string $recordType, object $item, int $eventAt): array { $record = $this->formatQueryRecord($recordType, $item); diff --git a/backend/plugin/produce/app/model/ExceptionRecord.php b/backend/plugin/produce/app/model/ExceptionRecord.php index 47ba974..f03d5fb 100644 --- a/backend/plugin/produce/app/model/ExceptionRecord.php +++ b/backend/plugin/produce/app/model/ExceptionRecord.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace plugin\produce\app\model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + class ExceptionRecord extends BaseModel { protected $table = 'produce_exception_record'; @@ -22,6 +24,8 @@ class ExceptionRecord extends BaseModel protected $fillable = [ 'id', 'order_no', + 'process_id', + 'step_id', 'user_code', 'reporter', 'reporter_id', @@ -50,6 +54,8 @@ class ExceptionRecord extends BaseModel protected $casts = [ 'images' => 'array', + 'process_id' => 'integer', + 'step_id' => 'integer', 'status' => 'integer', 'handled_at' => 'integer', 'reported_at' => 'integer', @@ -60,6 +66,26 @@ class ExceptionRecord extends BaseModel '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 getProcessNameAttribute(): string + { + return (string) ($this->process?->process_name ?? ''); + } + + public function getStepNameAttribute(): string + { + return (string) ($this->step?->step_name ?? ''); + } + public function getReportedDateAttribute(): ?string { return $this->reported_at ? date('Y-m-d H:i:s', (int) $this->reported_at) : null; diff --git a/backend/plugin/produce/app/service/admin/RepairRecordService.php b/backend/plugin/produce/app/service/admin/RepairRecordService.php index 88de94c..7a705f7 100644 --- a/backend/plugin/produce/app/service/admin/RepairRecordService.php +++ b/backend/plugin/produce/app/service/admin/RepairRecordService.php @@ -53,6 +53,11 @@ class RepairRecordService extends BaseService private function prepareData(array $data): array { + foreach (['category_ids', 'parts', 'images', 'operation_fault_steps', 'responsible_operators'] as $field) { + if (isset($data[$field]) && is_string($data[$field])) { + $data[$field] = $this->decodeArray($data[$field], $field); + } + } $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) { @@ -100,12 +105,28 @@ class RepairRecordService extends BaseService $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; } + /** + * 将 JSON 字符串转换为数组,避免前端表单提交字符串导致保存失败。 + */ + private function decodeArray(string $value, string $field): ?array + { + if (trim($value) === '') { + return null; + } + try { + $decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \InvalidArgumentException(sprintf('%s 格式不正确', $field), 0, $e); + } + if (!is_array($decoded)) { + throw new \InvalidArgumentException(sprintf('%s 格式不正确', $field)); + } + return $decoded; + } + private function changeFaultUsage(int|string|null $faultId, int $delta): void { if (!$faultId) { diff --git a/backend/plugin/produce/app/validate/RepairRecordValidate.php b/backend/plugin/produce/app/validate/RepairRecordValidate.php index 138d4ee..996771e 100644 --- a/backend/plugin/produce/app/validate/RepairRecordValidate.php +++ b/backend/plugin/produce/app/validate/RepairRecordValidate.php @@ -5,30 +5,39 @@ declare(strict_types=1); namespace plugin\produce\app\validate; use core\base\BaseValidate; +use Illuminate\Validation\Rule; +use plugin\produce\app\model\RepairRecord; class RepairRecordValidate extends BaseValidate { public function rules(): array { + $id = request()->route?->param('id'); + return [ - 'repair_no' => ['required', 'max:100'], - 'order_no' => ['max:100'], - 'product_code' => ['max:100'], - 'product_name' => ['max:200'], - 'category_id' => ['integer'], - 'category_ids' => ['array'], + 'repair_no' => ['required', 'max:100', Rule::unique(RepairRecord::class, 'repair_no')->ignore($id, 'id')], + 'order_no' => ['nullable', 'max:100'], + 'product_code' => ['nullable', 'max:100'], + 'product_name' => ['nullable', 'max:200'], + 'category_id' => ['nullable', 'integer'], + 'category_ids' => ['nullable', 'array'], 'category_ids.*' => ['integer'], - 'fault_description_id' => ['integer'], - 'repair_result' => ['max:50'], - 'operator' => ['max:100'], - 'repaired_at' => ['integer'], - 'remark' => ['max:1000'], + 'fault_description_id' => ['nullable', 'integer'], + 'repair_result' => ['nullable', 'max:50'], + 'parts' => ['nullable'], + 'images' => ['nullable'], + 'operation_fault_steps' => ['nullable'], + 'responsible_operators' => ['nullable'], + 'operator' => ['nullable', 'max:100'], + 'repaired_at' => ['nullable', 'integer'], + 'remark' => ['nullable', 'max:1000'], ]; } protected array $message = [ 'repair_no.required' => '维修单号不能为空', 'repair_no.max' => '维修单号不能超过100个字符', + 'repair_no.unique' => '维修单号已存在', 'order_no.max' => '订单号不能超过100个字符', 'product_code.max' => '商品编码不能超过100个字符', 'product_name.max' => '商品名称不能超过200个字符', diff --git a/backend/plugin/produce/resource/database/migrations/2026_09_01_000001_add_process_step_to_exception_record.php b/backend/plugin/produce/resource/database/migrations/2026_09_01_000001_add_process_step_to_exception_record.php new file mode 100644 index 0000000..8e8d9b3 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_09_01_000001_add_process_step_to_exception_record.php @@ -0,0 +1,44 @@ +hasTable('produce_exception_record')) { + return; + } + + $schema->table('produce_exception_record', function (Blueprint $table) use ($schema): void { + if (!$schema->hasColumn('produce_exception_record', 'process_id')) { + $table->bigInteger('process_id')->nullable()->comment('异常提报工序ID'); + $table->index('process_id', 'idx_exception_record_process_id'); + } + if (!$schema->hasColumn('produce_exception_record', 'step_id')) { + $table->bigInteger('step_id')->nullable()->comment('异常提报步骤ID'); + $table->index('step_id', 'idx_exception_record_step_id'); + } + }); + } + + public function down(Builder $schema): void + { + if (!$schema->hasTable('produce_exception_record')) { + return; + } + + $schema->table('produce_exception_record', function (Blueprint $table) use ($schema): void { + if ($schema->hasColumn('produce_exception_record', 'process_id')) { + $table->dropIndex('idx_exception_record_process_id'); + $table->dropColumn('process_id'); + } + if ($schema->hasColumn('produce_exception_record', 'step_id')) { + $table->dropIndex('idx_exception_record_step_id'); + $table->dropColumn('step_id'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/template/admin/api/index.ts b/backend/plugin/produce/resource/template/admin/api/index.ts index a182241..42b71e6 100644 --- a/backend/plugin/produce/resource/template/admin/api/index.ts +++ b/backend/plugin/produce/resource/template/admin/api/index.ts @@ -394,16 +394,21 @@ export interface ProduceRepairRecordRow { product_name?: string | null; category_id?: string | number | null; category_name?: string | null; + category_ids?: Array | string | null; + category_names?: string[] | 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; + images?: string[] | string | null; + operation_fault_steps?: Array> | string | null; responsible_operators?: Array> | string | null; operator?: string | null; repaired_at?: number | null; repaired_date?: string | null; + repair_result_name?: string; remark?: string | null; created_date?: string; } 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 index 648d1cb..40a7857 100644 --- a/backend/plugin/produce/resource/template/admin/views/produce/config/index.vue +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/index.vue @@ -30,6 +30,12 @@ const settingList = ref([ name: '基础设置', loadKey: 0 }, + { + component: markRaw(defineAsyncComponent(() => import('./modules/repair-flow.vue'))), + key: 'repair-flow', + name: '维修流程', + loadKey: 0 + }, { component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))), key: 'step-rule', 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 index ea96816..a10e8b2 100644 --- 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 @@ -122,7 +122,7 @@ const [BasicForm, formApi] = useForm({ class: 'w-full' } }, - { + { prop: 'recent_records_today_only', label: '最近记录只显示当日', component: 'RadioGroup', @@ -133,6 +133,54 @@ const [BasicForm, formApi] = useForm({ { label: '否', value: 0 } ] } + }, + { + prop: 'exception_process_id', + label: '异常提报工序', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择异常提报对应工序', + }, + }, + { + prop: 'exception_step_id', + label: '异常提报步骤', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择异常提报对应步骤', + }, + }, + { + prop: 'repair_process_id', + label: '维修记录工序', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择维修记录对应工序', + }, + }, + { + prop: 'repair_step_id', + label: '维修记录步骤', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择维修记录对应步骤', + }, } ] }) @@ -156,7 +204,11 @@ async function onSubmit() { { code: 'product_code_max_length', name: '商品编码最大长度', content: values.product_code_max_length ?? 20 }, { code: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) }, { code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 0 }, - { code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) } + { code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) }, + { code: 'exception_process_id', name: '异常提报工序', content: values.exception_process_id ?? '' }, + { code: 'exception_step_id', name: '异常提报步骤', content: values.exception_step_id ?? '' }, + { code: 'repair_process_id', name: '维修记录工序', content: values.repair_process_id ?? '' }, + { code: 'repair_step_id', name: '维修记录步骤', content: values.repair_step_id ?? '' } ] }) ElMessage.success('保存成功') diff --git a/backend/plugin/produce/resource/template/admin/views/produce/config/modules/repair-flow.vue b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/repair-flow.vue new file mode 100644 index 0000000..198515a --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/views/produce/config/modules/repair-flow.vue @@ -0,0 +1,87 @@ + + + + + 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..17a46fd --- /dev/null +++ b/backend/plugin/produce/resource/template/admin/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/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/exception/data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/exception/data/schemas/index.tsx index 3d4630e..17a46fd 100644 --- 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 @@ -4,7 +4,7 @@ import { ProduceExceptionRecordService } from '@/apps/produce/api' const statusOptions = [ { label: '待处理', value: 0 }, { label: '处理中', value: 1 }, - { label: '已处理', value: 2 }, + { label: '已处理', value: 2 } ] export const crudSchema = (): CrudSchema => ({ @@ -14,7 +14,7 @@ export const crudSchema = (): CrudSchema => ({ add: ProduceExceptionRecordService.create, edit: ProduceExceptionRecordService.update, remove: ProduceExceptionRecordService.remove, - view: ProduceExceptionRecordService.get, + view: ProduceExceptionRecordService.get }, useCrud: true, hasAdd: true, @@ -34,21 +34,21 @@ export const crudSchema = (): CrudSchema => ({ label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' }, - colSpan: 8, + colSpan: 8 }, { prop: 'type_code', label: '处理类型', component: 'Input', componentProps: { clearable: true, placeholder: '请输入处理类型编码' }, - colSpan: 8, + colSpan: 8 }, { prop: 'status', label: '处理状态', component: 'Select', componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions }, - colSpan: 8, + colSpan: 8 }, { prop: 'reported_at_range', @@ -59,11 +59,11 @@ export const crudSchema = (): CrudSchema => ({ clearable: true, valueFormat: 'YYYY-MM-DD', startPlaceholder: '开始日期', - endPlaceholder: '结束日期', + endPlaceholder: '结束日期' }, - colSpan: 12, - }, - ], + colSpan: 12 + } + ] }, columns: [ { type: 'selection' }, @@ -76,7 +76,7 @@ export const crudSchema = (): CrudSchema => ({ { prop: 'status_name', label: '处理状态', minWidth: 100 }, { prop: 'handler', label: '处理人', minWidth: 110 }, { prop: 'reported_date', label: '提报时间', minWidth: 170 }, - { prop: 'handled_date', label: '处理时间', minWidth: 170 }, + { prop: 'handled_date', label: '处理时间', minWidth: 170 } ], dialogProps: { width: '820px' }, formSchema: { @@ -85,43 +85,103 @@ export const crudSchema = (): CrudSchema => ({ schema: [ { component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 }, { - component: 'Input', prop: 'order_no', label: '单号/条码', + component: 'Input', + prop: 'order_no', + label: '单号/条码', componentProps: { placeholder: '请输入单号或条码', maxlength: 100 }, - rules: [{ required: true, message: '请输入单号或条码' }], colSpan: 12, + 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: '处理类型编码', + 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, + rules: [{ required: true, message: '请输入处理类型编码' }], + colSpan: 12 }, { - component: 'Input', prop: 'type_name', label: '处理类型', + component: 'Input', + prop: 'type_name', + label: '处理类型', componentProps: { placeholder: '例如 不良维修', maxlength: 100 }, - rules: [{ required: true, message: '请输入处理类型' }], colSpan: 12, + rules: [{ required: true, message: '请输入处理类型' }], + colSpan: 12 }, { - component: 'Input', prop: 'reason_code', label: '异常原因编码', + component: 'Input', + prop: 'reason_code', + label: '异常原因编码', componentProps: { placeholder: '例如 test_defect', maxlength: 64 }, - rules: [{ required: true, message: '请输入异常原因编码' }], colSpan: 12, + rules: [{ required: true, message: '请输入异常原因编码' }], + colSpan: 12 }, { - component: 'Input', prop: 'reason_name', label: '异常原因', + 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, + rules: [{ required: true, message: '请输入异常原因' }], + colSpan: 12 }, { - component: 'Input', prop: 'remark', label: '补充说明', - componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24, + 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/material/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/material/schemas/index.tsx index 71e2114..e835e51 100644 --- 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 @@ -25,7 +25,7 @@ export const crudSchema = (): CrudSchema => ({ 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 }, + { prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'label', valueField: 'value', clearable: true, placeholder: '请选择分类' }, colSpan: 8 }, ], }, columns: [ @@ -42,7 +42,7 @@ export const crudSchema = (): CrudSchema => ({ 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: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'label', valueField: 'value', 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/repair/repair-data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx index d2db915..2ce7601 100644 --- 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 @@ -297,7 +297,6 @@ export const crudSchema = (): CrudSchema => ({ prop: "id", label: "ID", show: false, - ifDetail: true, colSpan: 24, }, { diff --git a/backend/plugin/produce/resource/template/web/api/exception.ts b/backend/plugin/produce/resource/template/web/api/exception.ts index 292cb05..9c14527 100644 --- a/backend/plugin/produce/resource/template/web/api/exception.ts +++ b/backend/plugin/produce/resource/template/web/api/exception.ts @@ -56,12 +56,30 @@ export interface ProduceRepairPart { export interface ProduceScanRecord { id: string | number + process_id?: string | number | null + step_id?: string | number | null process_name?: string step_name?: string operator?: string user_code?: string } +export type ProduceOperationFaultStep = string | number + +export type ProduceResponsibleOperator = string | number + +export interface ProduceRepairRecordPayload { + exception_id: string | number + user_code: string + repair_result: string + images?: string[] + category_ids?: Array + parts?: ProduceRepairPart[] + operation_fault_steps?: ProduceOperationFaultStep[] + responsible_operators?: ProduceResponsibleOperator[] + remark?: string +} + export function getExceptionSettings() { return request.get('/produce/exception/settings', {}, { showErrorMessage: false }) } @@ -78,6 +96,10 @@ export function checkExceptionOrder(orderNo: string) { return request.get>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false }) } +export function checkMemberUser(userCode: string) { + return request.get<{ id: number; username: string; nickname: string }>('/produce/member-user/check', { user_code: userCode.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, @@ -127,7 +149,22 @@ export function lookupMaterial(productCode: string) { }>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false }) } -export function submitRepairRecord(data: Record) { +export function searchMaterials(productCode = '', productName = '', categoryId: string | number = '', page = 1, limit = 10) { + return request.get<{ + items: Array<{ + id: string | number + category_id: string | number + category_name: string + product_code: string + product_name: string + }> + total: number + page: number + limit: number + }>('/produce/material/options', { product_code: productCode.trim(), product_name: productName.trim(), category_id: categoryId, page, limit }, { showErrorMessage: false }) +} + +export function submitRepairRecord(data: ProduceRepairRecordPayload) { return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, { showErrorMessage: false, returnFullResponse: true, diff --git a/backend/plugin/produce/resource/template/web/api/query.ts b/backend/plugin/produce/resource/template/web/api/query.ts index 58cb956..2fdf3ef 100644 --- a/backend/plugin/produce/resource/template/web/api/query.ts +++ b/backend/plugin/produce/resource/template/web/api/query.ts @@ -9,6 +9,7 @@ export interface ProduceQueryRecord { order_no?: string product_code?: string process_name?: string + step_name?: string exception_reason?: string exception_content?: string material_category?: string diff --git a/backend/plugin/produce/resource/template/web/pages/exception/index.vue b/backend/plugin/produce/resource/template/web/pages/exception/index.vue index 4aa024a..b0be923 100644 --- a/backend/plugin/produce/resource/template/web/pages/exception/index.vue +++ b/backend/plugin/produce/resource/template/web/pages/exception/index.vue @@ -66,7 +66,7 @@ @@ -79,10 +79,13 @@ import { ElMessage } from 'element-plus' import type { UploadUserFile } from 'element-plus' import { computed, onMounted, ref } from 'vue' import { useMemberStore } from '~/stores/member' -import { checkExceptionOrder, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord, uploadProduceImage, type ProduceExceptionRecord, type ProduceExceptionSetting } from '~/apps/produce/api/exception' +import { checkExceptionOrder, checkMemberUser, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord, uploadProduceImage, type ProduceExceptionRecord, type ProduceExceptionSetting } from '~/apps/produce/api/exception' const memberStore = useMemberStore() const currentUser = computed(() => ({ name: String(memberStore.info?.nickname || memberStore.info?.real_name || memberStore.info?.username || ''), code: String(memberStore.info?.username || memberStore.info?.user_code || '') })) +const userCode = ref('') +const userName = ref('') +const userCodeError = ref('') const form = ref({ orderNo: '', type: '', reason: '', issue: '', remark: '' }) const settings = ref([]) const recentExceptions = ref([]) @@ -109,6 +112,11 @@ const loadData = async () => { if (!form.value.type) selectType(typeOptions.value[0]?.value || '') } catch { /* 请求层已提示错误 */ } } +const checkUserCode = async () => { + const value = userCode.value.trim() + if (!value) { userCodeError.value = '请输入用户编码'; userName.value = ''; return false } + try { const result = await checkMemberUser(value); userName.value = result?.nickname || result?.username || ''; userCodeError.value = ''; return true } catch (error: any) { userCodeError.value = error?.message || '用户编码不存在'; userName.value = ''; return false } +} const selectType = (value: string) => { form.value.type = value; form.value.reason = activeReasons.value[0]?.value || ''; form.value.issue = activeIssues.value[0]?.value || '' } const selectReason = (value: string) => { form.value.reason = value; form.value.issue = activeIssues.value[0]?.value || '' } const checkOrder = async () => { @@ -122,17 +130,17 @@ const resetForm = () => { form.value = { orderNo: '', type: typeOptions.value[0] const submit = async () => { if (!orderValid.value || !form.value.orderNo.trim()) { await checkOrder(); if (!orderValid.value) return } if (!form.value.type || !form.value.reason || (activeIssues.value.length && !form.value.issue)) { ElMessage.warning('请选择处理类型、异常原因和具体问题'); return } - if (!currentUser.value.code) { ElMessage.warning('请先登录后再提报异常'); return } + if (!await checkUserCode()) { ElMessage.warning(userCodeError.value); return } submitting.value = true try { const images: string[] = [] for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'exception'); if (url) images.push(url) } - const response = await submitExceptionRecord({ order_no: form.value.orderNo.trim(), user_code: currentUser.value.code, type_id: form.value.type, reason_id: form.value.reason, issue_id: form.value.issue || '', images, remark: form.value.remark.trim() }) + const response = await submitExceptionRecord({ order_no: form.value.orderNo.trim(), user_code: userCode.value.trim(), type_id: form.value.type, reason_id: form.value.reason, issue_id: form.value.issue || '', images, remark: form.value.remark.trim() }) if (response?.code !== 0) { ElMessage.error(response?.msg || '异常提交失败'); return } ElMessage.success('异常已提交'); resetForm(); await loadData() } finally { submitting.value = false } } -onMounted(() => { updateNow(); void loadData() }) +onMounted(() => { updateNow(); userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void loadData() }) diff --git a/backend/plugin/produce/resource/template/web/pages/repair/index.vue b/backend/plugin/produce/resource/template/web/pages/repair/index.vue index 27192a0..ba04e1e 100644 --- a/backend/plugin/produce/resource/template/web/pages/repair/index.vue +++ b/backend/plugin/produce/resource/template/web/pages/repair/index.vue @@ -39,43 +39,57 @@
重置完成维修
- + - +
可按编码、名称或物料分类筛选,已添加的备件会自动标记。
diff --git a/docs/2026-09-01/1-更新异常提报工序步骤.md b/docs/2026-09-01/1-更新异常提报工序步骤.md new file mode 100644 index 0000000..d670b3e --- /dev/null +++ b/docs/2026-09-01/1-更新异常提报工序步骤.md @@ -0,0 +1,11 @@ +# 异常提报自动提交工序步骤 + +## 更新内容 + +- 后台生产配置新增“异常提报工序”和“异常提报步骤”选项,管理员可选择有效的工序步骤。 +- 异常提报接口读取后台配置,将工序步骤直接保存到异常记录中。 +- 异常页面保持原有表单,提交时只调用异常提报接口,不在前端维护工序步骤数据。 + +## 行为说明 + +未配置或配置无效时,异常提报会提示管理员先完成工序步骤配置;配置有效时查询列表会在异常记录行直接展示对应工序和步骤。 diff --git a/docs/2026-09-01/10-用户编码样式优化.md b/docs/2026-09-01/10-用户编码样式优化.md new file mode 100644 index 0000000..9752c3d --- /dev/null +++ b/docs/2026-09-01/10-用户编码样式优化.md @@ -0,0 +1,5 @@ +# 用户编码样式优化 + +- 优化异常提报和维修页面用户编码输入区域的宽度、字号、内边距和行间距。 +- 输入框使用更明显的圆角边框和 40px 高度,标签不再窄小换行。 +- 模板源与 Web 运行目录同步。 diff --git a/docs/2026-09-01/12-责任人员文字间距优化.md b/docs/2026-09-01/12-责任人员文字间距优化.md new file mode 100644 index 0000000..1b2efe7 --- /dev/null +++ b/docs/2026-09-01/12-责任人员文字间距优化.md @@ -0,0 +1,5 @@ +# 责任人员文字间距优化 + +- 调整责任人员卡片姓名与工序步骤之间的间距。 +- 姓名固定单行显示并支持超长省略,工序步骤增加行高,提升可读性。 +- 模板源与 Web 运行目录同步。 diff --git a/docs/2026-09-01/13-维修备件搜索弹窗.md b/docs/2026-09-01/13-维修备件搜索弹窗.md new file mode 100644 index 0000000..8b053c0 --- /dev/null +++ b/docs/2026-09-01/13-维修备件搜索弹窗.md @@ -0,0 +1,22 @@ +# 维修备件搜索弹窗 + +## 变更内容 + +- 新增公开物料选项接口 `/produce/material/options`,支持按商品编码或商品名称模糊搜索及分页。 +- 维修记录的“添加备件”改为弹窗选择模式,展示商品编码、商品名称和物料分类。 +- 已添加备件在列表中显示“已添加”并禁止重复选择,选择后自动加载对应故障点。 +- 模板源页面与前端 Web 运行目录已同步。 + +## 涉及文件 + +- `backend/plugin/produce/app/api/controller/DataEntryController.php` +- `backend/plugin/produce/resource/template/web/api/exception.ts` +- `backend/plugin/produce/resource/template/web/pages/repair/index.vue` +- `frontend/web/app/apps/produce/api/exception.ts` +- `frontend/web/app/apps/produce/pages/repair/index.vue` + +## 验证 + +- `php -l backend/plugin/produce/app/api/controller/DataEntryController.php` 通过。 +- 模板源与运行目录页面、API 文件 SHA256 校验一致。 +- 前端类型检查仍受项目既有无关类型错误影响,维修页面及生产 API 未出现新增错误。 diff --git a/docs/2026-09-01/14-维修备件多条件筛选.md b/docs/2026-09-01/14-维修备件多条件筛选.md new file mode 100644 index 0000000..4da4822 --- /dev/null +++ b/docs/2026-09-01/14-维修备件多条件筛选.md @@ -0,0 +1,13 @@ +# 维修备件多条件筛选 + +## 变更内容 + +- 备件弹窗搜索区拆分为商品编码、商品名称两个独立输入框。 +- 新增物料分类下拉筛选,复用维修页面已加载的物料分类数据。 +- `/produce/material/options` 接口支持 `product_code`、`product_name`、`category_id` 三个筛选参数。 +- 模板源与 Web 运行目录页面、接口文件已同步。 + +## 验证 + +- `php -l backend/plugin/produce/app/api/controller/DataEntryController.php` 通过。 +- 模板源与运行目录文件 SHA256 校验一致。 diff --git a/docs/2026-09-01/15-维修备件按频率排序.md b/docs/2026-09-01/15-维修备件按频率排序.md new file mode 100644 index 0000000..fbc01df --- /dev/null +++ b/docs/2026-09-01/15-维修备件按频率排序.md @@ -0,0 +1,7 @@ +# 维修备件按使用频率排序 + +维修备件选项接口现在统计历史维修记录中各备件的使用数量,按使用频率从高到低排序;频率相同时按商品编码升序排列,保证列表稳定。 + +统计来源为维修记录 `parts` JSON 中的商品编码和数量字段,筛选、分页仍按原有接口参数执行。 + +验证:`php -l backend/plugin/produce/app/api/controller/DataEntryController.php` 通过。 diff --git a/docs/2026-09-01/16-物料分类下拉显示修复.md b/docs/2026-09-01/16-物料分类下拉显示修复.md new file mode 100644 index 0000000..2f88db2 --- /dev/null +++ b/docs/2026-09-01/16-物料分类下拉显示修复.md @@ -0,0 +1,7 @@ +# 物料分类下拉显示修复 + +修复物料库页面物料分类下拉选项为空白的问题。 + +原因是分类接口在 `format=select` 模式下返回标准字段 `label`、`value`,页面却按 `category_name`、`id` 读取。现已统一搜索条件和新增/编辑表单的 `ApiSelect` 字段映射为 `label`、`value`。 + +模板源与 Admin 运行目录已同步。 diff --git a/docs/2026-09-01/2-更新异常记录展示工序步骤.md b/docs/2026-09-01/2-更新异常记录展示工序步骤.md new file mode 100644 index 0000000..439f7ec --- /dev/null +++ b/docs/2026-09-01/2-更新异常记录展示工序步骤.md @@ -0,0 +1,3 @@ +# 异常记录直接展示工序步骤 + +异常提报不再创建额外的扫码记录,后台配置的工序和步骤改为直接保存到异常记录,并由记录查询接口展示在异常记录行中。这样查询列表只显示一条红色异常记录,不会重复出现一条扫码数据。 diff --git a/docs/2026-09-01/3-维修流程配置页.md b/docs/2026-09-01/3-维修流程配置页.md new file mode 100644 index 0000000..b466f10 --- /dev/null +++ b/docs/2026-09-01/3-维修流程配置页.md @@ -0,0 +1,6 @@ +# 维修流程配置页 + +- 在生产配置中新增“维修流程”页签。 +- 使用现有工序、工序步骤接口选择维修流程节点。 +- 配置保存至 `repair_flow` 配置组;读取时兼容回退到 `basic_setting` 中的同名字段。 +- 模板源与 `frontend/admin/src/apps/produce` 运行目录保持同步。 diff --git a/docs/2026-09-01/4-基础设置字段调整.md b/docs/2026-09-01/4-基础设置字段调整.md new file mode 100644 index 0000000..725a3ee --- /dev/null +++ b/docs/2026-09-01/4-基础设置字段调整.md @@ -0,0 +1,5 @@ +# 基础设置字段调整 + +- 基础设置页保留“异常提报工序”和“异常提报步骤”配置,用于异常提报自动记录工序节点。 +- 维修流程工序、步骤由独立的“维修流程”配置页维护,保存至 `repair_flow` 配置组。 +- 模板源与 `frontend/admin/src/apps/produce` 运行目录保持一致。 diff --git a/docs/2026-09-01/4-维修数据CRUD修复.md b/docs/2026-09-01/4-维修数据CRUD修复.md new file mode 100644 index 0000000..7371627 --- /dev/null +++ b/docs/2026-09-01/4-维修数据CRUD修复.md @@ -0,0 +1,6 @@ +# 维修数据 CRUD 修复 + +- 修复维修数据列表关键词查询未启用的问题。 +- 增强维修记录唯一单号校验,更新时忽略当前记录。 +- 支持分类、备件、图片及责任快照字段以 JSON 字符串提交并转换为数组。 +- 修复详情表单隐藏 ID 字段被 `ifDetail` 排除,导致更新地址缺少 ID 的问题。 diff --git a/docs/2026-09-01/5-维修记录工序步骤展示修复.md b/docs/2026-09-01/5-维修记录工序步骤展示修复.md new file mode 100644 index 0000000..854b59e --- /dev/null +++ b/docs/2026-09-01/5-维修记录工序步骤展示修复.md @@ -0,0 +1,5 @@ +# 维修记录工序步骤展示修复 + +- 维修记录时间线此前固定返回空工序和步骤,导致提交后显示为“-”。 +- 查询维修记录时读取基础设置中的 `repair_process_id`、`repair_step_id`,并兼容维修流程配置组 `repair_flow`。 +- 根据配置的工序、步骤 ID 查询名称后返回给前台时间线。 diff --git a/docs/2026-09-01/6-查询记录数量显示.md b/docs/2026-09-01/6-查询记录数量显示.md new file mode 100644 index 0000000..fc2a1fe --- /dev/null +++ b/docs/2026-09-01/6-查询记录数量显示.md @@ -0,0 +1,5 @@ +# 查询记录数量显示 + +- 异常提报和维修记录属于单条业务记录,查询接口统一返回数量 `1`。 +- 查询页面继续使用 `quantity` 字段展示,避免显示破折号。 +- Web 查询页模板源与运行目录保持同步。 diff --git a/docs/2026-09-01/7-模板源运行目录一致性检查.md b/docs/2026-09-01/7-模板源运行目录一致性检查.md new file mode 100644 index 0000000..219868c --- /dev/null +++ b/docs/2026-09-01/7-模板源运行目录一致性检查.md @@ -0,0 +1,6 @@ +# 模板源运行目录一致性检查 + +- 已递归核对 produce 插件 Admin、Web 模板源与前端运行目录。 +- Web 目录文件全部一致。 +- Admin 异常数据 Schema 以运行目录最新内容同步回模板源。 +- 运行目录中的扁平异常页面属于兼容路径,模板源保留正式嵌套目录结构,未删除任何文件。 diff --git a/docs/2026-09-01/8-维修记录工号显示修复.md b/docs/2026-09-01/8-维修记录工号显示修复.md new file mode 100644 index 0000000..7ff72ed --- /dev/null +++ b/docs/2026-09-01/8-维修记录工号显示修复.md @@ -0,0 +1,5 @@ +# 维修记录工号显示修复 + +- 修复订单全流程时间线中维修记录工号为空的问题。 +- 维修记录通过关联异常记录读取提报人工号,并统一返回 `user_code` 字段。 +- 查询页面无需额外改动,操作人区域会与扫码、异常记录一样显示工号。 diff --git a/docs/2026-09-01/9-异常维修用户编码录入.md b/docs/2026-09-01/9-异常维修用户编码录入.md new file mode 100644 index 0000000..9d902f2 --- /dev/null +++ b/docs/2026-09-01/9-异常维修用户编码录入.md @@ -0,0 +1,6 @@ +# 异常维修用户编码录入 + +- 异常提报和维修页面新增用户编码输入框。 +- 输入后调用生产录入使用的 `/produce/member-user/check` 接口校验,并显示对应用户名。 +- 提交时使用录入并校验通过的用户编码,不再强制使用当前登录账号。 +- 模板源与 Web 运行目录同步。 diff --git a/frontend/admin/src/apps/produce/api/index.ts b/frontend/admin/src/apps/produce/api/index.ts index a182241..42b71e6 100644 --- a/frontend/admin/src/apps/produce/api/index.ts +++ b/frontend/admin/src/apps/produce/api/index.ts @@ -394,16 +394,21 @@ export interface ProduceRepairRecordRow { product_name?: string | null; category_id?: string | number | null; category_name?: string | null; + category_ids?: Array | string | null; + category_names?: string[] | 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; + images?: string[] | string | null; + operation_fault_steps?: Array> | string | null; responsible_operators?: Array> | string | null; operator?: string | null; repaired_at?: number | null; repaired_date?: string | null; + repair_result_name?: string; remark?: string | null; created_date?: string; } diff --git a/frontend/admin/src/apps/produce/views/produce/config/index.vue b/frontend/admin/src/apps/produce/views/produce/config/index.vue index 648d1cb..40a7857 100644 --- a/frontend/admin/src/apps/produce/views/produce/config/index.vue +++ b/frontend/admin/src/apps/produce/views/produce/config/index.vue @@ -30,6 +30,12 @@ const settingList = ref([ name: '基础设置', loadKey: 0 }, + { + component: markRaw(defineAsyncComponent(() => import('./modules/repair-flow.vue'))), + key: 'repair-flow', + name: '维修流程', + loadKey: 0 + }, { component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))), key: 'step-rule', 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 index ea96816..a10e8b2 100644 --- 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 @@ -122,7 +122,7 @@ const [BasicForm, formApi] = useForm({ class: 'w-full' } }, - { + { prop: 'recent_records_today_only', label: '最近记录只显示当日', component: 'RadioGroup', @@ -133,6 +133,54 @@ const [BasicForm, formApi] = useForm({ { label: '否', value: 0 } ] } + }, + { + prop: 'exception_process_id', + label: '异常提报工序', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择异常提报对应工序', + }, + }, + { + prop: 'exception_step_id', + label: '异常提报步骤', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择异常提报对应步骤', + }, + }, + { + prop: 'repair_process_id', + label: '维修记录工序', + component: 'ApiSelect', + componentProps: { + api: '/produce/process', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择维修记录对应工序', + }, + }, + { + prop: 'repair_step_id', + label: '维修记录步骤', + component: 'ApiSelect', + componentProps: { + api: '/produce/process-step', + requestMethod: 'get', + params: { format: 'select' }, + clearable: true, + placeholder: '请选择维修记录对应步骤', + }, } ] }) @@ -156,7 +204,11 @@ async function onSubmit() { { code: 'product_code_max_length', name: '商品编码最大长度', content: values.product_code_max_length ?? 20 }, { code: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) }, { code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 0 }, - { code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) } + { code: 'recent_records_today_only', name: '最近记录只显示当日', content: Number(values.recent_records_today_only ?? 1) }, + { code: 'exception_process_id', name: '异常提报工序', content: values.exception_process_id ?? '' }, + { code: 'exception_step_id', name: '异常提报步骤', content: values.exception_step_id ?? '' }, + { code: 'repair_process_id', name: '维修记录工序', content: values.repair_process_id ?? '' }, + { code: 'repair_step_id', name: '维修记录步骤', content: values.repair_step_id ?? '' } ] }) ElMessage.success('保存成功') diff --git a/frontend/admin/src/apps/produce/views/produce/config/modules/repair-flow.vue b/frontend/admin/src/apps/produce/views/produce/config/modules/repair-flow.vue new file mode 100644 index 0000000..198515a --- /dev/null +++ b/frontend/admin/src/apps/produce/views/produce/config/modules/repair-flow.vue @@ -0,0 +1,87 @@ + + + + + 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 index 71e2114..e835e51 100644 --- a/frontend/admin/src/apps/produce/views/produce/material/schemas/index.tsx +++ b/frontend/admin/src/apps/produce/views/produce/material/schemas/index.tsx @@ -25,7 +25,7 @@ export const crudSchema = (): CrudSchema => ({ 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 }, + { prop: 'category_id', label: '物料分类', component: 'ApiSelect', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'label', valueField: 'value', clearable: true, placeholder: '请选择分类' }, colSpan: 8 }, ], }, columns: [ @@ -42,7 +42,7 @@ export const crudSchema = (): CrudSchema => ({ 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: 'ApiSelect', prop: 'category_id', label: '物料分类', componentProps: { api: ProduceMaterialService.categoryOptions, labelField: 'label', valueField: 'value', 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/repair/repair-data/schemas/index.tsx b/frontend/admin/src/apps/produce/views/produce/repair/repair-data/schemas/index.tsx index d2db915..2ce7601 100644 --- 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 @@ -297,7 +297,6 @@ export const crudSchema = (): CrudSchema => ({ prop: "id", label: "ID", show: false, - ifDetail: true, colSpan: 24, }, { diff --git a/frontend/web/app/apps/produce/api/exception.ts b/frontend/web/app/apps/produce/api/exception.ts index 292cb05..9c14527 100644 --- a/frontend/web/app/apps/produce/api/exception.ts +++ b/frontend/web/app/apps/produce/api/exception.ts @@ -56,12 +56,30 @@ export interface ProduceRepairPart { export interface ProduceScanRecord { id: string | number + process_id?: string | number | null + step_id?: string | number | null process_name?: string step_name?: string operator?: string user_code?: string } +export type ProduceOperationFaultStep = string | number + +export type ProduceResponsibleOperator = string | number + +export interface ProduceRepairRecordPayload { + exception_id: string | number + user_code: string + repair_result: string + images?: string[] + category_ids?: Array + parts?: ProduceRepairPart[] + operation_fault_steps?: ProduceOperationFaultStep[] + responsible_operators?: ProduceResponsibleOperator[] + remark?: string +} + export function getExceptionSettings() { return request.get('/produce/exception/settings', {}, { showErrorMessage: false }) } @@ -78,6 +96,10 @@ export function checkExceptionOrder(orderNo: string) { return request.get>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false }) } +export function checkMemberUser(userCode: string) { + return request.get<{ id: number; username: string; nickname: string }>('/produce/member-user/check', { user_code: userCode.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, @@ -127,7 +149,22 @@ export function lookupMaterial(productCode: string) { }>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false }) } -export function submitRepairRecord(data: Record) { +export function searchMaterials(productCode = '', productName = '', categoryId: string | number = '', page = 1, limit = 10) { + return request.get<{ + items: Array<{ + id: string | number + category_id: string | number + category_name: string + product_code: string + product_name: string + }> + total: number + page: number + limit: number + }>('/produce/material/options', { product_code: productCode.trim(), product_name: productName.trim(), category_id: categoryId, page, limit }, { showErrorMessage: false }) +} + +export function submitRepairRecord(data: ProduceRepairRecordPayload) { return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, { showErrorMessage: false, returnFullResponse: true, diff --git a/frontend/web/app/apps/produce/api/query.ts b/frontend/web/app/apps/produce/api/query.ts index 58cb956..2fdf3ef 100644 --- a/frontend/web/app/apps/produce/api/query.ts +++ b/frontend/web/app/apps/produce/api/query.ts @@ -9,6 +9,7 @@ export interface ProduceQueryRecord { order_no?: string product_code?: string process_name?: string + step_name?: string exception_reason?: string exception_content?: string material_category?: string diff --git a/frontend/web/app/apps/produce/pages/exception/index.vue b/frontend/web/app/apps/produce/pages/exception/index.vue index 4aa024a..b0be923 100644 --- a/frontend/web/app/apps/produce/pages/exception/index.vue +++ b/frontend/web/app/apps/produce/pages/exception/index.vue @@ -66,7 +66,7 @@ @@ -79,10 +79,13 @@ import { ElMessage } from 'element-plus' import type { UploadUserFile } from 'element-plus' import { computed, onMounted, ref } from 'vue' import { useMemberStore } from '~/stores/member' -import { checkExceptionOrder, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord, uploadProduceImage, type ProduceExceptionRecord, type ProduceExceptionSetting } from '~/apps/produce/api/exception' +import { checkExceptionOrder, checkMemberUser, getExceptionSettings, getExceptionStatistics, getRecentExceptions, submitExceptionRecord, uploadProduceImage, type ProduceExceptionRecord, type ProduceExceptionSetting } from '~/apps/produce/api/exception' const memberStore = useMemberStore() const currentUser = computed(() => ({ name: String(memberStore.info?.nickname || memberStore.info?.real_name || memberStore.info?.username || ''), code: String(memberStore.info?.username || memberStore.info?.user_code || '') })) +const userCode = ref('') +const userName = ref('') +const userCodeError = ref('') const form = ref({ orderNo: '', type: '', reason: '', issue: '', remark: '' }) const settings = ref([]) const recentExceptions = ref([]) @@ -109,6 +112,11 @@ const loadData = async () => { if (!form.value.type) selectType(typeOptions.value[0]?.value || '') } catch { /* 请求层已提示错误 */ } } +const checkUserCode = async () => { + const value = userCode.value.trim() + if (!value) { userCodeError.value = '请输入用户编码'; userName.value = ''; return false } + try { const result = await checkMemberUser(value); userName.value = result?.nickname || result?.username || ''; userCodeError.value = ''; return true } catch (error: any) { userCodeError.value = error?.message || '用户编码不存在'; userName.value = ''; return false } +} const selectType = (value: string) => { form.value.type = value; form.value.reason = activeReasons.value[0]?.value || ''; form.value.issue = activeIssues.value[0]?.value || '' } const selectReason = (value: string) => { form.value.reason = value; form.value.issue = activeIssues.value[0]?.value || '' } const checkOrder = async () => { @@ -122,17 +130,17 @@ const resetForm = () => { form.value = { orderNo: '', type: typeOptions.value[0] const submit = async () => { if (!orderValid.value || !form.value.orderNo.trim()) { await checkOrder(); if (!orderValid.value) return } if (!form.value.type || !form.value.reason || (activeIssues.value.length && !form.value.issue)) { ElMessage.warning('请选择处理类型、异常原因和具体问题'); return } - if (!currentUser.value.code) { ElMessage.warning('请先登录后再提报异常'); return } + if (!await checkUserCode()) { ElMessage.warning(userCodeError.value); return } submitting.value = true try { const images: string[] = [] for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'exception'); if (url) images.push(url) } - const response = await submitExceptionRecord({ order_no: form.value.orderNo.trim(), user_code: currentUser.value.code, type_id: form.value.type, reason_id: form.value.reason, issue_id: form.value.issue || '', images, remark: form.value.remark.trim() }) + const response = await submitExceptionRecord({ order_no: form.value.orderNo.trim(), user_code: userCode.value.trim(), type_id: form.value.type, reason_id: form.value.reason, issue_id: form.value.issue || '', images, remark: form.value.remark.trim() }) if (response?.code !== 0) { ElMessage.error(response?.msg || '异常提交失败'); return } ElMessage.success('异常已提交'); resetForm(); await loadData() } finally { submitting.value = false } } -onMounted(() => { updateNow(); void loadData() }) +onMounted(() => { updateNow(); userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void loadData() }) diff --git a/frontend/web/app/apps/produce/pages/repair/index.vue b/frontend/web/app/apps/produce/pages/repair/index.vue index 27192a0..ba04e1e 100644 --- a/frontend/web/app/apps/produce/pages/repair/index.vue +++ b/frontend/web/app/apps/produce/pages/repair/index.vue @@ -39,43 +39,57 @@
重置完成维修
- + - +
可按编码、名称或物料分类筛选,已添加的备件会自动标记。