From 5ca6ef2d5a320c0b8a4859bfdffc899156a51571 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Sun, 6 Sep 2026 21:06:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=9F=E4=BA=A7=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E4=B8=8E=E7=BB=B4=E4=BF=AE=E8=AE=B0=E5=BD=95=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .windsurf/rules.md | 7 - .windsurf/skills/frontend-app-common/SKILL.md | 20 +- .../api/controller/DataEntryController.php | 355 ++++++++++- .../appapi/controller/DataEntryController.php | 28 + .../produce/app/model/MemberNotification.php | 32 + ...eate_produce_member_notification_table.php | 46 ++ .../components/MachineEntryPanel.vue | 37 +- docs/2026-09-04/1-维修作业不良责任通知.md | 17 + docs/2026-09-04/2-修正生产通知加载组件.md | 9 + docs/2026-09-06/1-移除首页扫码原型入口.md | 3 + docs/2026-09-06/10-修复整机模式提交反馈.md | 18 + docs/2026-09-06/11-同步生产录入模板源.md | 14 + docs/2026-09-06/12-优化整机重复扫码提示.md | 13 + .../2026-09-06/13-首页不良责任订单按月展示.md | 18 + .../2026-09-06/14-首页维修工序统计字段兼容.md | 5 + docs/2026-09-06/15-细化APP个人统计页.md | 3 + docs/2026-09-06/16-移除统计页占比区域.md | 3 + docs/2026-09-06/17-统计页增加月度日历.md | 3 + docs/2026-09-06/18-调整统计页日历位置.md | 5 + docs/2026-09-06/19-调整当天明细指标名称.md | 5 + docs/2026-09-06/2-完善首页今日数据.md | 9 + docs/2026-09-06/20-完善统计页核心指标项.md | 3 + docs/2026-09-06/22-移除成功失败核心指标.md | 3 + docs/2026-09-06/23-核心指标三列布局.md | 3 + docs/2026-09-06/24-修复首页维修量用户匹配.md | 3 + docs/2026-09-06/25-修复整机统计用户过滤.md | 3 + .../3-首页今日数据按工序步骤与提报人统计.md | 15 + .../5-首页今日数据合并当前用户统计.md | 8 + .../6-首页今日数据卡片样式重设计.md | 8 + docs/2026-09-06/7-首页不良责任订单展示.md | 3 + docs/2026-09-06/8-首页删除处理状态显示.md | 3 + .../9-首页今日数据仅显示当前用户.md | 7 + frontend/app/api/produce/data-entry.js | 12 + frontend/app/api/produce/notification.js | 13 + frontend/app/manifest.json | 12 +- frontend/app/pages.json | 6 + frontend/app/pages/entry/machine.vue | 47 +- frontend/app/pages/index/index.vue | 478 +++++++++++++-- frontend/app/pages/notification/index.vue | 178 ++++++ frontend/app/pages/statistics/index.vue | 551 ++++++++++++++---- .../components/MachineEntryPanel.vue | 37 +- 41 files changed, 1838 insertions(+), 205 deletions(-) delete mode 100644 .windsurf/rules.md create mode 100644 backend/plugin/produce/app/model/MemberNotification.php create mode 100644 backend/plugin/produce/resource/database/migrations/2026_09_04_000001_create_produce_member_notification_table.php create mode 100644 docs/2026-09-04/1-维修作业不良责任通知.md create mode 100644 docs/2026-09-04/2-修正生产通知加载组件.md create mode 100644 docs/2026-09-06/1-移除首页扫码原型入口.md create mode 100644 docs/2026-09-06/10-修复整机模式提交反馈.md create mode 100644 docs/2026-09-06/11-同步生产录入模板源.md create mode 100644 docs/2026-09-06/12-优化整机重复扫码提示.md create mode 100644 docs/2026-09-06/13-首页不良责任订单按月展示.md create mode 100644 docs/2026-09-06/14-首页维修工序统计字段兼容.md create mode 100644 docs/2026-09-06/15-细化APP个人统计页.md create mode 100644 docs/2026-09-06/16-移除统计页占比区域.md create mode 100644 docs/2026-09-06/17-统计页增加月度日历.md create mode 100644 docs/2026-09-06/18-调整统计页日历位置.md create mode 100644 docs/2026-09-06/19-调整当天明细指标名称.md create mode 100644 docs/2026-09-06/2-完善首页今日数据.md create mode 100644 docs/2026-09-06/20-完善统计页核心指标项.md create mode 100644 docs/2026-09-06/22-移除成功失败核心指标.md create mode 100644 docs/2026-09-06/23-核心指标三列布局.md create mode 100644 docs/2026-09-06/24-修复首页维修量用户匹配.md create mode 100644 docs/2026-09-06/25-修复整机统计用户过滤.md create mode 100644 docs/2026-09-06/3-首页今日数据按工序步骤与提报人统计.md create mode 100644 docs/2026-09-06/5-首页今日数据合并当前用户统计.md create mode 100644 docs/2026-09-06/6-首页今日数据卡片样式重设计.md create mode 100644 docs/2026-09-06/7-首页不良责任订单展示.md create mode 100644 docs/2026-09-06/8-首页删除处理状态显示.md create mode 100644 docs/2026-09-06/9-首页今日数据仅显示当前用户.md create mode 100644 frontend/app/api/produce/notification.js create mode 100644 frontend/app/pages/notification/index.vue diff --git a/.windsurf/rules.md b/.windsurf/rules.md deleted file mode 100644 index 16692d0..0000000 --- a/.windsurf/rules.md +++ /dev/null @@ -1,7 +0,0 @@ -# 项目规则 - -## 本地项目与版本控制 - -- 本项目为本地项目,不使用 Git 进行状态检查、差异比较、提交或任何其他操作。 -- 后续回答、验证命令和开发说明中不出现 Git 或 Git 命令。 -- 如需确认文件改动,以实际文件内容、语法检查、自动化测试或运行时验证为准。 diff --git a/.windsurf/skills/frontend-app-common/SKILL.md b/.windsurf/skills/frontend-app-common/SKILL.md index 718b790..4a41370 100644 --- a/.windsurf/skills/frontend-app-common/SKILL.md +++ b/.windsurf/skills/frontend-app-common/SKILL.md @@ -1,15 +1,15 @@ --- name: frontend-app-common -description: scanapp 移动端公共开发规范,基于 uni-app、Vue 和 uView Pro,覆盖页面组织、路由、组件、扫码交互、视觉字号和交付检查 +description: app 移动端公共开发规范,基于 uni-app、Vue 和 uView Pro,覆盖页面组织、路由、组件、扫码交互、视觉字号和交付检查 globs: - - "frontend/scanapp/**/*.vue" - - "frontend/scanapp/**/*.js" - - "frontend/scanapp/pages.json" + - "frontend/app/**/*.vue" + - "frontend/app/**/*.js" + - "frontend/app/pages.json" --- ## 技术基线 -当前 APP 位于 `frontend/scanapp/`,使用以下技术约定: +当前 APP 位于 `frontend/app/`,使用以下技术约定: - uni-app 页面运行模式。 - 页面采用 Vue 单文件组件(`.vue`)。 @@ -22,7 +22,7 @@ globs: ## 目录与页面组织 ```text -frontend/scanapp/ +frontend/app/ ├── App.vue ├── main.js ├── pages.json @@ -154,9 +154,9 @@ uni.scanCode({ ### 接口封装 -- scanapp 对接后端时统一通过 `frontend/scanapp/utils/request.js` 发起 `uni.request`,页面中不要直接调用 `uni.request`。 -- 请求配置集中放在 `frontend/scanapp/config/index.js`,当前生产插件 APP 业务端默认 `baseURL` 为 `/appapi`;主项目系统 API(例如 `/file/image` 上传)实际注册在 `/api`,调用时必须显式传入 `baseURL: config.apiBaseURL`,不能沿用默认基址。 -- 业务接口按模块放入 `frontend/scanapp/api//`,例如生产扫码接口放在 `api/produce/data-entry.js`。 +- APP 对接后端时统一通过 `frontend/app/utils/request.js` 发起 `uni.request`,页面中不要直接调用 `uni.request`。 +- 请求配置集中放在 `frontend/app/config/index.js`,当前生产插件 APP 业务端默认 `baseURL` 为 `/appapi`;主项目系统 API(例如 `/file/image` 上传)实际注册在 `/api`,调用时必须显式传入 `baseURL: config.apiBaseURL`,不能沿用默认基址。 +- 业务接口按模块放入 `frontend/app/api//`,例如生产扫码接口放在 `api/produce/data-entry.js`。 - API 函数命名按页面语义表达,例如 `getProcessOptions`、`submitOrderRecord`、`getOrderStatistics`,页面只调用这些函数。 - 业务表单中的图片统一采用“本地预览、提交时上传”:选图后仅保存 `tempFilePaths` 用于立即预览;提交时调用 `@/api/upload.js` 的 `uploadImage(filePath, subDir)`,将返回的服务器 URL 放入业务请求。上传失败时中止提交并保留本地图片,便于用户重试。 - 图片上传使用 `/api/file/image` 和 `config.apiBaseURL`;公共 `normalizeImageUrl()` 会将后端本地存储返回的 `127.0.0.1` / `localhost` URL 改写为配置的局域网后端地址,避免 Android 真机将回环地址解析为手机自身。历史记录中的服务器图片地址也必须经过该方法规范化。 @@ -235,7 +235,7 @@ uni.scanCode({ ## 开发检查清单 -- [ ] 页面文件位于 `frontend/scanapp/pages/` 合适目录。 +- [ ] 页面文件位于 `frontend/app/pages/` 合适目录。 - [ ] 新页面已注册到 `pages.json`。 - [ ] 导航路径与 `pages.json` 完全一致。 - [ ] 已复用 uView Pro 组件和现有 `uni.*` 能力。 diff --git a/backend/plugin/produce/app/api/controller/DataEntryController.php b/backend/plugin/produce/app/api/controller/DataEntryController.php index 23af85b..73eddba 100644 --- a/backend/plugin/produce/app/api/controller/DataEntryController.php +++ b/backend/plugin/produce/app/api/controller/DataEntryController.php @@ -19,6 +19,7 @@ use plugin\produce\app\model\ExceptionSetting; use plugin\produce\app\model\HardwareFaultDescription; use plugin\produce\app\model\Material; use plugin\produce\app\model\MaterialCategory; +use plugin\produce\app\model\MemberNotification; use plugin\produce\app\model\Process; use plugin\produce\app\model\ProcessStep; use plugin\produce\app\model\ProductRecord; @@ -367,6 +368,8 @@ final class DataEntryController extends Base 'today_total' => $todayTotal, 'success_total' => $successTotal, 'failed_total' => $failedTotal, + 'process_breakdown' => $this->buildTodayProcessStepBreakdown(clone $query), + 'daily_trend' => $this->buildDailyTrend(clone $query, 'scan_time'), ]); } @@ -422,9 +425,166 @@ final class DataEntryController extends Base 'today_total' => $todayTotal, 'success_total' => $successTotal, 'failed_total' => $failedTotal, + 'process_breakdown' => $this->buildTodayProcessStepBreakdown(clone $query), + 'daily_trend' => $this->buildDailyTrend(clone $query, 'created_at'), ]); } + #[OA\Get(path: '/produce/dashboard/today', summary: '获取首页今日生产总览', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function todayDashboard(): Response + { + $member = $this->currentMember->user(); + if (!$member) { + return Json::fail('请先登录', [], 401); + } + + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $tomorrowStart = $todayStart + 86400; + $monthStart = strtotime(date('Y-m-01 00:00:00')); + $nextMonthStart = strtotime('+1 month', $monthStart); + $userCode = (string) $member->username; + $orders = ScanData::query() + ->where('user_code', $userCode) + ->whereBetween('scan_time', [$todayStart, $tomorrowStart - 1]); + $products = ProductRecord::query() + ->where('user_code', $userCode) + ->whereBetween('created_at', [$todayStart, $tomorrowStart - 1]); + $exceptions = ExceptionRecord::query() + ->where('user_code', $userCode) + ->whereBetween('reported_at', [$todayStart, $tomorrowStart - 1]); + $operatorNames = array_values(array_unique(array_filter([ + trim((string) ($member->nickname ?? '')), + $userCode, + ]))); + $repairs = RepairRecord::query() + ->where(function ($query) use ($operatorNames, $userCode): void { + $query->whereIn('operator', $operatorNames) + ->orWhereHas('exception', static fn ($exceptionQuery) => $exceptionQuery->where('user_code', $userCode)); + }) + ->whereBetween('repaired_at', [$todayStart, $tomorrowStart - 1]); + $exceptionTotal = (int) (clone $exceptions)->count(); + $responsibilityData = $this->buildMonthlyResponsibilityOrders($monthStart, $nextMonthStart, $userCode); + return Json::success('获取成功', [ + 'summary' => [ + 'order_scan' => (int) (clone $orders)->sum('quantity'), + 'product_scan' => (int) (clone $products)->sum('quantity'), + 'exception' => $exceptionTotal, + 'repair' => (int) (clone $repairs)->count(), + ], + 'order_breakdown' => $this->buildTodayProcessStepBreakdown(clone $orders), + 'repair_breakdown' => $this->buildTodayProcessStepBreakdown(clone $repairs), + 'product_breakdown' => $this->buildTodayProcessStepBreakdown(clone $products), + 'responsibility_order_total' => $responsibilityData['total'], + 'responsibility_orders' => $responsibilityData['items'], + 'trend' => $this->buildTodayHourlyTrend($orders, $products, $todayStart, $tomorrowStart), + 'exceptions' => [ + 'pending' => (int) (clone $exceptions)->where('status', 0)->count(), + 'processing' => (int) (clone $exceptions)->where('status', 1)->count(), + 'completed' => (int) (clone $exceptions)->where('status', 2)->count(), + 'total' => $exceptionTotal, + ], + 'updated_at' => time(), + ]); + } + + /** 统计本月当前会员承担责任的不良订单,并返回最近五条。 */ + private function buildMonthlyResponsibilityOrders(int $monthStart, int $nextMonthStart, string $userCode): array + { + $repairs = RepairRecord::query() + ->where('repair_result', 'operation_defect') + ->whereBetween('repaired_at', [$monthStart, $nextMonthStart - 1]) + ->orderByDesc('repaired_at') + ->orderByDesc('id') + ->get(['id', 'order_no', 'exception_reason', 'repaired_at', 'responsible_operators']); + $items = []; + foreach ($repairs as $repair) { + $snapshots = is_array($repair->responsible_operators) ? $repair->responsible_operators : []; + $isResponsible = collect($snapshots)->contains(static fn (array $snapshot): bool => trim((string) ($snapshot['user_code'] ?? '')) === $userCode); + if (!$isResponsible) { + continue; + } + + $items[] = [ + 'id' => (string) $repair->id, + 'order_no' => (string) $repair->order_no, + 'exception_reason' => (string) $repair->exception_reason, + 'quantity' => 1, + 'repaired_at' => (int) $repair->repaired_at, + ]; + } + + return [ + 'total' => count($items), + 'items' => array_slice($items, 0, 5), + ]; + } + + /** 统计最近六个整点时段的扫码数量。 */ + private function buildTodayHourlyTrend($orders, $products, int $todayStart, int $tomorrowStart): array + { + $trend = []; + $currentHour = strtotime(date('Y-m-d H:00:00')); + for ($offset = 5; $offset >= 0; $offset--) { + $hourStart = $currentHour - ($offset * 3600); + $rangeStart = max($todayStart, $hourStart); + $rangeEnd = min($tomorrowStart - 1, $hourStart + 3599); + $orderCount = (int) (clone $orders)->whereBetween('scan_time', [$rangeStart, $rangeEnd])->sum('quantity'); + $productCount = (int) (clone $products)->whereBetween('created_at', [$rangeStart, $rangeEnd])->sum('quantity'); + $trend[] = ['label' => date('H:i', $hourStart), 'value' => $orderCount + $productCount]; + } + return $trend; + } + + /** 汇总今日记录的工序与步骤数量。 */ + private function buildTodayProcessStepBreakdown($query): array + { + $isRepairRecord = $query->getModel()->getTable() === 'produce_repair_record'; + $quantityExpression = $isRepairRecord ? 'COUNT(*)' : 'SUM(quantity)'; + $snapshotColumns = $isRepairRecord ? ', MAX(process_name) as record_process_name, MAX(step_name) as record_step_name' : ''; + $rows = $query + ->selectRaw("process_id, step_id{$snapshotColumns}, {$quantityExpression} as quantity") + ->groupBy('process_id', 'step_id') + ->orderByDesc('quantity') + ->get(); + if ($rows->isEmpty()) { + return []; + } + + $processNames = Process::query() + ->whereIn('id', $rows->pluck('process_id')->filter()->all()) + ->pluck('process_name', 'id'); + $stepNames = ProcessStep::query() + ->whereIn('id', $rows->pluck('step_id')->filter()->all()) + ->pluck('step_name', 'id'); + + return $rows->map(static function ($row) use ($processNames, $stepNames): array { + $processId = (string) ($row->process_id ?? ''); + $stepId = (string) ($row->step_id ?? ''); + return [ + 'process_id' => $processId, + 'step_id' => $stepId, + 'process_name' => (string) (($row->record_process_name ?? '') ?: $processNames->get($row->process_id) ?: '未记录工序'), + 'step_name' => (string) (($row->record_step_name ?? '') ?: $stepNames->get($row->step_id) ?: '未记录步骤'), + 'quantity' => (int) $row->quantity, + ]; + })->values()->all(); + } + + /** 汇总查询区间内每日数量。 */ + private function buildDailyTrend($query, string $dateColumn): array + { + $rows = $query + ->selectRaw("FROM_UNIXTIME({$dateColumn}, '%Y-%m-%d') as day, SUM(quantity) as quantity") + ->groupBy('day') + ->orderBy('day') + ->get(); + return $rows->map(static fn ($row): array => [ + 'date' => (string) $row->day, + 'quantity' => (int) $row->quantity, + ])->values()->all(); + } + #[OA\Get(path: '/produce/product-record/user-code-stats', summary: '前台整机用户统计', tags: ['produce-web'])] #[DataResponse(description: '获取成功')] #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] @@ -860,13 +1020,24 @@ final class DataEntryController extends Base public function repairStatistics(Request $request): Response { $query = RepairRecord::query(); - $todayStart = strtotime(date('Y-m-d 00:00:00')); - $todayEnd = $todayStart + 86400; + $userCode = trim((string) $request->input('user_code', '')); + $startAt = $this->parseQueryDate($request->input('start_date')) ?: strtotime(date('Y-m-d 00:00:00')); + $endAt = $this->parseQueryDate($request->input('end_date'), true) ?: strtotime(date('Y-m-d 23:59:59')); + if ($userCode !== '') { + $member = Member::query()->where('username', $userCode)->first(); + $operators = array_values(array_unique(array_filter([$userCode, (string) ($member?->nickname ?? '')]))); + $query->where(function ($builder) use ($operators, $userCode): void { + $builder->whereIn('operator', $operators) + ->orWhereHas('exception', static fn ($exceptionQuery) => $exceptionQuery->where('user_code', $userCode)); + }); + } + $query->whereBetween('repaired_at', [$startAt, $endAt]); return Json::success('获取成功', [ 'pending' => ExceptionRecord::query()->where('status', 0)->count(), 'processing' => ExceptionRecord::query()->where('status', 1)->count(), - 'completed_today' => (clone $query)->whereBetween('repaired_at', [$todayStart, $todayEnd - 1])->count(), + 'completed_today' => (clone $query)->count(), + 'process_breakdown' => $this->buildTodayProcessStepBreakdown(clone $query), ]); } @@ -1097,6 +1268,16 @@ final class DataEntryController extends Base 'handled_at' => $now, ])->save(); + if ($repairResult === 'operation_defect') { + $this->createRepairResponsibilityNotifications( + $repair, + $exception, + $responsibleOperatorSnapshots, + $member, + $now + ); + } + return $repair; }); } catch (\Throwable $e) { @@ -1106,6 +1287,74 @@ final class DataEntryController extends Base return Json::success('提交成功', ['id' => (string) $record->id, 'repair_no' => $record->repair_no]); } + #[OA\Get(path: '/produce/member-notification', summary: '获取当前会员生产通知', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function memberNotifications(Request $request): Response + { + $memberId = (int) $this->currentMember->id(); + if ($memberId <= 0) { + return Json::fail('请先登录', [], 401); + } + + $page = max(1, (int) $request->input('page', 1)); + $limit = min(50, max(1, (int) $request->input('limit', 20))); + $query = MemberNotification::query() + ->where('member_id', $memberId) + ->orderByDesc('created_at') + ->orderByDesc('id'); + $total = $query->count(); + + return Json::success('ok', [ + 'items' => $query->forPage($page, $limit)->get(), + 'total' => $total, + ]); + } + + #[OA\Get(path: '/produce/member-notification/unread-count', summary: '获取当前会员未读生产通知数', tags: ['produce-web'])] + #[DataResponse(description: '获取成功')] + public function memberNotificationUnreadCount(): Response + { + $memberId = (int) $this->currentMember->id(); + if ($memberId <= 0) { + return Json::fail('请先登录', [], 401); + } + + $count = MemberNotification::query() + ->where('member_id', $memberId) + ->where('status', 0) + ->count(); + + return Json::success('ok', ['unread_count' => $count]); + } + + #[OA\Put(path: '/produce/member-notification/read', summary: '标记当前会员生产通知已读', tags: ['produce-web'])] + #[SimpleResponse(description: '操作成功')] + public function readMemberNotification(Request $request): Response + { + $memberId = (int) $this->currentMember->id(); + $notificationId = trim((string) $request->input('id', '')); + if ($memberId <= 0) { + return Json::fail('请先登录', [], 401); + } + if ($notificationId === '') { + return Json::fail('通知ID不能为空'); + } + + $notification = MemberNotification::query() + ->where('member_id', $memberId) + ->whereKey($notificationId) + ->first(); + if (!$notification) { + return Json::fail('通知不存在'); + } + if ((int) $notification->status === 1) { + return Json::success('ok'); + } + + $notification->fill(['status' => 1, 'read_at' => time()])->save(); + return Json::success('ok'); + } + #[OA\Post(path: '/produce/exception/record', summary: '提交异常记录', tags: ['produce-web'])] #[SimpleResponse(description: '提交成功')] #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] @@ -1382,9 +1631,17 @@ final class DataEntryController extends Base protected function applyExceptionRecordFilter($query, Request $request): void { $userCode = trim((string) $request->input('user_code', '')); + $startAt = $this->parseQueryDate($request->input('start_date')); + $endAt = $this->parseQueryDate($request->input('end_date'), true); if ($userCode !== '') { $query->where('user_code', $userCode); } + if ($startAt !== null) { + $query->where('reported_at', '>=', $startAt); + } + if ($endAt !== null) { + $query->where('reported_at', '<=', $endAt); + } } protected function applyScanDataFilter($query, Request $request): void @@ -1395,6 +1652,8 @@ final class DataEntryController extends Base $stepId = (int) $request->input('step_id', 0); $statusId = (int) $request->input('status_id', 0); $scanResult = $request->input('scan_result', ''); + $startAt = $this->parseQueryDate($request->input('start_date')); + $endAt = $this->parseQueryDate($request->input('end_date'), true); if ($orderNo !== '') { $query->where('order_no', $orderNo); @@ -1415,12 +1674,19 @@ final class DataEntryController extends Base $query->where('scan_result', (int) $scanResult); } + if ($startAt !== null) { + $query->where('scan_time', '>=', $startAt); + } + if ($endAt !== null) { + $query->where('scan_time', '<=', $endAt); + } + $recentOnly = (int) ProduceConfig::query() ->where('group_code', 'basic_setting') ->where('code', 'recent_records_today_only') ->value('content'); - if ($recentOnly === 1 && $orderNo === '') { + if ($recentOnly === 1 && $orderNo === '' && $startAt === null && $endAt === null) { $start = strtotime(date('Y-m-d 00:00:00')); $end = strtotime(date('Y-m-d 23:59:59')); $query->whereBetween('scan_time', [$start, $end]); @@ -1429,10 +1695,17 @@ final class DataEntryController extends Base protected function applyProductRecordFilter($query, Request $request): void { + $userCode = trim((string) $request->input('user_code', '')); $processId = (int) $request->input('process_id', 0); $stepId = (int) $request->input('step_id', 0); $statusId = (int) $request->input('status_id', 0); $scanResult = $request->input('scan_result', ''); + $startAt = $this->parseQueryDate($request->input('start_date')); + $endAt = $this->parseQueryDate($request->input('end_date'), true); + + if ($userCode !== '') { + $query->where('user_code', $userCode); + } if ($processId > 0) { $query->where('process_id', $processId); @@ -1447,12 +1720,19 @@ final class DataEntryController extends Base $query->where('scan_result', (int) $scanResult); } + if ($startAt !== null) { + $query->where('created_at', '>=', $startAt); + } + if ($endAt !== null) { + $query->where('created_at', '<=', $endAt); + } + $recentOnly = (int) ProduceConfig::query() ->where('group_code', 'basic_setting') ->where('code', 'recent_records_today_only') ->value('content'); - if ($recentOnly === 1) { + if ($recentOnly === 1 && $startAt === null && $endAt === null) { $start = strtotime(date('Y-m-d 00:00:00')); $end = strtotime(date('Y-m-d 23:59:59')); $query->whereBetween('created_at', [$start, $end]); @@ -1475,6 +1755,71 @@ final class DataEntryController extends Base return $data; } + /** 创建作业不良责任通知,每名责任人仅保留一条通知。 */ + private function createRepairResponsibilityNotifications( + RepairRecord $repair, + ExceptionRecord $exception, + array $snapshots, + Member $repairer, + int $now + ): void { + $snapshotsByUserCode = []; + foreach ($snapshots as $snapshot) { + $userCode = trim((string) ($snapshot['user_code'] ?? '')); + if ($userCode !== '') { + $snapshotsByUserCode[$userCode][] = $snapshot; + } + } + + foreach ($snapshotsByUserCode as $userCode => $userSnapshots) { + $responsibleMember = Member::query()->where('username', $userCode)->first(); + if (!$responsibleMember) { + continue; + } + + $steps = array_values(array_unique(array_filter(array_map(static function (array $snapshot): string { + return implode(' / ', array_filter([ + trim((string) ($snapshot['process_name'] ?? '')), + trim((string) ($snapshot['step_name'] ?? '')), + ])); + }, $userSnapshots)))); + MemberNotification::query()->create([ + 'id' => Snowflake::generate(), + 'member_id' => $responsibleMember->id, + 'user_code' => $userCode, + 'type' => 'repair_responsibility', + 'title' => '维修作业不良责任通知', + 'content' => $this->formatRepairResponsibilityContent($repair, $exception, $steps, $repairer), + 'repair_id' => $repair->id, + 'repair_no' => $repair->repair_no, + 'order_no' => $repair->order_no, + 'status' => 0, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + /** 组装维修定责通知的原因说明。 */ + private function formatRepairResponsibilityContent( + RepairRecord $repair, + ExceptionRecord $exception, + array $steps, + Member $repairer + ): string { + $content = [ + sprintf('订单 %s 已完成维修,维修判定为作业不良责任。', $repair->order_no), + $steps === [] ? '' : '责任工序/步骤:' . implode('、', $steps), + $exception->reason_name ? '异常原因:' . $exception->reason_name : '', + $exception->issue_name ? '具体问题:' . $exception->issue_name : '', + $repair->remark ? '维修说明:' . $repair->remark : '', + '维修单:' . $repair->repair_no, + '提交人:' . ($repairer->nickname ?: $repairer->username), + ]; + + return implode("\n", array_filter($content)); + } + protected function handleNextStepTransitions(ScanData $currentScanData, Request $request, Member $member, int $now): void { $previousRelations = \plugin\produce\app\model\StepRelation::query() diff --git a/backend/plugin/produce/app/appapi/controller/DataEntryController.php b/backend/plugin/produce/app/appapi/controller/DataEntryController.php index 70f9c45..4638051 100644 --- a/backend/plugin/produce/app/appapi/controller/DataEntryController.php +++ b/backend/plugin/produce/app/appapi/controller/DataEntryController.php @@ -186,6 +186,27 @@ final class DataEntryController return $this->delegate->repairRecord($request, $repairRecordService); } + #[OA\Get(path: '/produce/member-notification', summary: 'APP 获取当前会员生产通知', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function memberNotifications(Request $request): Response + { + return $this->delegate->memberNotifications($request); + } + + #[OA\Get(path: '/produce/member-notification/unread-count', summary: 'APP 获取未读生产通知数', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function memberNotificationUnreadCount(): Response + { + return $this->delegate->memberNotificationUnreadCount(); + } + + #[OA\Put(path: '/produce/member-notification/read', summary: 'APP 标记生产通知已读', tags: ['produce-app'])] + #[SimpleResponse(description: '操作成功')] + public function readMemberNotification(Request $request): Response + { + return $this->delegate->readMemberNotification($request); + } + #[OA\Post(path: '/produce/exception/record', summary: 'APP 提交异常记录', tags: ['produce-app'])] #[SimpleResponse(description: '提交成功')] #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] @@ -208,6 +229,13 @@ final class DataEntryController return $this->delegate->productStatistics($request); } + #[OA\Get(path: '/produce/dashboard/today', summary: 'APP 获取首页今日生产总览', tags: ['produce-app'])] + #[DataResponse(description: '获取成功')] + public function todayDashboard(): Response + { + return $this->delegate->todayDashboard(); + } + #[OA\Get(path: '/produce/product-record/user-code-stats', summary: 'APP 整机用户统计', tags: ['produce-app'])] #[DataResponse(description: '获取成功')] public function productUserCodeStats(Request $request): Response diff --git a/backend/plugin/produce/app/model/MemberNotification.php b/backend/plugin/produce/app/model/MemberNotification.php new file mode 100644 index 0000000..1bc8fbc --- /dev/null +++ b/backend/plugin/produce/app/model/MemberNotification.php @@ -0,0 +1,32 @@ + 'integer', + 'repair_id' => 'integer', + 'status' => 'integer', + 'read_at' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + 'deleted_at' => 'integer', + ]; + + public function getReadDateAttribute(): ?string + { + return $this->read_at ? date('Y-m-d H:i:s', (int) $this->read_at) : null; + } +} diff --git a/backend/plugin/produce/resource/database/migrations/2026_09_04_000001_create_produce_member_notification_table.php b/backend/plugin/produce/resource/database/migrations/2026_09_04_000001_create_produce_member_notification_table.php new file mode 100644 index 0000000..d6e3ff0 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_09_04_000001_create_produce_member_notification_table.php @@ -0,0 +1,46 @@ +hasTable($table)) { + return; + } + + $schema->create($table, function (Blueprint $table): void { + $table->bigInteger('id')->primary()->comment('主键'); + $table->bigInteger('member_id')->nullable()->comment('接收会员ID'); + $table->string('user_code', 64)->comment('接收用户编码'); + $table->string('type', 32)->default('repair_responsibility')->comment('通知类型'); + $table->string('title', 120)->comment('通知标题'); + $table->text('content')->comment('通知内容'); + $table->bigInteger('repair_id')->nullable()->comment('维修记录ID'); + $table->string('repair_no', 64)->nullable()->comment('维修单号'); + $table->string('order_no', 64)->nullable()->comment('订单号'); + $table->tinyInteger('status')->default(0)->comment('阅读状态:0未读,1已读'); + $table->bigInteger('read_at')->nullable()->comment('阅读时间'); + $table->bigInteger('created_at')->nullable()->comment('创建时间'); + $table->bigInteger('updated_at')->nullable()->comment('更新时间'); + $table->bigInteger('deleted_at')->nullable()->comment('删除时间'); + $table->index(['member_id', 'status'], 'idx_member_notification_status'); + $table->index(['user_code', 'created_at'], 'idx_member_notification_user_code'); + $table->index('repair_id', 'idx_member_notification_repair_id'); + }); + } + + public function down(Builder $schema): void + { + $table = 'produce_member_notification'; + if (!$schema->hasTable($table)) { + return; + } + + $schema->drop($table); + } +}; diff --git a/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue b/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue index a436d8d..2aeebe3 100644 --- a/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue +++ b/backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue @@ -400,6 +400,33 @@ const matchSpecificRule = (rule: ProduceDataEntrySpecificRule, valueMap: Record< } } +const hasExistingRecordForRule = (rule: ProduceDataEntrySpecificRule) => { + const matchField = rule.match_field || 'process_order_no' + const currentValue = String(({ + process_order_no: processOrderNo.value, + order_no: processOrderNo.value, + product_code: productCode.value, + user_code: userCode.value, + } as Record)[matchField] || '').trim() + if (!currentValue) return false + const matched = recentRecords.value.some((record) => { + const recordValue = String((record as Record)[matchField] || (matchField === 'order_no' ? record.process_order_no : '')).trim() + return recordValue === currentValue + }) + return matched +} + +const getDuplicateRuleMessage = (rule: ProduceDataEntrySpecificRule) => { + const fieldLabels: Record = { + order_no: '加工单号', + process_order_no: '加工单号', + product_code: '商品编码', + user_code: '用户编码', + } + const field = rule.match_field || 'process_order_no' + return `${fieldLabels[field] || '当前数据'}已录入,当前规则不允许重复扫码` +} + const validateSpecificRules = () => { if (!specificRules.value.length) { return true @@ -415,11 +442,11 @@ const validateSpecificRules = () => { if ((rule.apply_mode || 'order') !== 'machine' && rule.apply_mode !== 'both') { return false } - return Number(rule.rule_type) === 2 && matchSpecificRule(rule, context) + return Number(rule.rule_type) === 2 && matchSpecificRule(rule, context) && hasExistingRecordForRule(rule) }) if (blockedRule) { - ElMessage.warning(blockedRule.rule_name || '当前整机数据命中特定规则,禁止提交') + ElMessage.warning(getDuplicateRuleMessage(blockedRule)) const matchField = blockedRule.match_field || 'process_order_no' if (matchField === 'product_code') { focusProductCodeInput() @@ -484,12 +511,13 @@ const resetForm = () => { } const submitEntry = async () => { + if (submitting.value) return if (!validateBeforeSubmit()) return const passed = await runUserCodeCheck(userCode.value) if (!passed) { emit('play-error'); nextTick(() => focusUserCodeInput()); return } submitting.value = true try { - const result = await submitProductRecord({ + const payload = { process_order_no: processOrderNo.value.trim(), product_code: productCode.value.trim(), quantity: Number(quantity.value) || 1, @@ -497,7 +525,8 @@ const submitEntry = async () => { step_id: props.selectedStep, user_code: userCode.value.trim(), scan_result: 1, - }) + } + const result = await submitProductRecord(payload) if (Number(result?.code) !== 0) throw new Error(result?.msg || '录入失败') ElMessage.success('录入成功') emit('play-success') diff --git a/docs/2026-09-04/1-维修作业不良责任通知.md b/docs/2026-09-04/1-维修作业不良责任通知.md new file mode 100644 index 0000000..0157456 --- /dev/null +++ b/docs/2026-09-04/1-维修作业不良责任通知.md @@ -0,0 +1,17 @@ +# 维修作业不良责任通知 + +## 变更内容 + +- 新增 `produce_member_notification`,用于保存生产 APP 会员通知及已读状态。 +- 维修判定为“作业不良”并选择责任人员后,系统按责任人员用户编码去重创建通知。 +- 通知说明订单、责任工序步骤、异常原因、具体问题、维修说明、维修单号及提交人。 +- APP 首页铃铛显示未读数量;点击可进入生产通知列表,点击未读通知后标记为已读。 + +## 兼容说明 + +- 责任人员的用户编码找不到会员账号时,不创建通知,也不影响维修记录提交。 +- 通知查询和标记已读均根据当前 Token 对应会员过滤,前端不能指定其他人的通知。 + +## 数据库迁移 + +执行插件迁移 `2026_09_04_000001_create_produce_member_notification_table.php` 后生效。 diff --git a/docs/2026-09-04/2-修正生产通知加载组件.md b/docs/2026-09-04/2-修正生产通知加载组件.md new file mode 100644 index 0000000..9ed639c --- /dev/null +++ b/docs/2026-09-04/2-修正生产通知加载组件.md @@ -0,0 +1,9 @@ +# 修正生产通知加载组件 + +## 问题 + +生产通知页引用了项目未安装的 `u-loading-icon`,Vite 无法解析其组件路径,导致页面编译失败。 + +## 修正 + +改为项目实际存在的 uView Pro `u-loading` 组件,并保留原有圆形、蓝色、32rpx 的加载效果。 diff --git a/docs/2026-09-06/1-移除首页扫码原型入口.md b/docs/2026-09-06/1-移除首页扫码原型入口.md new file mode 100644 index 0000000..3bfce5c --- /dev/null +++ b/docs/2026-09-06/1-移除首页扫码原型入口.md @@ -0,0 +1,3 @@ +# 移除首页扫码原型入口 + +保留 `frontend/app/pages/scan/prototype.vue` 页面文件及其路由,仅从 APP 首页工作区菜单中移除“UTS 扫码原型”入口。 diff --git a/docs/2026-09-06/10-修复整机模式提交反馈.md b/docs/2026-09-06/10-修复整机模式提交反馈.md new file mode 100644 index 0000000..a5516fa --- /dev/null +++ b/docs/2026-09-06/10-修复整机模式提交反馈.md @@ -0,0 +1,18 @@ +# 修复整机模式提交反馈 + +## 变更内容 + +- APP 整机录入从登录资料读取用户编码,支持 `username` 和 `user_code`,不再回退到不存在的模拟编码。 +- APP 整机录入提交前增加登录信息有效性校验,并将接口失败原因通过提示展示给用户。 +- APP 与 Web 整机录入增加提交中的重复点击保护。 +- APP 提交整机记录时对用户编码进行统一去空格处理。 +- 修正“禁止重复”特定规则的判断,仅在已有相同整机记录时拦截,避免首次录入被错误阻止。 + +## 根因确认 + +后台启用“整机扫码”禁止重复规则时,重复提交会按规则拦截并提示规则名称;该行为属于配置生效,并非接口异常。 + +## 验证说明 + +- 已通过本地接口使用有效工序、步骤和用户编码验证 `produce/product-record/record` 可正常写入。 +- 前端修改已完成静态代码核对,建议在 Web 和 Android 真机分别提交一条测试记录,确认成功提示及最近记录刷新。 diff --git a/docs/2026-09-06/11-同步生产录入模板源.md b/docs/2026-09-06/11-同步生产录入模板源.md new file mode 100644 index 0000000..a472c7c --- /dev/null +++ b/docs/2026-09-06/11-同步生产录入模板源.md @@ -0,0 +1,14 @@ +# 同步生产录入模板源 + +## 变更内容 + +- 将 Web 端整机录入组件的最新实现同步至生产插件模板源。 +- 模板源与 Web 运行目录的整机录入组件、数据接口文件保持一致。 +- 同步内容包含重复扫码规则判断、提交保护及提交失败提示。 + +## 涉及文件 + +- `backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue` +- `backend/plugin/produce/resource/template/web/api/data-entry.ts` +- `frontend/web/app/apps/produce/pages/data-entry/components/MachineEntryPanel.vue` +- `frontend/web/app/apps/produce/api/data-entry.ts` diff --git a/docs/2026-09-06/12-优化整机重复扫码提示.md b/docs/2026-09-06/12-优化整机重复扫码提示.md new file mode 100644 index 0000000..03ebff0 --- /dev/null +++ b/docs/2026-09-06/12-优化整机重复扫码提示.md @@ -0,0 +1,13 @@ +# 优化整机重复扫码提示 + +## 变更内容 + +- 整机录入命中禁止重复扫码规则时,不再只显示后台规则名称。 +- 根据规则匹配字段展示完整提示,例如“加工单号已录入,当前规则不允许重复扫码”。 +- Web 插件模板源已先更新,并同步至 Web 运行目录;APP 使用相同提示语义。 + +## 涉及文件 + +- `backend/plugin/produce/resource/template/web/pages/data-entry/components/MachineEntryPanel.vue` +- `frontend/web/app/apps/produce/pages/data-entry/components/MachineEntryPanel.vue` +- `frontend/app/pages/entry/machine.vue` diff --git a/docs/2026-09-06/13-首页不良责任订单按月展示.md b/docs/2026-09-06/13-首页不良责任订单按月展示.md new file mode 100644 index 0000000..ecaaa5e --- /dev/null +++ b/docs/2026-09-06/13-首页不良责任订单按月展示.md @@ -0,0 +1,18 @@ +# 首页不良责任订单按月展示 + +## 变更内容 + +- 首页“不良责任订单”改为统计当前登录用户本月承担作业不良责任的维修记录。 +- 区域显示本月总次数,并按维修时间倒序展示最近 5 条数据。 +- 首页其余“今日数据”、扫码趋势仍保持当天统计口径。 +- 暗色主题下,本月统计说明文字同步使用暗色主题辅助文字颜色。 +- 最近记录仅展示订单号和不良原因,不再展示维修单号。 +- “订单扫码”工序步骤明细同步合并当前用户当天维修记录的工序步骤。 +- 维修记录按记录条数统计,兼容维修表不存在数量字段的实际结构。 +- 工序步骤名称优先读取维修记录保存的快照,与查询页面历史数据展示口径一致。 +- 修正统计查询仅在维修表读取快照字段,避免扫码表因不存在快照列导致接口 500。 + +## 涉及文件 + +- `backend/plugin/produce/app/api/controller/DataEntryController.php` +- `frontend/app/pages/index/index.vue` diff --git a/docs/2026-09-06/14-首页维修工序统计字段兼容.md b/docs/2026-09-06/14-首页维修工序统计字段兼容.md new file mode 100644 index 0000000..c69331d --- /dev/null +++ b/docs/2026-09-06/14-首页维修工序统计字段兼容.md @@ -0,0 +1,5 @@ +# 首页维修工序统计字段兼容 + +修复首页生产总览接口的工序步骤统计查询:仅维修记录读取工序和步骤快照字段,扫码记录与整机记录继续使用各自已有的工序、步骤 ID 映射名称,避免因数据表字段差异导致接口报错。 + +统计页接口同时返回日期趋势和工序步骤汇总,供 APP 统计页展示真实时间区间数据。 diff --git a/docs/2026-09-06/15-细化APP个人统计页.md b/docs/2026-09-06/15-细化APP个人统计页.md new file mode 100644 index 0000000..669bd87 --- /dev/null +++ b/docs/2026-09-06/15-细化APP个人统计页.md @@ -0,0 +1,3 @@ +# 细化 APP 个人统计页 + +个人统计页改用真实的日期区间统计:核心指标区分今日扫码与本月累计,成功率和异常数按本月数据计算;新增本月工序/步骤 Top 10,最近明细读取本月当前用户记录,维修记录也纳入工序步骤统计。统计接口补充日期筛选、每日趋势和工序步骤汇总,时间范围会正确绕过“仅显示今日”配置;异常统计改为成功率、失败率和待处理比例。 diff --git a/docs/2026-09-06/16-移除统计页占比区域.md b/docs/2026-09-06/16-移除统计页占比区域.md new file mode 100644 index 0000000..fbe9f75 --- /dev/null +++ b/docs/2026-09-06/16-移除统计页占比区域.md @@ -0,0 +1,3 @@ +# 移除统计页占比区域 + +根据页面需求,移除 APP 统计页中的“扫码类型占比”和“异常统计”区域,同时清理对应状态、计算逻辑和样式,保留核心指标、工序步骤统计、趋势和最近明细。 diff --git a/docs/2026-09-06/17-统计页增加月度日历.md b/docs/2026-09-06/17-统计页增加月度日历.md new file mode 100644 index 0000000..e7596aa --- /dev/null +++ b/docs/2026-09-06/17-统计页增加月度日历.md @@ -0,0 +1,3 @@ +# 统计页增加月度日历 + +APP 统计页新增月度日历区域,按月份展示日期网格和数据标记,支持前后月份切换;点击当月日期后读取该日订单扫码与整机记录统计,并展示当天订单扫码、整机条码和整机数量明细,同时兼容暗色主题。 diff --git a/docs/2026-09-06/18-调整统计页日历位置.md b/docs/2026-09-06/18-调整统计页日历位置.md new file mode 100644 index 0000000..f3748d2 --- /dev/null +++ b/docs/2026-09-06/18-调整统计页日历位置.md @@ -0,0 +1,5 @@ +# 调整统计页日历位置 + +将 APP 统计页的月度日历区域移动到“个人趋势”区域之后,保留月度切换、日期数据标记和当天明细交互。 + +当天明细进一步展示订单、整机和维修记录合并后的工序/步骤数量;汇总指标调整为订单扫码、整机数量和总计。 diff --git a/docs/2026-09-06/19-调整当天明细指标名称.md b/docs/2026-09-06/19-调整当天明细指标名称.md new file mode 100644 index 0000000..e02962a --- /dev/null +++ b/docs/2026-09-06/19-调整当天明细指标名称.md @@ -0,0 +1,5 @@ +# 调整当天明细指标名称 + +将统计页日历的当天明细指标调整为“订单扫码”“整机数量”“总计”,总计值包含当天订单扫码、整机记录和维修记录数量。 + +核心指标区采用稳定的双列卡片布局,限制内容收缩并保持指标完整显示。 diff --git a/docs/2026-09-06/2-完善首页今日数据.md b/docs/2026-09-06/2-完善首页今日数据.md new file mode 100644 index 0000000..81ee1df --- /dev/null +++ b/docs/2026-09-06/2-完善首页今日数据.md @@ -0,0 +1,9 @@ +# 完善首页今日数据 + +## 变更内容 + +- 新增生产插件首页今日总览接口,按当天订单扫码、整机记录、异常提报和维修完成数据统计。 +- 新增近 6 个小时扫码趋势,柱状图按实际数量自动计算比例。 +- 异常处理区域改为显示已处理、处理中、待处理的实际数量及占比。 +- APP 首页今日数据改为登录后加载真实数据,并展示更新时间;未登录时提示登录后查看。 +- 汇总卡片调整为 2 列布局,适配手机屏幕并区分状态颜色。 diff --git a/docs/2026-09-06/20-完善统计页核心指标项.md b/docs/2026-09-06/20-完善统计页核心指标项.md new file mode 100644 index 0000000..13bdec3 --- /dev/null +++ b/docs/2026-09-06/20-完善统计页核心指标项.md @@ -0,0 +1,3 @@ +# 完善统计页核心指标项 + +统计页核心指标显示订单数、整机数、本月总计、异常提报、维修量和责任订单,按三列恢复简洁卡片布局;分别读取当前登录用户对应月份的真实统计数据,本月总计为订单扫码数加整机数;异常提报接口增加用户和日期区间筛选。 diff --git a/docs/2026-09-06/22-移除成功失败核心指标.md b/docs/2026-09-06/22-移除成功失败核心指标.md new file mode 100644 index 0000000..3ef45c6 --- /dev/null +++ b/docs/2026-09-06/22-移除成功失败核心指标.md @@ -0,0 +1,3 @@ +# 移除成功失败核心指标 + +根据页面需求,从 APP 统计页核心指标中移除“成功量”和“失败量”,并清理对应的前端计算逻辑,保留六项业务统计指标。 diff --git a/docs/2026-09-06/23-核心指标三列布局.md b/docs/2026-09-06/23-核心指标三列布局.md new file mode 100644 index 0000000..b0add31 --- /dev/null +++ b/docs/2026-09-06/23-核心指标三列布局.md @@ -0,0 +1,3 @@ +# 核心指标三列布局 + +按照页面要求,核心指标调整为订单数、整机数、本月总计、异常提报、维修量和责任订单六项,使用三列网格并恢复简洁卡片样式。 diff --git a/docs/2026-09-06/24-修复首页维修量用户匹配.md b/docs/2026-09-06/24-修复首页维修量用户匹配.md new file mode 100644 index 0000000..6e8985a --- /dev/null +++ b/docs/2026-09-06/24-修复首页维修量用户匹配.md @@ -0,0 +1,3 @@ +# 修复首页维修量用户匹配 + +首页维修量统计同时匹配维修记录的操作人姓名和关联异常记录的用户编码,修复用户编码录入场景下查询页存在维修记录但首页显示为零的问题。 diff --git a/docs/2026-09-06/25-修复整机统计用户过滤.md b/docs/2026-09-06/25-修复整机统计用户过滤.md new file mode 100644 index 0000000..0126b9a --- /dev/null +++ b/docs/2026-09-06/25-修复整机统计用户过滤.md @@ -0,0 +1,3 @@ +# 修复整机统计用户过滤 + +修复整机记录统计和最近明细未应用 `user_code` 条件的问题。统计页传入当前登录用户编码后,只统计该用户的整机记录,避免将其他用户的整机数量显示到当前账号。 diff --git a/docs/2026-09-06/3-首页今日数据按工序步骤与提报人统计.md b/docs/2026-09-06/3-首页今日数据按工序步骤与提报人统计.md new file mode 100644 index 0000000..81016e2 --- /dev/null +++ b/docs/2026-09-06/3-首页今日数据按工序步骤与提报人统计.md @@ -0,0 +1,15 @@ +# 首页今日数据按工序步骤与提报人统计 + +## 变更内容 + +- 订单扫码和整机记录增加工序/步骤明细,显示每个工序步骤的实际数量及总量。 +- 异常提报增加按提报人分组的数量统计,同时保留处理状态统计。 +- 首页明细支持暂无数据状态,并兼容深色主题。 + +## 接口数据 + +`/appapi/produce/dashboard/today` 和 `/api/produce/dashboard/today` 新增: + +- `order_breakdown` +- `product_breakdown` +- `exception_reporters` diff --git a/docs/2026-09-06/5-首页今日数据合并当前用户统计.md b/docs/2026-09-06/5-首页今日数据合并当前用户统计.md new file mode 100644 index 0000000..75d84a9 --- /dev/null +++ b/docs/2026-09-06/5-首页今日数据合并当前用户统计.md @@ -0,0 +1,8 @@ +# 首页今日数据合并当前用户统计 + +## 变更内容 + +- 合并汇总与工序步骤明细,订单扫码、整机记录直接在汇总卡片中展示工序/步骤数量。 +- 移除重复的独立工序步骤卡片区域。 +- 异常提报统计按当前登录会员的 `username` 对应 `user_code` 过滤。 +- 新增“不良责任订单”列表,从作业不良维修记录的责任人员快照中匹配当前登录会员。 diff --git a/docs/2026-09-06/6-首页今日数据卡片样式重设计.md b/docs/2026-09-06/6-首页今日数据卡片样式重设计.md new file mode 100644 index 0000000..aae194d --- /dev/null +++ b/docs/2026-09-06/6-首页今日数据卡片样式重设计.md @@ -0,0 +1,8 @@ +# 首页今日数据卡片样式重设计 + +## 变更内容 + +- 统一今日数据卡片的内边距、圆角和高度层级。 +- 订单扫码、整机记录的工序步骤明细改为卡片内独立行,避免文字贴边或撑破布局。 +- 普通统计卡片保持数字与标签居中,工序明细卡片使用更清晰的横向标题布局。 +- 增加明细行背景、间距和长文本省略处理,兼容现有深色主题。 diff --git a/docs/2026-09-06/7-首页不良责任订单展示.md b/docs/2026-09-06/7-首页不良责任订单展示.md new file mode 100644 index 0000000..33979ff --- /dev/null +++ b/docs/2026-09-06/7-首页不良责任订单展示.md @@ -0,0 +1,3 @@ +# 首页不良责任订单展示 + +将首页原“我的异常提报”区域改为“不良责任订单”,数据来源调整为作业不良维修记录中的责任人员快照,仅展示当前登录用户承担责任的当日订单,并按订单汇总责任次数。 diff --git a/docs/2026-09-06/8-首页删除处理状态显示.md b/docs/2026-09-06/8-首页删除处理状态显示.md new file mode 100644 index 0000000..789fdaf --- /dev/null +++ b/docs/2026-09-06/8-首页删除处理状态显示.md @@ -0,0 +1,3 @@ +# 首页删除处理状态显示 + +移除首页“不良责任订单”区域下方的处理状态进度展示,仅保留当前登录用户承担责任的订单及责任次数。 diff --git a/docs/2026-09-06/9-首页今日数据仅显示当前用户.md b/docs/2026-09-06/9-首页今日数据仅显示当前用户.md new file mode 100644 index 0000000..0ce95d6 --- /dev/null +++ b/docs/2026-09-06/9-首页今日数据仅显示当前用户.md @@ -0,0 +1,7 @@ +# 首页今日数据仅显示当前用户 + +今日总览接口统一按当前登录会员过滤: + +- 订单扫码、整机记录和异常提报按 `user_code` 匹配当前会员账号。 +- 维修完成按维修记录保存的操作人昵称或账号匹配当前会员。 +- 工序/步骤明细、小时趋势和不良责任订单均基于上述过滤后的数据计算。 diff --git a/frontend/app/api/produce/data-entry.js b/frontend/app/api/produce/data-entry.js index d3f98dd..601098a 100644 --- a/frontend/app/api/produce/data-entry.js +++ b/frontend/app/api/produce/data-entry.js @@ -62,6 +62,18 @@ export function getProductStatistics(params = {}) { return request.get('/produce/product-record/statistics', params) } +export function getExceptionStatistics(params = {}) { + return request.get('/produce/exception/statistics', params) +} + +export function getRepairStatistics(params = {}) { + return request.get('/produce/repair-record/statistics', params) +} + +export function getTodayDashboard() { + return request.get('/produce/dashboard/today') +} + export function submitProductRecord(data) { return request.post('/produce/product-record/record', data) } diff --git a/frontend/app/api/produce/notification.js b/frontend/app/api/produce/notification.js new file mode 100644 index 0000000..7349ae1 --- /dev/null +++ b/frontend/app/api/produce/notification.js @@ -0,0 +1,13 @@ +import request from '@/utils/request.js' + +export function getMemberNotifications(params = {}) { + return request.get('/produce/member-notification', params) +} + +export function getUnreadNotificationCount() { + return request.get('/produce/member-notification/unread-count') +} + +export function markNotificationRead(id) { + return request.put('/produce/member-notification/read', { id }) +} diff --git a/frontend/app/manifest.json b/frontend/app/manifest.json index bc48381..603390f 100644 --- a/frontend/app/manifest.json +++ b/frontend/app/manifest.json @@ -2,8 +2,8 @@ "name" : "scanapp", "appid" : "__UNI__04C6DBA", "description" : "", - "versionName" : "1.1.0", - "versionCode" : 110, + "versionName" : "1.3.0", + "versionCode" : 130, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -46,10 +46,14 @@ }, /* ios打包配置 */ "ios" : { - "dSYMs" : false + "dSYMs" : false, + "idfa" : false }, /* SDK配置 */ - "sdkConfigs" : {} + "sdkConfigs" : {}, + "splashscreen" : { + "androidStyle" : "common" + } } }, /* 快应用特有相关 */ diff --git a/frontend/app/pages.json b/frontend/app/pages.json index d6ab42d..16c992c 100644 --- a/frontend/app/pages.json +++ b/frontend/app/pages.json @@ -78,6 +78,12 @@ "navigationBarTitleText": "待维修订单" } }, + { + "path": "pages/notification/index", + "style": { + "navigationBarTitleText": "生产通知" + } + }, { "path": "pages/settings/index", "style": { diff --git a/frontend/app/pages/entry/machine.vue b/frontend/app/pages/entry/machine.vue index d671eee..f0b61f4 100644 --- a/frontend/app/pages/entry/machine.vue +++ b/frontend/app/pages/entry/machine.vue @@ -183,7 +183,7 @@ export default { methods: { getCurrentUserCode() { const user = uni.getStorageSync('scanapp_user') || {} - return user.username || user.nickname || 'U001' + return String(user.username || user.user_code || '').trim() }, loadPdaMode() { const userCode = this.getCurrentUserCode() @@ -307,6 +307,31 @@ export default { return false } }, + hasExistingRecordForRule(rule) { + const matchField = rule.match_field || 'process_order_no' + const valueMap = { + process_order_no: this.form.processOrderNo, + order_no: this.form.processOrderNo, + product_code: this.form.productCode, + user_code: this.currentUser.userCode + } + const currentValue = String(valueMap[matchField] || '').trim() + if (!currentValue) return false + return this.recentRecords.some((record) => { + const recordValue = String(record[matchField] || (matchField === 'order_no' ? record.process_order_no : '')).trim() + return recordValue === currentValue + }) + }, + getDuplicateRuleMessage(rule) { + const fieldLabels = { + order_no: '加工单号', + process_order_no: '加工单号', + product_code: '商品编码', + user_code: '用户编码' + } + const field = rule.match_field || 'process_order_no' + return `${fieldLabels[field] || '当前数据'}已录入,当前规则不允许重复扫码` + }, validateSpecificRules() { const context = { order_no: this.form.processOrderNo.trim(), @@ -316,10 +341,10 @@ export default { } const blockedRule = this.specificRules.find((rule) => { if (rule.apply_mode && rule.apply_mode !== 'machine' && rule.apply_mode !== 'both') return false - return Number(rule.rule_type) === 2 && this.matchSpecificRule(rule, context) + return Number(rule.rule_type) === 2 && this.matchSpecificRule(rule, context) && this.hasExistingRecordForRule(rule) }) if (blockedRule) { - uni.showToast({ title: blockedRule.rule_name || '当前整机数据命中特定规则,禁止提交', icon: 'none' }) + uni.showToast({ title: this.getDuplicateRuleMessage(blockedRule), icon: 'none' }) return false } return true @@ -394,6 +419,10 @@ export default { this.form.quantity = Math.max(1, Number(this.form.quantity || 1) - 1) }, validateForm() { + if (!this.currentUser.userCode) { + uni.showToast({ title: '登录信息已失效,请重新登录', icon: 'none' }) + return false + } if (!this.form.processOrderNo.trim()) { uni.showToast({ title: '请输入加工单号', icon: 'none' }) return false @@ -430,26 +459,32 @@ export default { return true }, async submitEntry() { + if (this.submitting) return await this.loadDataEntryConfig() if (!this.validateForm()) return this.submitting = true try { const processItem = this.processOptions[this.selectedProcessIndex] const stepItem = this.stepOptions[this.selectedStepIndex] - await submitProductRecord({ + const payload = { process_order_no: this.form.processOrderNo.trim(), product_code: this.form.productCode.trim(), quantity: Number(this.form.quantity) || 1, - user_code: this.currentUser.userCode, + user_code: this.currentUser.userCode.trim(), process_id: processItem?.value, step_id: stepItem?.value, scan_result: 1 - }) + } + await submitProductRecord(payload) uni.showToast({ title: '整机录入成功', icon: 'success' }) this.resetForm() await this.loadPageData() } catch (error) { console.error('[scanapp] 整机录入失败', error) + uni.showToast({ + title: error?.message || '整机录入失败', + icon: 'none' + }) } finally { this.submitting = false } diff --git a/frontend/app/pages/index/index.vue b/frontend/app/pages/index/index.vue index acd93d7..6fc0944 100644 --- a/frontend/app/pages/index/index.vue +++ b/frontend/app/pages/index/index.vue @@ -5,8 +5,9 @@ 扫码工作台 高效完成生产扫码与库存管理 - + + {{ unreadNotificationText }} @@ -42,23 +43,32 @@ 今日数据 - 扫码与异常概览 + 扫码、异常与维修实时概览 - 实时 + {{ dashboardUpdatedAt }} - - {{ item.value }} - {{ item.label }} + + + {{ item.value }} + {{ item.label }} + + + + {{ detail.name }} + {{ detail.quantity }} + + 扫码趋势 - - + + {{ item.value }} + {{ item.label }} @@ -67,16 +77,22 @@ - 异常处理 - - - {{ item.label }} - {{ item.value }}% - - - + + + 不良责任订单 + 本月总计 {{ responsibilityOrderTotal }} 次 · 最近 5 条 + + + + 订单:{{ item.order_no }} + 不良原因:{{ item.exception_reason || '未记录不良原因' }} + + {{ item.quantity }} 次 + + + 本月暂无不良责任订单 @@ -84,6 +100,8 @@ + + diff --git a/frontend/app/pages/statistics/index.vue b/frontend/app/pages/statistics/index.vue index c4d18d3..d999719 100644 --- a/frontend/app/pages/statistics/index.vue +++ b/frontend/app/pages/statistics/index.vue @@ -27,7 +27,7 @@ 核心指标 - 近 30 天累计 + 本月累计 @@ -37,6 +37,20 @@ + + + 工序步骤统计 + 本月 Top 10 + + + + {{ item.name }} + {{ item.quantity }} + + + 本月暂无工序步骤记录 + + 个人趋势 @@ -52,6 +66,32 @@ + + + 月度总表({{ calendarMonthLabel }}) + ‹› + + 日历仅显示是否有数据,点击日期查看当天统计 + {{ day }} + + + {{ cell.day }} + + + + 当天明细{{ selectedDate }} + + 订单扫码{{ selectedDaySummary.order }} + 整机数量{{ selectedDaySummary.product }} + 总计{{ selectedDaySummary.total }} + + + {{ step.name }}{{ step.quantity }} + + 当天暂无工序数据 + + + 最近明细 @@ -72,43 +112,12 @@ - - - - 扫码类型占比 - - - - {{ item.label }} - {{ item.value }}% - - - - - - - - - - 异常统计 - - - - {{ item.label }} - {{ item.value }}% - - - - - - -