完善生产异常维修与备件管理功能
This commit is contained in:
@@ -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`
|
||||||
|
- [ ] 已确认本地分支与远程分支同步
|
||||||
@@ -49,7 +49,8 @@ class RepairRecordController extends Crud
|
|||||||
];
|
];
|
||||||
$order = $order ?: 'repaired_at DESC, id DESC';
|
$order = $order ?: 'repaired_at DESC, id DESC';
|
||||||
$total = $this->service->getCount($where);
|
$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);
|
return call_user_func([$this, $methods[$format] ?? 'formatNormal'], $list, $total);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return Json::fail($e->getMessage());
|
return Json::fail($e->getMessage());
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ final class DataEntryController extends Base
|
|||||||
{
|
{
|
||||||
protected ?BaseValidate $validate = null;
|
protected ?BaseValidate $validate = null;
|
||||||
protected ?BaseValidate $productRecordValidate = null;
|
protected ?BaseValidate $productRecordValidate = null;
|
||||||
|
private ?array $repairFlowCache = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected CurrentMember $currentMember,
|
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'])]
|
#[OA\Get(path: '/produce/scanapp/material-category', summary: '获取扫码端物料分类选项', tags: ['produce-web'])]
|
||||||
#[DataResponse(description: '获取成功')]
|
#[DataResponse(description: '获取成功')]
|
||||||
#[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')]
|
#[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')]
|
||||||
@@ -644,7 +700,7 @@ final class DataEntryController extends Base
|
|||||||
$query = match ($recordType) {
|
$query = match ($recordType) {
|
||||||
'scan' => ScanData::query()->with(['process', 'step', 'status']),
|
'scan' => ScanData::query()->with(['process', 'step', 'status']),
|
||||||
'product' => ProductRecord::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'),
|
'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 {
|
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));
|
$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));
|
$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 {
|
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('用户编码不存在');
|
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()
|
$type = ExceptionSetting::query()
|
||||||
->whereKey($typeId)
|
->whereKey($typeId)
|
||||||
->where('setting_type', 'type')
|
->where('setting_type', 'type')
|
||||||
@@ -1099,24 +1171,26 @@ final class DataEntryController extends Base
|
|||||||
return is_string($image) && preg_match('/^https?:\/\//', $image) === 1;
|
return is_string($image) && preg_match('/^https?:\/\//', $image) === 1;
|
||||||
}));
|
}));
|
||||||
$record = ExceptionRecord::query()->create([
|
$record = ExceptionRecord::query()->create([
|
||||||
'order_no' => $orderNo,
|
'order_no' => $orderNo,
|
||||||
'user_code' => $userCode,
|
'process_id' => $processId,
|
||||||
'reporter' => (string) ($member->nickname ?: $member->username),
|
'step_id' => $stepId,
|
||||||
'reporter_id' => (int) $member->id,
|
'user_code' => $userCode,
|
||||||
'type_id' => $type->id,
|
'reporter' => (string) ($member->nickname ?: $member->username),
|
||||||
'type_code' => $type->code,
|
'reporter_id' => (int) $member->id,
|
||||||
'type_name' => $type->name,
|
'type_id' => $type->id,
|
||||||
'reason_id' => $reason->id,
|
'type_code' => $type->code,
|
||||||
'reason_code' => $reason->code,
|
'type_name' => $type->name,
|
||||||
'reason_name' => $reason->name,
|
'reason_id' => $reason->id,
|
||||||
'issue_id' => $issue?->id,
|
'reason_code' => $reason->code,
|
||||||
'issue_code' => $issue?->code,
|
'reason_name' => $reason->name,
|
||||||
'issue_name' => $issue?->name,
|
'issue_id' => $issue?->id,
|
||||||
'images' => $images,
|
'issue_code' => $issue?->code,
|
||||||
'remark' => trim((string) ($data['remark'] ?? '')),
|
'issue_name' => $issue?->name,
|
||||||
'status' => 0,
|
'images' => $images,
|
||||||
'reported_at' => time(),
|
'remark' => trim((string) ($data['remark'] ?? '')),
|
||||||
]);
|
'status' => 0,
|
||||||
|
'reported_at' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
return Json::success('提交成功', ['id' => $record->id]);
|
return Json::success('提交成功', ['id' => $record->id]);
|
||||||
}
|
}
|
||||||
@@ -1223,13 +1297,13 @@ final class DataEntryController extends Base
|
|||||||
'record_type_name' => '异常提报',
|
'record_type_name' => '异常提报',
|
||||||
'order_no' => (string) $item->order_no,
|
'order_no' => (string) $item->order_no,
|
||||||
'product_code' => '',
|
'product_code' => '',
|
||||||
'process_name' => '',
|
'process_name' => (string) $item->process_name,
|
||||||
'step_name' => '',
|
'step_name' => (string) $item->step_name,
|
||||||
'status_name' => (string) $item->status_name,
|
'status_name' => (string) $item->status_name,
|
||||||
'result_name' => (string) $item->type_name,
|
'result_name' => (string) $item->type_name,
|
||||||
'operator' => (string) $item->reporter,
|
'operator' => (string) $item->reporter,
|
||||||
'user_code' => (string) $item->user_code,
|
'user_code' => (string) $item->user_code,
|
||||||
'quantity' => null,
|
'quantity' => 1,
|
||||||
'exception_reason' => (string) $item->reason_name,
|
'exception_reason' => (string) $item->reason_name,
|
||||||
'exception_content' => (string) $item->issue_name,
|
'exception_content' => (string) $item->issue_name,
|
||||||
'event_at' => (int) $item->reported_at,
|
'event_at' => (int) $item->reported_at,
|
||||||
@@ -1242,13 +1316,13 @@ final class DataEntryController extends Base
|
|||||||
'record_type_name' => '维修记录',
|
'record_type_name' => '维修记录',
|
||||||
'order_no' => (string) $item->order_no,
|
'order_no' => (string) $item->order_no,
|
||||||
'product_code' => (string) $item->product_code,
|
'product_code' => (string) $item->product_code,
|
||||||
'process_name' => '',
|
'process_name' => $this->getRepairFlowNodeNames()['process_name'],
|
||||||
'step_name' => '',
|
'step_name' => $this->getRepairFlowNodeNames()['step_name'],
|
||||||
'status_name' => '已维修',
|
'status_name' => '已维修',
|
||||||
'result_name' => (string) $item->repair_result_name,
|
'result_name' => (string) $item->repair_result_name,
|
||||||
'operator' => (string) $item->operator,
|
'operator' => (string) $item->operator,
|
||||||
'user_code' => '',
|
'user_code' => (string) ($item->exception?->user_code ?? ''),
|
||||||
'quantity' => null,
|
'quantity' => 1,
|
||||||
'material_category' => (string) ($item->category_name ?: $item->category_names),
|
'material_category' => (string) ($item->category_name ?: $item->category_names),
|
||||||
'fault_description' => (string) ($item->fault_description_name ?: $item->fault_description),
|
'fault_description' => (string) ($item->fault_description_name ?: $item->fault_description),
|
||||||
'event_at' => (int) ($item->repaired_at ?: $item->created_at),
|
'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
|
private function formatTimelineRecord(string $recordType, object $item, int $eventAt): array
|
||||||
{
|
{
|
||||||
$record = $this->formatQueryRecord($recordType, $item);
|
$record = $this->formatQueryRecord($recordType, $item);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace plugin\produce\app\model;
|
namespace plugin\produce\app\model;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class ExceptionRecord extends BaseModel
|
class ExceptionRecord extends BaseModel
|
||||||
{
|
{
|
||||||
protected $table = 'produce_exception_record';
|
protected $table = 'produce_exception_record';
|
||||||
@@ -22,6 +24,8 @@ class ExceptionRecord extends BaseModel
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'id',
|
'id',
|
||||||
'order_no',
|
'order_no',
|
||||||
|
'process_id',
|
||||||
|
'step_id',
|
||||||
'user_code',
|
'user_code',
|
||||||
'reporter',
|
'reporter',
|
||||||
'reporter_id',
|
'reporter_id',
|
||||||
@@ -50,6 +54,8 @@ class ExceptionRecord extends BaseModel
|
|||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'images' => 'array',
|
'images' => 'array',
|
||||||
|
'process_id' => 'integer',
|
||||||
|
'step_id' => 'integer',
|
||||||
'status' => 'integer',
|
'status' => 'integer',
|
||||||
'handled_at' => 'integer',
|
'handled_at' => 'integer',
|
||||||
'reported_at' => 'integer',
|
'reported_at' => 'integer',
|
||||||
@@ -60,6 +66,26 @@ class ExceptionRecord extends BaseModel
|
|||||||
'deleted_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 getProcessNameAttribute(): string
|
||||||
|
{
|
||||||
|
return (string) ($this->process?->process_name ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStepNameAttribute(): string
|
||||||
|
{
|
||||||
|
return (string) ($this->step?->step_name ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
public function getReportedDateAttribute(): ?string
|
public function getReportedDateAttribute(): ?string
|
||||||
{
|
{
|
||||||
return $this->reported_at ? date('Y-m-d H:i:s', (int) $this->reported_at) : null;
|
return $this->reported_at ? date('Y-m-d H:i:s', (int) $this->reported_at) : null;
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ class RepairRecordService extends BaseService
|
|||||||
|
|
||||||
private function prepareData(array $data): array
|
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);
|
$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 !== '')));
|
$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) {
|
if ($hasCategoryIds) {
|
||||||
@@ -100,12 +105,28 @@ class RepairRecordService extends BaseService
|
|||||||
$data['category_names'] = [$data['category_name']];
|
$data['category_names'] = [$data['category_name']];
|
||||||
$data['fault_description'] = $fault->description;
|
$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;
|
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
|
private function changeFaultUsage(int|string|null $faultId, int $delta): void
|
||||||
{
|
{
|
||||||
if (!$faultId) {
|
if (!$faultId) {
|
||||||
|
|||||||
@@ -5,30 +5,39 @@ declare(strict_types=1);
|
|||||||
namespace plugin\produce\app\validate;
|
namespace plugin\produce\app\validate;
|
||||||
|
|
||||||
use core\base\BaseValidate;
|
use core\base\BaseValidate;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use plugin\produce\app\model\RepairRecord;
|
||||||
|
|
||||||
class RepairRecordValidate extends BaseValidate
|
class RepairRecordValidate extends BaseValidate
|
||||||
{
|
{
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$id = request()->route?->param('id');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'repair_no' => ['required', 'max:100'],
|
'repair_no' => ['required', 'max:100', Rule::unique(RepairRecord::class, 'repair_no')->ignore($id, 'id')],
|
||||||
'order_no' => ['max:100'],
|
'order_no' => ['nullable', 'max:100'],
|
||||||
'product_code' => ['max:100'],
|
'product_code' => ['nullable', 'max:100'],
|
||||||
'product_name' => ['max:200'],
|
'product_name' => ['nullable', 'max:200'],
|
||||||
'category_id' => ['integer'],
|
'category_id' => ['nullable', 'integer'],
|
||||||
'category_ids' => ['array'],
|
'category_ids' => ['nullable', 'array'],
|
||||||
'category_ids.*' => ['integer'],
|
'category_ids.*' => ['integer'],
|
||||||
'fault_description_id' => ['integer'],
|
'fault_description_id' => ['nullable', 'integer'],
|
||||||
'repair_result' => ['max:50'],
|
'repair_result' => ['nullable', 'max:50'],
|
||||||
'operator' => ['max:100'],
|
'parts' => ['nullable'],
|
||||||
'repaired_at' => ['integer'],
|
'images' => ['nullable'],
|
||||||
'remark' => ['max:1000'],
|
'operation_fault_steps' => ['nullable'],
|
||||||
|
'responsible_operators' => ['nullable'],
|
||||||
|
'operator' => ['nullable', 'max:100'],
|
||||||
|
'repaired_at' => ['nullable', 'integer'],
|
||||||
|
'remark' => ['nullable', 'max:1000'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected array $message = [
|
protected array $message = [
|
||||||
'repair_no.required' => '维修单号不能为空',
|
'repair_no.required' => '维修单号不能为空',
|
||||||
'repair_no.max' => '维修单号不能超过100个字符',
|
'repair_no.max' => '维修单号不能超过100个字符',
|
||||||
|
'repair_no.unique' => '维修单号已存在',
|
||||||
'order_no.max' => '订单号不能超过100个字符',
|
'order_no.max' => '订单号不能超过100个字符',
|
||||||
'product_code.max' => '商品编码不能超过100个字符',
|
'product_code.max' => '商品编码不能超过100个字符',
|
||||||
'product_name.max' => '商品名称不能超过200个字符',
|
'product_name.max' => '商品名称不能超过200个字符',
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Schema\Builder;
|
||||||
|
|
||||||
|
return new class {
|
||||||
|
public function up(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->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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -394,16 +394,21 @@ export interface ProduceRepairRecordRow {
|
|||||||
product_name?: string | null;
|
product_name?: string | null;
|
||||||
category_id?: string | number | null;
|
category_id?: string | number | null;
|
||||||
category_name?: string | null;
|
category_name?: string | null;
|
||||||
|
category_ids?: Array<string | number> | string | null;
|
||||||
|
category_names?: string[] | string | null;
|
||||||
fault_description_id?: string | number | null;
|
fault_description_id?: string | number | null;
|
||||||
fault_description_ids?: Array<string | number>;
|
fault_description_ids?: Array<string | number>;
|
||||||
fault_description?: string | null;
|
fault_description?: string | null;
|
||||||
fault_descriptions?: string[];
|
fault_descriptions?: string[];
|
||||||
repair_result?: string | null;
|
repair_result?: string | null;
|
||||||
parts?: Array<Record<string, any>> | string | null;
|
parts?: Array<Record<string, any>> | string | null;
|
||||||
|
images?: string[] | string | null;
|
||||||
|
operation_fault_steps?: Array<Record<string, any>> | string | null;
|
||||||
responsible_operators?: Array<Record<string, any>> | string | null;
|
responsible_operators?: Array<Record<string, any>> | string | null;
|
||||||
operator?: string | null;
|
operator?: string | null;
|
||||||
repaired_at?: number | null;
|
repaired_at?: number | null;
|
||||||
repaired_date?: string | null;
|
repaired_date?: string | null;
|
||||||
|
repair_result_name?: string;
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
created_date?: string;
|
created_date?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const settingList = ref([
|
|||||||
name: '基础设置',
|
name: '基础设置',
|
||||||
loadKey: 0
|
loadKey: 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(defineAsyncComponent(() => import('./modules/repair-flow.vue'))),
|
||||||
|
key: 'repair-flow',
|
||||||
|
name: '维修流程',
|
||||||
|
loadKey: 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
|
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
|
||||||
key: 'step-rule',
|
key: 'step-rule',
|
||||||
|
|||||||
+54
-2
@@ -122,7 +122,7 @@ const [BasicForm, formApi] = useForm({
|
|||||||
class: 'w-full'
|
class: 'w-full'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'recent_records_today_only',
|
prop: 'recent_records_today_only',
|
||||||
label: '最近记录只显示当日',
|
label: '最近记录只显示当日',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
@@ -133,6 +133,54 @@ const [BasicForm, formApi] = useForm({
|
|||||||
{ label: '否', value: 0 }
|
{ 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: '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: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) },
|
||||||
{ code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 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('保存成功')
|
ElMessage.success('保存成功')
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repair-flow-page">
|
||||||
|
<BasicForm />
|
||||||
|
<el-button class="site-submit" type="primary" :loading="saving" @click="onSubmit">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useForm } from '@/components/form'
|
||||||
|
import { ProduceConfigApi } from '@/apps/produce/api'
|
||||||
|
|
||||||
|
const saving = ref(false)
|
||||||
|
const [BasicForm, formApi] = useForm({
|
||||||
|
labelPosition: 'right',
|
||||||
|
labelWidth: '160px',
|
||||||
|
inline: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
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: '请选择维修流程对应步骤',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
const current = await ProduceConfigApi.info({ group_code: 'repair_flow' }) as Record<string, any>
|
||||||
|
if (current?.repair_process_id || current?.repair_step_id) {
|
||||||
|
await formApi.setValues(current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const legacy = await ProduceConfigApi.info({ group_code: 'basic_setting' }) as Record<string, any>
|
||||||
|
await formApi.setValues({
|
||||||
|
repair_process_id: legacy?.repair_process_id || '',
|
||||||
|
repair_step_id: legacy?.repair_step_id || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const values = await formApi.getValues()
|
||||||
|
await ProduceConfigApi.save({
|
||||||
|
group_code: 'repair_flow',
|
||||||
|
configs: [
|
||||||
|
{ code: 'repair_process_id', name: '维修流程工序', content: values.repair_process_id ?? '' },
|
||||||
|
{ code: 'repair_step_id', name: '维修流程步骤', content: values.repair_step_id ?? '' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.site-submit {
|
||||||
|
margin-left: 160px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="exception-analysis-page">
|
||||||
|
<div class="analysis-toolbar">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filters.reported_at_range"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
/>
|
||||||
|
<el-input v-model="filters.type_code" clearable placeholder="处理类型编码" @keyup.enter="loadData" />
|
||||||
|
<el-select v-model="filters.status" clearable placeholder="处理状态">
|
||||||
|
<el-option label="待处理" :value="0" />
|
||||||
|
<el-option label="处理中" :value="1" />
|
||||||
|
<el-option label="已处理" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item"><span>异常总数</span><strong>{{ overview.total }}</strong></div>
|
||||||
|
<div class="metric-item pending"><span>待处理</span><strong>{{ overview.pending }}</strong></div>
|
||||||
|
<div class="metric-item processing"><span>处理中</span><strong>{{ overview.processing }}</strong></div>
|
||||||
|
<div class="metric-item completed"><span>已处理</span><strong>{{ overview.completed }}</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="analysis-grid" v-loading="loading">
|
||||||
|
<section class="analysis-section">
|
||||||
|
<h3>处理类型分布</h3>
|
||||||
|
<el-table :data="overview.by_type" size="small" height="260" empty-text="暂无数据">
|
||||||
|
<el-table-column prop="name" label="处理类型" min-width="150" />
|
||||||
|
<el-table-column prop="total" label="数量" width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
<section class="analysis-section">
|
||||||
|
<h3>异常原因排行</h3>
|
||||||
|
<el-table :data="overview.by_reason" size="small" height="260" empty-text="暂无数据">
|
||||||
|
<el-table-column type="index" label="排名" width="70" />
|
||||||
|
<el-table-column prop="name" label="异常原因" min-width="150" />
|
||||||
|
<el-table-column prop="total" label="数量" width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
<section class="analysis-section">
|
||||||
|
<h3>具体问题排行</h3>
|
||||||
|
<el-table :data="overview.by_issue" size="small" height="260" empty-text="暂无数据">
|
||||||
|
<el-table-column type="index" label="排名" width="70" />
|
||||||
|
<el-table-column prop="name" label="具体问题" min-width="150" />
|
||||||
|
<el-table-column prop="total" label="数量" width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
<section class="analysis-section full-width">
|
||||||
|
<h3>每日提报趋势</h3>
|
||||||
|
<el-table :data="overview.by_date" size="small" height="260" empty-text="暂无数据">
|
||||||
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
|
<el-table-column prop="date" label="日期" min-width="180" />
|
||||||
|
<el-table-column prop="total" label="异常数量" width="130" align="right" />
|
||||||
|
</el-table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
interface AnalysisItem {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DateItem {
|
||||||
|
date: string
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnalysisOverview {
|
||||||
|
total: number
|
||||||
|
pending: number
|
||||||
|
processing: number
|
||||||
|
completed: number
|
||||||
|
by_type: AnalysisItem[]
|
||||||
|
by_reason: AnalysisItem[]
|
||||||
|
by_issue: AnalysisItem[]
|
||||||
|
by_date: DateItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProduceExceptionAnalysis' })
|
||||||
|
|
||||||
|
const defaultRange = (): [string, string] => {
|
||||||
|
const today = new Date()
|
||||||
|
const start = new Date(today)
|
||||||
|
start.setDate(today.getDate() - 29)
|
||||||
|
const format = (date: Date) => date.toISOString().slice(0, 10)
|
||||||
|
return [format(start), format(today)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
reported_at_range: defaultRange() as string[] | undefined,
|
||||||
|
type_code: '',
|
||||||
|
status: undefined as number | undefined,
|
||||||
|
})
|
||||||
|
const loading = ref(false)
|
||||||
|
const overview = ref<AnalysisOverview>({
|
||||||
|
total: 0,
|
||||||
|
pending: 0,
|
||||||
|
processing: 0,
|
||||||
|
completed: 0,
|
||||||
|
by_type: [],
|
||||||
|
by_reason: [],
|
||||||
|
by_issue: [],
|
||||||
|
by_date: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
overview.value = await request.get<AnalysisOverview>({
|
||||||
|
url: '/produce/exception-analysis/overview',
|
||||||
|
params: filters,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
filters.reported_at_range = defaultRange()
|
||||||
|
filters.type_code = ''
|
||||||
|
filters.status = undefined
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.exception-analysis-page {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--el-bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-toolbar,
|
||||||
|
.metric-grid,
|
||||||
|
.analysis-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-toolbar {
|
||||||
|
grid-template-columns: minmax(260px, 1fr) 180px 150px auto auto;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item,
|
||||||
|
.analysis-section {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item span,
|
||||||
|
.metric-item strong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item span {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item strong {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item.pending strong { color: var(--el-color-danger); }
|
||||||
|
.metric-item.processing strong { color: var(--el-color-warning); }
|
||||||
|
.metric-item.completed strong { color: var(--el-color-success); }
|
||||||
|
|
||||||
|
.analysis-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-section {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-section h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.analysis-toolbar,
|
||||||
|
.analysis-grid { grid-template-columns: 1fr 1fr; }
|
||||||
|
.metric-grid { grid-template-columns: 1fr 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.analysis-toolbar,
|
||||||
|
.analysis-grid,
|
||||||
|
.metric-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="produce-exception-data-page art-full-height">
|
||||||
|
<BasicCrud />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { useCrud } from '@/components/crud'
|
||||||
|
import { crudSchema } from './schemas'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProduceExceptionDataList' })
|
||||||
|
|
||||||
|
const [BasicCrud] = useCrud({
|
||||||
|
...crudSchema(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.produce-exception-data-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+187
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="exception-setting-page art-full-height">
|
||||||
|
<BasicCrud />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { useCrud } from '@/components/crud'
|
||||||
|
import { crudSchema } from './schemas'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProduceExceptionSettingList' })
|
||||||
|
|
||||||
|
const [BasicCrud] = useCrud({
|
||||||
|
...crudSchema(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.exception-setting-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+100
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
+93
-33
@@ -4,7 +4,7 @@ import { ProduceExceptionRecordService } from '@/apps/produce/api'
|
|||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: '待处理', value: 0 },
|
{ label: '待处理', value: 0 },
|
||||||
{ label: '处理中', value: 1 },
|
{ label: '处理中', value: 1 },
|
||||||
{ label: '已处理', value: 2 },
|
{ label: '已处理', value: 2 }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const crudSchema = (): CrudSchema => ({
|
export const crudSchema = (): CrudSchema => ({
|
||||||
@@ -14,7 +14,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
add: ProduceExceptionRecordService.create,
|
add: ProduceExceptionRecordService.create,
|
||||||
edit: ProduceExceptionRecordService.update,
|
edit: ProduceExceptionRecordService.update,
|
||||||
remove: ProduceExceptionRecordService.remove,
|
remove: ProduceExceptionRecordService.remove,
|
||||||
view: ProduceExceptionRecordService.get,
|
view: ProduceExceptionRecordService.get
|
||||||
},
|
},
|
||||||
useCrud: true,
|
useCrud: true,
|
||||||
hasAdd: true,
|
hasAdd: true,
|
||||||
@@ -34,21 +34,21 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
label: '关键字',
|
label: '关键字',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' },
|
componentProps: { clearable: true, placeholder: '单号、提报人、处理类型、原因或具体问题' },
|
||||||
colSpan: 8,
|
colSpan: 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'type_code',
|
prop: 'type_code',
|
||||||
label: '处理类型',
|
label: '处理类型',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: { clearable: true, placeholder: '请输入处理类型编码' },
|
componentProps: { clearable: true, placeholder: '请输入处理类型编码' },
|
||||||
colSpan: 8,
|
colSpan: 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '处理状态',
|
label: '处理状态',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions },
|
componentProps: { clearable: true, placeholder: '请选择处理状态', options: statusOptions },
|
||||||
colSpan: 8,
|
colSpan: 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'reported_at_range',
|
prop: 'reported_at_range',
|
||||||
@@ -59,11 +59,11 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
clearable: true,
|
clearable: true,
|
||||||
valueFormat: 'YYYY-MM-DD',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
startPlaceholder: '开始日期',
|
startPlaceholder: '开始日期',
|
||||||
endPlaceholder: '结束日期',
|
endPlaceholder: '结束日期'
|
||||||
},
|
},
|
||||||
colSpan: 12,
|
colSpan: 12
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{ type: 'selection' },
|
{ type: 'selection' },
|
||||||
@@ -76,7 +76,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
{ prop: 'status_name', label: '处理状态', minWidth: 100 },
|
{ prop: 'status_name', label: '处理状态', minWidth: 100 },
|
||||||
{ prop: 'handler', label: '处理人', minWidth: 110 },
|
{ prop: 'handler', label: '处理人', minWidth: 110 },
|
||||||
{ prop: 'reported_date', label: '提报时间', minWidth: 170 },
|
{ prop: 'reported_date', label: '提报时间', minWidth: 170 },
|
||||||
{ prop: 'handled_date', label: '处理时间', minWidth: 170 },
|
{ prop: 'handled_date', label: '处理时间', minWidth: 170 }
|
||||||
],
|
],
|
||||||
dialogProps: { width: '820px' },
|
dialogProps: { width: '820px' },
|
||||||
formSchema: {
|
formSchema: {
|
||||||
@@ -85,43 +85,103 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
schema: [
|
schema: [
|
||||||
{ component: 'Input', prop: 'id', label: 'ID', show: false, ifDetail: true, colSpan: 24 },
|
{ 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 },
|
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 },
|
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 },
|
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 },
|
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 },
|
componentProps: { placeholder: '例如 测试不良', maxlength: 100 },
|
||||||
rules: [{ required: true, message: '请输入异常原因' }], colSpan: 12,
|
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: '补充说明',
|
component: 'Input',
|
||||||
componentProps: { type: 'textarea', rows: 3, placeholder: '请输入现场情况或处理说明' }, colSpan: 24,
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
labelWidth: '90px',
|
labelWidth: '90px',
|
||||||
schema: [
|
schema: [
|
||||||
{ prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 },
|
{ 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: [
|
columns: [
|
||||||
@@ -42,7 +42,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
labelWidth: '90px',
|
labelWidth: '90px',
|
||||||
schema: [
|
schema: [
|
||||||
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
|
{ 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_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 },
|
{ component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 },
|
||||||
],
|
],
|
||||||
|
|||||||
-1
@@ -297,7 +297,6 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
prop: "id",
|
prop: "id",
|
||||||
label: "ID",
|
label: "ID",
|
||||||
show: false,
|
show: false,
|
||||||
ifDetail: true,
|
|
||||||
colSpan: 24,
|
colSpan: 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,12 +56,30 @@ export interface ProduceRepairPart {
|
|||||||
|
|
||||||
export interface ProduceScanRecord {
|
export interface ProduceScanRecord {
|
||||||
id: string | number
|
id: string | number
|
||||||
|
process_id?: string | number | null
|
||||||
|
step_id?: string | number | null
|
||||||
process_name?: string
|
process_name?: string
|
||||||
step_name?: string
|
step_name?: string
|
||||||
operator?: string
|
operator?: string
|
||||||
user_code?: 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<string | number>
|
||||||
|
parts?: ProduceRepairPart[]
|
||||||
|
operation_fault_steps?: ProduceOperationFaultStep[]
|
||||||
|
responsible_operators?: ProduceResponsibleOperator[]
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getExceptionSettings() {
|
export function getExceptionSettings() {
|
||||||
return request.get<ProduceExceptionSetting[]>('/produce/exception/settings', {}, { showErrorMessage: false })
|
return request.get<ProduceExceptionSetting[]>('/produce/exception/settings', {}, { showErrorMessage: false })
|
||||||
}
|
}
|
||||||
@@ -78,6 +96,10 @@ export function checkExceptionOrder(orderNo: string) {
|
|||||||
return request.get<Record<string, any>>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false })
|
return request.get<Record<string, any>>('/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<string, any>) {
|
export function submitExceptionRecord(data: Record<string, any>) {
|
||||||
return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, {
|
return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, {
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
@@ -127,7 +149,22 @@ export function lookupMaterial(productCode: string) {
|
|||||||
}>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false })
|
}>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitRepairRecord(data: Record<string, any>) {
|
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, {
|
return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, {
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
returnFullResponse: true,
|
returnFullResponse: true,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ProduceQueryRecord {
|
|||||||
order_no?: string
|
order_no?: string
|
||||||
product_code?: string
|
product_code?: string
|
||||||
process_name?: string
|
process_name?: string
|
||||||
|
step_name?: string
|
||||||
exception_reason?: string
|
exception_reason?: string
|
||||||
exception_content?: string
|
exception_content?: string
|
||||||
material_category?: string
|
material_category?: string
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside class="side-column">
|
<aside class="side-column">
|
||||||
<section class="panel operator-panel"><h2>提报信息</h2><div><span>提报人</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>提交时间</span><strong>{{ nowText }}</strong></div></section>
|
<section class="panel operator-panel"><h2>提报信息</h2><div><span>用户编码</span><el-input v-model="userCode" size="small" clearable placeholder="请输入用户编码" @blur="checkUserCode" @keyup.enter="checkUserCode" /></div><div><span>提报人</span><strong>{{ userName || '-' }}</strong></div><div><span>提交时间</span><strong>{{ nowText }}</strong></div><p v-if="userCodeError" class="field-error">{{ userCodeError }}</p></section>
|
||||||
<section class="panel"><div class="side-title"><h2>最近异常</h2><span>现场回看</span></div><el-empty v-if="!recentExceptions.length" description="暂无异常记录" :image-size="70" /><div v-for="item in recentExceptions" :key="item.id" class="recent-item"><div><strong>{{ item.order_no || '-' }}</strong><p>{{ item.type_name || '-' }} · {{ [item.reason_name, item.issue_name].filter(Boolean).join(' · ') }}</p></div><el-tag :type="item.status === 2 ? 'success' : item.status === 1 ? 'warning' : 'danger'">{{ item.status_name || '待处理' }}</el-tag></div></section>
|
<section class="panel"><div class="side-title"><h2>最近异常</h2><span>现场回看</span></div><el-empty v-if="!recentExceptions.length" description="暂无异常记录" :image-size="70" /><div v-for="item in recentExceptions" :key="item.id" class="recent-item"><div><strong>{{ item.order_no || '-' }}</strong><p>{{ item.type_name || '-' }} · {{ [item.reason_name, item.issue_name].filter(Boolean).join(' · ') }}</p></div><el-tag :type="item.status === 2 ? 'success' : item.status === 1 ? 'warning' : 'danger'">{{ item.status_name || '待处理' }}</el-tag></div></section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,10 +79,13 @@ import { ElMessage } from 'element-plus'
|
|||||||
import type { UploadUserFile } from 'element-plus'
|
import type { UploadUserFile } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useMemberStore } from '~/stores/member'
|
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 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 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 form = ref({ orderNo: '', type: '', reason: '', issue: '', remark: '' })
|
||||||
const settings = ref<ProduceExceptionSetting[]>([])
|
const settings = ref<ProduceExceptionSetting[]>([])
|
||||||
const recentExceptions = ref<ProduceExceptionRecord[]>([])
|
const recentExceptions = ref<ProduceExceptionRecord[]>([])
|
||||||
@@ -109,6 +112,11 @@ const loadData = async () => {
|
|||||||
if (!form.value.type) selectType(typeOptions.value[0]?.value || '')
|
if (!form.value.type) selectType(typeOptions.value[0]?.value || '')
|
||||||
} catch { /* 请求层已提示错误 */ }
|
} 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 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 selectReason = (value: string) => { form.value.reason = value; form.value.issue = activeIssues.value[0]?.value || '' }
|
||||||
const checkOrder = async () => {
|
const checkOrder = async () => {
|
||||||
@@ -122,17 +130,17 @@ const resetForm = () => { form.value = { orderNo: '', type: typeOptions.value[0]
|
|||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!orderValid.value || !form.value.orderNo.trim()) { await checkOrder(); if (!orderValid.value) return }
|
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 (!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
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const images: string[] = []
|
const images: string[] = []
|
||||||
for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'exception'); if (url) images.push(url) }
|
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 }
|
if (response?.code !== 0) { ElMessage.error(response?.msg || '异常提交失败'); return }
|
||||||
ElMessage.success('异常已提交'); resetForm(); await loadData()
|
ElMessage.success('异常已提交'); resetForm(); await loadData()
|
||||||
} finally { submitting.value = false }
|
} finally { submitting.value = false }
|
||||||
}
|
}
|
||||||
onMounted(() => { updateNow(); void loadData() })
|
onMounted(() => { updateNow(); userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void loadData() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -147,4 +155,10 @@ onMounted(() => { updateNow(); void loadData() })
|
|||||||
.inline-form { display:flex; gap:12px; margin-top:20px; } .inline-form .el-input { flex:1; } .validation { margin-top:10px; font-size:13px; } .validation.success { color:#26b36c; } .validation.error { color:#ef625f; } .option-grid { display:grid; gap:12px; margin-top:18px; } .type-grid { grid-template-columns:repeat(2,1fr); } button { font:inherit; cursor:pointer; } .choice-card,.reason-card { border:1px solid #e6edf5; border-radius:14px; background:#f8fafc; text-align:left; transition:.2s; } .choice-card { padding:16px; } .choice-card strong,.choice-card small { display:block; } .choice-card strong { color:#2b3853; } .choice-card small { margin-top:8px; color:#9aa4b4; } .choice-card.active,.reason-card.active { border-color:#ff9a3c; background:#fff5ed; color:#e97020; } .sub-title { align-items:center; justify-content:space-between; margin-top:26px; } .sub-title span,.side-title span { color:#9aa4b4; font-size:12px; } .reason-grid { grid-template-columns:repeat(3,1fr); } .reason-card { min-height:46px; padding:8px; text-align:center; color:#56647a; } .issue-box { margin-top:18px; padding:16px; border-radius:14px; background:#fffaf5; } .issue-box .sub-title { margin:0 0 12px; } .full-width { width:100%; }
|
.inline-form { display:flex; gap:12px; margin-top:20px; } .inline-form .el-input { flex:1; } .validation { margin-top:10px; font-size:13px; } .validation.success { color:#26b36c; } .validation.error { color:#ef625f; } .option-grid { display:grid; gap:12px; margin-top:18px; } .type-grid { grid-template-columns:repeat(2,1fr); } button { font:inherit; cursor:pointer; } .choice-card,.reason-card { border:1px solid #e6edf5; border-radius:14px; background:#f8fafc; text-align:left; transition:.2s; } .choice-card { padding:16px; } .choice-card strong,.choice-card small { display:block; } .choice-card strong { color:#2b3853; } .choice-card small { margin-top:8px; color:#9aa4b4; } .choice-card.active,.reason-card.active { border-color:#ff9a3c; background:#fff5ed; color:#e97020; } .sub-title { align-items:center; justify-content:space-between; margin-top:26px; } .sub-title span,.side-title span { color:#9aa4b4; font-size:12px; } .reason-grid { grid-template-columns:repeat(3,1fr); } .reason-card { min-height:46px; padding:8px; text-align:center; color:#56647a; } .issue-box { margin-top:18px; padding:16px; border-radius:14px; background:#fffaf5; } .issue-box .sub-title { margin:0 0 12px; } .full-width { width:100%; }
|
||||||
:deep(.el-upload--picture-card),:deep(.el-upload-list__item) { width:92px; height:92px; } .upload-plus { font-size:30px; color:#ff9a3c; } .remark-input { margin-top:18px; } .form-actions { display:flex; justify-content:flex-end; gap:12px; } .operator-panel h2 { margin-bottom:18px; } .operator-panel div { display:flex; justify-content:space-between; padding:13px 0; border-bottom:1px solid #eef2f7; font-size:13px; } .operator-panel div:last-child { border-bottom:0; } .operator-panel span { color:#9aa4b4; } .side-title { align-items:center; justify-content:space-between; } .recent-item { display:flex; justify-content:space-between; gap:10px; padding:16px 0; border-bottom:1px solid #eef2f7; } .recent-item:last-child { border-bottom:0; } .recent-item strong { font-size:14px; } .recent-item p { margin:6px 0 0; color:#8a94a6; font-size:12px; }
|
:deep(.el-upload--picture-card),:deep(.el-upload-list__item) { width:92px; height:92px; } .upload-plus { font-size:30px; color:#ff9a3c; } .remark-input { margin-top:18px; } .form-actions { display:flex; justify-content:flex-end; gap:12px; } .operator-panel h2 { margin-bottom:18px; } .operator-panel div { display:flex; justify-content:space-between; padding:13px 0; border-bottom:1px solid #eef2f7; font-size:13px; } .operator-panel div:last-child { border-bottom:0; } .operator-panel span { color:#9aa4b4; } .side-title { align-items:center; justify-content:space-between; } .recent-item { display:flex; justify-content:space-between; gap:10px; padding:16px 0; border-bottom:1px solid #eef2f7; } .recent-item:last-child { border-bottom:0; } .recent-item strong { font-size:14px; } .recent-item p { margin:6px 0 0; color:#8a94a6; font-size:12px; }
|
||||||
@media (max-width: 900px) { .produce-work-page { padding:16px; } .content-grid { grid-template-columns:1fr; } .side-column { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); } } @media (max-width: 640px) { .page-hero { padding:24px; align-items:flex-start; flex-direction:column; } .hero-user { align-self:stretch; text-align:left; } .summary-grid,.side-column { grid-template-columns:1fr; } .type-grid,.reason-grid { grid-template-columns:1fr; } .inline-form { flex-direction:column; } .form-actions { justify-content:stretch; } .form-actions .el-button { flex:1; } }
|
@media (max-width: 900px) { .produce-work-page { padding:16px; } .content-grid { grid-template-columns:1fr; } .side-column { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); } } @media (max-width: 640px) { .page-hero { padding:24px; align-items:flex-start; flex-direction:column; } .hero-user { align-self:stretch; text-align:left; } .summary-grid,.side-column { grid-template-columns:1fr; } .type-grid,.reason-grid { grid-template-columns:1fr; } .inline-form { flex-direction:column; } .form-actions { justify-content:stretch; } .form-actions .el-button { flex:1; } }
|
||||||
|
.operator-panel > div:first-of-type { align-items: center; gap: 16px; }
|
||||||
|
.operator-panel > div:first-of-type > span { flex: 0 0 72px; white-space: nowrap; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input) { flex: 1; min-width: 0; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__wrapper) { min-height: 40px; padding: 0 13px; border-radius: 9px; box-shadow: 0 0 0 1px #d8e2ef inset; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__inner) { font-size: 15px; font-weight: 600; }
|
||||||
|
.operator-panel > div:not(:first-of-type) { min-height: 44px; align-items: center; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,43 +39,57 @@
|
|||||||
|
|
||||||
<div class="form-actions"><el-button size="large" @click="resetForm">重置</el-button><el-button type="primary" size="large" :loading="submitting" @click="completeRepair">完成维修</el-button></div>
|
<div class="form-actions"><el-button size="large" @click="resetForm">重置</el-button><el-button type="primary" size="large" :loading="submitting" @click="completeRepair">完成维修</el-button></div>
|
||||||
</main>
|
</main>
|
||||||
<aside class="side-column"><section class="panel operator-panel"><h2>当前操作人</h2><div><span>姓名</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>当前任务</span><strong>{{ task?.device_no || '未选择' }}</strong></div></section><section class="panel tip-panel"><h2>处理提示</h2><p>物料不良请登记替换备件;作业不良需选择责任人员;搭配错误需选择对应物料分类。</p><el-button link type="primary" @click="goTasks">查看待维修订单</el-button></section></aside>
|
<aside class="side-column"><section class="panel operator-panel"><h2>当前操作人</h2><div><span>用户编码</span><el-input v-model="userCode" size="small" clearable placeholder="请输入用户编码" @blur="checkUserCode" @keyup.enter="checkUserCode" /></div><div><span>姓名</span><strong>{{ userName || '-' }}</strong></div><div><span>当前任务</span><strong>{{ task?.device_no || '未选择' }}</strong></div><p v-if="userCodeError" class="field-error">{{ userCodeError }}</p></section><section class="panel tip-panel"><h2>处理提示</h2><p>物料不良请登记替换备件;作业不良需选择责任人员;搭配错误需选择对应物料分类。</p><el-button link type="primary" @click="goTasks">查看待维修订单</el-button></section></aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="partDialogVisible" title="添加维修备件" width="460px"><el-input v-model="partCode" autofocus placeholder="请输入商品编码" @keyup.enter="addPart" /><template #footer><el-button @click="partDialogVisible = false">取消</el-button><el-button type="primary" :loading="loadingPart" @click="addPart">确定</el-button></template></el-dialog>
|
<el-dialog v-model="partDialogVisible" title="选择维修备件" width="760px" class="material-dialog" @closed="resetMaterialSearch"><div class="material-search"><el-input v-model="materialProductCode" clearable autofocus placeholder="商品编码" @keyup.enter="searchMaterialList" /><el-input v-model="materialProductName" clearable placeholder="商品名称" @keyup.enter="searchMaterialList" /><el-select v-model="materialCategoryId" clearable filterable placeholder="物料分类" @change="searchMaterialList"><el-option v-for="item in categories" :key="item.id" :label="item.category_name" :value="String(item.id)" /></el-select><el-button type="primary" :loading="materialLoading" @click="searchMaterialList">搜索</el-button></div><div class="material-hint">可按编码、名称或物料分类筛选,已添加的备件会自动标记。</div><el-table v-loading="materialLoading" :data="materialItems" border stripe class="material-table"><el-table-column prop="product_code" label="商品编码" min-width="180" /><el-table-column prop="product_name" label="商品名称" min-width="220" show-overflow-tooltip /><el-table-column prop="category_name" label="物料分类" min-width="140"><template #default="scope">{{ scope.row.category_name || '-' }}</template></el-table-column><el-table-column label="操作" width="100" fixed="right"><template #default="scope"><el-button link type="primary" :disabled="isPartAdded(scope.row.product_code)" @click="selectMaterial(scope.row)">{{ isPartAdded(scope.row.product_code) ? '已添加' : '选择' }}</el-button></template></el-table-column></el-table><el-empty v-if="!materialLoading && !materialItems.length" description="暂无匹配的备件" :image-size="72" /><div class="material-pagination"><el-pagination v-model:current-page="materialPage" v-model:page-size="materialLimit" layout="total, prev, pager, next" :total="materialTotal" :page-sizes="[10, 20, 50]" @current-change="searchMaterialList" @size-change="handleMaterialSizeChange" /></div><template #footer><el-button @click="partDialogVisible = false">关闭</el-button></template></el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CircleCheckFilled, Grid, EditPen, Tools, Box } from '@element-plus/icons-vue'
|
import { CircleCheckFilled, Grid, EditPen, Tools, Box, Search } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { UploadUserFile } from 'element-plus'
|
import type { UploadUserFile } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { navigateTo, useRoute } from 'nuxt/app'
|
import { navigateTo, useRoute } from 'nuxt/app'
|
||||||
import { useMemberStore } from '~/stores/member'
|
import { useMemberStore } from '~/stores/member'
|
||||||
import { getFaultDescriptionOptions, getMaterialCategories, getRepairStatistics, getRepairTask, getScanRecords, lookupMaterial, submitRepairRecord, uploadProduceImage, type ProduceRepairTask } from '~/apps/produce/api/exception'
|
import { checkMemberUser, getFaultDescriptionOptions, getMaterialCategories, getRepairStatistics, getRepairTask, getScanRecords, searchMaterials, submitRepairRecord, uploadProduceImage, type ProduceRepairTask, type ProduceScanRecord } from '~/apps/produce/api/exception'
|
||||||
|
|
||||||
interface Part { id: string; product_code: string; product_name?: string; category_id?: string | number; category_name?: string; source: string; quantity: number; fault_description_ids: string[] }
|
interface Part { id: string; product_code: string; product_name?: string; category_id?: string | number; category_name?: string; source: string; quantity: number; fault_description_ids: string[] }
|
||||||
interface ScanRecord { id: string | number; process_name?: string; step_name?: string; operator?: string; user_code?: string }
|
|
||||||
interface FaultDescription { id: string | number; category_id: string | number; description: string }
|
interface FaultDescription { id: string | number; category_id: string | number; description: string }
|
||||||
|
interface MaterialOption { id: string | number; product_code: string; product_name: string; category_id: string | number; category_name: string }
|
||||||
const memberStore = useMemberStore()
|
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 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 summaries = ref([{ label: '待维修', value: 0, type: 'warning', action: true }, { label: '处理中', value: 0, type: 'primary' }, { label: '今日完成', value: 0, type: 'success' }])
|
const summaries = ref([{ label: '待维修', value: 0, type: 'warning', action: true }, { label: '处理中', value: 0, type: 'primary' }, { label: '今日完成', value: 0, type: 'success' }])
|
||||||
const resultOptions = [{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: Box }, { label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: EditPen }, { label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: Grid }]
|
const resultOptions = [{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: Box }, { label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: EditPen }, { label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: Grid }]
|
||||||
const deviceNo = ref('')
|
const deviceNo = ref('')
|
||||||
const task = ref<ProduceRepairTask | null>(null)
|
const task = ref<ProduceRepairTask | null>(null)
|
||||||
const categories = ref<Array<{ id: string | number; category_name: string }>>([])
|
const categories = ref<Array<{ id: string | number; category_name: string }>>([])
|
||||||
const scanRecords = ref<ScanRecord[]>([])
|
const scanRecords = ref<ProduceScanRecord[]>([])
|
||||||
const faults = ref<FaultDescription[]>([])
|
const faults = ref<FaultDescription[]>([])
|
||||||
const imageFiles = ref<UploadUserFile[]>([])
|
const imageFiles = ref<UploadUserFile[]>([])
|
||||||
const form = ref({ repair_result: 'material_defect', category_ids: [] as string[], responsible_ids: [] as string[], parts: [] as Part[], remark: '' })
|
const form = ref({ repair_result: 'material_defect', category_ids: [] as string[], responsible_ids: [] as string[], parts: [] as Part[], remark: '' })
|
||||||
const loadingTask = ref(false)
|
const loadingTask = ref(false)
|
||||||
const loadingPart = ref(false)
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const partDialogVisible = ref(false)
|
const partDialogVisible = ref(false)
|
||||||
const partCode = ref('')
|
const materialProductCode = ref('')
|
||||||
|
const materialProductName = ref('')
|
||||||
|
const materialCategoryId = ref<string | number>('')
|
||||||
|
const materialItems = ref<MaterialOption[]>([])
|
||||||
|
const materialPage = ref(1)
|
||||||
|
const materialLimit = ref(10)
|
||||||
|
const materialTotal = ref(0)
|
||||||
|
const materialLoading = ref(false)
|
||||||
const reasonText = computed(() => [task.value?.reason_name, task.value?.issue_name].filter(Boolean).join(' · ') || '-')
|
const reasonText = computed(() => [task.value?.reason_name, task.value?.issue_name].filter(Boolean).join(' · ') || '-')
|
||||||
|
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 loadStatistics = async () => {
|
const loadStatistics = async () => {
|
||||||
const data = await getRepairStatistics({ user_code: currentUser.value.code }).catch(() => null)
|
const data = await getRepairStatistics({ user_code: currentUser.value.code }).catch(() => null)
|
||||||
@@ -88,18 +102,38 @@ const loadStatistics = async () => {
|
|||||||
}
|
}
|
||||||
const loadTask = async () => { const value = deviceNo.value.trim(); if (!value || loadingTask.value) return ElMessage.warning('请输入设备编码或订单号'); loadingTask.value = true; try { task.value = await getRepairTask(value); form.value.responsible_ids = []; scanRecords.value = []; if (task.value?.device_no) { const result = await getScanRecords({ order_no: task.value.device_no, limit: 50 }); scanRecords.value = Array.isArray(result?.items) ? result.items : []; } } finally { loadingTask.value = false } }
|
const loadTask = async () => { const value = deviceNo.value.trim(); if (!value || loadingTask.value) return ElMessage.warning('请输入设备编码或订单号'); loadingTask.value = true; try { task.value = await getRepairTask(value); form.value.responsible_ids = []; scanRecords.value = []; if (task.value?.device_no) { const result = await getScanRecords({ order_no: task.value.device_no, limit: 50 }); scanRecords.value = Array.isArray(result?.items) ? result.items : []; } } finally { loadingTask.value = false } }
|
||||||
const selectResult = (value: string) => { form.value.repair_result = value; if (value !== 'assembly_mismatch') form.value.category_ids = []; if (value !== 'operation_defect') form.value.responsible_ids = [] }
|
const selectResult = (value: string) => { form.value.repair_result = value; if (value !== 'assembly_mismatch') form.value.category_ids = []; if (value !== 'operation_defect') form.value.responsible_ids = [] }
|
||||||
const openPartDialog = () => { partCode.value = ''; partDialogVisible.value = true }
|
const openPartDialog = () => { materialProductCode.value = ''; materialProductName.value = ''; materialCategoryId.value = ''; materialPage.value = 1; partDialogVisible.value = true; void searchMaterialList() }
|
||||||
const addPart = async () => { const code = partCode.value.trim(); if (!code) return ElMessage.warning('请输入商品编码'); if (form.value.parts.some((item: Part) => item.product_code === code)) return ElMessage.warning('该物料已添加'); loadingPart.value = true; try { const material = await lookupMaterial(code); const part: Part = { id: `${Date.now()}-${code}`, product_code: material.product_code || code, product_name: material.product_name, category_id: material.category_id, category_name: material.category_name, source: 'new', quantity: 1, fault_description_ids: [] }; form.value.parts.push(part); await loadFaults(); partDialogVisible.value = false } finally { loadingPart.value = false } }
|
const searchMaterialList = async () => { materialLoading.value = true; try { const result = await searchMaterials(materialProductCode.value, materialProductName.value, materialCategoryId.value, materialPage.value, materialLimit.value); materialItems.value = Array.isArray(result?.items) ? result.items : []; materialTotal.value = Number(result?.total || 0) } finally { materialLoading.value = false } }
|
||||||
|
const handleMaterialSizeChange = (size: number) => { materialLimit.value = size; materialPage.value = 1; void searchMaterialList() }
|
||||||
|
const isPartAdded = (productCode: string) => form.value.parts.some((item: Part) => item.product_code === productCode)
|
||||||
|
const appendMaterial = async (material: MaterialOption) => { if (isPartAdded(material.product_code)) return ElMessage.warning('该物料已添加'); const part: Part = { id: `${Date.now()}-${material.product_code}`, product_code: material.product_code, product_name: material.product_name, category_id: material.category_id, category_name: material.category_name, source: 'new', quantity: 1, fault_description_ids: [] }; form.value.parts.push(part); await loadFaults(); ElMessage.success('备件已添加') }
|
||||||
|
const selectMaterial = (material: MaterialOption) => { void appendMaterial(material) }
|
||||||
|
const resetMaterialSearch = () => { materialProductCode.value = ''; materialProductName.value = ''; materialCategoryId.value = ''; materialItems.value = []; materialPage.value = 1; materialTotal.value = 0 }
|
||||||
const loadFaults = async () => { const ids = [...new Set(form.value.parts.map((item: Part) => String(item.category_id || '')).filter(Boolean))]; faults.value = ids.length ? await getFaultDescriptionOptions(ids) : [] }
|
const loadFaults = async () => { const ids = [...new Set(form.value.parts.map((item: Part) => String(item.category_id || '')).filter(Boolean))]; faults.value = ids.length ? await getFaultDescriptionOptions(ids) : [] }
|
||||||
const faultsForPart = (part: Part) => faults.value.filter((item: FaultDescription) => String(item.category_id) === String(part.category_id))
|
const faultsForPart = (part: Part) => faults.value.filter((item: FaultDescription) => String(item.category_id) === String(part.category_id))
|
||||||
const removePart = (index: number) => { form.value.parts.splice(index, 1); void loadFaults() }
|
const removePart = (index: number) => { form.value.parts.splice(index, 1); void loadFaults() }
|
||||||
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
|
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
|
||||||
const resetForm = () => { deviceNo.value = ''; task.value = null; scanRecords.value = []; form.value = { repair_result: 'material_defect', category_ids: [], responsible_ids: [], parts: [], remark: '' }; imageFiles.value = [] }
|
const resetForm = () => { deviceNo.value = ''; task.value = null; scanRecords.value = []; form.value = { repair_result: 'material_defect', category_ids: [], responsible_ids: [], parts: [], remark: '' }; imageFiles.value = [] }
|
||||||
const completeRepair = async () => { if (!task.value?.id) return ElMessage.warning('请先查询有效的维修任务'); if (!currentUser.value.code) return ElMessage.warning('请先登录'); if (form.value.repair_result === 'assembly_mismatch' && !form.value.category_ids.length) return ElMessage.warning('请选择搭配错误分类'); if (form.value.repair_result === 'operation_defect' && !form.value.responsible_ids.length) return ElMessage.warning('请选择责任人员'); submitting.value = true; try { const images: string[] = []; for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'repair'); if (url) images.push(url) } const response = await submitRepairRecord({ exception_id: task.value.id, user_code: currentUser.value.code, repair_result: form.value.repair_result, images, category_ids: form.value.category_ids, parts: form.value.parts.map((part: Part) => ({ product_code: part.product_code, source: part.source, quantity: part.quantity, fault_description_ids: part.fault_description_ids })), operation_fault_steps: form.value.responsible_ids, responsible_operators: form.value.responsible_ids, remark: form.value.remark.trim() }); if (response?.code !== 0) { ElMessage.error(response?.msg || '维修提交失败'); return } ElMessage.success('维修记录已完成'); resetForm(); void loadStatistics() } finally { submitting.value = false } }
|
const completeRepair = async () => { if (!task.value?.id) return ElMessage.warning('请先查询有效的维修任务'); if (!await checkUserCode()) return ElMessage.warning(userCodeError.value); if (form.value.repair_result === 'assembly_mismatch' && !form.value.category_ids.length) return ElMessage.warning('请选择搭配错误分类'); if (form.value.repair_result === 'operation_defect' && !form.value.responsible_ids.length) return ElMessage.warning('请选择责任人员'); submitting.value = true; try { const images: string[] = []; for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'repair'); if (url) images.push(url) } const selectedScanIds = scanRecords.value.filter(record => form.value.responsible_ids.includes(String(record.id))).map(record => record.id); const response = await submitRepairRecord({ exception_id: task.value.id, user_code: userCode.value.trim(), repair_result: form.value.repair_result, images, category_ids: form.value.category_ids, parts: form.value.parts.map((part: Part) => ({ product_code: part.product_code, source: part.source, quantity: part.quantity, fault_description_ids: part.fault_description_ids })), operation_fault_steps: selectedScanIds, responsible_operators: selectedScanIds, remark: form.value.remark.trim() }); if (response?.code !== 0) { ElMessage.error(response?.msg || '维修提交失败'); return } ElMessage.success('维修记录已完成'); resetForm(); void loadStatistics() } finally { submitting.value = false } }
|
||||||
const goTasks = () => navigateTo('/produce/repair/tasks')
|
const goTasks = () => navigateTo('/produce/repair/tasks')
|
||||||
onMounted(() => { void loadStatistics(); void getMaterialCategories().then((data: Array<{ id: string | number; category_name: string }>) => { categories.value = Array.isArray(data) ? data : [] }); const query = useRoute().query.device_no; if (query) { deviceNo.value = String(query); void loadTask() } })
|
onMounted(() => { userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void checkUserCode(); void loadStatistics(); void getMaterialCategories().then((data: Array<{ id: string | number; category_name: string }>) => { categories.value = Array.isArray(data) ? data : [] }); const query = useRoute().query.device_no; if (query) { deviceNo.value = String(query); void loadTask() } })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;padding:32px 36px;border-radius:22px;color:#fff;box-shadow:0 14px 32px rgba(37,99,235,.12)}.repair-hero{background:linear-gradient(135deg,#2f6cf6,#35bfa3)}.eyebrow{font-size:13px;opacity:.85}h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.summary-card,.panel{background:#fff;border:1px solid #e7edf5;border-radius:18px;box-shadow:0 8px 24px rgba(36,59,92,.04)}.summary-card{display:flex;align-items:center;gap:12px;padding:18px 22px}.summary-card strong{font-size:28px}.summary-card span{color:#8a94a6;font-size:13px}.summary-card .el-button{margin-left:auto}.warning{color:#e59a2f}.primary{color:#2563eb}.success{color:#26b36c}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:20px;align-items:start}.main-column,.side-column{display:flex;flex-direction:column;gap:20px}.panel{padding:26px}.panel-title{display:flex;align-items:flex-start;gap:14px}.panel-title>span{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#eaf2ff;color:#2563eb;font-weight:700;flex:none}.panel-title h2{margin:0;font-size:17px}.panel-title p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.panel-title em{margin-left:auto;padding:5px 10px;border-radius:999px;color:#2563eb;background:#edf4ff;font-size:12px;font-style:normal}.panel-title em.optional{color:#8793a6;background:#f3f5f8}.panel-title small{margin-left:auto;color:#9aa4b4}.inline-form{display:flex;gap:12px;margin-top:20px}.inline-form .el-input{flex:1}.task-found{display:flex;align-items:center;gap:12px;margin-top:16px;padding:14px;border-radius:12px;background:#f0fbf6;color:#26b36c}.task-found>div{display:flex;flex-direction:column;gap:4px;flex:1}.task-found span{color:#2b3853;font-weight:600}.task-found .el-tag{margin-left:auto}.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.info-grid div{padding:14px;border-radius:12px;background:#f8fafc}.info-grid span,.info-grid strong{display:block}.info-grid span{color:#9aa4b4;font-size:12px}.info-grid strong{margin-top:8px;font-size:14px;word-break:break-all}.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:20px}.result-card{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:18px;border:1px solid #e6edf5;border-radius:14px;background:#f8fafc;color:#56647a;text-align:left;cursor:pointer}.result-card.active{border-color:#409eff;background:#edf4ff;color:#2563eb}.result-card small{color:#9aa4b4}.category-grid,.responsibility-grid{display:flex;flex-wrap:wrap;gap:14px;margin-top:22px}.responsibility-grid .el-checkbox{height:auto;margin:0;padding:12px 16px;border:1px solid #e6edf5;border-radius:12px;background:#f8fafc}.responsibility-grid .el-checkbox.is-checked{border-color:#409eff;background:#edf4ff}.responsibility-grid small{display:block;color:#9aa4b4}.part-toolbar{display:flex;justify-content:space-between;align-items:center;margin:22px 0 12px}.part-row{display:flex;align-items:flex-start;gap:12px;padding:16px 0;border-bottom:1px solid #eef2f7}.part-index{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#edf4ff;color:#2563eb}.part-info{display:flex;flex-direction:column;gap:8px;flex:1}.part-info>span{color:#8a94a6;font-size:12px}.fault-select{width:100%}.remark-input{margin-top:18px}.form-actions{display:flex;justify-content:flex-end;gap:12px}.operator-panel h2,.tip-panel h2{margin:0 0 18px}.operator-panel div{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #eef2f7;font-size:13px}.operator-panel div:last-child{border:0}.operator-panel span{color:#9aa4b4}.tip-panel p{color:#718096;font-size:13px;line-height:1.8}.upload-plus{font-size:30px;color:#409eff}:deep(.el-upload--picture-card),:deep(.el-upload-list__item){width:92px;height:92px}@media(max-width:900px){.produce-work-page{padding:16px}.content-grid{grid-template-columns:1fr}.side-column{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.page-hero{padding:24px;align-items:flex-start;flex-direction:column;gap:18px}.summary-grid,.side-column,.info-grid,.result-grid{grid-template-columns:1fr}.inline-form,.form-actions{flex-direction:column}.form-actions .el-button{width:100%}.part-row{flex-wrap:wrap}}
|
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;padding:32px 36px;border-radius:22px;color:#fff;box-shadow:0 14px 32px rgba(37,99,235,.12)}.repair-hero{background:linear-gradient(135deg,#2f6cf6,#35bfa3)}.eyebrow{font-size:13px;opacity:.85}h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.summary-card,.panel{background:#fff;border:1px solid #e7edf5;border-radius:18px;box-shadow:0 8px 24px rgba(36,59,92,.04)}.summary-card{display:flex;align-items:center;gap:12px;padding:18px 22px}.summary-card strong{font-size:28px}.summary-card span{color:#8a94a6;font-size:13px}.summary-card .el-button{margin-left:auto}.warning{color:#e59a2f}.primary{color:#2563eb}.success{color:#26b36c}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:20px;align-items:start}.main-column,.side-column{display:flex;flex-direction:column;gap:20px}.panel{padding:26px}.panel-title{display:flex;align-items:flex-start;gap:14px}.panel-title>span{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#eaf2ff;color:#2563eb;font-weight:700;flex:none}.panel-title h2{margin:0;font-size:17px}.panel-title p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.panel-title em{margin-left:auto;padding:5px 10px;border-radius:999px;color:#2563eb;background:#edf4ff;font-size:12px;font-style:normal}.panel-title em.optional{color:#8793a6;background:#f3f5f8}.panel-title small{margin-left:auto;color:#9aa4b4}.inline-form{display:flex;gap:12px;margin-top:20px}.inline-form .el-input{flex:1}.task-found{display:flex;align-items:center;gap:12px;margin-top:16px;padding:14px;border-radius:12px;background:#f0fbf6;color:#26b36c}.task-found>div{display:flex;flex-direction:column;gap:4px;flex:1}.task-found span{color:#2b3853;font-weight:600}.task-found .el-tag{margin-left:auto}.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.info-grid div{padding:14px;border-radius:12px;background:#f8fafc}.info-grid span,.info-grid strong{display:block}.info-grid span{color:#9aa4b4;font-size:12px}.info-grid strong{margin-top:8px;font-size:14px;word-break:break-all}.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:20px}.result-card{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:18px;border:1px solid #e6edf5;border-radius:14px;background:#f8fafc;color:#56647a;text-align:left;cursor:pointer}.result-card.active{border-color:#409eff;background:#edf4ff;color:#2563eb}.result-card small{color:#9aa4b4}.category-grid,.responsibility-grid{display:flex;flex-wrap:wrap;gap:14px;margin-top:22px}.responsibility-grid .el-checkbox{height:auto;margin:0;padding:12px 16px;border:1px solid #e6edf5;border-radius:12px;background:#f8fafc}.responsibility-grid .el-checkbox.is-checked{border-color:#409eff;background:#edf4ff}.responsibility-grid small{display:block;color:#9aa4b4}.part-toolbar{display:flex;justify-content:space-between;align-items:center;margin:22px 0 12px}.part-row{display:flex;align-items:flex-start;gap:12px;padding:16px 0;border-bottom:1px solid #eef2f7}.part-index{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#edf4ff;color:#2563eb}.part-info{display:flex;flex-direction:column;gap:8px;flex:1}.part-info>span{color:#8a94a6;font-size:12px}.fault-select{width:100%}.remark-input{margin-top:18px}.form-actions{display:flex;justify-content:flex-end;gap:12px}.operator-panel h2,.tip-panel h2{margin:0 0 18px}.operator-panel div{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #eef2f7;font-size:13px}.operator-panel div:last-child{border:0}.operator-panel span{color:#9aa4b4}.tip-panel p{color:#718096;font-size:13px;line-height:1.8}.upload-plus{font-size:30px;color:#409eff}:deep(.el-upload--picture-card),:deep(.el-upload-list__item){width:92px;height:92px}@media(max-width:900px){.produce-work-page{padding:16px}.content-grid{grid-template-columns:1fr}.side-column{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.page-hero{padding:24px;align-items:flex-start;flex-direction:column;gap:18px}.summary-grid,.side-column,.info-grid,.result-grid{grid-template-columns:1fr}.inline-form,.form-actions{flex-direction:column}.form-actions .el-button{width:100%}.part-row{flex-wrap:wrap}}
|
||||||
|
.operator-panel > div:first-of-type { align-items: center; gap: 16px; }
|
||||||
|
.operator-panel > div:first-of-type > span { flex: 0 0 72px; white-space: nowrap; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input) { flex: 1; min-width: 0; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__wrapper) { min-height: 40px; padding: 0 13px; border-radius: 9px; box-shadow: 0 0 0 1px #d8e2ef inset; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__inner) { font-size: 15px; font-weight: 600; }
|
||||||
|
.operator-panel > div:not(:first-of-type) { min-height: 44px; align-items: center; }
|
||||||
|
.responsibility-grid .el-checkbox__label { gap: 10px; line-height: 1.25; }
|
||||||
|
.responsibility-grid .el-checkbox__label > span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.responsibility-grid small { line-height: 1.35; }
|
||||||
|
.material-search { display: flex; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.material-search .el-input { flex: 1; min-width: 0; }
|
||||||
|
.material-search .el-select { width: 170px; flex: none; }
|
||||||
|
.material-hint { margin-bottom: 12px; color: #8a94a6; font-size: 13px; }
|
||||||
|
.material-table { width: 100%; }
|
||||||
|
.material-pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 异常提报自动提交工序步骤
|
||||||
|
|
||||||
|
## 更新内容
|
||||||
|
|
||||||
|
- 后台生产配置新增“异常提报工序”和“异常提报步骤”选项,管理员可选择有效的工序步骤。
|
||||||
|
- 异常提报接口读取后台配置,将工序步骤直接保存到异常记录中。
|
||||||
|
- 异常页面保持原有表单,提交时只调用异常提报接口,不在前端维护工序步骤数据。
|
||||||
|
|
||||||
|
## 行为说明
|
||||||
|
|
||||||
|
未配置或配置无效时,异常提报会提示管理员先完成工序步骤配置;配置有效时查询列表会在异常记录行直接展示对应工序和步骤。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 用户编码样式优化
|
||||||
|
|
||||||
|
- 优化异常提报和维修页面用户编码输入区域的宽度、字号、内边距和行间距。
|
||||||
|
- 输入框使用更明显的圆角边框和 40px 高度,标签不再窄小换行。
|
||||||
|
- 模板源与 Web 运行目录同步。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 责任人员文字间距优化
|
||||||
|
|
||||||
|
- 调整责任人员卡片姓名与工序步骤之间的间距。
|
||||||
|
- 姓名固定单行显示并支持超长省略,工序步骤增加行高,提升可读性。
|
||||||
|
- 模板源与 Web 运行目录同步。
|
||||||
@@ -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 未出现新增错误。
|
||||||
@@ -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 校验一致。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 维修备件按使用频率排序
|
||||||
|
|
||||||
|
维修备件选项接口现在统计历史维修记录中各备件的使用数量,按使用频率从高到低排序;频率相同时按商品编码升序排列,保证列表稳定。
|
||||||
|
|
||||||
|
统计来源为维修记录 `parts` JSON 中的商品编码和数量字段,筛选、分页仍按原有接口参数执行。
|
||||||
|
|
||||||
|
验证:`php -l backend/plugin/produce/app/api/controller/DataEntryController.php` 通过。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 物料分类下拉显示修复
|
||||||
|
|
||||||
|
修复物料库页面物料分类下拉选项为空白的问题。
|
||||||
|
|
||||||
|
原因是分类接口在 `format=select` 模式下返回标准字段 `label`、`value`,页面却按 `category_name`、`id` 读取。现已统一搜索条件和新增/编辑表单的 `ApiSelect` 字段映射为 `label`、`value`。
|
||||||
|
|
||||||
|
模板源与 Admin 运行目录已同步。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 异常记录直接展示工序步骤
|
||||||
|
|
||||||
|
异常提报不再创建额外的扫码记录,后台配置的工序和步骤改为直接保存到异常记录,并由记录查询接口展示在异常记录行中。这样查询列表只显示一条红色异常记录,不会重复出现一条扫码数据。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 维修流程配置页
|
||||||
|
|
||||||
|
- 在生产配置中新增“维修流程”页签。
|
||||||
|
- 使用现有工序、工序步骤接口选择维修流程节点。
|
||||||
|
- 配置保存至 `repair_flow` 配置组;读取时兼容回退到 `basic_setting` 中的同名字段。
|
||||||
|
- 模板源与 `frontend/admin/src/apps/produce` 运行目录保持同步。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 基础设置字段调整
|
||||||
|
|
||||||
|
- 基础设置页保留“异常提报工序”和“异常提报步骤”配置,用于异常提报自动记录工序节点。
|
||||||
|
- 维修流程工序、步骤由独立的“维修流程”配置页维护,保存至 `repair_flow` 配置组。
|
||||||
|
- 模板源与 `frontend/admin/src/apps/produce` 运行目录保持一致。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 维修数据 CRUD 修复
|
||||||
|
|
||||||
|
- 修复维修数据列表关键词查询未启用的问题。
|
||||||
|
- 增强维修记录唯一单号校验,更新时忽略当前记录。
|
||||||
|
- 支持分类、备件、图片及责任快照字段以 JSON 字符串提交并转换为数组。
|
||||||
|
- 修复详情表单隐藏 ID 字段被 `ifDetail` 排除,导致更新地址缺少 ID 的问题。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 维修记录工序步骤展示修复
|
||||||
|
|
||||||
|
- 维修记录时间线此前固定返回空工序和步骤,导致提交后显示为“-”。
|
||||||
|
- 查询维修记录时读取基础设置中的 `repair_process_id`、`repair_step_id`,并兼容维修流程配置组 `repair_flow`。
|
||||||
|
- 根据配置的工序、步骤 ID 查询名称后返回给前台时间线。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 查询记录数量显示
|
||||||
|
|
||||||
|
- 异常提报和维修记录属于单条业务记录,查询接口统一返回数量 `1`。
|
||||||
|
- 查询页面继续使用 `quantity` 字段展示,避免显示破折号。
|
||||||
|
- Web 查询页模板源与运行目录保持同步。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 模板源运行目录一致性检查
|
||||||
|
|
||||||
|
- 已递归核对 produce 插件 Admin、Web 模板源与前端运行目录。
|
||||||
|
- Web 目录文件全部一致。
|
||||||
|
- Admin 异常数据 Schema 以运行目录最新内容同步回模板源。
|
||||||
|
- 运行目录中的扁平异常页面属于兼容路径,模板源保留正式嵌套目录结构,未删除任何文件。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 维修记录工号显示修复
|
||||||
|
|
||||||
|
- 修复订单全流程时间线中维修记录工号为空的问题。
|
||||||
|
- 维修记录通过关联异常记录读取提报人工号,并统一返回 `user_code` 字段。
|
||||||
|
- 查询页面无需额外改动,操作人区域会与扫码、异常记录一样显示工号。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 异常维修用户编码录入
|
||||||
|
|
||||||
|
- 异常提报和维修页面新增用户编码输入框。
|
||||||
|
- 输入后调用生产录入使用的 `/produce/member-user/check` 接口校验,并显示对应用户名。
|
||||||
|
- 提交时使用录入并校验通过的用户编码,不再强制使用当前登录账号。
|
||||||
|
- 模板源与 Web 运行目录同步。
|
||||||
@@ -394,16 +394,21 @@ export interface ProduceRepairRecordRow {
|
|||||||
product_name?: string | null;
|
product_name?: string | null;
|
||||||
category_id?: string | number | null;
|
category_id?: string | number | null;
|
||||||
category_name?: string | null;
|
category_name?: string | null;
|
||||||
|
category_ids?: Array<string | number> | string | null;
|
||||||
|
category_names?: string[] | string | null;
|
||||||
fault_description_id?: string | number | null;
|
fault_description_id?: string | number | null;
|
||||||
fault_description_ids?: Array<string | number>;
|
fault_description_ids?: Array<string | number>;
|
||||||
fault_description?: string | null;
|
fault_description?: string | null;
|
||||||
fault_descriptions?: string[];
|
fault_descriptions?: string[];
|
||||||
repair_result?: string | null;
|
repair_result?: string | null;
|
||||||
parts?: Array<Record<string, any>> | string | null;
|
parts?: Array<Record<string, any>> | string | null;
|
||||||
|
images?: string[] | string | null;
|
||||||
|
operation_fault_steps?: Array<Record<string, any>> | string | null;
|
||||||
responsible_operators?: Array<Record<string, any>> | string | null;
|
responsible_operators?: Array<Record<string, any>> | string | null;
|
||||||
operator?: string | null;
|
operator?: string | null;
|
||||||
repaired_at?: number | null;
|
repaired_at?: number | null;
|
||||||
repaired_date?: string | null;
|
repaired_date?: string | null;
|
||||||
|
repair_result_name?: string;
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
created_date?: string;
|
created_date?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const settingList = ref([
|
|||||||
name: '基础设置',
|
name: '基础设置',
|
||||||
loadKey: 0
|
loadKey: 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(defineAsyncComponent(() => import('./modules/repair-flow.vue'))),
|
||||||
|
key: 'repair-flow',
|
||||||
|
name: '维修流程',
|
||||||
|
loadKey: 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
|
component: markRaw(defineAsyncComponent(() => import('./modules/step-rule.vue'))),
|
||||||
key: 'step-rule',
|
key: 'step-rule',
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const [BasicForm, formApi] = useForm({
|
|||||||
class: 'w-full'
|
class: 'w-full'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'recent_records_today_only',
|
prop: 'recent_records_today_only',
|
||||||
label: '最近记录只显示当日',
|
label: '最近记录只显示当日',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
@@ -133,6 +133,54 @@ const [BasicForm, formApi] = useForm({
|
|||||||
{ label: '否', value: 0 }
|
{ 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: '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: 'global_allow_repeat', name: '全局允许重复', content: Number(values.global_allow_repeat ?? 0) },
|
||||||
{ code: 'repeat_scan_interval', name: '重复扫码最小间隔', content: values.repeat_scan_interval ?? 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('保存成功')
|
ElMessage.success('保存成功')
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repair-flow-page">
|
||||||
|
<BasicForm />
|
||||||
|
<el-button class="site-submit" type="primary" :loading="saving" @click="onSubmit">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useForm } from '@/components/form'
|
||||||
|
import { ProduceConfigApi } from '@/apps/produce/api'
|
||||||
|
|
||||||
|
const saving = ref(false)
|
||||||
|
const [BasicForm, formApi] = useForm({
|
||||||
|
labelPosition: 'right',
|
||||||
|
labelWidth: '160px',
|
||||||
|
inline: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
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: '请选择维修流程对应步骤',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
const current = await ProduceConfigApi.info({ group_code: 'repair_flow' }) as Record<string, any>
|
||||||
|
if (current?.repair_process_id || current?.repair_step_id) {
|
||||||
|
await formApi.setValues(current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const legacy = await ProduceConfigApi.info({ group_code: 'basic_setting' }) as Record<string, any>
|
||||||
|
await formApi.setValues({
|
||||||
|
repair_process_id: legacy?.repair_process_id || '',
|
||||||
|
repair_step_id: legacy?.repair_step_id || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const values = await formApi.getValues()
|
||||||
|
await ProduceConfigApi.save({
|
||||||
|
group_code: 'repair_flow',
|
||||||
|
configs: [
|
||||||
|
{ code: 'repair_process_id', name: '维修流程工序', content: values.repair_process_id ?? '' },
|
||||||
|
{ code: 'repair_step_id', name: '维修流程步骤', content: values.repair_step_id ?? '' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.site-submit {
|
||||||
|
margin-left: 160px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,7 +25,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
labelWidth: '90px',
|
labelWidth: '90px',
|
||||||
schema: [
|
schema: [
|
||||||
{ prop: 'keyword', label: '关键字', component: 'Input', componentProps: { clearable: true, placeholder: '商品编码或商品名称' }, colSpan: 8 },
|
{ 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: [
|
columns: [
|
||||||
@@ -42,7 +42,7 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
labelWidth: '90px',
|
labelWidth: '90px',
|
||||||
schema: [
|
schema: [
|
||||||
{ component: 'Input', show: false, ifDetail: true, colSpan: 24, prop: 'id', label: 'ID' },
|
{ 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_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 },
|
{ component: 'Input', prop: 'product_name', label: '商品名称', componentProps: { maxlength: 200, placeholder: '请输入商品名称' }, rules: [{ required: true, message: '请输入商品名称' }], colSpan: 24 },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ export const crudSchema = (): CrudSchema => ({
|
|||||||
prop: "id",
|
prop: "id",
|
||||||
label: "ID",
|
label: "ID",
|
||||||
show: false,
|
show: false,
|
||||||
ifDetail: true,
|
|
||||||
colSpan: 24,
|
colSpan: 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,12 +56,30 @@ export interface ProduceRepairPart {
|
|||||||
|
|
||||||
export interface ProduceScanRecord {
|
export interface ProduceScanRecord {
|
||||||
id: string | number
|
id: string | number
|
||||||
|
process_id?: string | number | null
|
||||||
|
step_id?: string | number | null
|
||||||
process_name?: string
|
process_name?: string
|
||||||
step_name?: string
|
step_name?: string
|
||||||
operator?: string
|
operator?: string
|
||||||
user_code?: 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<string | number>
|
||||||
|
parts?: ProduceRepairPart[]
|
||||||
|
operation_fault_steps?: ProduceOperationFaultStep[]
|
||||||
|
responsible_operators?: ProduceResponsibleOperator[]
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getExceptionSettings() {
|
export function getExceptionSettings() {
|
||||||
return request.get<ProduceExceptionSetting[]>('/produce/exception/settings', {}, { showErrorMessage: false })
|
return request.get<ProduceExceptionSetting[]>('/produce/exception/settings', {}, { showErrorMessage: false })
|
||||||
}
|
}
|
||||||
@@ -78,6 +96,10 @@ export function checkExceptionOrder(orderNo: string) {
|
|||||||
return request.get<Record<string, any>>('/produce/exception/order-check', { order_no: orderNo.trim() }, { showErrorMessage: false })
|
return request.get<Record<string, any>>('/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<string, any>) {
|
export function submitExceptionRecord(data: Record<string, any>) {
|
||||||
return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, {
|
return request.post<{ code: number; msg: string; data?: { id: string | number } }>('/produce/exception/record', data, {
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
@@ -127,7 +149,22 @@ export function lookupMaterial(productCode: string) {
|
|||||||
}>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false })
|
}>('/produce/material/lookup', { product_code: productCode.trim() }, { showErrorMessage: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitRepairRecord(data: Record<string, any>) {
|
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, {
|
return request.post<{ code: number; msg: string; data?: { id: string | number; repair_no: string } }>('/produce/repair-record/record', data, {
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
returnFullResponse: true,
|
returnFullResponse: true,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ProduceQueryRecord {
|
|||||||
order_no?: string
|
order_no?: string
|
||||||
product_code?: string
|
product_code?: string
|
||||||
process_name?: string
|
process_name?: string
|
||||||
|
step_name?: string
|
||||||
exception_reason?: string
|
exception_reason?: string
|
||||||
exception_content?: string
|
exception_content?: string
|
||||||
material_category?: string
|
material_category?: string
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside class="side-column">
|
<aside class="side-column">
|
||||||
<section class="panel operator-panel"><h2>提报信息</h2><div><span>提报人</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>提交时间</span><strong>{{ nowText }}</strong></div></section>
|
<section class="panel operator-panel"><h2>提报信息</h2><div><span>用户编码</span><el-input v-model="userCode" size="small" clearable placeholder="请输入用户编码" @blur="checkUserCode" @keyup.enter="checkUserCode" /></div><div><span>提报人</span><strong>{{ userName || '-' }}</strong></div><div><span>提交时间</span><strong>{{ nowText }}</strong></div><p v-if="userCodeError" class="field-error">{{ userCodeError }}</p></section>
|
||||||
<section class="panel"><div class="side-title"><h2>最近异常</h2><span>现场回看</span></div><el-empty v-if="!recentExceptions.length" description="暂无异常记录" :image-size="70" /><div v-for="item in recentExceptions" :key="item.id" class="recent-item"><div><strong>{{ item.order_no || '-' }}</strong><p>{{ item.type_name || '-' }} · {{ [item.reason_name, item.issue_name].filter(Boolean).join(' · ') }}</p></div><el-tag :type="item.status === 2 ? 'success' : item.status === 1 ? 'warning' : 'danger'">{{ item.status_name || '待处理' }}</el-tag></div></section>
|
<section class="panel"><div class="side-title"><h2>最近异常</h2><span>现场回看</span></div><el-empty v-if="!recentExceptions.length" description="暂无异常记录" :image-size="70" /><div v-for="item in recentExceptions" :key="item.id" class="recent-item"><div><strong>{{ item.order_no || '-' }}</strong><p>{{ item.type_name || '-' }} · {{ [item.reason_name, item.issue_name].filter(Boolean).join(' · ') }}</p></div><el-tag :type="item.status === 2 ? 'success' : item.status === 1 ? 'warning' : 'danger'">{{ item.status_name || '待处理' }}</el-tag></div></section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,10 +79,13 @@ import { ElMessage } from 'element-plus'
|
|||||||
import type { UploadUserFile } from 'element-plus'
|
import type { UploadUserFile } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useMemberStore } from '~/stores/member'
|
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 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 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 form = ref({ orderNo: '', type: '', reason: '', issue: '', remark: '' })
|
||||||
const settings = ref<ProduceExceptionSetting[]>([])
|
const settings = ref<ProduceExceptionSetting[]>([])
|
||||||
const recentExceptions = ref<ProduceExceptionRecord[]>([])
|
const recentExceptions = ref<ProduceExceptionRecord[]>([])
|
||||||
@@ -109,6 +112,11 @@ const loadData = async () => {
|
|||||||
if (!form.value.type) selectType(typeOptions.value[0]?.value || '')
|
if (!form.value.type) selectType(typeOptions.value[0]?.value || '')
|
||||||
} catch { /* 请求层已提示错误 */ }
|
} 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 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 selectReason = (value: string) => { form.value.reason = value; form.value.issue = activeIssues.value[0]?.value || '' }
|
||||||
const checkOrder = async () => {
|
const checkOrder = async () => {
|
||||||
@@ -122,17 +130,17 @@ const resetForm = () => { form.value = { orderNo: '', type: typeOptions.value[0]
|
|||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!orderValid.value || !form.value.orderNo.trim()) { await checkOrder(); if (!orderValid.value) return }
|
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 (!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
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const images: string[] = []
|
const images: string[] = []
|
||||||
for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'exception'); if (url) images.push(url) }
|
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 }
|
if (response?.code !== 0) { ElMessage.error(response?.msg || '异常提交失败'); return }
|
||||||
ElMessage.success('异常已提交'); resetForm(); await loadData()
|
ElMessage.success('异常已提交'); resetForm(); await loadData()
|
||||||
} finally { submitting.value = false }
|
} finally { submitting.value = false }
|
||||||
}
|
}
|
||||||
onMounted(() => { updateNow(); void loadData() })
|
onMounted(() => { updateNow(); userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void loadData() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -147,4 +155,10 @@ onMounted(() => { updateNow(); void loadData() })
|
|||||||
.inline-form { display:flex; gap:12px; margin-top:20px; } .inline-form .el-input { flex:1; } .validation { margin-top:10px; font-size:13px; } .validation.success { color:#26b36c; } .validation.error { color:#ef625f; } .option-grid { display:grid; gap:12px; margin-top:18px; } .type-grid { grid-template-columns:repeat(2,1fr); } button { font:inherit; cursor:pointer; } .choice-card,.reason-card { border:1px solid #e6edf5; border-radius:14px; background:#f8fafc; text-align:left; transition:.2s; } .choice-card { padding:16px; } .choice-card strong,.choice-card small { display:block; } .choice-card strong { color:#2b3853; } .choice-card small { margin-top:8px; color:#9aa4b4; } .choice-card.active,.reason-card.active { border-color:#ff9a3c; background:#fff5ed; color:#e97020; } .sub-title { align-items:center; justify-content:space-between; margin-top:26px; } .sub-title span,.side-title span { color:#9aa4b4; font-size:12px; } .reason-grid { grid-template-columns:repeat(3,1fr); } .reason-card { min-height:46px; padding:8px; text-align:center; color:#56647a; } .issue-box { margin-top:18px; padding:16px; border-radius:14px; background:#fffaf5; } .issue-box .sub-title { margin:0 0 12px; } .full-width { width:100%; }
|
.inline-form { display:flex; gap:12px; margin-top:20px; } .inline-form .el-input { flex:1; } .validation { margin-top:10px; font-size:13px; } .validation.success { color:#26b36c; } .validation.error { color:#ef625f; } .option-grid { display:grid; gap:12px; margin-top:18px; } .type-grid { grid-template-columns:repeat(2,1fr); } button { font:inherit; cursor:pointer; } .choice-card,.reason-card { border:1px solid #e6edf5; border-radius:14px; background:#f8fafc; text-align:left; transition:.2s; } .choice-card { padding:16px; } .choice-card strong,.choice-card small { display:block; } .choice-card strong { color:#2b3853; } .choice-card small { margin-top:8px; color:#9aa4b4; } .choice-card.active,.reason-card.active { border-color:#ff9a3c; background:#fff5ed; color:#e97020; } .sub-title { align-items:center; justify-content:space-between; margin-top:26px; } .sub-title span,.side-title span { color:#9aa4b4; font-size:12px; } .reason-grid { grid-template-columns:repeat(3,1fr); } .reason-card { min-height:46px; padding:8px; text-align:center; color:#56647a; } .issue-box { margin-top:18px; padding:16px; border-radius:14px; background:#fffaf5; } .issue-box .sub-title { margin:0 0 12px; } .full-width { width:100%; }
|
||||||
:deep(.el-upload--picture-card),:deep(.el-upload-list__item) { width:92px; height:92px; } .upload-plus { font-size:30px; color:#ff9a3c; } .remark-input { margin-top:18px; } .form-actions { display:flex; justify-content:flex-end; gap:12px; } .operator-panel h2 { margin-bottom:18px; } .operator-panel div { display:flex; justify-content:space-between; padding:13px 0; border-bottom:1px solid #eef2f7; font-size:13px; } .operator-panel div:last-child { border-bottom:0; } .operator-panel span { color:#9aa4b4; } .side-title { align-items:center; justify-content:space-between; } .recent-item { display:flex; justify-content:space-between; gap:10px; padding:16px 0; border-bottom:1px solid #eef2f7; } .recent-item:last-child { border-bottom:0; } .recent-item strong { font-size:14px; } .recent-item p { margin:6px 0 0; color:#8a94a6; font-size:12px; }
|
:deep(.el-upload--picture-card),:deep(.el-upload-list__item) { width:92px; height:92px; } .upload-plus { font-size:30px; color:#ff9a3c; } .remark-input { margin-top:18px; } .form-actions { display:flex; justify-content:flex-end; gap:12px; } .operator-panel h2 { margin-bottom:18px; } .operator-panel div { display:flex; justify-content:space-between; padding:13px 0; border-bottom:1px solid #eef2f7; font-size:13px; } .operator-panel div:last-child { border-bottom:0; } .operator-panel span { color:#9aa4b4; } .side-title { align-items:center; justify-content:space-between; } .recent-item { display:flex; justify-content:space-between; gap:10px; padding:16px 0; border-bottom:1px solid #eef2f7; } .recent-item:last-child { border-bottom:0; } .recent-item strong { font-size:14px; } .recent-item p { margin:6px 0 0; color:#8a94a6; font-size:12px; }
|
||||||
@media (max-width: 900px) { .produce-work-page { padding:16px; } .content-grid { grid-template-columns:1fr; } .side-column { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); } } @media (max-width: 640px) { .page-hero { padding:24px; align-items:flex-start; flex-direction:column; } .hero-user { align-self:stretch; text-align:left; } .summary-grid,.side-column { grid-template-columns:1fr; } .type-grid,.reason-grid { grid-template-columns:1fr; } .inline-form { flex-direction:column; } .form-actions { justify-content:stretch; } .form-actions .el-button { flex:1; } }
|
@media (max-width: 900px) { .produce-work-page { padding:16px; } .content-grid { grid-template-columns:1fr; } .side-column { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); } } @media (max-width: 640px) { .page-hero { padding:24px; align-items:flex-start; flex-direction:column; } .hero-user { align-self:stretch; text-align:left; } .summary-grid,.side-column { grid-template-columns:1fr; } .type-grid,.reason-grid { grid-template-columns:1fr; } .inline-form { flex-direction:column; } .form-actions { justify-content:stretch; } .form-actions .el-button { flex:1; } }
|
||||||
|
.operator-panel > div:first-of-type { align-items: center; gap: 16px; }
|
||||||
|
.operator-panel > div:first-of-type > span { flex: 0 0 72px; white-space: nowrap; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input) { flex: 1; min-width: 0; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__wrapper) { min-height: 40px; padding: 0 13px; border-radius: 9px; box-shadow: 0 0 0 1px #d8e2ef inset; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__inner) { font-size: 15px; font-weight: 600; }
|
||||||
|
.operator-panel > div:not(:first-of-type) { min-height: 44px; align-items: center; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,43 +39,57 @@
|
|||||||
|
|
||||||
<div class="form-actions"><el-button size="large" @click="resetForm">重置</el-button><el-button type="primary" size="large" :loading="submitting" @click="completeRepair">完成维修</el-button></div>
|
<div class="form-actions"><el-button size="large" @click="resetForm">重置</el-button><el-button type="primary" size="large" :loading="submitting" @click="completeRepair">完成维修</el-button></div>
|
||||||
</main>
|
</main>
|
||||||
<aside class="side-column"><section class="panel operator-panel"><h2>当前操作人</h2><div><span>姓名</span><strong>{{ currentUser.name || '-' }}</strong></div><div><span>工号</span><strong>{{ currentUser.code || '-' }}</strong></div><div><span>当前任务</span><strong>{{ task?.device_no || '未选择' }}</strong></div></section><section class="panel tip-panel"><h2>处理提示</h2><p>物料不良请登记替换备件;作业不良需选择责任人员;搭配错误需选择对应物料分类。</p><el-button link type="primary" @click="goTasks">查看待维修订单</el-button></section></aside>
|
<aside class="side-column"><section class="panel operator-panel"><h2>当前操作人</h2><div><span>用户编码</span><el-input v-model="userCode" size="small" clearable placeholder="请输入用户编码" @blur="checkUserCode" @keyup.enter="checkUserCode" /></div><div><span>姓名</span><strong>{{ userName || '-' }}</strong></div><div><span>当前任务</span><strong>{{ task?.device_no || '未选择' }}</strong></div><p v-if="userCodeError" class="field-error">{{ userCodeError }}</p></section><section class="panel tip-panel"><h2>处理提示</h2><p>物料不良请登记替换备件;作业不良需选择责任人员;搭配错误需选择对应物料分类。</p><el-button link type="primary" @click="goTasks">查看待维修订单</el-button></section></aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="partDialogVisible" title="添加维修备件" width="460px"><el-input v-model="partCode" autofocus placeholder="请输入商品编码" @keyup.enter="addPart" /><template #footer><el-button @click="partDialogVisible = false">取消</el-button><el-button type="primary" :loading="loadingPart" @click="addPart">确定</el-button></template></el-dialog>
|
<el-dialog v-model="partDialogVisible" title="选择维修备件" width="760px" class="material-dialog" @closed="resetMaterialSearch"><div class="material-search"><el-input v-model="materialProductCode" clearable autofocus placeholder="商品编码" @keyup.enter="searchMaterialList" /><el-input v-model="materialProductName" clearable placeholder="商品名称" @keyup.enter="searchMaterialList" /><el-select v-model="materialCategoryId" clearable filterable placeholder="物料分类" @change="searchMaterialList"><el-option v-for="item in categories" :key="item.id" :label="item.category_name" :value="String(item.id)" /></el-select><el-button type="primary" :loading="materialLoading" @click="searchMaterialList">搜索</el-button></div><div class="material-hint">可按编码、名称或物料分类筛选,已添加的备件会自动标记。</div><el-table v-loading="materialLoading" :data="materialItems" border stripe class="material-table"><el-table-column prop="product_code" label="商品编码" min-width="180" /><el-table-column prop="product_name" label="商品名称" min-width="220" show-overflow-tooltip /><el-table-column prop="category_name" label="物料分类" min-width="140"><template #default="scope">{{ scope.row.category_name || '-' }}</template></el-table-column><el-table-column label="操作" width="100" fixed="right"><template #default="scope"><el-button link type="primary" :disabled="isPartAdded(scope.row.product_code)" @click="selectMaterial(scope.row)">{{ isPartAdded(scope.row.product_code) ? '已添加' : '选择' }}</el-button></template></el-table-column></el-table><el-empty v-if="!materialLoading && !materialItems.length" description="暂无匹配的备件" :image-size="72" /><div class="material-pagination"><el-pagination v-model:current-page="materialPage" v-model:page-size="materialLimit" layout="total, prev, pager, next" :total="materialTotal" :page-sizes="[10, 20, 50]" @current-change="searchMaterialList" @size-change="handleMaterialSizeChange" /></div><template #footer><el-button @click="partDialogVisible = false">关闭</el-button></template></el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CircleCheckFilled, Grid, EditPen, Tools, Box } from '@element-plus/icons-vue'
|
import { CircleCheckFilled, Grid, EditPen, Tools, Box, Search } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { UploadUserFile } from 'element-plus'
|
import type { UploadUserFile } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { navigateTo, useRoute } from 'nuxt/app'
|
import { navigateTo, useRoute } from 'nuxt/app'
|
||||||
import { useMemberStore } from '~/stores/member'
|
import { useMemberStore } from '~/stores/member'
|
||||||
import { getFaultDescriptionOptions, getMaterialCategories, getRepairStatistics, getRepairTask, getScanRecords, lookupMaterial, submitRepairRecord, uploadProduceImage, type ProduceRepairTask } from '~/apps/produce/api/exception'
|
import { checkMemberUser, getFaultDescriptionOptions, getMaterialCategories, getRepairStatistics, getRepairTask, getScanRecords, searchMaterials, submitRepairRecord, uploadProduceImage, type ProduceRepairTask, type ProduceScanRecord } from '~/apps/produce/api/exception'
|
||||||
|
|
||||||
interface Part { id: string; product_code: string; product_name?: string; category_id?: string | number; category_name?: string; source: string; quantity: number; fault_description_ids: string[] }
|
interface Part { id: string; product_code: string; product_name?: string; category_id?: string | number; category_name?: string; source: string; quantity: number; fault_description_ids: string[] }
|
||||||
interface ScanRecord { id: string | number; process_name?: string; step_name?: string; operator?: string; user_code?: string }
|
|
||||||
interface FaultDescription { id: string | number; category_id: string | number; description: string }
|
interface FaultDescription { id: string | number; category_id: string | number; description: string }
|
||||||
|
interface MaterialOption { id: string | number; product_code: string; product_name: string; category_id: string | number; category_name: string }
|
||||||
const memberStore = useMemberStore()
|
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 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 summaries = ref([{ label: '待维修', value: 0, type: 'warning', action: true }, { label: '处理中', value: 0, type: 'primary' }, { label: '今日完成', value: 0, type: 'success' }])
|
const summaries = ref([{ label: '待维修', value: 0, type: 'warning', action: true }, { label: '处理中', value: 0, type: 'primary' }, { label: '今日完成', value: 0, type: 'success' }])
|
||||||
const resultOptions = [{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: Box }, { label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: EditPen }, { label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: Grid }]
|
const resultOptions = [{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: Box }, { label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: EditPen }, { label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: Grid }]
|
||||||
const deviceNo = ref('')
|
const deviceNo = ref('')
|
||||||
const task = ref<ProduceRepairTask | null>(null)
|
const task = ref<ProduceRepairTask | null>(null)
|
||||||
const categories = ref<Array<{ id: string | number; category_name: string }>>([])
|
const categories = ref<Array<{ id: string | number; category_name: string }>>([])
|
||||||
const scanRecords = ref<ScanRecord[]>([])
|
const scanRecords = ref<ProduceScanRecord[]>([])
|
||||||
const faults = ref<FaultDescription[]>([])
|
const faults = ref<FaultDescription[]>([])
|
||||||
const imageFiles = ref<UploadUserFile[]>([])
|
const imageFiles = ref<UploadUserFile[]>([])
|
||||||
const form = ref({ repair_result: 'material_defect', category_ids: [] as string[], responsible_ids: [] as string[], parts: [] as Part[], remark: '' })
|
const form = ref({ repair_result: 'material_defect', category_ids: [] as string[], responsible_ids: [] as string[], parts: [] as Part[], remark: '' })
|
||||||
const loadingTask = ref(false)
|
const loadingTask = ref(false)
|
||||||
const loadingPart = ref(false)
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const partDialogVisible = ref(false)
|
const partDialogVisible = ref(false)
|
||||||
const partCode = ref('')
|
const materialProductCode = ref('')
|
||||||
|
const materialProductName = ref('')
|
||||||
|
const materialCategoryId = ref<string | number>('')
|
||||||
|
const materialItems = ref<MaterialOption[]>([])
|
||||||
|
const materialPage = ref(1)
|
||||||
|
const materialLimit = ref(10)
|
||||||
|
const materialTotal = ref(0)
|
||||||
|
const materialLoading = ref(false)
|
||||||
const reasonText = computed(() => [task.value?.reason_name, task.value?.issue_name].filter(Boolean).join(' · ') || '-')
|
const reasonText = computed(() => [task.value?.reason_name, task.value?.issue_name].filter(Boolean).join(' · ') || '-')
|
||||||
|
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 loadStatistics = async () => {
|
const loadStatistics = async () => {
|
||||||
const data = await getRepairStatistics({ user_code: currentUser.value.code }).catch(() => null)
|
const data = await getRepairStatistics({ user_code: currentUser.value.code }).catch(() => null)
|
||||||
@@ -88,18 +102,38 @@ const loadStatistics = async () => {
|
|||||||
}
|
}
|
||||||
const loadTask = async () => { const value = deviceNo.value.trim(); if (!value || loadingTask.value) return ElMessage.warning('请输入设备编码或订单号'); loadingTask.value = true; try { task.value = await getRepairTask(value); form.value.responsible_ids = []; scanRecords.value = []; if (task.value?.device_no) { const result = await getScanRecords({ order_no: task.value.device_no, limit: 50 }); scanRecords.value = Array.isArray(result?.items) ? result.items : []; } } finally { loadingTask.value = false } }
|
const loadTask = async () => { const value = deviceNo.value.trim(); if (!value || loadingTask.value) return ElMessage.warning('请输入设备编码或订单号'); loadingTask.value = true; try { task.value = await getRepairTask(value); form.value.responsible_ids = []; scanRecords.value = []; if (task.value?.device_no) { const result = await getScanRecords({ order_no: task.value.device_no, limit: 50 }); scanRecords.value = Array.isArray(result?.items) ? result.items : []; } } finally { loadingTask.value = false } }
|
||||||
const selectResult = (value: string) => { form.value.repair_result = value; if (value !== 'assembly_mismatch') form.value.category_ids = []; if (value !== 'operation_defect') form.value.responsible_ids = [] }
|
const selectResult = (value: string) => { form.value.repair_result = value; if (value !== 'assembly_mismatch') form.value.category_ids = []; if (value !== 'operation_defect') form.value.responsible_ids = [] }
|
||||||
const openPartDialog = () => { partCode.value = ''; partDialogVisible.value = true }
|
const openPartDialog = () => { materialProductCode.value = ''; materialProductName.value = ''; materialCategoryId.value = ''; materialPage.value = 1; partDialogVisible.value = true; void searchMaterialList() }
|
||||||
const addPart = async () => { const code = partCode.value.trim(); if (!code) return ElMessage.warning('请输入商品编码'); if (form.value.parts.some((item: Part) => item.product_code === code)) return ElMessage.warning('该物料已添加'); loadingPart.value = true; try { const material = await lookupMaterial(code); const part: Part = { id: `${Date.now()}-${code}`, product_code: material.product_code || code, product_name: material.product_name, category_id: material.category_id, category_name: material.category_name, source: 'new', quantity: 1, fault_description_ids: [] }; form.value.parts.push(part); await loadFaults(); partDialogVisible.value = false } finally { loadingPart.value = false } }
|
const searchMaterialList = async () => { materialLoading.value = true; try { const result = await searchMaterials(materialProductCode.value, materialProductName.value, materialCategoryId.value, materialPage.value, materialLimit.value); materialItems.value = Array.isArray(result?.items) ? result.items : []; materialTotal.value = Number(result?.total || 0) } finally { materialLoading.value = false } }
|
||||||
|
const handleMaterialSizeChange = (size: number) => { materialLimit.value = size; materialPage.value = 1; void searchMaterialList() }
|
||||||
|
const isPartAdded = (productCode: string) => form.value.parts.some((item: Part) => item.product_code === productCode)
|
||||||
|
const appendMaterial = async (material: MaterialOption) => { if (isPartAdded(material.product_code)) return ElMessage.warning('该物料已添加'); const part: Part = { id: `${Date.now()}-${material.product_code}`, product_code: material.product_code, product_name: material.product_name, category_id: material.category_id, category_name: material.category_name, source: 'new', quantity: 1, fault_description_ids: [] }; form.value.parts.push(part); await loadFaults(); ElMessage.success('备件已添加') }
|
||||||
|
const selectMaterial = (material: MaterialOption) => { void appendMaterial(material) }
|
||||||
|
const resetMaterialSearch = () => { materialProductCode.value = ''; materialProductName.value = ''; materialCategoryId.value = ''; materialItems.value = []; materialPage.value = 1; materialTotal.value = 0 }
|
||||||
const loadFaults = async () => { const ids = [...new Set(form.value.parts.map((item: Part) => String(item.category_id || '')).filter(Boolean))]; faults.value = ids.length ? await getFaultDescriptionOptions(ids) : [] }
|
const loadFaults = async () => { const ids = [...new Set(form.value.parts.map((item: Part) => String(item.category_id || '')).filter(Boolean))]; faults.value = ids.length ? await getFaultDescriptionOptions(ids) : [] }
|
||||||
const faultsForPart = (part: Part) => faults.value.filter((item: FaultDescription) => String(item.category_id) === String(part.category_id))
|
const faultsForPart = (part: Part) => faults.value.filter((item: FaultDescription) => String(item.category_id) === String(part.category_id))
|
||||||
const removePart = (index: number) => { form.value.parts.splice(index, 1); void loadFaults() }
|
const removePart = (index: number) => { form.value.parts.splice(index, 1); void loadFaults() }
|
||||||
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
|
const handleImageExceed = () => ElMessage.warning('最多上传 4 张图片')
|
||||||
const resetForm = () => { deviceNo.value = ''; task.value = null; scanRecords.value = []; form.value = { repair_result: 'material_defect', category_ids: [], responsible_ids: [], parts: [], remark: '' }; imageFiles.value = [] }
|
const resetForm = () => { deviceNo.value = ''; task.value = null; scanRecords.value = []; form.value = { repair_result: 'material_defect', category_ids: [], responsible_ids: [], parts: [], remark: '' }; imageFiles.value = [] }
|
||||||
const completeRepair = async () => { if (!task.value?.id) return ElMessage.warning('请先查询有效的维修任务'); if (!currentUser.value.code) return ElMessage.warning('请先登录'); if (form.value.repair_result === 'assembly_mismatch' && !form.value.category_ids.length) return ElMessage.warning('请选择搭配错误分类'); if (form.value.repair_result === 'operation_defect' && !form.value.responsible_ids.length) return ElMessage.warning('请选择责任人员'); submitting.value = true; try { const images: string[] = []; for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'repair'); if (url) images.push(url) } const response = await submitRepairRecord({ exception_id: task.value.id, user_code: currentUser.value.code, repair_result: form.value.repair_result, images, category_ids: form.value.category_ids, parts: form.value.parts.map((part: Part) => ({ product_code: part.product_code, source: part.source, quantity: part.quantity, fault_description_ids: part.fault_description_ids })), operation_fault_steps: form.value.responsible_ids, responsible_operators: form.value.responsible_ids, remark: form.value.remark.trim() }); if (response?.code !== 0) { ElMessage.error(response?.msg || '维修提交失败'); return } ElMessage.success('维修记录已完成'); resetForm(); void loadStatistics() } finally { submitting.value = false } }
|
const completeRepair = async () => { if (!task.value?.id) return ElMessage.warning('请先查询有效的维修任务'); if (!await checkUserCode()) return ElMessage.warning(userCodeError.value); if (form.value.repair_result === 'assembly_mismatch' && !form.value.category_ids.length) return ElMessage.warning('请选择搭配错误分类'); if (form.value.repair_result === 'operation_defect' && !form.value.responsible_ids.length) return ElMessage.warning('请选择责任人员'); submitting.value = true; try { const images: string[] = []; for (const file of imageFiles.value) if (file.raw) { const url = await uploadProduceImage(file.raw, 'repair'); if (url) images.push(url) } const selectedScanIds = scanRecords.value.filter(record => form.value.responsible_ids.includes(String(record.id))).map(record => record.id); const response = await submitRepairRecord({ exception_id: task.value.id, user_code: userCode.value.trim(), repair_result: form.value.repair_result, images, category_ids: form.value.category_ids, parts: form.value.parts.map((part: Part) => ({ product_code: part.product_code, source: part.source, quantity: part.quantity, fault_description_ids: part.fault_description_ids })), operation_fault_steps: selectedScanIds, responsible_operators: selectedScanIds, remark: form.value.remark.trim() }); if (response?.code !== 0) { ElMessage.error(response?.msg || '维修提交失败'); return } ElMessage.success('维修记录已完成'); resetForm(); void loadStatistics() } finally { submitting.value = false } }
|
||||||
const goTasks = () => navigateTo('/produce/repair/tasks')
|
const goTasks = () => navigateTo('/produce/repair/tasks')
|
||||||
onMounted(() => { void loadStatistics(); void getMaterialCategories().then((data: Array<{ id: string | number; category_name: string }>) => { categories.value = Array.isArray(data) ? data : [] }); const query = useRoute().query.device_no; if (query) { deviceNo.value = String(query); void loadTask() } })
|
onMounted(() => { userCode.value = currentUser.value.code; userName.value = currentUser.value.name; void checkUserCode(); void loadStatistics(); void getMaterialCategories().then((data: Array<{ id: string | number; category_name: string }>) => { categories.value = Array.isArray(data) ? data : [] }); const query = useRoute().query.device_no; if (query) { deviceNo.value = String(query); void loadTask() } })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;padding:32px 36px;border-radius:22px;color:#fff;box-shadow:0 14px 32px rgba(37,99,235,.12)}.repair-hero{background:linear-gradient(135deg,#2f6cf6,#35bfa3)}.eyebrow{font-size:13px;opacity:.85}h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.summary-card,.panel{background:#fff;border:1px solid #e7edf5;border-radius:18px;box-shadow:0 8px 24px rgba(36,59,92,.04)}.summary-card{display:flex;align-items:center;gap:12px;padding:18px 22px}.summary-card strong{font-size:28px}.summary-card span{color:#8a94a6;font-size:13px}.summary-card .el-button{margin-left:auto}.warning{color:#e59a2f}.primary{color:#2563eb}.success{color:#26b36c}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:20px;align-items:start}.main-column,.side-column{display:flex;flex-direction:column;gap:20px}.panel{padding:26px}.panel-title{display:flex;align-items:flex-start;gap:14px}.panel-title>span{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#eaf2ff;color:#2563eb;font-weight:700;flex:none}.panel-title h2{margin:0;font-size:17px}.panel-title p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.panel-title em{margin-left:auto;padding:5px 10px;border-radius:999px;color:#2563eb;background:#edf4ff;font-size:12px;font-style:normal}.panel-title em.optional{color:#8793a6;background:#f3f5f8}.panel-title small{margin-left:auto;color:#9aa4b4}.inline-form{display:flex;gap:12px;margin-top:20px}.inline-form .el-input{flex:1}.task-found{display:flex;align-items:center;gap:12px;margin-top:16px;padding:14px;border-radius:12px;background:#f0fbf6;color:#26b36c}.task-found>div{display:flex;flex-direction:column;gap:4px;flex:1}.task-found span{color:#2b3853;font-weight:600}.task-found .el-tag{margin-left:auto}.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.info-grid div{padding:14px;border-radius:12px;background:#f8fafc}.info-grid span,.info-grid strong{display:block}.info-grid span{color:#9aa4b4;font-size:12px}.info-grid strong{margin-top:8px;font-size:14px;word-break:break-all}.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:20px}.result-card{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:18px;border:1px solid #e6edf5;border-radius:14px;background:#f8fafc;color:#56647a;text-align:left;cursor:pointer}.result-card.active{border-color:#409eff;background:#edf4ff;color:#2563eb}.result-card small{color:#9aa4b4}.category-grid,.responsibility-grid{display:flex;flex-wrap:wrap;gap:14px;margin-top:22px}.responsibility-grid .el-checkbox{height:auto;margin:0;padding:12px 16px;border:1px solid #e6edf5;border-radius:12px;background:#f8fafc}.responsibility-grid .el-checkbox.is-checked{border-color:#409eff;background:#edf4ff}.responsibility-grid small{display:block;color:#9aa4b4}.part-toolbar{display:flex;justify-content:space-between;align-items:center;margin:22px 0 12px}.part-row{display:flex;align-items:flex-start;gap:12px;padding:16px 0;border-bottom:1px solid #eef2f7}.part-index{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#edf4ff;color:#2563eb}.part-info{display:flex;flex-direction:column;gap:8px;flex:1}.part-info>span{color:#8a94a6;font-size:12px}.fault-select{width:100%}.remark-input{margin-top:18px}.form-actions{display:flex;justify-content:flex-end;gap:12px}.operator-panel h2,.tip-panel h2{margin:0 0 18px}.operator-panel div{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #eef2f7;font-size:13px}.operator-panel div:last-child{border:0}.operator-panel span{color:#9aa4b4}.tip-panel p{color:#718096;font-size:13px;line-height:1.8}.upload-plus{font-size:30px;color:#409eff}:deep(.el-upload--picture-card),:deep(.el-upload-list__item){width:92px;height:92px}@media(max-width:900px){.produce-work-page{padding:16px}.content-grid{grid-template-columns:1fr}.side-column{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.page-hero{padding:24px;align-items:flex-start;flex-direction:column;gap:18px}.summary-grid,.side-column,.info-grid,.result-grid{grid-template-columns:1fr}.inline-form,.form-actions{flex-direction:column}.form-actions .el-button{width:100%}.part-row{flex-wrap:wrap}}
|
.produce-work-page{min-height:calc(100vh - 60px);padding:28px;background:#f5f7fb;color:#17233d}.page-shell{max-width:1440px;margin:0 auto}.page-hero{display:flex;align-items:center;justify-content:space-between;padding:32px 36px;border-radius:22px;color:#fff;box-shadow:0 14px 32px rgba(37,99,235,.12)}.repair-hero{background:linear-gradient(135deg,#2f6cf6,#35bfa3)}.eyebrow{font-size:13px;opacity:.85}h1{margin:8px 0 0;font-size:30px}.page-hero p{margin:8px 0 0;opacity:.86}.summary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:20px 0}.summary-card,.panel{background:#fff;border:1px solid #e7edf5;border-radius:18px;box-shadow:0 8px 24px rgba(36,59,92,.04)}.summary-card{display:flex;align-items:center;gap:12px;padding:18px 22px}.summary-card strong{font-size:28px}.summary-card span{color:#8a94a6;font-size:13px}.summary-card .el-button{margin-left:auto}.warning{color:#e59a2f}.primary{color:#2563eb}.success{color:#26b36c}.content-grid{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:20px;align-items:start}.main-column,.side-column{display:flex;flex-direction:column;gap:20px}.panel{padding:26px}.panel-title{display:flex;align-items:flex-start;gap:14px}.panel-title>span{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#eaf2ff;color:#2563eb;font-weight:700;flex:none}.panel-title h2{margin:0;font-size:17px}.panel-title p{margin:6px 0 0;color:#9aa4b4;font-size:13px}.panel-title em{margin-left:auto;padding:5px 10px;border-radius:999px;color:#2563eb;background:#edf4ff;font-size:12px;font-style:normal}.panel-title em.optional{color:#8793a6;background:#f3f5f8}.panel-title small{margin-left:auto;color:#9aa4b4}.inline-form{display:flex;gap:12px;margin-top:20px}.inline-form .el-input{flex:1}.task-found{display:flex;align-items:center;gap:12px;margin-top:16px;padding:14px;border-radius:12px;background:#f0fbf6;color:#26b36c}.task-found>div{display:flex;flex-direction:column;gap:4px;flex:1}.task-found span{color:#2b3853;font-weight:600}.task-found .el-tag{margin-left:auto}.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.info-grid div{padding:14px;border-radius:12px;background:#f8fafc}.info-grid span,.info-grid strong{display:block}.info-grid span{color:#9aa4b4;font-size:12px}.info-grid strong{margin-top:8px;font-size:14px;word-break:break-all}.result-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:20px}.result-card{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:18px;border:1px solid #e6edf5;border-radius:14px;background:#f8fafc;color:#56647a;text-align:left;cursor:pointer}.result-card.active{border-color:#409eff;background:#edf4ff;color:#2563eb}.result-card small{color:#9aa4b4}.category-grid,.responsibility-grid{display:flex;flex-wrap:wrap;gap:14px;margin-top:22px}.responsibility-grid .el-checkbox{height:auto;margin:0;padding:12px 16px;border:1px solid #e6edf5;border-radius:12px;background:#f8fafc}.responsibility-grid .el-checkbox.is-checked{border-color:#409eff;background:#edf4ff}.responsibility-grid small{display:block;color:#9aa4b4}.part-toolbar{display:flex;justify-content:space-between;align-items:center;margin:22px 0 12px}.part-row{display:flex;align-items:flex-start;gap:12px;padding:16px 0;border-bottom:1px solid #eef2f7}.part-index{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#edf4ff;color:#2563eb}.part-info{display:flex;flex-direction:column;gap:8px;flex:1}.part-info>span{color:#8a94a6;font-size:12px}.fault-select{width:100%}.remark-input{margin-top:18px}.form-actions{display:flex;justify-content:flex-end;gap:12px}.operator-panel h2,.tip-panel h2{margin:0 0 18px}.operator-panel div{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #eef2f7;font-size:13px}.operator-panel div:last-child{border:0}.operator-panel span{color:#9aa4b4}.tip-panel p{color:#718096;font-size:13px;line-height:1.8}.upload-plus{font-size:30px;color:#409eff}:deep(.el-upload--picture-card),:deep(.el-upload-list__item){width:92px;height:92px}@media(max-width:900px){.produce-work-page{padding:16px}.content-grid{grid-template-columns:1fr}.side-column{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.page-hero{padding:24px;align-items:flex-start;flex-direction:column;gap:18px}.summary-grid,.side-column,.info-grid,.result-grid{grid-template-columns:1fr}.inline-form,.form-actions{flex-direction:column}.form-actions .el-button{width:100%}.part-row{flex-wrap:wrap}}
|
||||||
|
.operator-panel > div:first-of-type { align-items: center; gap: 16px; }
|
||||||
|
.operator-panel > div:first-of-type > span { flex: 0 0 72px; white-space: nowrap; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input) { flex: 1; min-width: 0; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__wrapper) { min-height: 40px; padding: 0 13px; border-radius: 9px; box-shadow: 0 0 0 1px #d8e2ef inset; }
|
||||||
|
.operator-panel > div:first-of-type :deep(.el-input__inner) { font-size: 15px; font-weight: 600; }
|
||||||
|
.operator-panel > div:not(:first-of-type) { min-height: 44px; align-items: center; }
|
||||||
|
.responsibility-grid .el-checkbox__label { gap: 10px; line-height: 1.25; }
|
||||||
|
.responsibility-grid .el-checkbox__label > span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.responsibility-grid small { line-height: 1.35; }
|
||||||
|
.material-search { display: flex; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.material-search .el-input { flex: 1; min-width: 0; }
|
||||||
|
.material-search .el-select { width: 170px; flex: none; }
|
||||||
|
.material-hint { margin-bottom: 12px; color: #8a94a6; font-size: 13px; }
|
||||||
|
.material-table { width: 100%; }
|
||||||
|
.material-pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user