diff --git a/AGENTS.md b/AGENTS.md index 94bcb0e..dea72ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,8 +62,3 @@ Auto-evolution workflow: - 插件相关前端内容必须先修改并核对 `backend/plugin/{plugin}/resource/template/admin` 下的模板源文件;模板源修改完成后,必须同步修改对应的 `frontend/admin/src/apps/{plugin}` 运行目录,并核对两者实现一致 - 避免将未更改的内容总结为"文件其余部分保持不变" -# 本地项目与版本控制 - -- 本项目为本地项目,不使用 Git 进行状态检查、差异比较、提交或任何其他操作。 -- 后续回答、验证命令和开发说明中不出现 Git 或 Git 命令。 -- 如需确认文件改动,以实际文件内容、语法检查、自动化测试或运行时验证为准。 diff --git a/backend/app/service/api/auth/AuthService.php b/backend/app/service/api/auth/AuthService.php index 33061f7..05a5111 100644 --- a/backend/app/service/api/auth/AuthService.php +++ b/backend/app/service/api/auth/AuthService.php @@ -59,6 +59,8 @@ class AuthService extends BaseService // 更新登录信息 $this->updateLoginInfo($member); + $channel = strtolower((string)request()->header('channel', 'pc')); + $clientType = $channel === 'app' ? 'app' : 'web'; $userInfo = [ 'id' => $member->id, 'username' => $member->username, @@ -66,13 +68,14 @@ class AuthService extends BaseService 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, - 'client' => 'web', - 'access_exp' => 7200, + 'client' => $clientType, + 'access_exp' => 3600, 'refresh_exp' => 604800, ]; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); - $tokenObj = $jwt->generate((string)$member->id, 'api', $userInfo); + $tokenObj = $jwt->generate((string)$member->id, $clientType, $userInfo); + $userInfo['access_exp'] = $tokenObj->expiresIn; $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, @@ -110,6 +113,8 @@ class AuthService extends BaseService $this->updateLoginInfo($member); // 生成token + $channel = strtolower((string)request()->header('channel', 'pc')); + $clientType = $channel === 'app' ? 'app' : 'web'; $userInfo = [ 'id' => $member->id, 'username' => $member->username, @@ -117,13 +122,14 @@ class AuthService extends BaseService 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, - 'client' => 'web', - 'access_exp' => 7200, + 'client' => $clientType, + 'access_exp' => 3600, 'refresh_exp' => 604800, ]; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); - $tokenObj = $jwt->generate((string)$member->id, 'api', $userInfo); + $tokenObj = $jwt->generate((string)$member->id, $clientType, $userInfo); + $userInfo['access_exp'] = $tokenObj->expiresIn; $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, @@ -702,4 +708,4 @@ HTML; throw new \Exception('登录已过期', 401); } } -} \ No newline at end of file +} diff --git a/backend/core/jwt/JwtToken.php b/backend/core/jwt/JwtToken.php index 4aee979..a42d123 100644 --- a/backend/core/jwt/JwtToken.php +++ b/backend/core/jwt/JwtToken.php @@ -21,6 +21,7 @@ use Firebase\JWT\ExpiredException; use Firebase\JWT\JWT; use Firebase\JWT\Key; use Firebase\JWT\SignatureInvalidException; +use app\model\system\Config; use UnexpectedValueException; class JwtToken @@ -126,7 +127,7 @@ class JwtToken $jti = $this->generateJti(); // 生成 Access Token - $accessTtl = $this->config['ttl']['access'] ?? 7200; + $accessTtl = $this->getConfiguredTtl('access', $clientType); $accessPayload = $this->buildPayload( id: $id, client: $clientType, @@ -137,7 +138,7 @@ class JwtToken $accessToken = $this->makeToken($accessPayload); // 生成 Refresh Token - $refreshTtl = $this->config['ttl']['refresh'] ?? 604800; + $refreshTtl = $this->getConfiguredTtl('refresh', $clientType); $refreshJti = $this->generateJti(); $refreshPayload = $this->buildPayload( id: $id, @@ -156,7 +157,7 @@ class JwtToken 'extra' => $extraData, 'login_mode' => $loginMode, 'created_at' => time(), - ], $accessTtl); + ], max($accessTtl, $refreshTtl)); // 创建 Token 对象 $fullPayload = array_merge($accessPayload, ['id' => $id]); @@ -217,7 +218,7 @@ class JwtToken $loginMode = $this->config['login_mode'] ?? $oldTokenInfo['login_mode'] ?? LoginMode::MULTI->value; // 将旧 access_token 和 refresh_token 加入黑名单 - $blacklistTtl = $this->config['ttl']['refresh'] ?? 604800; + $blacklistTtl = $this->getConfiguredTtl('refresh', $clientType); if ($oldAccessJti !== '') { $this->blacklistStorage->add($oldAccessJti, $blacklistTtl); $this->tokenStorage->delete($oldAccessJti); @@ -288,7 +289,7 @@ class JwtToken return null; } - $accessTtl = $this->config['ttl']['access'] ?? 7200; + $accessTtl = $this->getConfiguredTtl('access', $payload['client'] ?? ''); $this->currentToken = Token::create($accessToken, '', $accessTtl, $payload); return $this->currentToken; @@ -739,6 +740,33 @@ class JwtToken /** * 构建 Token 载荷 */ + protected function getConfiguredTtl(string $type, string $clientType): int + { + $defaults = $this->config['ttl'] ?? ['access' => 3600, 'refresh' => 604800]; + $default = (int)($defaults[$type] ?? ($type === 'access' ? 3600 : 604800)); + $content = Config::query()->where('code', 'auth_token_setting')->where('enabled', 1)->value('content'); + if (is_string($content)) { + $decoded = json_decode($content, true); + $content = json_last_error() === JSON_ERROR_NONE ? $decoded : null; + } + if (!is_array($content)) { + return $default; + } + + $client = strtolower($clientType); + $key = match ($client) { + 'admin' => $type === 'refresh' ? 'admin_refresh_ttl' : 'admin_access_ttl', + 'app', 'mobile' => $type === 'refresh' ? 'app_refresh_ttl' : 'app_access_ttl', + default => $type === 'refresh' ? 'web_refresh_ttl' : 'web_access_ttl', + }; + if ($type === 'refresh' && !array_key_exists($key, $content)) { + $key = 'refresh_ttl'; + } + $value = (int)($content[$key] ?? $default); + $minimum = $type === 'refresh' ? 300 : 60; + return $value >= $minimum ? $value : $default; + } + protected function buildPayload( string $id, string $client, @@ -749,8 +777,8 @@ class JwtToken { $now = time(); $ttl = match ($type) { - TokenType::ACCESS => $this->config['ttl']['access'] ?? 7200, - TokenType::REFRESH => $this->config['ttl']['refresh'] ?? 604800, + TokenType::ACCESS => $this->getConfiguredTtl('access', $client), + TokenType::REFRESH => $this->getConfiguredTtl('refresh', $client), }; $payload = [ diff --git a/backend/core/jwt/enum/ClientType.php b/backend/core/jwt/enum/ClientType.php index 7534435..4800fc9 100644 --- a/backend/core/jwt/enum/ClientType.php +++ b/backend/core/jwt/enum/ClientType.php @@ -12,6 +12,7 @@ enum ClientType: string case API = 'api'; case WEB = 'web'; case MOBILE = 'mobile'; + case APP = 'app'; case MINI = 'mini'; /** diff --git a/backend/plugin/produce/app/api/controller/DataEntryController.php b/backend/plugin/produce/app/api/controller/DataEntryController.php index 813a8bd..23af85b 100644 --- a/backend/plugin/produce/app/api/controller/DataEntryController.php +++ b/backend/plugin/produce/app/api/controller/DataEntryController.php @@ -679,13 +679,14 @@ final class DataEntryController extends Base #[OA\Get(path: '/produce/query/records', summary: '查询生产记录', tags: ['produce-web-query'])] #[DataResponse(description: '查询成功')] - #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] public function queryRecords(Request $request): Response { $recordType = trim((string) $request->input('record_type', 'scan')); $orderNo = trim((string) $request->input('order_no', '')); $keyword = trim((string) $request->input('keyword', '')); $userCode = trim((string) $request->input('user_code', '')); + $processId = (int) $request->input('process_id', 0); + $stepId = (int) $request->input('step_id', 0); $status = $request->input('status', ''); $scanResult = $request->input('scan_result', ''); $page = max(1, (int) $request->input('page', 1)); @@ -707,6 +708,8 @@ final class DataEntryController extends Base if ($recordType === 'scan') { $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($processId > 0, fn ($builder) => $builder->where('process_id', $processId)); + $query->when($stepId > 0, fn ($builder) => $builder->where('step_id', $stepId)); $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status_id', (int) $status)); $query->when($scanResult !== '' && $scanResult !== null, fn ($builder) => $builder->where('scan_result', (int) $scanResult)); $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { @@ -721,6 +724,8 @@ final class DataEntryController extends Base } elseif ($recordType === 'product') { $query->when($orderNo !== '', fn ($builder) => $builder->where('process_order_no', $orderNo)); $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($processId > 0, fn ($builder) => $builder->where('process_id', $processId)); + $query->when($stepId > 0, fn ($builder) => $builder->where('step_id', $stepId)); $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status_id', (int) $status)); $query->when($scanResult !== '' && $scanResult !== null, fn ($builder) => $builder->where('scan_result', (int) $scanResult)); $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { @@ -736,6 +741,8 @@ final class DataEntryController extends Base } elseif ($recordType === 'exception') { $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); $query->when($userCode !== '', fn ($builder) => $builder->where('user_code', $userCode)); + $query->when($processId > 0, fn ($builder) => $builder->where('process_id', $processId)); + $query->when($stepId > 0, fn ($builder) => $builder->where('step_id', $stepId)); $query->when($status !== '' && $status !== null, fn ($builder) => $builder->where('status', (int) $status)); $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { $nested->where('order_no', 'like', "%{$keyword}%") @@ -751,6 +758,8 @@ final class DataEntryController extends Base $query->orderByDesc('reported_at')->orderByDesc('id'); } else { $query->when($orderNo !== '', fn ($builder) => $builder->where('order_no', $orderNo)); + $query->when($processId > 0, fn ($builder) => $builder->where('process_id', $processId)); + $query->when($stepId > 0, fn ($builder) => $builder->where('step_id', $stepId)); $query->when($keyword !== '', fn ($builder) => $builder->where(function ($nested) use ($keyword) { $nested->where('repair_no', 'like', "%{$keyword}%") ->orWhere('order_no', 'like', "%{$keyword}%") @@ -772,7 +781,6 @@ final class DataEntryController extends Base #[OA\Get(path: '/produce/query/timeline', summary: '查询订单生产时间线', tags: ['produce-web-query'])] #[DataResponse(description: '查询成功')] - #[AllowAnonymous(requireToken: false, requirePermission: false, description: '公共接口')] public function queryTimeline(Request $request): Response { $orderNo = trim((string) $request->input('order_no', '')); @@ -936,9 +944,12 @@ final class DataEntryController extends Base return Json::fail('用户编码不存在'); } + $repairFlow = $this->getRepairFlowNodeNames(); + try { - $record = Db::transaction(function () use ($exceptionId, $repairResult, $categoryIds, $parts, $images, $operationFaultSteps, $responsibleOperators, $data, $member, $repairRecordService) { + $record = Db::transaction(function () use ($exceptionId, $repairResult, $categoryIds, $parts, $images, $operationFaultSteps, $responsibleOperators, $data, $member, $repairRecordService, $repairFlow) { $exception = ExceptionRecord::query() + ->with(['process', 'step']) ->lockForUpdate() ->whereKey($exceptionId) ->whereIn('status', [0, 1]) @@ -1062,6 +1073,10 @@ final class DataEntryController extends Base 'exception_reason' => (string) $exception->reason_name, 'exception_issue' => (string) ($exception->issue_name ?? ''), 'order_no' => (string) $exception->order_no, + 'process_id' => $repairFlow['process_id'], + 'step_id' => $repairFlow['step_id'], + 'process_name' => $repairFlow['process_name'], + 'step_name' => $repairFlow['step_name'], 'category_id' => $repairResult === 'assembly_mismatch' ? $categoryIds[0] : $primaryFault?->category_id, 'category_ids' => $repairResult === 'assembly_mismatch' ? $categoryIds : null, 'fault_description_id' => $primaryFault?->id, @@ -1316,8 +1331,8 @@ final class DataEntryController extends Base 'record_type_name' => '维修记录', 'order_no' => (string) $item->order_no, 'product_code' => (string) $item->product_code, - 'process_name' => $this->getRepairFlowNodeNames()['process_name'], - 'step_name' => $this->getRepairFlowNodeNames()['step_name'], + 'process_name' => (string) ($item->process_name ?: '历史数据未记录'), + 'step_name' => (string) ($item->step_name ?: '历史数据未记录'), 'status_name' => '已维修', 'result_name' => (string) $item->repair_result_name, 'operator' => (string) $item->operator, @@ -1349,8 +1364,10 @@ final class DataEntryController extends Base $step = $stepId > 0 ? ProcessStep::query()->withTrashed()->find($stepId) : null; return $this->repairFlowCache = [ - 'process_name' => (string) ($process?->process_name ?? ''), - 'step_name' => (string) ($step?->step_name ?? ''), + 'process_id' => $processId > 0 ? $processId : null, + 'step_id' => $stepId > 0 ? $stepId : null, + 'process_name' => (string) ($process?->process_name ?? '维修'), + 'step_name' => (string) ($step?->step_name ?? '维修'), ]; } diff --git a/backend/plugin/produce/app/appapi/controller/DataEntryController.php b/backend/plugin/produce/app/appapi/controller/DataEntryController.php index 17b6593..70f9c45 100644 --- a/backend/plugin/produce/app/appapi/controller/DataEntryController.php +++ b/backend/plugin/produce/app/appapi/controller/DataEntryController.php @@ -86,7 +86,6 @@ final class DataEntryController #[OA\Get(path: '/produce/query/records', summary: 'APP 查询生产记录', tags: ['produce-app'])] #[DataResponse(description: '查询成功')] - #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] public function queryRecords(Request $request): Response { return $this->delegate->queryRecords($request); @@ -94,7 +93,6 @@ final class DataEntryController #[OA\Get(path: '/produce/query/timeline', summary: 'APP 查询订单生产时间线', tags: ['produce-app'])] #[DataResponse(description: '查询成功')] - #[AllowAnonymous(requireToken: false, requirePermission: false, description: 'APP 公共接口')] public function queryTimeline(Request $request): Response { return $this->delegate->queryTimeline($request); @@ -224,4 +222,3 @@ final class DataEntryController return $this->delegate->productRecord($request); } } - diff --git a/backend/plugin/produce/app/model/RepairRecord.php b/backend/plugin/produce/app/model/RepairRecord.php index a57aa12..b63e3e8 100644 --- a/backend/plugin/produce/app/model/RepairRecord.php +++ b/backend/plugin/produce/app/model/RepairRecord.php @@ -21,7 +21,7 @@ class RepairRecord extends BaseModel protected $with = ['exception']; protected $fillable = [ - 'id', 'repair_no', 'exception_id', 'exception_reason', 'exception_issue', 'order_no', 'product_code', 'product_name', 'category_id', 'category_ids', 'category_name', 'category_names', + 'id', 'repair_no', 'exception_id', 'exception_reason', 'exception_issue', 'order_no', 'product_code', 'product_name', 'process_id', 'step_id', 'process_name', 'step_name', 'category_id', 'category_ids', 'category_name', 'category_names', 'fault_description_id', 'fault_description', 'repair_result', 'parts', 'images', 'operation_fault_steps', 'responsible_operators', 'operator', 'repaired_at', 'remark', 'created_by', 'updated_by', 'created_at', 'updated_at', 'deleted_at', ]; @@ -33,6 +33,8 @@ class RepairRecord extends BaseModel 'images' => 'array', 'operation_fault_steps' => 'array', 'responsible_operators' => 'array', + 'process_id' => 'integer', + 'step_id' => 'integer', 'repaired_at' => 'integer', 'created_at' => 'integer', 'updated_at' => 'integer', diff --git a/backend/plugin/produce/app/service/admin/WorkStatisticsService.php b/backend/plugin/produce/app/service/admin/WorkStatisticsService.php index 6d72c7b..7d6a64c 100644 --- a/backend/plugin/produce/app/service/admin/WorkStatisticsService.php +++ b/backend/plugin/produce/app/service/admin/WorkStatisticsService.php @@ -91,14 +91,20 @@ class WorkStatisticsService $connection = $model->getConnection(); $table = $model->getTable(); $exceptionTable = $exceptionModel->getTable(); - $flow = $this->getRepairFlow(); $query = $connection->table($table) ->leftJoin($exceptionTable, $exceptionTable . '.id', '=', $table . '.exception_id') ->whereBetween($table . '.repaired_at', [$start, $end]) ->whereNull($table . '.deleted_at'); - if (!$this->matchesRepairFlow($params, $flow)) { - $query->whereRaw('1 = 0'); + foreach (['process_id', 'step_id'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($table . '.' . $field, $params[$field]); + } + } + foreach (['process_name', 'step_name'] as $field) { + if (($params[$field] ?? '') !== '') { + $query->where($table . '.' . $field, 'like', '%' . $params[$field] . '%'); + } } if (($params['user_code'] ?? '') !== '') { $query->where($exceptionTable . '.user_code', 'like', '%' . $params['user_code'] . '%'); @@ -109,8 +115,8 @@ class WorkStatisticsService return $query->select([$exceptionTable . '.user_code', $table . '.operator']) ->selectRaw($periodExpression($connection->getTablePrefix() . $table . '.repaired_at') . ' as period') - ->selectRaw('? as process_name', [$flow['process_name']]) - ->selectRaw('? as step_name', [$flow['step_name']]) + ->selectRaw("COALESCE(NULLIF({$connection->getTablePrefix()}{$table}.process_name, ''), '历史数据未记录') as process_name") + ->selectRaw("COALESCE(NULLIF({$connection->getTablePrefix()}{$table}.step_name, ''), '历史数据未记录') as step_name") ->selectRaw('COUNT(*) as amount') ->groupBy('user_code', 'operator', 'process_name', 'step_name', 'period'); } diff --git a/backend/plugin/produce/resource/database/migrations/2026_09_03_000001_add_process_step_snapshot_to_repair_record.php b/backend/plugin/produce/resource/database/migrations/2026_09_03_000001_add_process_step_snapshot_to_repair_record.php new file mode 100644 index 0000000..e4c2e15 --- /dev/null +++ b/backend/plugin/produce/resource/database/migrations/2026_09_03_000001_add_process_step_snapshot_to_repair_record.php @@ -0,0 +1,98 @@ +hasTable($table)) { + return; + } + + $hasProcessId = $schema->hasColumn($table, 'process_id'); + $hasStepId = $schema->hasColumn($table, 'step_id'); + $hasProcessName = $schema->hasColumn($table, 'process_name'); + $hasStepName = $schema->hasColumn($table, 'step_name'); + + $schema->table($table, function (Blueprint $blueprint) use ($hasProcessId, $hasStepId, $hasProcessName, $hasStepName): void { + if (!$hasProcessId) { + $blueprint->bigInteger('process_id')->nullable()->comment('维修工序ID快照'); + $blueprint->index('process_id', 'idx_repair_record_process_id'); + } + if (!$hasStepId) { + $blueprint->bigInteger('step_id')->nullable()->comment('维修步骤ID快照'); + $blueprint->index('step_id', 'idx_repair_record_step_id'); + } + if (!$hasProcessName) { + $blueprint->string('process_name', 100)->nullable()->comment('维修工序名称快照'); + } + if (!$hasStepName) { + $blueprint->string('step_name', 100)->nullable()->comment('维修步骤名称快照'); + } + }); + + $this->backfillSnapshots($schema); + } + + private function backfillSnapshots(Builder $schema): void + { + $connection = $schema->getConnection(); + if (!$schema->hasTable('produce_exception_record')) { + $connection->table('produce_repair_record')->update([ + 'process_name' => '历史数据未记录', + 'step_name' => '历史数据未记录', + ]); + return; + } + $records = $connection->table('produce_repair_record')->get(['id', 'exception_id']); + foreach ($records as $record) { + $exception = $connection->table('produce_exception_record') + ->where('id', $record->exception_id) + ->first(['process_id', 'step_id']); + $processId = $exception?->process_id; + $stepId = $exception?->step_id; + $processName = $processId + ? (string) ($connection->table('produce_process')->where('id', $processId)->value('process_name') ?? '') + : ''; + $stepName = $stepId + ? (string) ($connection->table('produce_process_step')->where('id', $stepId)->value('step_name') ?? '') + : ''; + + $connection->table('produce_repair_record')->where('id', $record->id)->update([ + 'process_id' => $processId, + 'step_id' => $stepId, + 'process_name' => $processName ?: '历史数据未记录', + 'step_name' => $stepName ?: '历史数据未记录', + ]); + } + } + + public function down(Builder $schema): void + { + $table = 'produce_repair_record'; + if (!$schema->hasTable($table)) { + return; + } + + $schema->table($table, function (Blueprint $blueprint) use ($schema): void { + if ($schema->hasColumn('produce_repair_record', 'process_id')) { + $blueprint->dropIndex('idx_repair_record_process_id'); + $blueprint->dropColumn('process_id'); + } + if ($schema->hasColumn('produce_repair_record', 'step_id')) { + $blueprint->dropIndex('idx_repair_record_step_id'); + $blueprint->dropColumn('step_id'); + } + if ($schema->hasColumn('produce_repair_record', 'process_name')) { + $blueprint->dropColumn('process_name'); + } + if ($schema->hasColumn('produce_repair_record', 'step_name')) { + $blueprint->dropColumn('step_name'); + } + }); + } +}; diff --git a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx index 2ce7601..a4e1b0a 100644 --- a/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx +++ b/backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx @@ -152,6 +152,8 @@ export const crudSchema = (): CrudSchema => ({ { field: "order_product", title: "订单号/商品编码" }, { field: "exception_type_name", title: "异常分类" }, { field: "exception_reason", title: "异常原因" }, + { field: "process_name", title: "维修工序" }, + { field: "step_name", title: "维修步骤" }, { field: "part_category", title: "物料分类" }, { field: "part_code", title: "物料编码" }, { field: "part_name", title: "更换备件" }, @@ -221,6 +223,8 @@ export const crudSchema = (): CrudSchema => ({ }, { prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } }, { prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "process_name", label: "维修工序", minWidth: 140, cellStyle: { whiteSpace: "nowrap" } }, + { prop: "step_name", label: "维修步骤", minWidth: 140, cellStyle: { whiteSpace: "nowrap" } }, { prop: "part_category", label: "物料分类", @@ -380,6 +384,22 @@ export const crudSchema = (): CrudSchema => ({ ifDetail: true, colSpan: 24, }, + { + component: "Input", + prop: "process_name", + label: "维修工序", + componentProps: { readonly: true }, + ifDetail: true, + colSpan: 12, + }, + { + component: "Input", + prop: "step_name", + label: "维修步骤", + componentProps: { readonly: true }, + ifDetail: true, + colSpan: 12, + }, { component: "Input", prop: "fault_descriptions", diff --git a/backend/resource/data/config/config.php b/backend/resource/data/config/config.php index 2ff0719..661de02 100644 --- a/backend/resource/data/config/config.php +++ b/backend/resource/data/config/config.php @@ -229,6 +229,7 @@ return [ 'content' => [ "site_name" => "", "site_url" => "", + "require_login" => 1, "seo_title" => "", "seo_keywords" => "", "seo_description" => "", diff --git a/docs/2026-09-02/1-Web首页原型.md b/docs/2026-09-02/1-Web首页原型.md new file mode 100644 index 0000000..79d2385 --- /dev/null +++ b/docs/2026-09-02/1-Web首页原型.md @@ -0,0 +1,12 @@ +# Web 首页原型 + +## 调整内容 + +- 将首页从通用文档展示改为生产运营中心原型。 +- 新增生产达成率欢迎区、核心指标卡片、快捷工作入口、订单流程进度、现场动态和生产统计入口。 +- 快捷入口连接现有生产录入、异常提报、维修记录、订单查询及月度统计页面。 +- 使用项目已有 Nuxt、Element Plus、Iconify 和广告组件,补充 1440px、1024px、768px 下的响应式布局。 + +## 验证 + +- Web 端 `vue-tsc --noEmit` 已执行;项目存在多处既有类型错误,本次首页额外的广告组件目录导入已改为显式 `index.vue` 路径。 diff --git a/docs/2026-09-02/2-Web端登录门禁.md b/docs/2026-09-02/2-Web端登录门禁.md new file mode 100644 index 0000000..28e06fa --- /dev/null +++ b/docs/2026-09-02/2-Web端登录门禁.md @@ -0,0 +1,11 @@ +# Web 端登录门禁 + +## 调整内容 + +- 新增全局路由中间件,未登录访问任意 Web 页面时自动打开现有登录弹窗。 +- 默认布局在未登录状态下不渲染页面插槽,仅显示“请先登录”提示卡和登录按钮,避免预览业务页面内容。 +- 登录成功后依赖 Pinia 用户状态响应式更新,页面主体自动恢复,无需刷新。 + +## 验证 + +- Web 端 `pnpm build` 构建验证通过。 diff --git a/docs/2026-09-02/3-Web端登录访问开关.md b/docs/2026-09-02/3-Web端登录访问开关.md new file mode 100644 index 0000000..098b68b --- /dev/null +++ b/docs/2026-09-02/3-Web端登录访问开关.md @@ -0,0 +1,12 @@ +# Web 端登录访问开关 + +## 调整内容 + +- 在后台 `/setting/config` 的“前台设置”中新增“访问页面需要登录”配置。 +- 选择“是”时,Web 端未登录用户只能看到登录提示并打开登录弹窗;选择“否”时允许游客正常访问页面。 +- Web 端配置 Store 读取 `web_site_setting.require_login`,默认保持需要登录。 +- 全局认证中间件等待前台配置加载后再执行门禁判断,避免配置关闭时误拦截游客。 + +## 验证 + +- 在 `frontend/web` 目录执行 `pnpm build` 构建验证。 diff --git a/docs/2026-09-02/4-Web端主题色调整为蓝色.md b/docs/2026-09-02/4-Web端主题色调整为蓝色.md new file mode 100644 index 0000000..afa45d7 --- /dev/null +++ b/docs/2026-09-02/4-Web端主题色调整为蓝色.md @@ -0,0 +1,10 @@ +# Web 端主题色调整为蓝色 + +## 调整内容 + +- 将 Element Plus 全局主色从紫色调整为蓝色 `#2563eb`。 +- 页面中已有的蓝色业务配色继续保留,统一使用蓝色主题视觉。 + +## 验证 + +- 在 `frontend/web` 目录执行 `pnpm build` 构建验证。 diff --git a/docs/2026-09-02/5-生产插件功能完善建议.md b/docs/2026-09-02/5-生产插件功能完善建议.md new file mode 100644 index 0000000..de9a29a --- /dev/null +++ b/docs/2026-09-02/5-生产插件功能完善建议.md @@ -0,0 +1,306 @@ +# 生产插件功能完善建议 + +## 1. 文档目的 + +本文档基于当前生产插件的 Web 端、Admin 端、扫码端和后端实现整理,目标是明确后续功能补强方向,避免继续出现“页面已完成但流程数据无法追溯”或“不同端实现不一致”的问题。 + +涉及目录: + +- 后端:`backend/plugin/produce` +- Web 端:`frontend/web/app/apps/produce` +- Admin 端:`frontend/admin/src/apps/produce` +- 扫码端:`frontend/scanapp` + +## 2. 当前能力概览 + +### 2.1 已具备的功能 + +- 生产录入:订单模式、整机模式、一单多机、工序/步骤/状态联动。 +- 异常处理:异常类型、原因、具体问题、图片、异常提报和异常查询。 +- 维修处理:待维修订单、维修结果、责任人员、故障描述、备件更换和维修提交。 +- 工艺配置:工序、工序步骤、步骤状态、步骤关系、状态转换、转换日志。 +- 物料管理:物料分类、物料库、物料搜索、故障描述和备件使用频率排序。 +- 查询统计:订单流程时间线、扫码数据、异常数据、维修数据、月度统计、日统计。 +- 多端入口:Web 端、Admin 管理端和 `scanapp` 扫码端均已有生产相关页面。 + +### 2.2 当前实现特点 + +- 后端 Admin API 采用标准 CRUD,前台和扫码业务集中在 `DataEntryController`。 +- 工序状态机已经支持前置步骤、终态校验、重复扫码策略和自动状态转换。 +- 异常记录已保存工序和步骤;维修记录目前主要通过异常记录关联维修流程。 +- 维修备件和责任人员采用 JSON 快照保存,能够保留提交时的名称信息。 +- 统计服务已经支持异常、维修、扫码、整机数据的合并统计。 + +## 3. 建议优先修复的基础问题(P0) + +### 3.1 维修记录保存工序/步骤快照 + +**现状问题** + +- `produce_repair_record` 当前没有独立的 `process_id`、`step_id`、工序名称和步骤名称字段。 +- 统计服务中的维修记录工序步骤来自当前“维修流程”配置,而不是维修提交当时的实际流程。 +- 后台修改维修流程后,历史维修数据可能被统计到新的工序步骤,历史数据无法准确复盘。 + +**建议方案** + +- 维修提交时保存 `process_id`、`step_id`、`process_name`、`step_name` 四个快照字段。 +- 维修记录列表、详情、导出、订单时间线和月/日统计优先使用记录自身快照。 +- 对历史数据执行一次回填:优先读取关联异常记录的工序步骤,无法匹配时标记为“历史数据未记录”。 +- 后续修改维修流程只影响新提交数据,不改变历史统计口径。 + +**验收标准** + +- 修改后台维修流程后,旧维修记录的工序步骤不发生变化。 +- Web、Admin、扫码端和月/日统计显示同一组工序步骤。 +- 维修记录导出包含工序名称、步骤名称和对应编码。 + +### 3.2 防止扫码、异常和维修重复提交 + +**现状问题** + +- 当前主要依赖业务规则判断重复扫码,网络重试或用户连续点击仍可能产生重复请求。 +- 扫码端更容易遇到弱网、超时和重复点击场景。 + +**建议方案** + +- 每次提交由客户端生成 `client_request_id`,服务端建立唯一约束或幂等记录。 +- 相同请求重复提交时返回第一次提交结果,而不是再次写入数据。 +- 提交按钮增加处理中状态,扫码成功后短时间内锁定同一条码。 +- 对订单号、工序、步骤、用户编码、数量等关键字段增加服务端事务锁和最终校验。 + +**验收标准** + +- 同一请求重试不增加记录数量。 +- 网络超时后再次点击能够得到明确的“已提交”结果。 +- 并发提交不会绕过重复扫码和数量限制。 + +### 3.3 区分提报人、处理人和实际作业人员 + +**现状问题** + +- 当前多个接口同时使用 `user_code`、`operator`、`reporter` 等字段,不同页面对“登录账号”和“实际作业人员”的理解容易混淆。 +- 维修责任人员已经支持手工录入用户编码,但审计信息还不够统一。 + +**建议方案** + +- 统一保存以下信息:实际作业人员编码/姓名、系统提交人 ID/姓名、最后处理人 ID/姓名、提交时间和客户端来源。 +- 用户编码校验统一复用成员查询服务,不允许各端自行按不同字段判断。 +- 所有列表和时间线同时展示“工号 + 姓名”,详情中展示系统操作人。 +- 对手工代录场景增加“代录原因”和“授权人”字段,并写入操作日志。 + +### 3.4 建立异常到维修的完整闭环 + +**现状问题** + +- 当前异常、待维修、维修提交和查询已经存在,但缺少统一的状态流转约束和超时提醒。 +- 异常处理状态、维修结果和订单当前状态可能出现不一致。 + +**建议方案** + +- 明确状态机:待处理 → 已分派 → 维修中 → 待验证 → 已完成 / 已关闭 / 已退回。 +- 异常只能通过维修提交、人工关闭或退回操作改变状态,禁止直接修改造成跳状态。 +- 一条异常允许多次维修尝试,但必须保留每次维修记录和失败原因。 +- 增加维修超时、待验证超时和重复返修提醒。 + +## 4. 工艺和流程能力增强(P1) + +### 4.1 工序流程可视化和发布机制 + +当前已有工序、步骤、步骤关系和状态转换四类配置,但配置结果缺少可视化验证。 + +建议增加: + +- 工序流程图:显示正常流转、异常流转、前置步骤和终态。 +- 流程校验:检查孤立步骤、循环关系、无初始状态、多个初始状态、无可达终态等问题。 +- 草稿/发布机制:编辑中的流程不立即影响现场,发布后生成流程版本。 +- 生效时间和版本回滚:历史记录绑定流程版本,支持按版本追溯。 +- 流程模拟器:输入订单、步骤、状态和事件,预览下一步可执行转换。 + +### 4.2 前置步骤和重复扫码规则可视化 + +`ProcessStep` 已具备前置步骤、前置验证模式、终态要求和重复扫码状态等字段,建议在 Admin 页面完整开放: + +- 用步骤选择器代替手工填写 ID。 +- 用“全部完成 / 任意完成 / 无需验证”下拉框代替编码输入。 +- 用状态多选展示允许重复扫码的状态。 +- 保存前给出规则冲突提示,避免把当前步骤设置为自己的前置步骤。 + +### 4.3 状态转换配置器 + +状态转换中的 `event_config` 和 `side_effect_config` 当前适合技术人员维护,不适合普通管理员。 + +建议分层实现: + +- 首期提供 JSON 格式校验、格式化和示例模板。 +- 后续提供条件构造器、通知配置器、字段更新配置器和事件参数表单。 +- 配置保存前执行字段白名单校验,禁止任意表达式或脚本执行。 + +## 5. 质量、维修和物料能力增强(P1) + +### 5.1 维修备件管理从快照升级为库存业务 + +当前维修记录的 `parts` 以 JSON 快照保存,适合记录历史,但不能支持库存管理。 + +建议增加: + +- 备件单位、库存数量、安全库存、库位、供应商和启用状态。 +- 维修领料、退料、旧件回收和库存流水。 +- 备件替代关系:一个物料允许配置可替代物料。 +- 维修提交时校验库存,支持“只记录不扣库存”和“提交即扣库存”两种策略。 +- 备件消耗排行、故障关联排行和库存预警。 + +### 5.2 故障描述和责任分析 + +当前已有故障描述使用次数和责任人员快照,建议补充: + +- 故障描述的启用范围:按产品、物料分类、工序或步骤限制。 +- 故障原因、责任类型、责任部门和责任人员的分层统计。 +- 缺陷 Pareto 图、重复故障率、一次修复率和返修率。 +- 支持从统计图直接下钻到异常记录和维修记录。 + +### 5.3 附件和图片统一管理 + +异常和维修均支持图片,但建议从 JSON URL 记录进一步完善为附件管理: + +- 限制文件类型、单文件大小、数量和图片分辨率。 +- 记录上传人、上传时间、业务类型和关联记录。 +- 删除业务记录时保留附件引用检查,避免误删仍被其他记录使用的文件。 +- 列表只显示缩略图,详情支持原图查看和下载。 + +## 6. 统计和查询能力增强(P1) + +### 6.1 统一统计口径 + +当前月/日统计已合并扫码、整机、异常和维修数据,建议明确每类指标的定义: + +- 生产量:按订单数量、扫码次数还是合格数量统计。 +- 异常量:按异常单数还是异常问题数统计。 +- 维修量:按维修单数、维修尝试次数还是完成数量统计。 +- 维修工序步骤:必须使用提交时快照,不能读取当前配置。 + +### 6.2 增加质量和效率指标 + +建议新增以下指标: + +- 首次通过率(FPY)。 +- 异常率、每千台缺陷数和缺陷密度。 +- 平均维修时长(MTTR)。 +- 异常响应时长和待维修积压时长。 +- 各工序/步骤产能、异常率、维修率和返修率。 +- 按产品、物料、班组、工号、责任人员和时间段进行对比。 + +### 6.3 查询页面增强 + +- 统一异常、维修、扫码、整机四类记录的筛选条件和分页行为。 +- 时间线支持按事件类型、工序、步骤、工号、操作人和状态筛选。 +- 查询结果支持导出当前筛选条件,并在导出文件中保留快照字段。 +- 对大时间范围查询增加异步导出,避免请求超时。 +- 为常用查询保存筛选方案,例如“今日待维修”“本周高频故障”。 + +## 7. 扫码端和现场作业增强(P1) + +### 7.1 弱网和离线队列 + +`scanapp` 目前以接口实时提交为主,现场网络中断时容易影响作业连续性。 + +建议增加: + +- 本地待提交队列,保存条码、工序、步骤、状态、工号和请求编号。 +- 网络恢复后自动重试,服务端按请求编号幂等处理。 +- 队列状态页:待同步、同步中、成功、失败和需人工处理。 +- 离线期间禁止使用无法本地校验的高风险操作,并明确提示用户。 + +### 7.2 扫码效率和设备能力 + +- 连续扫码模式和批量数量录入。 +- 扫码音效、震动、闪光灯和成功/失败颜色反馈可配置。 +- 设备绑定、设备编号、车间/产线绑定和异常设备上报。 +- 支持扫码枪键盘输入与摄像头扫码自动切换。 +- 对重复条码、无效条码和跨工序条码提供明确的现场提示。 + +### 7.3 现场首页 + +扫码端首页建议显示: + +- 当前登录工号和当前班次。 +- 当前工序/步骤和待完成数量。 +- 待处理异常、待维修订单和离线待同步数量。 +- 常用操作入口:生产录入、异常提报、维修处理、查询。 + +## 8. 权限、审计和安全(P0/P1) + +- 按角色限制生产录入、异常提报、维修处理、流程配置、统计查看和数据导出。 +- 工艺配置修改必须记录前后值、修改人、修改时间和发布版本。 +- 敏感字段如用户编码、手机号和图片下载地址按权限脱敏。 +- 所有导出记录写入审计日志,包含筛选条件和导出人。 +- 前台公共接口只开放必要的查询和提交能力,管理接口继续使用权限中间件。 +- 关键业务接口增加限流、请求编号、来源端标识和异常告警。 + +## 9. 多端一致性和工程维护建议 + +### 9.1 抽取共享业务规则 + +当前前台 API 和 AppAPI 都由 `DataEntryController` 承担大量业务方法,长期维护容易出现逻辑分叉。 + +建议: + +- 将工序校验、步骤校验、重复扫码、异常关联维修、物料校验抽到领域 Service。 +- Web、扫码端和后台只负责参数接收、权限处理和结果格式化。 +- 为共享 Service 增加接口级测试和关键场景测试。 + +### 9.2 模板源优先 + +生产插件前端正式维护顺序应保持: + +1. 先修改 `backend/plugin/produce/resource/template`。 +2. 再同步到 `frontend/admin/src/apps/produce` 或 `frontend/web/app/apps/produce`。 +3. 核对模板源和运行目录内容一致。 +4. 对涉及安装复制的改动执行一次重装回归验证。 + +### 9.3 接口契约和错误码 + +- 统一返回字段:记录 ID、请求编号、业务状态、可重试标记和用户提示。 +- 错误信息区分“用户输入错误”“流程配置错误”“权限错误”“系统暂时不可用”。 +- 为工序、步骤、状态、异常、维修和统计接口补充稳定的请求/响应类型定义。 + +## 10. 推荐实施路线 + +### 第一阶段:数据正确性和现场稳定性 + +1. 维修记录增加工序/步骤快照并回填历史数据。 +2. 增加提交幂等机制和重复点击保护。 +3. 统一实际作业人员、系统操作人和代录信息。 +4. 梳理异常到维修的状态闭环。 +5. 修正统计和时间线的数据口径。 + +### 第二阶段:管理效率和统计决策 + +1. 工序流程可视化、规则校验和流程发布版本。 +2. 统计增加 FPY、异常率、MTTR、返修率和 Pareto 分析。 +3. 维修备件库存、领退料和库存预警。 +4. 附件统一管理和导出能力增强。 +5. 查询页面支持保存筛选方案和异步导出。 + +### 第三阶段:现场数字化和智能化 + +1. 扫码端离线队列和自动同步。 +2. 设备、产线、班次和工位管理。 +3. 现场首页和班组看板。 +4. 流程模拟器、异常趋势预警和责任分析。 +5. 在数据稳定后再评估预测性维修或智能推荐功能。 + +## 11. 建议优先立项的五项功能 + +如果只选择最有价值的五项,建议顺序如下: + +1. **维修工序/步骤快照**:直接解决历史统计失真的核心问题。 +2. **提交幂等和离线同步基础**:减少现场重复数据和漏数据。 +3. **异常—维修状态闭环**:让待维修、维修中、验证和关闭有统一规则。 +4. **流程版本和可视化校验**:降低工艺配置错误对生产现场的影响。 +5. **质量效率指标看板**:把现有记录转化为可执行的管理决策。 + +## 12. 结论 + +生产插件的基础模块已经比较完整,下一步重点不应只是继续增加页面,而应优先保证“数据在提交时被正确记录、历史可追溯、现场可持续作业、统计口径一致”。 + +建议以第一阶段五项功能为主线,完成数据模型和接口契约后,再扩展库存、流程版本、离线和智能分析,能够以较小风险获得最大的业务收益。 diff --git a/docs/2026-09-03/1-维修记录工序步骤快照.md b/docs/2026-09-03/1-维修记录工序步骤快照.md new file mode 100644 index 0000000..dbacc0e --- /dev/null +++ b/docs/2026-09-03/1-维修记录工序步骤快照.md @@ -0,0 +1,22 @@ +# 维修记录工序步骤快照 + +## 调整内容 + +- 新增维修记录的 `process_id`、`step_id`、`process_name`、`step_name` 字段,保存维修提交时的工序步骤快照。 +- 新增迁移会优先从关联异常记录回填历史维修数据;无法匹配的记录标记为“历史数据未记录”。 +- 维修提交使用当时的维修流程配置写入快照,后续修改维修流程不会改变历史记录。 +- Web 查询、订单时间线、Admin 维修数据列表/详情/导出以及月度、日度统计改为优先使用维修记录快照。 + +## 涉及文件 + +- `backend/plugin/produce/resource/database/migrations/2026_09_03_000001_add_process_step_snapshot_to_repair_record.php` +- `backend/plugin/produce/app/model/RepairRecord.php` +- `backend/plugin/produce/app/api/controller/DataEntryController.php` +- `backend/plugin/produce/app/service/admin/WorkStatisticsService.php` +- `backend/plugin/produce/resource/template/admin/views/produce/repair/repair-data/schemas/index.tsx` +- `frontend/admin/src/apps/produce/views/produce/repair/repair-data/schemas/index.tsx` + +## 验证 + +- 已执行相关 PHP 文件语法检查。 +- 已核对 Admin 维修数据模板源与运行目录 Schema 内容一致。 diff --git a/docs/2026-09-03/10-APP查询按钮安卓间距.md b/docs/2026-09-03/10-APP查询按钮安卓间距.md new file mode 100644 index 0000000..c7c11ce --- /dev/null +++ b/docs/2026-09-03/10-APP查询按钮安卓间距.md @@ -0,0 +1,3 @@ +# APP 查询按钮安卓间距 + +针对安卓真机上按钮间距过小的问题,改用显式 `margin-right` 设置记录类型按钮间隔,并取消依赖 `gap`,保持五个按钮等宽且不相互贴合。 diff --git a/docs/2026-09-03/11-APP查询详情弹窗状态修复.md b/docs/2026-09-03/11-APP查询详情弹窗状态修复.md new file mode 100644 index 0000000..f448455 --- /dev/null +++ b/docs/2026-09-03/11-APP查询详情弹窗状态修复.md @@ -0,0 +1,5 @@ +# APP 查询详情弹窗状态修复 + +- 查询开始时清理上一条记录的详情状态,避免刷新期间旧弹窗残留。 +- 页面隐藏时关闭详情弹窗。 +- 查询结果变化后若当前记录已不存在,自动关闭旧详情并重新选择当前结果。 diff --git a/docs/2026-09-03/12-APP查询结果详情点击修复.md b/docs/2026-09-03/12-APP查询结果详情点击修复.md new file mode 100644 index 0000000..f72c4c1 --- /dev/null +++ b/docs/2026-09-03/12-APP查询结果详情点击修复.md @@ -0,0 +1,4 @@ +# APP 查询结果详情点击修复 + +- 查询结果卡片同时兼容 `tap` 和 `click` 事件,适配 Android 真机触摸事件。 +- 详情打开逻辑增加空记录保护,并在下一次渲染周期显示详情弹窗。 diff --git a/docs/2026-09-03/13-APP安卓详情点击兼容修复.md b/docs/2026-09-03/13-APP安卓详情点击兼容修复.md new file mode 100644 index 0000000..4aed6f3 --- /dev/null +++ b/docs/2026-09-03/13-APP安卓详情点击兼容修复.md @@ -0,0 +1,3 @@ +# APP 安卓详情点击兼容修复 + +针对 Web 正常、安卓真机无法打开详情的问题,列表卡片恢复使用 uni-app 原生 `@tap` 事件,移除组合事件、事件修饰符和延迟状态更新,避免不同端编译行为差异。 diff --git a/docs/2026-09-03/14-APP安卓详情弹窗宽度修复.md b/docs/2026-09-03/14-APP安卓详情弹窗宽度修复.md new file mode 100644 index 0000000..b69a849 --- /dev/null +++ b/docs/2026-09-03/14-APP安卓详情弹窗宽度修复.md @@ -0,0 +1,3 @@ +# APP 安卓详情弹窗宽度修复 + +Android WebView 对 `inset` 定位兼容性不一致,详情遮罩改为显式设置上下左右边界并补充宽度,底部详情面板强制铺满可用宽度,修复弹窗偏窄或错位问题。 diff --git a/docs/2026-09-03/15-APP详情弹窗底部定位修复.md b/docs/2026-09-03/15-APP详情弹窗底部定位修复.md new file mode 100644 index 0000000..9a8cdd2 --- /dev/null +++ b/docs/2026-09-03/15-APP详情弹窗底部定位修复.md @@ -0,0 +1,3 @@ +# APP 详情弹窗底部定位修复 + +修正 flex 布局中 `align-self: stretch` 导致详情面板高度被拉满、从顶部展开的问题。遮罩改为纵向 flex 并使用 `justify-content: flex-end`,确保弹窗始终从底部弹出且保持全宽。 diff --git a/docs/2026-09-03/16-APP查询输入框高度优化.md b/docs/2026-09-03/16-APP查询输入框高度优化.md new file mode 100644 index 0000000..fd0fb76 --- /dev/null +++ b/docs/2026-09-03/16-APP查询输入框高度优化.md @@ -0,0 +1,3 @@ +# APP 查询输入框高度优化 + +查询输入框、扫码按钮和查询按钮统一调整为 `80rpx` 高度,并同步输入文字行高,改善真机上输入区域偏小的问题。 diff --git a/docs/2026-09-03/17-APP查询按钮宽度优化.md b/docs/2026-09-03/17-APP查询按钮宽度优化.md new file mode 100644 index 0000000..0e4191b --- /dev/null +++ b/docs/2026-09-03/17-APP查询按钮宽度优化.md @@ -0,0 +1,3 @@ +# APP 查询按钮宽度优化 + +扫码按钮宽度调整为 `80rpx`,查询按钮宽度调整为 `128rpx`,与输入框高度同步优化并保持操作区域比例协调。 diff --git a/docs/2026-09-03/18-查询页面登录权限控制.md b/docs/2026-09-03/18-查询页面登录权限控制.md new file mode 100644 index 0000000..1912354 --- /dev/null +++ b/docs/2026-09-03/18-查询页面登录权限控制.md @@ -0,0 +1,4 @@ +# 查询页面登录权限控制 + +- APP 查询页进入时检查登录令牌,未登录仅提示“请先登录后再查询”,不自动跳转。 +- Web/API 与 APP/API 的生产记录、订单时间线查询接口取消匿名访问,必须携带有效登录令牌。 diff --git a/docs/2026-09-03/19-查询页未登录仅提示.md b/docs/2026-09-03/19-查询页未登录仅提示.md new file mode 100644 index 0000000..2192432 --- /dev/null +++ b/docs/2026-09-03/19-查询页未登录仅提示.md @@ -0,0 +1,3 @@ +# 查询页未登录仅提示 + +未登录访问 APP 查询页时仅显示提示信息,不再自动跳转登录页面;页面生命周期不会重复触发登录跳转。 diff --git a/docs/2026-09-03/2-登录Token有效期配置.md b/docs/2026-09-03/2-登录Token有效期配置.md new file mode 100644 index 0000000..2017983 --- /dev/null +++ b/docs/2026-09-03/2-登录Token有效期配置.md @@ -0,0 +1,13 @@ +# 登录 Token 有效期配置 + +## 变更内容 + +- 增加系统配置“登录Token”,可分别设置 Admin、Web、APP 三端 Access Token 有效期。 +- Refresh Token 有效期支持按 Admin、Web、APP 分别设置,默认均为 7 天。 +- JWT Redis 会话记录按 Refresh Token 有效期保存,Access Token 过期后可正常刷新。 +- APP 请求收到 401 时自动使用 Refresh Token 刷新并重试原请求。 + +## 默认值 + +- 三端 Access Token:3600 秒(1 小时)。 +- Refresh Token:604800 秒(7 天)。 diff --git a/docs/2026-09-03/3-附件管理浏览功能.md b/docs/2026-09-03/3-附件管理浏览功能.md new file mode 100644 index 0000000..bc7976a --- /dev/null +++ b/docs/2026-09-03/3-附件管理浏览功能.md @@ -0,0 +1,16 @@ +# 附件管理浏览功能 + +## 变更内容 + +- 在 `/tools/files` 附件列表新增“浏览”操作。 +- 图片、视频、音频和 PDF 使用弹窗在线预览。 +- 其他文件类型提示下载,并支持新窗口打开。 +- 预览内容通过现有下载接口获取,沿用当前登录权限。 +- 附件列表新增预览列:图片显示缩略图,PDF、文档、表格、演示文稿、压缩包、音视频和未知格式显示对应文件图标。 +- 缩略图根据表格密度自适应:紧凑模式 38×38、默认模式 50×50、宽松模式 58×58,避免图片撑大行高。 +- 图片缩略图在列表单元格中居中显示,点击缩略图可打开图片预览弹窗。 + +## 验证 + +- 已检查页面代码结构。 +- Admin 全量构建仍受项目原有缺失文件 `src/components/form/components/image-selector/index.vue` 阻塞,与本次附件页面无关。 diff --git a/docs/2026-09-03/4-APP查询页面数据对接.md b/docs/2026-09-03/4-APP查询页面数据对接.md new file mode 100644 index 0000000..4e1a805 --- /dev/null +++ b/docs/2026-09-03/4-APP查询页面数据对接.md @@ -0,0 +1,17 @@ +# APP 查询页面数据对接 + +## 变更内容 + +- 新增 `frontend/app/api/produce/query.js`,统一封装生产记录和订单时间线查询接口。 +- APP 查询页改用 `/appapi/produce/query/records` 真实数据,支持扫码、整机、异常、维修及全部类型查询。 +- 接入关键词查询、服务端分页参数、加载状态和失败提示。 +- 高级筛选调整为时间区间、工序和步骤选择,选项通过后端配置接口加载。 +- 查询接口补充 `process_id`、`step_id` 筛选参数,确保工序步骤筛选在服务端生效。 +- 页面初始保持待查询状态,不自动展示结果;点击查询或扫码后才显示数据。 +- 查询按钮增加输入校验,未填写关键词、订单号或用户工号时不会查询全部数据。 +- 工序选项根据返回记录动态补充,记录详情展示后端返回的工序、步骤、操作人、产品及业务信息。 + +## 验证 + +- `node --check frontend/app/api/produce/query.js` +- 提取查询页 ` diff --git a/frontend/admin/src/views/setting/config/modules/auth-token.vue b/frontend/admin/src/views/setting/config/modules/auth-token.vue new file mode 100644 index 0000000..bcf0271 --- /dev/null +++ b/frontend/admin/src/views/setting/config/modules/auth-token.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/admin/src/views/setting/config/modules/web-site.vue b/frontend/admin/src/views/setting/config/modules/web-site.vue index 0293f54..186d98d 100644 --- a/frontend/admin/src/views/setting/config/modules/web-site.vue +++ b/frontend/admin/src/views/setting/config/modules/web-site.vue @@ -37,6 +37,19 @@ }, rules: [] }, + { + prop: 'require_login', + label: $t('setting.config.web_site.require_login'), + component: 'RadioGroup', + defaultValue: 1, + componentProps: { + options: [ + { label: $t('setting.config.web_site.require_login_options.enabled'), value: 1 }, + { label: $t('setting.config.web_site.require_login_options.disabled'), value: 0 } + ] + }, + rules: [] + }, { prop: 'logo', label: $t('setting.config.web_site.logo'), @@ -169,7 +182,10 @@ const retrieveData = async () => { const record = (await ConfigService.getByCode('web_site_setting')) as any - await formApi.setValues({ ...convertStringNumbers(record) }) + // 兼容旧配置数据:未保存该字段时默认要求登录 + const values = convertStringNumbers(record || {}) + values.require_login = values.require_login ?? 1 + await formApi.setValues(values) } onMounted(async () => { diff --git a/frontend/admin/src/views/tools/files/index.vue b/frontend/admin/src/views/tools/files/index.vue index 01dd21c..1aa3cfe 100644 --- a/frontend/admin/src/views/tools/files/index.vue +++ b/frontend/admin/src/views/tools/files/index.vue @@ -1,13 +1,81 @@