初始化

This commit is contained in:
2026-08-08 18:28:49 +08:00
parent 9bef4420e0
commit f082037a6e
854 changed files with 217171 additions and 11 deletions
+17
View File
@@ -0,0 +1,17 @@
# MES 环境配置文件
# 复制此文件为 .env 并修改实际值
# 请勿将 .env 提交到版本控制系统
# 数据库连接
DB_HOST=localhost
DB_USER=root
DB_PASS=
DB_NAME=mes2
DB_COMPANY=Default Company
# 运行模式(生产环境设为 false)
APP_DEBUG=true
APP_ENV=development
# Session 安全密钥(请修改为随机字符串)
SESSION_SECRET=change_me_to_random_string
+38 -11
View File
@@ -1,13 +1,40 @@
# ---> AL # 环境配置(含数据库密码)
.vscode/* .env
!.vscode/settings.json .env.local
!.vscode/tasks.json .env.*.local
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code # 数据库备份
.history/ backups/
*.app *.sql
.snapshots/* !sql/*.sql
# 安装锁
config/install.lock
# 文档(含系统架构和默认密码)
*.md
!README.md
# 依赖
/vendor/
node_modules/
# 临时文件
*.log
*.tmp
*.cache
*.swp
# IDE
.idea/
.vscode/
*.sublime-*
# 操作系统
Thumbs.db
.DS_Store
Desktop.ini
# 上传文件
uploads/
/static/uploads/
+49
View File
@@ -0,0 +1,49 @@
RewriteEngine On
# If the request is not for a real file or directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirect all requests to index.php
RewriteRule ^(.*)$ index.php [QSA,L]
# Prevent directory listing
Options -Indexes
# Set default charset
AddDefaultCharset UTF-8
# ===== 安全加固 =====
# 禁止访问敏感文件
<FilesMatch "\.(env|sql|log|md|yml|yaml|json|lock)$">
Order allow,deny
Deny from all
</FilesMatch>
# 禁止直接访问诊断/调试/安装/修复脚本
<FilesMatch "^(diag|debug_|verify_|php83_check|fix_db|fix_db_password|install|logintest|pwdtest|cachecheck|checkpath|check|hexcheck|dbcheck)\.php$">
Order allow,deny
Deny from all
</FilesMatch>
# 禁止访问备份目录(含 backups/ 和 _backup_/
<IfModule mod_rewrite.c>
RewriteRule ^backups/ - [F,L]
RewriteRule ^_backup_ - [F,L]
</IfModule>
# 安全头
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# P2-6 / v3-P2CSP 已迁移至 index.php 按请求动态生成(含 nonce),此处不再静态设置,
# 以避免与 PHP 端的 nonce 策略形成「双重 CSP 策略」冲突(浏览器会要求两者同时满足)。
# 当前 script-src 仍保留 'unsafe-inline' 作为迁移过渡;待全站内联事件处理器
#onclick 等)改造为 addEventListener 后,再于 index.php 移除 'unsafe-inline'。
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
# 仅 HTTPS 时启用 HSTS(站点已 301 强制 HTTPS,可安全启用)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
+3
View File
@@ -0,0 +1,3 @@
open_basedir=/www/wwwroot/mes2/:/tmp/:/www/php_session/51chip.net/
session.save_path=/www/php_session/51chip.net/
session.save_handler = files
@@ -0,0 +1 @@
7vSb6XUQAHK2bLMijB2yZdF7y4YRdhqt2HdkTdtm0lY.EgW56fMct7Vw7Pg8dB7a_Izx7j9jHmVtrm_VZFphzHg
+5
View File
@@ -0,0 +1,5 @@
Contact: mailto:security@51chip.net
Expires: 2026-12-31T23:59:59.000Z
Preferred-Languages: zh, en
Canonical: https://51chip.net/.well-known/security.txt
Policy: https://51chip.net/security-policy.html
+34
View File
@@ -0,0 +1,34 @@
MES2 站点上线/上传包 — 说明
================================
生成时间:2026-08-01
域名:51chip.net 服务器网站根目录(示例):/www/wwwroot/mes2/
本包包含本轮所有改动,分两类:
【A 类:直接覆盖网站目录的文件】
将 app/ 目录整体解压到网站根目录即可,按相对路径覆盖同名文件(仅覆盖以下 5 个文件,不影响其它文件):
- app/views/Login/index.php 登录页页脚(备案号)
- app/views/Dashboard/index.php 控制台页脚(备案号)
- app/views/layouts/station_footer.php 工位前台页脚(备案号)
- app/views/layouts/admin_footer.php 后台统一页脚(备案号)
- app/views/layouts/front_footer.php 备用页脚(备案号,未被引用但保持同步)
Linux 服务器解压命令(在网站根目录执行):
tar -xzf mes2-upload-20260801.tar.gz -C /www/wwwroot/mes2/
(若用 FTP 先上传到服务器再解压,注意只覆盖上述 5 个文件即可)
【B 类:参考/配置文件,不要丢进网站根目录】
- reference/nginx-vhost-mes2.conf 宝塔 Nginx 站点配置(Apache→Nginx 迁移用)
- reference/NGINX_MIGRATION_GUIDE.md 迁移与部署指南
使用方法:宝塔面板 → 网站 → 51chip.net → 设置 → 配置文件,
用 nginx-vhost-mes2.conf 的内容整体替换(注意把 enable-php-XX 改成实际 PHP 版本,当前为 8.3),
保存后宝塔会自动 nginx -t 校验。详见 NGINX_MIGRATION_GUIDE.md。
【本次改动摘要】
1. 备案号:粤ICP备16118477号-1,已按规范显示在全部对外页脚并超链到 https://beian.miit.gov.cn/
2. Nginx 配置已修复 /Admin/index 等漂亮 URL 的 404(前端控制器 try_files 内联,不再依赖空伪静态文件)
【验证】
curl -i https://51chip.net/Admin/index 应返回 200/302(非 404
浏览器打开首页,页脚可见「备案号:粤ICP备16118477号-1」并指向 beian.miit.gov.cn
File diff suppressed because it is too large Load Diff
+918
View File
@@ -0,0 +1,918 @@
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\AsdTestData;
use app\models\AsdDevice;
/**
* 昂盛达上位机 MES 接口 API
* 接口文档版本:1.5
*
* 工作流:UserInfoVerification → GetWorkInfo → CheckFlow → UpDataInfo
*
* 接口地址:http://host/Api/{method}
* 协议:HTTP POST
* 数据格式:JSON
*/
class ApiController extends Controller
{
/**
* 构造函数:所有 API 响应统一输出 JSON。
* 注意:框架 Core::route() 不会自动调用 init(),因此此处设置 Content-Type。
*/
public function __construct($controller, $action)
{
parent::__construct($controller, $action);
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
// 注册 API 专用异常处理器:PDO 处于 ERRMODE_EXCEPTION 模式,
// 任何未捕获异常(如缺列、连接异常)都会冒泡成 500 显示「系统内部错误」。
// 这里覆盖为 JSON 响应,既避免 500,又把真实错误暴露给调用方便于排查。
set_exception_handler(function ($e) {
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
http_response_code(500);
$msg = '服务器处理异常';
if (APP_DEBUG && $e instanceof \Throwable) {
$msg .= ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
}
echo json_encode(['Result' => 0, 'Message' => $msg], JSON_UNESCAPED_UNICODE);
exit;
});
}
/**
* 验证 API Token
*
* 与 generateToken() 一致,统一使用 DGZXY_asd_api_token 表。
* 开放模式(符合接口文档 v1.5「鉴权为可选项」):
* - 未携带 Token:放行(返回 null),便于设备直接上传;
* - 携带了 Token 但无效/过期:拒绝。
*
* @return array|null 成功/开放模式返回用户信息或 null,失败直接 exit
*/
protected function apiAuth()
{
// 从 Authorization 头获取 Token
$token = '';
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
$token = trim($matches[1]);
}
// 也支持 POST/GET 参数传递 token(向后兼容)
if (empty($token)) {
$input = $this->getJsonInput();
$token = $input['token'] ?? $_GET['token'] ?? '';
}
// 开放模式:未携带 Token 直接放行
if (empty($token)) {
return null;
}
// 携带了 Token,则必须有效
try {
$pdo = \core\db\Db::pdo();
$stmt = $pdo->prepare("SELECT * FROM DGZXY_asd_api_token WHERE token = :token");
$stmt->execute([':token' => $token]);
$tokenInfo = $stmt->fetch(\PDO::FETCH_ASSOC);
} catch (\Throwable $e) {
if (APP_DEBUG) {
error_log('API auth error: ' . $e->getMessage());
}
$this->jsonResponse(0, '认证服务异常');
exit;
}
if (!$tokenInfo) {
$this->jsonResponse(0, 'Token无效或已过期');
exit;
}
return [
'id' => 0,
'emp_no' => $tokenInfo['emp_no'] ?? '',
'emp_name' => $tokenInfo['tester'] ?? '',
];
}
// ==================== 工具方法 ====================
/**
* 频率限制(基于文件滑动窗口,按 IP + key 计数)
* @param string $key 业务标识
* @param int $max 时间窗口内最大允许次数
* @param int $window 时间窗口(秒)
* @return bool true=已超限需拒绝
*/
private function rateLimitExceeded($key, $max, $window)
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$dir = sys_get_temp_dir() . '/mes2_ratelimit';
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (!is_dir($dir)) {
return false; // 无法记录时放行,不影响业务
}
$file = $dir . '/' . md5($key . '|' . $ip);
$now = time();
$hits = [];
if (file_exists($file)) {
$raw = @file_get_contents($file);
$hits = $raw ? json_decode($raw, true) ?: [] : [];
}
// 清除窗口外的记录
$hits = array_filter($hits, function ($t) use ($now, $window) {
return ($now - $t) < $window;
});
if (count($hits) >= $max) {
return true;
}
$hits[] = $now;
@file_put_contents($file, json_encode($hits));
return false;
}
/**
* API 默认入口:不暴露任何方法列表,返回干净的 404
*/
public function index()
{
if (!headers_sent()) {
header('HTTP/1.1 404 Not Found');
}
$this->jsonResponse(0, 'API endpoint not found');
}
/**
* 通用 JSON 响应
*/
private function jsonResponse($result, $message, $extra = [])
{
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
$data = array_merge(['Result' => (int)$result, 'Message' => $message], $extra);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
/**
* 获取原始 JSON 请求体
*/
private function getJsonInput()
{
$input = file_get_contents('php://input');
$data = json_decode($input, true);
return is_array($data) ? $data : [];
}
/**
* 生成持久化 Token(存入数据库以便后续校验)
*/
private function generateToken($empNo, $tester)
{
$token = bin2hex(random_bytes(32)); // 使用加密安全的随机数替代 md5
try {
$pdo = \core\db\Db::pdo();
$stmt = $pdo->prepare(
"INSERT INTO DGZXY_asd_api_token (token, emp_no, tester, created_at)
VALUES (:token, :empNo, :tester, NOW())
ON DUPLICATE KEY UPDATE tester = :tester2, created_at = NOW()"
);
$stmt->execute([
':token' => $token,
':empNo' => $empNo,
':tester' => $tester,
':tester2' => $tester,
]);
} catch (\Throwable $e) {
// Token 表不存在时记录日志,但仍返回 token
// 注意:verifyToken 也会因表不存在而拒绝,保证一致性
if (APP_DEBUG) {
error_log('API Token table error: ' . $e->getMessage());
}
}
return $token;
}
/**
* 校验 Token 是否有效
* 安全修复:表不存在时拒绝通过(而非降级放行)
*/
private function verifyToken($token, $tester = '')
{
if (empty($token)) return false;
try {
$pdo = \core\db\Db::pdo();
$sql = "SELECT * FROM DGZXY_asd_api_token WHERE token = :token";
$params = [':token' => $token];
if (!empty($tester)) {
$sql .= " AND tester = :tester";
$params[':tester'] = $tester;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: false;
} catch (\Throwable $e) {
// 安全修复:表不存在时拒绝通过,而非降级放行
if (APP_DEBUG) {
error_log('API Token verification error: ' . $e->getMessage());
}
return false;
}
}
// ==================== 接口 1: 用户登录身份信息校验 ====================
/**
* 1. AsdUserInfoVerification - 用户登录系统身份信息校验
* POST /Api/asdUserInfoVerification
*
* 请求参数 (JSON)
* Parameter1 - 参数1(员工编号/工号,扫码或手动输入)
* Parameter2 - 参数2(密码或其它验证信息,可选)
* Parameter3 - 参数3(可选)
* Parameter4 - 参数4(可选)
*
* 成功响应:
* Result=1, Message="验证成功", Tester="张三", Token="xxx"
* 失败响应:
* Result=0, Message="工号不存在" 或 "密码错误"
*/
public function asdUserInfoVerification()
{
// 频率限制:同一 IP 60 秒内最多 10 次,防止枚举/暴力破解
if ($this->rateLimitExceeded('asdUserInfoVerification', 10, 60)) {
$this->jsonResponse(0, '尝试过于频繁,请稍后重试');
return;
}
$data = $this->getJsonInput();
$empNo = $data['Parameter1'] ?? '';
$password = $data['Parameter2'] ?? '';
$param3 = $data['Parameter3'] ?? '';
$param4 = $data['Parameter4'] ?? '';
// 安全修复(P0):API 认证必须同时提供工号与密码,禁止无密码签发 Token
if (empty($empNo)) {
$this->jsonResponse(0, '工号(Parameter1)不能为空');
return;
}
if (empty($password)) {
$this->jsonResponse(0, '缺少密码(Parameter2)API 认证必须提供密码');
return;
}
$employee = new \app\models\Employee();
$emp = $employee->findByEmpNo($empNo);
if (!$emp) {
$this->jsonResponse(0, '工号不存在: ' . $empNo);
return;
}
// 严格校验密码(bcrypt 验证)
$valid = $employee->validateLogin($empNo, $password);
if (!$valid) {
$this->jsonResponse(0, '密码错误');
return;
}
$token = $this->generateToken($empNo, $emp['emp_name']);
$this->jsonResponse(1, '验证成功', [
'Tester' => $emp['emp_name'],
'Token' => $token,
]);
}
// ==================== 接口 2: 获取站点/工位/关联数据 ====================
/**
* 2. AsdGetWorkInfo - 获取站点、工位或其它关联数据
* POST /Api/asdGetWorkInfo
*
* 请求参数 (JSON)
* Parameter1 - 参数1(条码/二维码,用于查找关联工单)
* Parameter2 - 参数2(工位编码,可选)
* Parameter3 - 参数3(设备编码,可选)
* Parameter4 - 参数4(可选)
*
* 成功响应:
* Result=1, Message="获取成功", Mo="xxx", Site="xxx", Station="xxx", WorkOrder="xxx"
*/
public function asdGetWorkInfo()
{
$this->apiAuth();
$data = $this->getJsonInput();
$param1 = $data['Parameter1'] ?? ''; // 条码/二维码
$param2 = $data['Parameter2'] ?? ''; // 工位编码
$param3 = $data['Parameter3'] ?? ''; // 设备编码
$param4 = $data['Parameter4'] ?? ''; // 备用
$mo = '';
$site = '';
$station = '';
$workOrder = '';
try {
$pdo = \core\db\Db::pdo();
// 1. 如果有条码,查询该条码已关联的制令单/工单
if (!empty($param1)) {
$stmt = $pdo->prepare(
"SELECT mo, site, station, work_order FROM DGZXY_asd_test_record
WHERE qr_code = :qc ORDER BY id DESC LIMIT 1"
);
$stmt->execute([':qc' => $param1]);
$record = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($record && !empty($record['mo'])) {
$mo = $record['mo'];
$site = $record['site'];
$station = $record['station'];
$workOrder = $record['work_order'];
}
}
// 2. 根据设备编码查找默认工位
if (!empty($param3)) {
$deviceModel = new AsdDevice();
$device = $deviceModel->findByCode($param3);
if ($device) {
if (empty($station) && !empty($device['station_code'])) {
$station = $device['station_code'];
}
if (empty($site) && !empty($device['line_name'])) {
$site = $device['line_name'];
}
}
}
// 3. 根据传入工位编码查工位
if (empty($station) && !empty($param2)) {
$station = $param2;
}
// 4. 从工位表获取详细信息
$workstationModel = new \app\models\Workstation();
$allStations = $workstationModel->getAll();
// 自动补全 site 和 station
if ((empty($site) || empty($station)) && !empty($allStations)) {
$first = $allStations[0];
if (empty($site)) $site = $first['station_name'] ?? '';
if (empty($station)) $station = $first['station_code'] ?? '';
}
// 5. 生成制令单号(如果未找到已关联的)
if (empty($mo)) {
$stmt = $pdo->prepare(
"SELECT MAX(mo) AS last_mo FROM DGZXY_asd_test_record WHERE mo LIKE :prefix"
);
$todayPrefix = 'MO' . date('Ymd') . '%';
$stmt->execute([':prefix' => $todayPrefix]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!empty($row['last_mo'])) {
$lastSeq = intval(substr($row['last_mo'], 10));
$mo = 'MO' . date('Ymd') . str_pad($lastSeq + 1, 3, '0', STR_PAD_LEFT);
} else {
$mo = 'MO' . date('Ymd') . '001';
}
}
// 6. 生成工单号
if (empty($workOrder)) {
$stmt = $pdo->prepare(
"SELECT MAX(work_order) AS last_wo FROM DGZXY_asd_test_record WHERE work_order LIKE :prefix"
);
$woPrefix = 'WO-' . date('Ymd') . '%';
$stmt->execute([':prefix' => $woPrefix]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!empty($row['last_wo'])) {
$lastSeq = intval(substr($row['last_wo'], 12));
$workOrder = 'WO-' . date('Ymd') . '-' . str_pad($lastSeq + 1, 2, '0', STR_PAD_LEFT);
} else {
$workOrder = 'WO-' . date('Ymd') . '-01';
}
}
} catch (\Throwable $e) {
// 容错:即使数据库异常,也返回可用数据
if (empty($mo)) $mo = 'MO' . date('Ymd') . '001';
if (empty($workOrder)) $workOrder = 'WO-' . date('Ymd') . '-01';
}
$this->jsonResponse(1, '获取成功', [
'Mo' => $mo,
'Site' => $site,
'Station' => $station,
'WorkOrder' => $workOrder,
]);
}
// ==================== 接口 3: 数据检验/工艺检查 ====================
/**
* 3. AsdCheckFlow - 数据检验、工艺检查
* POST /Api/asdCheckFlow
*
* 请求参数 (JSON)
* QrCode - 条码/二维码(有则必传)
* Mo - GetWorkInfo 返回的 Mo
* Site - GetWorkInfo 返回的 Site
* Station - GetWorkInfo 返回的 Station
* WorkOrder - GetWorkInfo 返回的 WorkOrder
* Group - 组别(必填)
* Device - 设备序列号(必填)
*
* 成功响应:
* Result=1, QrCode="xxx", Message="检验通过,可以开始测试"
* 失败响应:
* Result=0, QrCode="xxx", Message="该条码已完成测试" / "设备未注册" / ...
*/
public function asdCheckFlow()
{
$this->apiAuth();
$data = $this->getJsonInput();
$qrCode = $data['QrCode'] ?? '';
$mo = $data['Mo'] ?? '';
$site = $data['Site'] ?? '';
$station = $data['Station'] ?? '';
$workOrder = $data['WorkOrder'] ?? '';
$group = $data['Group'] ?? '';
$device = $data['Device'] ?? '';
// 1. 必填字段校验:Group & Device
if (empty($group)) {
$this->jsonResponse(0, '组别(Group)不能为空', ['QrCode' => $qrCode]);
return;
}
if (empty($device)) {
$this->jsonResponse(0, '设备序列号(Device)不能为空', ['QrCode' => $qrCode]);
return;
}
// 2. 设备存在性校验(开放模式:设备未注册不阻断,仅做组别一致性校验)
$deviceModel = new AsdDevice();
try {
$deviceRecord = $deviceModel->isActive($device);
if ($deviceRecord
&& !empty($group)
&& !empty($deviceRecord['group_name'])
&& $deviceRecord['group_name'] !== $group) {
// 设备已注册但组别与提交不一致:拒绝(数据完整性保护)
$this->jsonResponse(0, "设备 {$device} 属于组别 [{$deviceRecord['group_name']}],与提交组别 [{$group}] 不匹配", [
'QrCode' => $qrCode,
]);
return;
}
// 设备未注册时放行(开放接口,便于先上线采集再补录设备)
} catch (\Throwable $e) {
// 设备表异常时不阻止流程
}
// 3. 条码防重校验(如果有条码)
if (!empty($qrCode)) {
try {
$testData = new AsdTestData();
$existing = $testData->query(
"SELECT id, status FROM DGZXY_asd_test_record WHERE qr_code = :qc ORDER BY id DESC LIMIT 1",
[':qc' => $qrCode]
);
if (!empty($existing)) {
$this->jsonResponse(0, '该条码已完成测试,请勿重复测试', [
'QrCode' => $qrCode,
]);
return;
}
} catch (\Throwable $e) {
// 容错
}
}
$this->jsonResponse(1, '检验通过,可以开始测试', [
'QrCode' => $qrCode,
]);
}
// ==================== 接口 4: 提交测试数据 ====================
/**
* 4. AsdUpDataInfo - 提交测试数据(核心接口)
* POST /Api/asdUpDataInfo
*
* 请求参数 (JSON)
* Tester - 测试员
* QrCode - 条码/二维码
* Mo - 制令单
* Site - 工位
* Station - 站点
* WorkOrder - 工单
* ProjectName - 项目名称
* Status - 测试状态 PASS/NG(必填)
* Group - 组别(必填)
* Device - 设备号(必填)
* RunningTime - 运行时间(必填)
* TestTime - 测试时间(必填)
* DataList - 测试工步数据(必填)
*
* 成功响应:
* Result=1, Message="测试数据上传成功", RecordId="xxx", DetailCount=N
* 失败响应:
* Result=0, Message="错误描述"
*/
public function asdUpDataInfo()
{
$this->apiAuth();
$data = $this->getJsonInput();
// ===== 必填字段校验 =====
// Status 必填
if (empty($data['Status'])) {
$this->jsonResponse(0, '测试状态(Status)是必填项,值必须为 PASS 或 NG');
return;
}
$status = strtoupper($data['Status']);
if (!in_array($status, ['PASS', 'NG'])) {
$this->jsonResponse(0, 'Status 值无效,必须为 PASS 或 NG,当前值: ' . $status);
return;
}
// Group 必填
if (empty($data['Group'])) {
$this->jsonResponse(0, '组别(Group)是必填项');
return;
}
// Device 必填
if (empty($data['Device'])) {
$this->jsonResponse(0, '设备号(Device)是必填项');
return;
}
// RunningTime 必填
if (empty($data['RunningTime'])) {
$this->jsonResponse(0, '运行时间(RunningTime)是必填项');
return;
}
// TestTime 必填
if (empty($data['TestTime'])) {
$this->jsonResponse(0, '测试时间(TestTime)是必填项');
return;
}
// DataList 必填且为数组
if (empty($data['DataList']) || !is_array($data['DataList'])) {
$this->jsonResponse(0, '测试数据(DataList)是必填项,且必须为数组');
return;
}
// ===== 设备存在性校验(可选但推荐) =====
$deviceCode = $data['Device'];
try {
$deviceModel = new AsdDevice();
$deviceRecord = $deviceModel->isActive($deviceCode);
if (!$deviceRecord) {
// 不阻止上传,但记录警告信息
// 需要在 asd_device 表中预先注册设备
}
} catch (\Throwable $e) {
// 设备表异常时不影响主流程
}
// ===== 写入数据库 =====
try {
$testData = new AsdTestData();
// 插入主记录
$recordId = $testData->addRecord([
'tester' => $data['Tester'] ?? '',
'qr_code' => $data['QrCode'] ?? '',
'mo' => $data['Mo'] ?? '',
'site' => $data['Site'] ?? '',
'station' => $data['Station'] ?? '',
'work_order' => $data['WorkOrder'] ?? '',
'project_name' => $data['ProjectName'] ?? '',
'status' => $status,
'group_name' => $data['Group'],
'device' => $deviceCode,
'running_time' => $data['RunningTime'],
'test_time' => $data['TestTime'],
]);
if (!$recordId) {
$err = $testData->getError();
$this->jsonResponse(0, '保存测试记录失败: ' . ($err ?: '数据库写入错误'));
return;
}
// 插入工步详情
$detailCount = 0;
foreach ($data['DataList'] as $item) {
$detailResult = $testData->addDetail([
'record_id' => $recordId,
'seq' => $item['Seq'] ?? ++$detailCount,
'test_name' => $item['TestName'] ?? '',
'test_item' => $item['TestItem'] ?? '',
'test_units' => $item['TestUnits'] ?? '',
'data_value' => $item['DataValue'] ?? null,
'lower_limit' => $item['LowerLimit'] ?? null,
'upper_limit' => $item['UpperLimit'] ?? null,
'test_value' => $item['TestValue'] ?? '',
'test_limit' => $item['TestLimit'] ?? '',
'test_result' => $item['TestResult'] ?? 'PASS',
]);
if ($detailResult) {
$detailCount++;
}
}
$this->jsonResponse(1, '测试数据上传成功', [
'RecordId' => $recordId,
'DetailCount' => $detailCount,
]);
} catch (\Throwable $e) {
// PDO 异常模式:缺列/约束等会抛异常,这里转成 JSON 错误而非 500
$this->jsonResponse(0, '保存测试记录异常: ' . $e->getMessage());
}
}
// ==================== 接口 5: 获取设备列表 ====================
/**
* 5. AsdGetDevices - 获取已注册设备列表(供扫码选择使用)
* POST /Api/asdGetDevices
*
* 请求参数 (JSON)
* Keyword - 搜索关键词(可选,模糊匹配设备编码/名称/IP/组别)
*
* 返回:
* Result=1, Devices=[{id, device_code, device_name, device_ip, group_name, status}, ...]
*/
public function asdGetDevices()
{
$this->apiAuth();
$data = $this->getJsonInput();
$keyword = $data['Keyword'] ?? '';
try {
$pdo = \core\db\Db::pdo();
if (!empty($keyword)) {
$like = '%' . $keyword . '%';
$stmt = $pdo->prepare(
"SELECT id, device_code, device_name, device_ip, device_type, group_name, line_name, station_code, status, remark
FROM DGZXY_asd_device
WHERE device_code LIKE :kw1 OR device_name LIKE :kw2 OR device_ip LIKE :kw3 OR group_name LIKE :kw4
ORDER BY group_name ASC, device_code ASC"
);
$stmt->execute([':kw1' => $like, ':kw2' => $like, ':kw3' => $like, ':kw4' => $like]);
} else {
$stmt = $pdo->prepare("SELECT id,device_code,device_name,device_ip,device_type,group_name,line_name,station_code,status,remark FROM DGZXY_asd_device ORDER BY group_name ASC, device_code ASC");
$stmt->execute();
}
$devices = $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\Throwable $e) {
$devices = [];
}
$this->jsonResponse(1, '获取成功', ['Devices' => $devices]);
}
// ==================== 接口 6: 设备连接测试 ====================
/**
* 6. AsdDeviceConnect - 连接设备并获取设备信息
* POST /Api/asdDeviceConnect
*
* 请求参数 (JSON)
* DeviceCode - 设备编码(必填)
* DeviceIp - 设备IP(可选,传入后自动更新设备IP)
*
* 成功响应:
* Result=1, Message="设备连接成功",
* Device={device_code, device_name, device_ip, group_name, status},
* ConnectionInfo={ip, port, reachable, latency_ms}
*/
public function asdDeviceConnect()
{
$this->apiAuth();
$data = $this->getJsonInput();
$deviceCode = $data['DeviceCode'] ?? '';
$deviceIp = $data['DeviceIp'] ?? '';
if (empty($deviceCode)) {
$this->jsonResponse(0, '设备编码(DeviceCode)不能为空');
return;
}
try {
$deviceModel = new AsdDevice();
$device = $deviceModel->findByCode($deviceCode);
if (!$device) {
$this->jsonResponse(0, '设备不存在: ' . $deviceCode);
return;
}
if ($device['status'] != 1) {
$this->jsonResponse(0, '设备已禁用: ' . $deviceCode);
return;
}
// 如果传入了新 IP,更新到数据库
if (!empty($deviceIp) && $deviceIp !== ($device['device_ip'] ?? '')) {
$deviceModel->updateDevice($device['id'], ['device_ip' => $deviceIp]);
$device['device_ip'] = $deviceIp;
}
// 检查 IP 是否存在
if (empty($device['device_ip'])) {
$this->jsonResponse(0, '设备 IP 未设置,请先填写设备 IP 地址', [
'Device' => $device,
]);
return;
}
// 尝试 TCP 连接(如果有 socket 扩展)
$reachable = false;
$latency = 0;
$port = 502; // 默认 Modbus TCP 端口
if (function_exists('fsockopen')) {
$start = microtime(true);
$errno = 0;
$errstr = '';
$fp = @fsockopen($device['device_ip'], $port, $errno, $errstr, 3);
if ($fp) {
$reachable = true;
$latency = round((microtime(true) - $start) * 1000, 1);
fclose($fp);
}
} else {
// 无 socket 时,仅做数据库校验即视为"逻辑连接成功"
$reachable = true;
$latency = 0;
$port = 0;
}
$this->jsonResponse(1, $reachable ? '设备连接成功' : '设备网络不可达,请检查 IP 和网络', [
'Device' => $device,
'ConnectionInfo' => [
'ip' => $device['device_ip'],
'port' => $reachable ? $port : 502,
'reachable' => $reachable,
'latency_ms' => $latency,
],
]);
} catch (\Throwable $e) {
$this->jsonResponse(0, '连接异常: ' . $e->getMessage());
}
}
// ==================== 接口 7: 批量读取设备数据 ====================
/**
* 7. AsdBatchRead - 批量读取指定设备的测试数据
* POST /Api/asdBatchRead
*
* 请求参数 (JSON)
* DeviceCodes - 设备编码列表(数组,如 ["ASD-989A-001","ASD-989A-002"]
* Limit - 每台设备最多返回条数(默认 20)
*
* 返回:
* Result=1, DevicesData={
* "ASD-989A-001": {device_info:{...}, records:[{id,qr_code,status,step_count,test_time,...}], total:N}
* }
*/
public function asdBatchRead()
{
$this->apiAuth();
$data = $this->getJsonInput();
$deviceCodes = $data['DeviceCodes'] ?? [];
$limit = intval($data['Limit'] ?? 20);
if (empty($deviceCodes) || !is_array($deviceCodes)) {
$this->jsonResponse(0, '设备编码列表(DeviceCodes)不能为空,且必须为数组');
return;
}
$devicesData = [];
try {
$pdo = \core\db\Db::pdo();
foreach ($deviceCodes as $code) {
// 获取设备信息
$deviceModel = new AsdDevice();
$device = $deviceModel->findByCode($code);
// 获取该设备的测试记录(含统计)
$stmt = $pdo->prepare(
"SELECT r.id, r.qr_code, r.product_model AS project_name, r.tester, r.site,
r.device_code AS device, r.test_result AS status, r.test_date AS test_time, r.running_time,
COUNT(d.id) as step_count,
SUM(CASE WHEN d.test_result = 'FAIL' THEN 1 ELSE 0 END) as fail_count
FROM DGZXY_asd_test_record r
LEFT JOIN DGZXY_asd_test_detail d ON r.id = d.record_id
WHERE r.device_code = :code
GROUP BY r.id
ORDER BY r.id DESC
LIMIT " . intval($limit)
);
$stmt->execute([':code' => $code]);
$records = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// 总数
$countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device = :code");
$countStmt->execute([':code' => $code]);
$total = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0;
$devicesData[$code] = [
'device_info' => $device,
'records' => $records,
'total' => (int)$total,
];
}
} catch (\Throwable $e) {
$this->jsonResponse(0, '批量读取异常: ' . $e->getMessage());
return;
}
$this->jsonResponse(1, '批量读取成功', [
'DevicesData' => $devicesData,
'DeviceCount' => count($deviceCodes),
]);
}
// ==================== 接口 8: 删除设备数据 ====================
/**
* 8. AsdDeviceDataDelete - 按设备删除测试数据
* POST /Api/asdDeviceDataDelete
*
* 请求参数 (JSON)
* DeviceCode - 设备编码(必填)
* Confirm - 确认删除(必须为 "YES"
*
* 返回:
* Result=1, DeletedCount=N, Message="已删除 N 条测试记录"
*/
public function asdDeviceDataDelete()
{
$this->apiAuth();
$data = $this->getJsonInput();
$deviceCode = $data['DeviceCode'] ?? '';
$confirm = $data['Confirm'] ?? '';
if (empty($deviceCode)) {
$this->jsonResponse(0, '设备编码(DeviceCode)不能为空');
return;
}
if ($confirm !== 'YES') {
$this->jsonResponse(0, '请确认删除操作(Confirm=YES');
return;
}
try {
$pdo = \core\db\Db::pdo();
// 先查该设备有多少条记录
$countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device_code = :code");
$countStmt->execute([':code' => $deviceCode]);
$recordCount = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0;
if ($recordCount == 0) {
$this->jsonResponse(1, '该设备无数据可删除', ['DeletedCount' => 0]);
return;
}
// 获取所有记录 ID
$idsStmt = $pdo->prepare("SELECT id FROM DGZXY_asd_test_record WHERE device_code = :code");
$idsStmt->execute([':code' => $deviceCode]);
$ids = $idsStmt->fetchAll(\PDO::FETCH_COLUMN);
// 删除详情(级联)
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$pdo->prepare("DELETE FROM DGZXY_asd_test_detail WHERE record_id IN ({$placeholders})")->execute($ids);
// 删除主记录
$pdo->prepare("DELETE FROM DGZXY_asd_test_record WHERE device_code = :code")->execute([':code' => $deviceCode]);
$this->jsonResponse(1, "已删除 {$recordCount} 条测试记录", [
'DeletedCount' => (int)$recordCount,
]);
} catch (\Throwable $e) {
$this->jsonResponse(0, '删除失败: ' . $e->getMessage());
}
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* 前台工位看板 - 独立单页(不嵌入 AdminLTE 框架)
* 显示所有工位的实时生产状态
*/
namespace app\controllers;
use core\base\Controller;
use app\models\Workstation;
use app\models\StationDefinition;
class DashboardController extends Controller
{
public function index()
{
$this->checkLogin();
$user = $this->getCurrentUser();
$workstation = new Workstation();
$stations = $workstation->getAll();
// 统计各工位今日数据(使用 StationDefinition 统一模型)
$stats = [];
$today = date('Y-m-d');
$stationDef = new StationDefinition();
// 内置工位列表(station_definitions 中配置的)
$builtinTypes = ['inbound', 'pcb_test', 'battery', 'assembly', 'finished_test', 'warehouse', 'delivery'];
foreach ($stations as $station) {
$type = $station['station_type'];
$count = 0;
if (in_array($type, $builtinTypes)) {
$count = $stationDef->countTodayByOperator($type, $user['emp_name']);
}
$stats[$type] = [
'name' => $station['station_name'],
'code' => $station['station_code'],
'count' => $count,
'status' => $station['status'],
];
}
// 总计
$totalRecords = array_sum(array_column($stats, 'count'));
$this->assign('title', '前台工位看板');
$this->assign('user', $user);
$this->assign('stations', $stations);
$this->assign('stats', $stats);
$this->assign('totalRecords', $totalRecords);
$this->assign('today', $today);
$this->render();
}
}
+474
View File
@@ -0,0 +1,474 @@
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\FinanceRecord;
use app\models\ReceivablePayable;
/**
* 财务管理控制器
* 访问权限:超级管理员 或 财务类目管理员
*/
class FinanceController extends Controller
{
private function checkCategoryAccess()
{
$this->checkLogin();
$user = $this->getCurrentUser();
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
return $user;
}
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_FINANCE) {
return $user;
}
header('Location: ' . BASE_URL . '/Front/index');
exit;
}
public function index()
{
$user = $this->checkCategoryAccess();
$financeRecord = new FinanceRecord();
$receivablePayable = new ReceivablePayable();
// 本月收支汇总
$monthStart = date('Y-m-01');
$monthEnd = date('Y-m-t');
$summary = $financeRecord->getSummary($monthStart, $monthEnd);
$incomeTotal = 0;
$expenseTotal = 0;
foreach ($summary as $row) {
if ($row['type'] === 'income') $incomeTotal = (float)$row['total'];
if ($row['type'] === 'expense') $expenseTotal = (float)$row['total'];
}
// 应收应付统计
$receivables = $receivablePayable->getByType('receivable');
$payables = $receivablePayable->getByType('payable');
$receivableTotal = 0;
$payableTotal = 0;
foreach ($receivables as $r) {
$receivableTotal += (float)($r['amount'] ?? 0);
}
foreach ($payables as $p) {
$payableTotal += (float)($p['amount'] ?? 0);
}
$this->assign('title', '财务概览');
$this->assign('user', $user);
$this->assign('activeMenu', 'finance_dashboard');
$this->assign('incomeTotal', $incomeTotal);
$this->assign('expenseTotal', $expenseTotal);
$this->assign('profit', $incomeTotal - $expenseTotal);
$this->assign('receivableTotal', $receivableTotal);
$this->assign('payableTotal', $payableTotal);
$this->assign('recordCount', $financeRecord->getCount());
$this->render();
}
// ========== 账务管理 ==========
public function accounts()
{
$user = $this->checkCategoryAccess();
$financeRecord = new FinanceRecord();
$typeFilter = $_GET['type'] ?? '';
$startDate = $_GET['start_date'] ?? date('Y-m-01');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
if ($typeFilter || $startDate || $endDate) {
$records = $financeRecord->getByDateRange($startDate, $endDate, $typeFilter);
} else {
$records = $financeRecord->getAll();
}
$this->assign('title', '账务管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('records', $records);
$this->assign('typeFilter', $typeFilter);
$this->assign('startDate', $startDate);
$this->assign('endDate', $endDate);
$this->render();
}
public function accountsAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'record_no' => trim($_POST['record_no'] ?? ''),
'type' => trim($_POST['type'] ?? 'expense'),
'category' => trim($_POST['category'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'record_date' => trim($_POST['record_date'] ?? date('Y-m-d')),
'description' => trim($_POST['description'] ?? ''),
'related_party' => trim($_POST['related_party'] ?? ''),
'payment_method' => trim($_POST['payment_method'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
'operator' => $user['emp_name'],
];
if (empty($data['description'])) {
$this->assign('error', '摘要不能为空');
$this->assign('title', '添加账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
if ($data['amount'] <= 0) {
$this->assign('error', '金额必须大于0');
$this->assign('title', '添加账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$financeRecord = new FinanceRecord();
$financeRecord->add($data);
header('Location: ' . BASE_URL . '/Finance/accounts');
exit;
} catch (\Throwable $e) {
error_log('[accountsAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function accountsEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$financeRecord = new FinanceRecord();
$record = $financeRecord->getById($id);
if (!$record) exit('记录不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'record_no' => trim($_POST['record_no'] ?? ''),
'type' => trim($_POST['type'] ?? 'expense'),
'category' => trim($_POST['category'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'record_date' => trim($_POST['record_date'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'related_party' => trim($_POST['related_party'] ?? ''),
'payment_method' => trim($_POST['payment_method'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$financeRecord->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/Finance/accounts');
exit;
} catch (\Throwable $e) {
error_log('[accountsEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑账务记录');
$this->assign('user', $user);
$this->assign('activeMenu', 'accounts');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function accountsDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$financeRecord = new FinanceRecord();
$financeRecord->delete($id);
header('Location: ' . BASE_URL . '/Finance/accounts');
exit;
}
// ========== 应收账款 ==========
public function receivable()
{
$user = $this->checkCategoryAccess();
$receivablePayable = new ReceivablePayable();
$records = $receivablePayable->getByType('receivable');
$this->assign('title', '应收账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'receivable');
$this->assign('records', $records);
$this->render();
}
public function receivableAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'type' => 'receivable',
'party_name' => trim($_POST['party_name'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
'due_date' => trim($_POST['due_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'description' => trim($_POST['description'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
'operator' => $user['emp_name'],
];
try {
$receivablePayable = new ReceivablePayable();
$receivablePayable->add($data);
header('Location: ' . BASE_URL . '/Finance/receivable');
exit;
} catch (\Throwable $e) {
error_log('[receivableAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加应收账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'receivable');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加应收账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'receivable');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function receivableEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$receivablePayable = new ReceivablePayable();
$record = $receivablePayable->getById($id);
if (!$record) exit('记录不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'party_name' => trim($_POST['party_name'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
'due_date' => trim($_POST['due_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'description' => trim($_POST['description'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$receivablePayable->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/Finance/receivable');
exit;
} catch (\Throwable $e) {
error_log('[receivableEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑应收账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'receivable');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑应收账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'receivable');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function receivableDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$receivablePayable = new ReceivablePayable();
$receivablePayable->delete($id);
header('Location: ' . BASE_URL . '/Finance/receivable');
exit;
}
// ========== 应付账款 ==========
public function payable()
{
$user = $this->checkCategoryAccess();
$receivablePayable = new ReceivablePayable();
$records = $receivablePayable->getByType('payable');
$this->assign('title', '应付账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'payable');
$this->assign('records', $records);
$this->render();
}
public function payableAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'type' => 'payable',
'party_name' => trim($_POST['party_name'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
'due_date' => trim($_POST['due_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'description' => trim($_POST['description'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
'operator' => $user['emp_name'],
];
try {
$receivablePayable = new ReceivablePayable();
$receivablePayable->add($data);
header('Location: ' . BASE_URL . '/Finance/payable');
exit;
} catch (\Throwable $e) {
error_log('[payableAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加应付账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'payable');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加应付账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'payable');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function payableEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$receivablePayable = new ReceivablePayable();
$record = $receivablePayable->getById($id);
if (!$record) exit('记录不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'party_name' => trim($_POST['party_name'] ?? ''),
'amount' => (float)($_POST['amount'] ?? 0),
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
'due_date' => trim($_POST['due_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'description' => trim($_POST['description'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$receivablePayable->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/Finance/payable');
exit;
} catch (\Throwable $e) {
error_log('[payableEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑应付账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'payable');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑应付账款');
$this->assign('user', $user);
$this->assign('activeMenu', 'payable');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function payableDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$receivablePayable = new ReceivablePayable();
$receivablePayable->delete($id);
header('Location: ' . BASE_URL . '/Finance/payable');
exit;
}
// ========== 财务报表 ==========
public function report()
{
$user = $this->checkCategoryAccess();
$financeRecord = new FinanceRecord();
$month = $_GET['month'] ?? date('Y-m');
$startDate = $month . '-01';
$endDate = date('Y-m-t', strtotime($startDate));
$summary = $financeRecord->getSummary($startDate, $endDate);
$incomeTotal = 0;
$expenseTotal = 0;
foreach ($summary as $row) {
if ($row['type'] === 'income') $incomeTotal = (float)$row['total'];
if ($row['type'] === 'expense') $expenseTotal = (float)$row['total'];
}
// 按类别统计
$records = $financeRecord->getByDateRange($startDate, $endDate);
$categorySummary = [];
foreach ($records as $r) {
$cat = $r['category'] ?: '未分类';
$type = $r['type'];
if (!isset($categorySummary[$cat])) {
$categorySummary[$cat] = ['income' => 0, 'expense' => 0];
}
$categorySummary[$cat][$type] += (float)$r['amount'];
}
$this->assign('title', '财务报表');
$this->assign('user', $user);
$this->assign('activeMenu', 'report');
$this->assign('incomeTotal', $incomeTotal);
$this->assign('expenseTotal', $expenseTotal);
$this->assign('profit', $incomeTotal - $expenseTotal);
$this->assign('categorySummary', $categorySummary);
$this->assign('month', $month);
$this->render();
}
}
File diff suppressed because it is too large Load Diff
+540
View File
@@ -0,0 +1,540 @@
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\HrEmployee;
use app\models\Asset;
use app\models\Document;
/**
* 综合管理控制器
* 访问权限:超级管理员 或 综合管理类目管理员
*/
class GeneralController extends Controller
{
private function checkCategoryAccess()
{
$this->checkLogin();
$user = $this->getCurrentUser();
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
return $user;
}
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_GENERAL) {
return $user;
}
header('Location: ' . BASE_URL . '/Front/index');
exit;
}
public function index()
{
$user = $this->checkCategoryAccess();
$hrEmployee = new HrEmployee();
$asset = new Asset();
$document = new Document();
$this->assign('title', '综合管理概览');
$this->assign('user', $user);
$this->assign('activeMenu', 'general_dashboard');
$this->assign('employeeCount', $hrEmployee->getCount());
$this->assign('assetStats', $asset->getCount());
$this->assign('documentCount', $document->getCount());
$this->render();
}
// ========== 人事管理 ==========
public function hr()
{
$user = $this->checkCategoryAccess();
$hrEmployee = new HrEmployee();
$employees = $hrEmployee->getAll();
$this->assign('title', '人事管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('employees', $employees);
$this->render();
}
public function hrAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'emp_no' => trim($_POST['emp_no'] ?? ''),
'name' => trim($_POST['name'] ?? ''),
'gender' => trim($_POST['gender'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'position' => trim($_POST['position'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'id_card' => trim($_POST['id_card'] ?? ''),
'entry_date' => trim($_POST['entry_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'active'),
'education' => trim($_POST['education'] ?? ''),
'emergency_contact' => trim($_POST['emergency_contact'] ?? ''),
'emergency_phone' => trim($_POST['emergency_phone'] ?? ''),
'address' => trim($_POST['address'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
if (empty($data['name'])) {
$this->assign('error', '姓名不能为空');
$this->assign('title', '添加员工');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$hrEmployee = new HrEmployee();
$hrEmployee->add($data);
header('Location: ' . BASE_URL . '/General/hr');
exit;
} catch (\Throwable $e) {
error_log('[hrAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加员工');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加员工');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function hrEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$hrEmployee = new HrEmployee();
$record = $hrEmployee->getById($id);
if (!$record) exit('员工不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'emp_no' => trim($_POST['emp_no'] ?? ''),
'name' => trim($_POST['name'] ?? ''),
'gender' => trim($_POST['gender'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'position' => trim($_POST['position'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'id_card' => trim($_POST['id_card'] ?? ''),
'entry_date' => trim($_POST['entry_date'] ?? ''),
'leave_date' => trim($_POST['leave_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'active'),
'education' => trim($_POST['education'] ?? ''),
'emergency_contact' => trim($_POST['emergency_contact'] ?? ''),
'emergency_phone' => trim($_POST['emergency_phone'] ?? ''),
'address' => trim($_POST['address'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$hrEmployee->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/General/hr');
exit;
} catch (\Throwable $e) {
error_log('[hrEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑员工');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑员工');
$this->assign('user', $user);
$this->assign('activeMenu', 'hr');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function hrDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$hrEmployee = new HrEmployee();
$hrEmployee->delete($id);
header('Location: ' . BASE_URL . '/General/hr');
exit;
}
// ========== 资产管理 ==========
public function asset()
{
$user = $this->checkCategoryAccess();
$asset = new Asset();
$assets = $asset->getAll();
$this->assign('title', '资产管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('assets', $assets);
$this->render();
}
public function assetAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'asset_no' => trim($_POST['asset_no'] ?? ''),
'name' => trim($_POST['name'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'model' => trim($_POST['model'] ?? ''),
'quantity' => (int)($_POST['quantity'] ?? 1),
'unit' => trim($_POST['unit'] ?? '台'),
'purchase_price' => (float)($_POST['purchase_price'] ?? 0),
'purchase_date' => trim($_POST['purchase_date'] ?? ''),
'supplier' => trim($_POST['supplier'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'user_name' => trim($_POST['user_name'] ?? ''),
'location' => trim($_POST['location'] ?? ''),
'status' => trim($_POST['status'] ?? 'normal'),
'warranty_expire' => trim($_POST['warranty_expire'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
if (empty($data['name'])) {
$this->assign('error', '资产名称不能为空');
$this->assign('title', '添加资产');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$asset = new Asset();
$asset->add($data);
header('Location: ' . BASE_URL . '/General/asset');
exit;
} catch (\Throwable $e) {
error_log('[assetAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加资产');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加资产');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function assetEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$asset = new Asset();
$record = $asset->getById($id);
if (!$record) exit('资产不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'asset_no' => trim($_POST['asset_no'] ?? ''),
'name' => trim($_POST['name'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'model' => trim($_POST['model'] ?? ''),
'quantity' => (int)($_POST['quantity'] ?? 1),
'unit' => trim($_POST['unit'] ?? '台'),
'purchase_price' => (float)($_POST['purchase_price'] ?? 0),
'purchase_date' => trim($_POST['purchase_date'] ?? ''),
'supplier' => trim($_POST['supplier'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'user_name' => trim($_POST['user_name'] ?? ''),
'location' => trim($_POST['location'] ?? ''),
'status' => trim($_POST['status'] ?? 'normal'),
'warranty_expire' => trim($_POST['warranty_expire'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$asset->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/General/asset');
exit;
} catch (\Throwable $e) {
error_log('[assetEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑资产');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑资产');
$this->assign('user', $user);
$this->assign('activeMenu', 'asset');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function assetDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$asset = new Asset();
$asset->delete($id);
header('Location: ' . BASE_URL . '/General/asset');
exit;
}
// ========== 文档管理 ==========
public function document()
{
$user = $this->checkCategoryAccess();
$document = new Document();
$documents = $document->getAll();
$categories = $document->getCategories();
$this->assign('title', '文档管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('documents', $documents);
$this->assign('categories', $categories);
$this->render();
}
public function documentAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'title' => trim($_POST['title'] ?? ''),
'doc_no' => trim($_POST['doc_no'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'version' => trim($_POST['version'] ?? '1.0'),
'author' => trim($_POST['author'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'status' => trim($_POST['status'] ?? 'active'),
'keywords' => trim($_POST['keywords'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
'operator' => $user['emp_name'],
];
// 文件上传 — 仅允许安全文档类型
if (isset($_FILES['doc_file']) && $_FILES['doc_file']['error'] === UPLOAD_ERR_OK) {
$allowedExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'zip', 'rar', 'jpg', 'jpeg', 'png', 'gif'];
$allowedMimeTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'text/csv',
'application/zip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'image/jpeg',
'image/png',
'image/gif',
];
$originalName = $_FILES['doc_file']['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$tmpPath = $_FILES['doc_file']['tmp_name'];
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmpPath);
if (!in_array($ext, $allowedExtensions, true) || !in_array($mime, $allowedMimeTypes, true)) {
error_log('[documentAdd] Upload rejected: ext=' . $ext . ' mime=' . $mime . ' file=' . $originalName);
$this->assign('error', '不支持的文件类型,仅允许常见文档、图片和压缩包格式');
$this->assign('title', '添加文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
$uploadDir = APP_PATH . 'static/uploads/documents/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$saveName = date('YmdHis') . '_' . uniqid() . '.' . $ext;
$savePath = $uploadDir . $saveName;
if (move_uploaded_file($tmpPath, $savePath)) {
$data['file_name'] = $originalName;
$data['file_path'] = '/static/uploads/documents/' . $saveName;
$data['file_size'] = $_FILES['doc_file']['size'];
$data['file_type'] = $ext;
}
}
if (empty($data['title'])) {
$this->assign('error', '文档标题不能为空');
$this->assign('title', '添加文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$document = new Document();
$document->add($data);
header('Location: ' . BASE_URL . '/General/document');
exit;
} catch (\Throwable $e) {
error_log('[documentAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function documentEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$document = new Document();
$record = $document->getById($id);
if (!$record) exit('文档不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'title' => trim($_POST['title'] ?? ''),
'doc_no' => trim($_POST['doc_no'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'version' => trim($_POST['version'] ?? '1.0'),
'author' => trim($_POST['author'] ?? ''),
'department' => trim($_POST['department'] ?? ''),
'status' => trim($_POST['status'] ?? 'active'),
'keywords' => trim($_POST['keywords'] ?? ''),
'description' => trim($_POST['description'] ?? ''),
];
// 文件上传 — 仅允许安全文档类型
if (isset($_FILES['doc_file']) && $_FILES['doc_file']['error'] === UPLOAD_ERR_OK) {
$allowedExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'zip', 'rar', 'jpg', 'jpeg', 'png', 'gif'];
$allowedMimeTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'text/csv',
'application/zip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'image/jpeg',
'image/png',
'image/gif',
];
$originalName = $_FILES['doc_file']['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$tmpPath = $_FILES['doc_file']['tmp_name'];
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmpPath);
if (!in_array($ext, $allowedExtensions, true) || !in_array($mime, $allowedMimeTypes, true)) {
error_log('[documentEdit] Upload rejected: ext=' . $ext . ' mime=' . $mime . ' file=' . $originalName);
$this->assign('error', '不支持的文件类型,仅允许常见文档、图片和压缩包格式');
$this->assign('title', '编辑文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
$uploadDir = APP_PATH . 'static/uploads/documents/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$saveName = date('YmdHis') . '_' . uniqid() . '.' . $ext;
$savePath = $uploadDir . $saveName;
if (move_uploaded_file($tmpPath, $savePath)) {
$data['file_name'] = $originalName;
$data['file_path'] = '/static/uploads/documents/' . $saveName;
$data['file_size'] = $_FILES['doc_file']['size'];
$data['file_type'] = $ext;
}
}
try {
$document->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/General/document');
exit;
} catch (\Throwable $e) {
error_log('[documentEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑文档');
$this->assign('user', $user);
$this->assign('activeMenu', 'document');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function documentDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$document = new Document();
$document->delete($id);
header('Location: ' . BASE_URL . '/General/document');
exit;
}
}
+726
View File
@@ -0,0 +1,726 @@
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\Supplier;
use app\models\PurchaseOrder;
use app\models\PurchaseItem;
use app\models\Inventory;
use app\models\SalesOrder;
use app\models\SalesItem;
/**
* 进销存管理控制器
* 访问权限:超级管理员 或 进销存类目管理员
*/
class InventoryController extends Controller
{
private function checkCategoryAccess()
{
$this->checkLogin();
$user = $this->getCurrentUser();
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
return $user;
}
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_INVENTORY) {
return $user;
}
header('Location: ' . BASE_URL . '/Front/index');
exit;
}
public function index()
{
$user = $this->checkCategoryAccess();
$supplier = new Supplier();
$purchaseOrder = new PurchaseOrder();
$inventory = new Inventory();
$salesOrder = new SalesOrder();
$this->assign('title', '进销存概览');
$this->assign('user', $user);
$this->assign('activeMenu', 'inventory_dashboard');
$this->assign('supplierCount', count($supplier->getAll()));
$this->assign('purchaseCount', $purchaseOrder->getCount());
$this->assign('stockCount', $inventory->getCount());
$this->assign('salesCount', $salesOrder->getCount());
$this->assign('lowStockItems', $inventory->getLowStock());
$this->render();
}
// ========== 供应商管理 ==========
public function supplier()
{
$user = $this->checkCategoryAccess();
$supplier = new Supplier();
$suppliers = $supplier->getAll();
$this->assign('title', '供应商管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('suppliers', $suppliers);
$this->render();
}
public function supplierAdd()
{
$user = $this->checkCategoryAccess();
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
exit('无权限访问');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'name' => trim($_POST['name'] ?? ''),
'contact_person' => trim($_POST['contact_person'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'address' => trim($_POST['address'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
'status' => (int)($_POST['status'] ?? 1),
];
if (empty($data['name'])) {
$this->assign('error', '供应商名称不能为空');
$this->assign('title', '添加供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$supplier = new Supplier();
$supplier->add($data);
header('Location: ' . BASE_URL . '/Inventory/supplier');
exit;
} catch (\Throwable $e) {
error_log('[supplierAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function supplierEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$supplier = new Supplier();
$record = $supplier->getById($id);
if (!$record) exit('供应商不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'name' => trim($_POST['name'] ?? ''),
'contact_person' => trim($_POST['contact_person'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'address' => trim($_POST['address'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
'status' => (int)($_POST['status'] ?? 1),
];
if (empty($data['name'])) {
$this->assign('error', '供应商名称不能为空');
$this->assign('title', '编辑供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$supplier->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/Inventory/supplier');
exit;
} catch (\Throwable $e) {
error_log('[supplierEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑供应商');
$this->assign('user', $user);
$this->assign('activeMenu', 'supplier');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function supplierDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$supplier = new Supplier();
$supplier->delete($id);
header('Location: ' . BASE_URL . '/Inventory/supplier');
exit;
}
// ========== 采购管理 ==========
public function purchase()
{
$user = $this->checkCategoryAccess();
$purchaseOrder = new PurchaseOrder();
$orders = $purchaseOrder->getAll();
$this->assign('title', '采购管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('orders', $orders);
$this->render();
}
public function purchaseAdd()
{
$user = $this->checkCategoryAccess();
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
exit('无权限访问');
}
$supplierModel = new Supplier();
$suppliers = $supplierModel->getActiveList();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$supplierInfo = $supplierModel->getById($supplierId);
$orderData = [
'order_no' => trim($_POST['order_no'] ?? ''),
'supplier_id' => $supplierId,
'supplier_name' => $supplierInfo['name'] ?? '',
'order_date' => trim($_POST['order_date'] ?? date('Y-m-d')),
'total_amount' => 0,
'status' => trim($_POST['status'] ?? 'pending'),
'remark' => trim($_POST['remark'] ?? ''),
'operator' => $user['emp_name'],
];
if (empty($orderData['order_no'])) {
$this->assign('error', '采购单号不能为空');
$this->assign('title', '添加采购订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('suppliers', $suppliers);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$purchaseOrder = new PurchaseOrder();
$purchaseOrder->add($orderData);
$orderId = $this->pdo->lastInsertId();
$productNames = $_POST['product_name'] ?? [];
$productModels = $_POST['product_model'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$units = $_POST['unit'] ?? [];
$unitPrices = $_POST['unit_price'] ?? [];
$totalAmount = 0;
$purchaseItem = new PurchaseItem();
foreach ($productNames as $i => $pname) {
if (empty(trim($pname))) continue;
$qty = (int)($quantities[$i] ?? 0);
$price = (float)($unitPrices[$i] ?? 0);
$amount = $qty * $price;
$totalAmount += $amount;
$purchaseItem->add([
'order_id' => $orderId,
'product_name' => trim($pname),
'product_model' => trim($productModels[$i] ?? ''),
'quantity' => $qty,
'unit' => trim($units[$i] ?? '个'),
'unit_price' => $price,
'amount' => $amount,
]);
}
$purchaseOrder->where(['id = :id'], [':id' => $orderId])->update(['total_amount' => $totalAmount]);
header('Location: ' . BASE_URL . '/Inventory/purchase');
exit;
} catch (\Throwable $e) {
error_log('[purchaseAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加采购订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('suppliers', $suppliers);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加采购订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('suppliers', $suppliers);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function purchaseDetail()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$purchaseOrder = new PurchaseOrder();
$order = $purchaseOrder->getById($id);
if (!$order) exit('订单不存在');
$purchaseItem = new PurchaseItem();
$items = $purchaseItem->getByOrder($id);
$this->assign('title', '采购订单详情');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('order', $order);
$this->assign('items', $items);
$this->render();
}
public function purchaseEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$purchaseOrder = new PurchaseOrder();
$order = $purchaseOrder->getById($id);
if (!$order) exit('订单不存在');
$supplierModel = new Supplier();
$suppliers = $supplierModel->getActiveList();
$purchaseItem = new PurchaseItem();
$items = $purchaseItem->getByOrder($id);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$supplierInfo = $supplierModel->getById($supplierId);
$orderData = [
'order_no' => trim($_POST['order_no'] ?? ''),
'supplier_id' => $supplierId,
'supplier_name' => $supplierInfo['name'] ?? '',
'order_date' => trim($_POST['order_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$purchaseOrder->where(['id = :id'], [':id' => $id])->update($orderData);
$purchaseItem->deleteByOrder($id);
$productNames = $_POST['product_name'] ?? [];
$productModels = $_POST['product_model'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$units = $_POST['unit'] ?? [];
$unitPrices = $_POST['unit_price'] ?? [];
$totalAmount = 0;
foreach ($productNames as $i => $pname) {
if (empty(trim($pname))) continue;
$qty = (int)($quantities[$i] ?? 0);
$price = (float)($unitPrices[$i] ?? 0);
$amount = $qty * $price;
$totalAmount += $amount;
$purchaseItem->add([
'order_id' => $id,
'product_name' => trim($pname),
'product_model' => trim($productModels[$i] ?? ''),
'quantity' => $qty,
'unit' => trim($units[$i] ?? '个'),
'unit_price' => $price,
'amount' => $amount,
]);
}
$purchaseOrder->where(['id = :id'], [':id' => $id])->update(['total_amount' => $totalAmount]);
header('Location: ' . BASE_URL . '/Inventory/purchase');
exit;
} catch (\Throwable $e) {
error_log('[purchaseEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑采购订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('order', $order);
$this->assign('items', $items);
$this->assign('suppliers', $suppliers);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑采购订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'purchase');
$this->assign('order', $order);
$this->assign('items', $items);
$this->assign('suppliers', $suppliers);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function purchaseDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$purchaseItem = new PurchaseItem();
$purchaseItem->deleteByOrder($id);
$purchaseOrder = new PurchaseOrder();
$purchaseOrder->delete($id);
header('Location: ' . BASE_URL . '/Inventory/purchase');
exit;
}
// ========== 库存管理 ==========
public function stock()
{
$user = $this->checkCategoryAccess();
$inventory = new Inventory();
$stocks = $inventory->getAll();
$this->assign('title', '库存管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('stocks', $stocks);
$this->render();
}
public function stockAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'product_name' => trim($_POST['product_name'] ?? ''),
'product_model' => trim($_POST['product_model'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'quantity' => (int)($_POST['quantity'] ?? 0),
'unit' => trim($_POST['unit'] ?? '个'),
'unit_price' => (float)($_POST['unit_price'] ?? 0),
'min_stock' => (int)($_POST['min_stock'] ?? 0),
'max_stock' => (int)($_POST['max_stock'] ?? 0),
'warehouse' => trim($_POST['warehouse'] ?? ''),
'location' => trim($_POST['location'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
if (empty($data['product_name'])) {
$this->assign('error', '产品名称不能为空');
$this->assign('title', '添加库存');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$inventory = new Inventory();
$inventory->add($data);
header('Location: ' . BASE_URL . '/Inventory/stock');
exit;
} catch (\Throwable $e) {
error_log('[stockAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加库存');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加库存');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function stockEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$inventory = new Inventory();
$record = $inventory->getById($id);
if (!$record) exit('库存记录不存在');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$data = [
'product_name' => trim($_POST['product_name'] ?? ''),
'product_model' => trim($_POST['product_model'] ?? ''),
'category' => trim($_POST['category'] ?? ''),
'quantity' => (int)($_POST['quantity'] ?? 0),
'unit' => trim($_POST['unit'] ?? '个'),
'unit_price' => (float)($_POST['unit_price'] ?? 0),
'min_stock' => (int)($_POST['min_stock'] ?? 0),
'max_stock' => (int)($_POST['max_stock'] ?? 0),
'warehouse' => trim($_POST['warehouse'] ?? ''),
'location' => trim($_POST['location'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$inventory->where(['id = :id'], [':id' => $id])->update($data);
header('Location: ' . BASE_URL . '/Inventory/stock');
exit;
} catch (\Throwable $e) {
error_log('[stockEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑库存');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑库存');
$this->assign('user', $user);
$this->assign('activeMenu', 'stock');
$this->assign('record', $record);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function stockDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$inventory = new Inventory();
$inventory->delete($id);
header('Location: ' . BASE_URL . '/Inventory/stock');
exit;
}
// ========== 销售管理 ==========
public function sales()
{
$user = $this->checkCategoryAccess();
$salesOrder = new SalesOrder();
$orders = $salesOrder->getAll();
$this->assign('title', '销售管理');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('orders', $orders);
$this->render();
}
public function salesAdd()
{
$user = $this->checkCategoryAccess();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$orderData = [
'order_no' => trim($_POST['order_no'] ?? ''),
'customer_name' => trim($_POST['customer_name'] ?? ''),
'order_date' => trim($_POST['order_date'] ?? date('Y-m-d')),
'total_amount' => 0,
'status' => trim($_POST['status'] ?? 'pending'),
'remark' => trim($_POST['remark'] ?? ''),
'operator' => $user['emp_name'],
];
if (empty($orderData['order_no'])) {
$this->assign('error', '销售单号不能为空');
$this->assign('title', '添加销售订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
try {
$salesOrder = new SalesOrder();
$salesOrder->add($orderData);
$orderId = $this->pdo->lastInsertId();
$productNames = $_POST['product_name'] ?? [];
$productModels = $_POST['product_model'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$units = $_POST['unit'] ?? [];
$unitPrices = $_POST['unit_price'] ?? [];
$totalAmount = 0;
$salesItem = new SalesItem();
foreach ($productNames as $i => $pname) {
if (empty(trim($pname))) continue;
$qty = (int)($quantities[$i] ?? 0);
$price = (float)($unitPrices[$i] ?? 0);
$amount = $qty * $price;
$totalAmount += $amount;
$salesItem->add([
'order_id' => $orderId,
'product_name' => trim($pname),
'product_model' => trim($productModels[$i] ?? ''),
'quantity' => $qty,
'unit' => trim($units[$i] ?? '个'),
'unit_price' => $price,
'amount' => $amount,
]);
}
$salesOrder->where(['id = :id'], [':id' => $orderId])->update(['total_amount' => $totalAmount]);
header('Location: ' . BASE_URL . '/Inventory/sales');
exit;
} catch (\Throwable $e) {
error_log('[salesAdd] ' . $e->getMessage());
$this->assign('error', '添加失败:' . $e->getMessage());
$this->assign('title', '添加销售订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '添加销售订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function salesDetail()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$salesOrder = new SalesOrder();
$order = $salesOrder->getById($id);
if (!$order) exit('订单不存在');
$salesItem = new SalesItem();
$items = $salesItem->getByOrder($id);
$this->assign('title', '销售订单详情');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('order', $order);
$this->assign('items', $items);
$this->render();
}
public function salesEdit()
{
$user = $this->checkCategoryAccess();
$id = (int)($_GET['id'] ?? 0);
$salesOrder = new SalesOrder();
$order = $salesOrder->getById($id);
if (!$order) exit('订单不存在');
$salesItem = new SalesItem();
$items = $salesItem->getByOrder($id);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->csrfVerify();
$orderData = [
'order_no' => trim($_POST['order_no'] ?? ''),
'customer_name' => trim($_POST['customer_name'] ?? ''),
'order_date' => trim($_POST['order_date'] ?? ''),
'status' => trim($_POST['status'] ?? 'pending'),
'remark' => trim($_POST['remark'] ?? ''),
];
try {
$salesOrder->where(['id = :id'], [':id' => $id])->update($orderData);
$salesItem->deleteByOrder($id);
$productNames = $_POST['product_name'] ?? [];
$productModels = $_POST['product_model'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$units = $_POST['unit'] ?? [];
$unitPrices = $_POST['unit_price'] ?? [];
$totalAmount = 0;
foreach ($productNames as $i => $pname) {
if (empty(trim($pname))) continue;
$qty = (int)($quantities[$i] ?? 0);
$price = (float)($unitPrices[$i] ?? 0);
$amount = $qty * $price;
$totalAmount += $amount;
$salesItem->add([
'order_id' => $id,
'product_name' => trim($pname),
'product_model' => trim($productModels[$i] ?? ''),
'quantity' => $qty,
'unit' => trim($units[$i] ?? '个'),
'unit_price' => $price,
'amount' => $amount,
]);
}
$salesOrder->where(['id = :id'], [':id' => $id])->update(['total_amount' => $totalAmount]);
header('Location: ' . BASE_URL . '/Inventory/sales');
exit;
} catch (\Throwable $e) {
error_log('[salesEdit] ' . $e->getMessage());
$this->assign('error', '更新失败:' . $e->getMessage());
$this->assign('title', '编辑销售订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('order', $order);
$this->assign('items', $items);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
return;
}
}
$this->assign('title', '编辑销售订单');
$this->assign('user', $user);
$this->assign('activeMenu', 'sales');
$this->assign('order', $order);
$this->assign('items', $items);
$this->assign('csrfToken', $this->csrfToken());
$this->render();
}
public function salesDelete()
{
$this->checkLogin();
$this->requireMethod('post');
$this->csrfVerify();
$id = (int)($_POST['id'] ?? 0);
$salesItem = new SalesItem();
$salesItem->deleteByOrder($id);
$salesOrder = new SalesOrder();
$salesOrder->delete($id);
header('Location: ' . BASE_URL . '/Inventory/sales');
exit;
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\Employee;
class LoginController extends Controller
{
// 暴力破解防护配置
const MAX_LOGIN_ATTEMPTS = 5; // 最大尝试次数
const LOCKOUT_DURATION = 900; // 锁定时间(秒),15分钟
const ATTEMPT_WINDOW = 300; // 计数窗口(秒),5分钟
public function index()
{
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 登录页不强制 CSRF 验证,原因:
// 1. 部分设备/浏览器 Session Cookie 不稳定(SameSite=Lax、隐私模式等)
// 导致每次请求创建新 SessionCSRF Token 永远对不上
// 2. 登录本身无敏感副作用(不修改数据),攻击者 CSRF 登录成功
// 也无法获知密码,且暴力破解防护仍在生效
// 3. 登录成功后的所有操作仍受 CSRF 保护
$emp_no = isset($_POST['emp_no']) ? trim($_POST['emp_no']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if (empty($emp_no) || empty($password)) {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
error_log("[LOGIN_DIAG] POST_EMPTY emp_no_len=" . strlen($emp_no) . " pwd_len=" . strlen($password) . " ua={$ua} sess_hash={$sess}");
$error = '请输入员工编号和密码';
} else {
// 暴力破解检查
$lockError = $this->checkBruteForce($emp_no);
if ($lockError) {
$error = $lockError;
} else {
$employee = new Employee();
$user = $employee->validateLogin($emp_no, $password);
if ($user) {
// 登录成功 — 清除失败记录
$this->clearLoginAttempts($emp_no);
// 重新生成 Session ID 防止会话固定攻击
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['emp_no'] = $user['emp_no'];
$_SESSION['emp_name'] = $user['emp_name'];
$_SESSION['role'] = $user['role'];
$_SESSION['category'] = $user['category'] ?? self::CATEGORY_MES;
// 根据角色和类目跳转到不同页面
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
// 超级管理员:默认进入 MES 后台,可通过顶部切换类目
header('Location: ' . BASE_URL . '/Admin/index');
} elseif ($user['role'] === self::ROLE_ADMIN) {
// 管理员:根据分配的类目跳转到对应管理界面
$cat = $user['category'] ?? self::CATEGORY_MES;
$homeRoute = self::CATEGORY_HOME[$cat] ?? 'Admin/index';
header('Location: ' . BASE_URL . '/' . $homeRoute);
} else {
// 操作员:进入前台工位界面
header('Location: ' . BASE_URL . '/Front/index');
}
exit;
} else {
// 记录失败
$this->recordLoginAttempt($emp_no);
$remaining = $this->getRemainingAttempts($emp_no);
// 诊断日志:validateLogin 返回 false(详细日志已在 Employee 中记录)
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
error_log("[LOGIN_DIAG] RESULT=FAIL emp_no_len=" . strlen($emp_no) . " remaining={$remaining} ua={$ua} sess_hash={$sess}");
$error = '员工编号或密码错误' . ($remaining > 0 ? "(还可尝试 {$remaining} 次)" : '(账号已临时锁定)');
}
}
}
}
$this->assign('title', 'MES 系统登录');
$this->assign('error', $error);
$this->assign('csrf_token', $this->csrfToken());
$this->render();
}
/**
* 检查暴力破解状态
*/
private function checkBruteForce($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
// 清理过期记录
$now = time();
$attempts = array_filter($attempts, function($t) use ($now) {
return $t > ($now - self::ATTEMPT_WINDOW);
});
if (count($attempts) >= self::MAX_LOGIN_ATTEMPTS) {
$lastAttempt = max($attempts);
$waitTime = self::LOCKOUT_DURATION - ($now - $lastAttempt);
if ($waitTime > 0) {
$minutes = ceil($waitTime / 60);
return "登录尝试次数过多,请 {$minutes} 分钟后再试";
}
// 锁定时间已过,清除记录
unset($_SESSION[$key]);
return null;
}
$_SESSION[$key] = array_values($attempts);
return null;
}
/**
* 记录失败登录尝试
*/
private function recordLoginAttempt($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
$now = time();
$attempts = array_filter($attempts, function($t) use ($now) {
return $t > ($now - self::ATTEMPT_WINDOW);
});
$attempts[] = $now;
$_SESSION[$key] = array_values($attempts);
}
/**
* 清除登录尝试记录
*/
private function clearLoginAttempts($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
unset($_SESSION[$key]);
}
/**
* 获取剩余尝试次数
*/
private function getRemainingAttempts($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
return max(0, self::MAX_LOGIN_ATTEMPTS - count($attempts));
}
public function logout()
{
session_destroy();
header('Location: ' . BASE_URL . '/');
exit;
}
}
File diff suppressed because it is too large Load Diff
+975
View File
@@ -0,0 +1,975 @@
<?php
/**
* 工位字段配置辅助函数
* 用于后台配置和前端动态渲染
*/
/**
* 获取各工位类型的默认字段配置
*/
function getDefaultFieldsConfig($stationType) {
$configs = [
'inbound' => [
['name'=>'product_type','label'=>'类型','type'=>'select','source'=>'type_list','required'=>true,'enabled'=>true,'placeholder'=>'请选择类型','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>false,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'pcb_test' => [
['name'=>'product_type','label'=>'产品类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'PCBA','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品类型','fixed_value'=>'PCBA'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'static_power','label'=>'静态功耗','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'静态功耗'],
['name'=>'input_output','label'=>'输入输出','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'正常/异常','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'输入输出'],
['name'=>'test_status','label'=>'测试状态','type'=>'select','source'=>'test_status_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试状态'],
['name'=>'serial_no','label'=>'PCBA序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描 PCBA 序列号后按回车','col_width'=>6,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'PCBA序列号'],
],
'battery' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'电池','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'电池'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'voltage_platform','label'=>'电压平台','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'如:3.7V','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'电压平台'],
['name'=>'no_load_voltage','label'=>'空载电压','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'空载电压'],
['name'=>'internal_resistance','label'=>'电池内阻','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'0.0000','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'电池内阻'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'assembly' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'shell_color','label'=>'外壳颜色','type'=>'select','source'=>'shell_color_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择外壳颜色','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'外壳颜色'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描成品序列号','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
['name'=>'battery_serial','label'=>'电池序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描电池序列号','col_width'=>3,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'电池序列号'],
['name'=>'pcb_serial','label'=>'PCB序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描PCB序列号','col_width'=>2,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'PCB序列号'],
],
'finished_test' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>false,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'test_item','label'=>'测试项目','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'如:功能测试','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试项目'],
['name'=>'test_result','label'=>'测试结果','type'=>'select','source'=>'test_result_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试结果'],
['name'=>'test_steps','label'=>'测试步骤','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'步骤描述','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'测试步骤'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描序列号后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
],
'warehouse' => [
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>false,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'finished_serial','label'=>'成品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描成品序列号后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'成品序列号'],
['name'=>'box_serial','label'=>'箱序列号','type'=>'text','required'=>false,'enabled'=>true,'placeholder'=>'箱序列号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'箱序列号'],
],
'delivery' => [
['name'=>'customer_name','label'=>'客户名称','type'=>'select','source'=>'customers','required'=>true,'enabled'=>true,'placeholder'=>'请选择客户','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'客户名称'],
['name'=>'finished_serial','label'=>'成品/箱序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描序列号后按回车(自动识别型号)','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'序列号'],
['name'=>'product_model','label'=>'产品型号','type'=>'hidden','required'=>false,'enabled'=>true,'placeholder'=>'','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
],
'semi_finished' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'半成品','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'半成品'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
],
'aging' => [
['name'=>'product_type','label'=>'类型','type'=>'hidden','required'=>true,'enabled'=>true,'placeholder'=>'半成品','col_width'=>0,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'类型','fixed_value'=>'半成品'],
['name'=>'product_model','label'=>'产品型号','type'=>'select','source'=>'product_models','required'=>true,'enabled'=>true,'placeholder'=>'请选择产品型号','col_width'=>3,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'产品型号'],
['name'=>'serial_no','label'=>'产品序列号','type'=>'text','required'=>true,'enabled'=>true,'placeholder'=>'扫描/输入后按回车','col_width'=>4,'is_scan'=>true,'show_in_table'=>true,'table_label'=>'产品序列号'],
['name'=>'aging_duration','label'=>'老化时长(h)','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'如:24','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化时长'],
['name'=>'aging_temp','label'=>'老化温度(℃)','type'=>'number','required'=>false,'enabled'=>true,'placeholder'=>'如:85','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化温度'],
['name'=>'aging_result','label'=>'老化结果','type'=>'select','source'=>'test_result_options','required'=>false,'enabled'=>true,'placeholder'=>'请选择','col_width'=>2,'is_scan'=>false,'show_in_table'=>true,'table_label'=>'老化结果'],
],
];
$fields = isset($configs[$stationType]) ? $configs[$stationType] : [];
// ★ 关键修复:附加默认 _meta 信息
// 当 workstation.fields_config 为空时,FrontController::getStationFieldConfig()
// 会 fallback 到 getDefaultFieldsConfig(),但之前没有附加 _meta
// 导致 model_type_filter、type_filter 等元信息丢失。
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$fields['_meta'] = $meta;
}
return $fields;
}
/**
* 获取各工位类型的默认元信息配置
* 当数据库 fields_config 为空时作为兜底
*/
function getDefaultMetaConfig($stationType) {
$metas = [
'inbound' => [
'model_type_filter' => '半成品',
'type_filter' => 'inbound',
],
'pcb_test' => [
'model_type_filter' => 'PCBA',
],
'battery' => [
'model_type_filter' => '电池',
],
'assembly' => [
'model_type_filter' => '成品',
'extra_data_sources' => ['shellColors' => true],
],
'finished_test' => [
'model_type_filter' => '成品',
],
'warehouse' => [
'model_type_filter' => '成品',
],
'delivery' => [],
'semi_finished' => [
'model_type_filter' => '半成品',
],
'aging' => [
'model_type_filter' => '半成品',
],
];
return $metas[$stationType] ?? [];
}
/**
* 解析工位字段配置
* 返回数组,如果为空则使用默认配置
*
* JSON 格式支持两种结构:
* 1. 纯字段数组:[{name,label,...}, ...] (旧格式,兼容)
* 2. 元信息 + 字段:{"_meta":{...}, "fields":[{...}, ...]} (新格式)
*
* _meta 支持的字段:
* - model_type_filter: string 产品型号筛选类型(如 "成品"、"PCBA"、"电池"
* - type_filter: string 类型列表筛选方式(如 "inbound"、"enabled"
* - extra_data_sources: object 额外数据源配置
*/
function parseFieldsConfig($stationType, $fieldsConfigJson = '') {
if (!empty($fieldsConfigJson)) {
$config = json_decode($fieldsConfigJson, true);
if (is_array($config) && !empty($config)) {
// 新格式:{"_meta":{...}, "fields":[...]}
if (isset($config['_meta']) || isset($config['fields'])) {
$result = $config['fields'] ?? [];
$meta = $config['_meta'] ?? [];
// 用默认元信息补全缺失的 key(如 model_type_filter
$defaultMeta = getDefaultMetaConfig($stationType);
if (!empty($defaultMeta)) {
foreach ($defaultMeta as $k => $v) {
if (!array_key_exists($k, $meta)) {
$meta[$k] = $v;
}
}
}
$result['_meta'] = $meta;
return $result;
}
// 旧格式:纯字段数组 — 附加默认元信息
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$config['_meta'] = $meta;
}
return $config;
}
}
// 回退到默认配置,附加默认元信息
$defaultFields = getDefaultFieldsConfig($stationType);
$meta = getDefaultMetaConfig($stationType);
if (!empty($meta)) {
$defaultFields['_meta'] = $meta;
}
return $defaultFields;
}
/**
* 渲染前端表单输入字段 (根据配置动态生成)
* $fieldConfigs: 解析后的字段配置数组
* $dataSources: ['types' => [], 'models' => [], 'customers' => []]
* $isReadonly: 操作人显示名称
*/
function renderStationFields($fieldConfigs, $dataSources = [], $operatorName = '', $excludeFields = []) {
$html = '';
$enabledFields = array_filter($fieldConfigs, function($f) { return !empty($f['enabled']); });
// 排除指定字段
if (!empty($excludeFields)) {
$enabledFields = array_filter($enabledFields, function($f) use ($excludeFields) {
return !in_array($f['name'], $excludeFields);
});
}
// 分组:扫描字段和其他字段
$scanFields = array_filter($enabledFields, function($f) { return !empty($f['is_scan']); });
$normalFields = array_filter($enabledFields, function($f) { return empty($f['is_scan']); });
// 输出所有非扫描字段 + 操作人(CSS Grid 自适应布局)
$html .= '<div class="station-fields-grid" id="stationFieldsGrid">';
foreach ($normalFields as $field) {
$html .= '<div class="station-field-item">';
$html .= renderSingleField($field, $dataSources);
$html .= '</div>';
}
// 操作人(固定显示)
if ($operatorName) {
$html .= '<div class="station-field-item">';
$html .= '<label>操作人</label>';
$html .= '<input type="text" class="form-control" value="' . htmlspecialchars($operatorName) . '" disabled>';
$html .= '</div>';
}
$html .= '</div>';
// 扫描字段(序列号类)- 单独一行,宽度自适应
if (!empty($scanFields)) {
$html .= '<div class="station-scan-grid" style="margin-top:15px;">';
foreach ($scanFields as $field) {
$html .= '<div class="station-field-item station-scan-item">';
$html .= renderSingleField($field, $dataSources, true);
$html .= '</div>';
}
$html .= '</div>';
}
return $html;
}
/**
* 渲染单个字段
*/
function renderSingleField($field, $dataSources = [], $isScan = false) {
$name = htmlspecialchars($field['name']);
$label = htmlspecialchars($field['label']);
$type = $field['type'] ?? 'text';
$required = !empty($field['required']) ? 'required' : '';
$placeholder = htmlspecialchars($field['placeholder'] ?? '');
$html = '';
// hidden 类型:只输出隐藏域,不显示标签
if ($type === 'hidden') {
$fixedValue = htmlspecialchars($field['fixed_value'] ?? '');
$html = '<input type="hidden" name="' . $name . '" value="' . $fixedValue . '">';
return $html;
}
$html .= '<label>' . $label;
if ($required) $html .= ' <span class="text-danger">*</span>';
if ($isScan) $html .= ' <small class="text-muted">(回车添加)</small>';
$html .= '</label>';
if ($type === 'select') {
$source = $field['source'] ?? '';
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$html .= '<select name="' . $name . '" class="' . $cssClass . '" ' . $required . '>';
$html .= '<option value="">' . $placeholder . '</option>';
if ($source === 'type_list' && isset($dataSources['types'])) {
// 去重(防止重复的类型名称)
$seen = [];
foreach ($dataSources['types'] as $type) {
// 兼容多种数据源格式:
// - product_type_config 表:type_name 列
// - ProductModel.getTypes()type 列
// - ProductModel.getProductTypes()product_type 列
$val = htmlspecialchars($type['type_name'] ?? $type['type'] ?? $type['product_type'] ?? '');
if ($val === '' || isset($seen[$val])) continue;
$seen[$val] = true;
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'product_types' && isset($dataSources['types'])) {
$seen = [];
foreach ($dataSources['types'] as $type) {
$val = htmlspecialchars($type['product_type'] ?? '');
if ($val === '' || isset($seen[$val])) continue;
$seen[$val] = true;
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'product_models' && isset($dataSources['models'])) {
if (empty($dataSources['models'])) {
// 没有传入型号列表时,显示提示(由前端联动动态加载)
$html .= '<option value="">请先选择产品类型</option>';
} else {
foreach ($dataSources['models'] as $model) {
$val = htmlspecialchars($model['model_code']);
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
}
} elseif ($source === 'customers' && isset($dataSources['customers'])) {
foreach ($dataSources['customers'] as $cust) {
$val = htmlspecialchars($cust['company_name'] ?? '');
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} elseif ($source === 'test_status_options') {
$html .= '<option value="pending">待测试</option>';
$html .= '<option value="passed">通过</option>';
$html .= '<option value="failed">失败</option>';
} elseif ($source === 'test_result_options') {
$html .= '<option value="通过">通过</option>';
$html .= '<option value="失败">失败</option>';
$html .= '<option value="未测试">未测试</option>';
} elseif ($source === 'shell_color_options') {
if (isset($dataSources['shell_colors']) && !empty($dataSources['shell_colors'])) {
foreach ($dataSources['shell_colors'] as $color) {
$val = htmlspecialchars($color['model_code']);
$html .= '<option value="' . $val . '">' . $val . '</option>';
}
} else {
$html .= '<option value="">暂无外壳颜色数据</option>';
}
}
$html .= '</select>';
} elseif ($type === 'number') {
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$html .= '<input type="number" step="any" name="' . $name . '" class="' . $cssClass . '" placeholder="' . $placeholder . '" ' . $required . '>';
} elseif ($type === 'textarea') {
$html .= '<textarea name="' . $name . '" class="form-control sticky-save" placeholder="' . $placeholder . '" rows="2" ' . $required . '></textarea>';
} else {
// text 类型
$cssClass = $isScan ? 'form-control scan-submit' : 'form-control sticky-save';
$autofocus = $isScan ? ' autofocus' : '';
$html .= '<input type="text" name="' . $name . '" class="' . $cssClass . '" placeholder="' . $placeholder . '" ' . $required . $autofocus . '>';
}
return $html;
}
/**
* 渲染记录表格表头
*/
function renderRecordTableHeader($fieldConfigs) {
$html = '<th data-col="seq">序号</th>';
$colIdx = 0;
foreach ($fieldConfigs as $field) {
if (!empty($field['enabled']) && !empty($field['show_in_table'])) {
$colKey = htmlspecialchars($field['name']);
$html .= '<th data-col="' . $colKey . '">' . htmlspecialchars($field['table_label'] ?? $field['label']) . '</th>';
$colIdx++;
}
}
$html .= '<th data-col="operator">操作人</th><th data-col="time">时间</th>';
return $html;
}
/**
* 渲染记录表格数据行
*/
function renderRecordTableRow($record, $fieldConfigs, $specialFormats = []) {
$html = '<tr>';
$html .= '<td data-col="seq">' . $record['id'] . '</td>';
foreach ($fieldConfigs as $field) {
if (empty($field['enabled']) || empty($field['show_in_table'])) continue;
$name = $field['name'];
$val = $record[$name] ?? '';
$colKey = htmlspecialchars($name);
if (isset($specialFormats[$name])) {
$html .= '<td data-col="' . $colKey . '">' . $specialFormats[$name]($val, $record) . '</td>';
} else {
$html .= '<td data-col="' . $colKey . '">' . htmlspecialchars($val) . '</td>';
}
}
$html .= '<td data-col="operator">' . htmlspecialchars($record['operator'] ?? '') . '</td>';
// 兼容各种时间列名:created_at, create_time, assembly_time, test_time, delivery_time, in_time
$timeValue = $record['created_at']
?? $record['create_time']
?? $record['assembly_time']
?? $record['test_time']
?? $record['delivery_time']
?? $record['in_time']
?? '';
$html .= '<td data-col="time">' . htmlspecialchars($timeValue) . '</td>';
$html .= '</tr>';
return $html;
}
/**
* 渲染表格列选择器(下拉多选按钮)
* @param string $pageKey 页面唯一标识,用于 localStorage 区分不同页面
* @param array $fieldConfigs 字段配置数组
* @return string HTML
*/
function renderTableColumnSelector($pageKey, $fieldConfigs) {
$cols = [['key' => 'seq', 'label' => '序号']];
foreach ($fieldConfigs as $field) {
if (!empty($field['enabled']) && !empty($field['show_in_table'])) {
$cols[] = [
'key' => $field['name'],
'label'=> $field['table_label'] ?? $field['label'],
];
}
}
$cols[] = ['key' => 'operator', 'label' => '操作人'];
$cols[] = ['key' => 'time', 'label' => '时间'];
$colsJson = json_encode($cols, JSON_UNESCAPED_UNICODE);
$pageKeyJson = json_encode($pageKey);
$html = '<span class="table-column-selector" title="选择显示列" style="margin-left:8px;">';
$html .= '<button type="button" class="col-btn col-btn-dropdown" id="colSelectBtn_' . htmlspecialchars($pageKey) . '">';
$html .= '<i class="fa fa-columns"></i> 列选择 <i class="fa fa-caret-down"></i>';
$html .= '</button>';
$html .= '<div class="col-dropdown-panel" id="colDropdown_' . htmlspecialchars($pageKey) . '" style="display:none;">';
$html .= '<div class="col-dropdown-header">';
$html .= '<label><input type="checkbox" class="col-check-all" checked> 全选</label>';
$html .= '<button type="button" class="col-reset-btn" title="恢复默认"><i class="fa fa-refresh"></i></button>';
$html .= '</div>';
$html .= '<div class="col-dropdown-list">';
foreach ($cols as $col) {
$html .= '<label><input type="checkbox" class="col-check-item" data-col="' . htmlspecialchars($col['key']) . '" checked> '
. htmlspecialchars($col['label']) . '</label>';
}
$html .= '</div>';
$html .= '</div>';
$html .= '</span>';
// 内联初始化脚本(在元素渲染后立即执行)
$html .= '<script nonce="' . csp_nonce() . '">window._colSelectorQueue = window._colSelectorQueue || [];';
$html .= 'window._colSelectorQueue.push({pageKey:' . $pageKeyJson . ',cols:' . $colsJson . '});</script>';
return $html;
}
/**
* 生成字段配置编辑UI的JSON字符串(供JS使用)
*/
function renderFieldConfigEditor($stationType, $fieldsConfigJson = '', $stationDefinition = null) {
$rawConfig = parseFieldsConfig($stationType, $fieldsConfigJson);
// 分离 _meta 和 fields
$meta = $rawConfig['_meta'] ?? [];
$fields = $rawConfig;
unset($fields['_meta']);
$fields = array_values($fields);
$fieldsJson = json_encode($fields, JSON_UNESCAPED_UNICODE);
$metaJson = json_encode($meta, JSON_UNESCAPED_UNICODE);
$stationTypeJson = json_encode($stationType);
// 工位定义元数据(用于数据表结构管控)
$stationDef = $stationDefinition ?: [];
$hasProductType = !empty($stationDef['has_product_type']);
$hasProductModel = !empty($stationDef['has_product_model']);
$filterEnabled = !empty($stationDef['filter_enabled']);
$showTodayCount = !empty($stationDef['show_today_count']);
$showTotalCount = !empty($stationDef['show_total_count']);
$defId = $stationDef['id'] ?? 0;
// 产品类型选项(用于型号筛选下拉)
$productTypeOptions = json_encode(['', '半成品', '成品', 'PCBA', '电池', '外壳', '外壳颜色'], JSON_UNESCAPED_UNICODE);
ob_start();
?>
<div id="fieldConfigEditor" style="margin-top:15px; border-top:1px solid #eee; padding-top:15px;">
<h4><i class="fa fa-cog"></i> 输入数据项配置
<button type="button" class="btn btn-xs btn-success" id="addFieldBtn"><i class="fa fa-plus"></i> 添加字段</button>
<button type="button" class="btn btn-xs btn-warning" id="resetDefaultsBtn"><i class="fa fa-refresh"></i> 恢复默认</button>
</h4>
<!-- 工位定义元数据配置区(管控数据表结构) -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-database"></i> 数据表结构管控
<?php if ($defId): ?><span class="pull-right text-muted" style="font-size:12px;">工位定义 ID: <?php echo $defId; ?></span><?php endif; ?>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_has_product_type" value="1" <?php echo $hasProductType ? 'checked' : ''; ?>>
数据表含 product_type 列
</label>
<br><small class="text-muted">记录可按产品类型筛选</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_has_product_model" value="1" <?php echo $hasProductModel ? 'checked' : ''; ?>>
数据表含 product_model 列
</label>
<br><small class="text-muted">记录可按产品型号筛选</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_filter_enabled" value="1" <?php echo $filterEnabled ? 'checked' : ''; ?>>
启用前端筛选
</label>
<br><small class="text-muted">前端显示类型/型号筛选联动</small>
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_show_today_count" value="1" <?php echo $showTodayCount ? 'checked' : ''; ?>>
显示今日录入计数
</label>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" name="sd_show_total_count" value="1" <?php echo $showTotalCount ? 'checked' : ''; ?>>
显示总记录计数
</label>
</div>
</div>
</div>
</div>
<!-- 元信息配置区 -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-sliders"></i> 数据筛选设置</div>
<div class="panel-body">
<div class="row">
<div class="col-md-4">
<label>产品型号筛选类型
<select class="form-control input-sm" id="metaModelTypeFilter">
<option value="">不限(显示全部型号)</option>
</select>
</label>
<small class="text-muted">选择后产品型号下拉只显示该类型</small>
</div>
<div class="col-md-4">
<label>类型列表筛选方式
<select class="form-control input-sm" id="metaTypeFilter">
<option value="">默认(所有启用的类型)</option>
<option value="inbound">入库可见类型</option>
<option value="enabled">已启用类型</option>
</select>
</label>
<small class="text-muted">控制"类型"下拉框显示哪些选项</small>
</div>
<div class="col-md-4">
<label>
<input type="checkbox" id="metaShellColors">
启用外壳颜色数据源
</label>
<br><small class="text-muted">组装工位使用,加载外壳颜色选项</small>
</div>
</div>
</div>
</div>
<!-- 序列号前缀映射区 -->
<div class="panel panel-default" style="margin-bottom:15px;">
<div class="panel-heading"><i class="fa fa-shield"></i> 序列号前缀规则(防错)
<span class="pull-right text-muted" style="font-size:12px;">在 <a href="<?php echo BASE_URL; ?>/Admin/sysConfig" target="_blank">系统配置 → 序列号前缀规则</a> 中管理前缀</span>
</div>
<div class="panel-body">
<p class="text-muted" style="margin-bottom:10px;">
<i class="fa fa-info-circle"></i>
为扫描字段(is_scan=true)指定对应的产品类型,提交时将自动验证序列号前缀是否正确。<br>
选择「动态匹配」则根据提交时选择的 product_type 自动匹配前缀。
</p>
<div id="prefixFieldMapArea" style="max-height:200px; overflow-y:auto;">
<table class="table table-condensed table-bordered" style="margin-bottom:0;">
<thead>
<tr>
<th style="width:30%">扫描字段</th>
<th style="width:40%">对应产品类型</th>
<th style="width:30%">预期前缀</th>
</tr>
</thead>
<tbody id="prefixFieldMapTbody">
</tbody>
</table>
</div>
<small class="text-muted" style="display:block;margin-top:5px;">
提示:留空则自动从工位的 product_type 字段推断(如有 hidden 类型的 product_type 则使用其 fixed_value,否则动态匹配)
</small>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover" id="fieldsTable" style="margin-bottom:0;">
<thead>
<tr>
<th style="width:3%">启用</th>
<th style="width:10%">字段名</th>
<th style="width:10%">显示标签</th>
<th style="width:8%">类型</th>
<th style="width:10%">数据源</th>
<th style="width:4%">必填</th>
<th style="width:4%">唯一</th>
<th style="width:10%">占位提示</th>
<th style="width:5%">列宽</th>
<th style="width:5%">扫码</th>
<th style="width:5%">表中</th>
<th style="width:10%">表头标签</th>
<th style="width:5%">操作</th>
</tr>
</thead>
<tbody id="fieldsTbody"></tbody>
</table>
</div>
<input type="hidden" name="fields_config" id="fieldsConfigInput" value="">
<small class="text-muted"><i class="fa fa-info-circle"></i> 勾选「扫码」的字段会以扫码枪回车提交方式处理。勾选「表中」会在记录表格中显示该列。</small>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
var stationType = <?php echo $stationTypeJson; ?>;
var defaultFields = <?php echo $fieldsJson; ?>;
var currentFields = JSON.parse(JSON.stringify(defaultFields));
var currentMeta = <?php echo $metaJson; ?>;
var productTypeOptions = <?php echo $productTypeOptions; ?>;
var fieldTypes = [
{v:'text',n:'文本'},
{v:'number',n:'数字'},
{v:'select',n:'下拉选择'},
{v:'textarea',n:'多行文本'}
];
var fieldSources = [
{v:'',n:'无(手动输入)'},
{v:'product_types',n:'产品类型列表'},
{v:'product_models',n:'产品型号列表'},
{v:'customers',n:'客户列表'},
{v:'test_status_options',n:'测试状态(待测/通过/失败)'},
{v:'test_result_options',n:'测试结果(通过/失败/未测试)'},
{v:'shell_color_options',n:'外壳颜色选项'}
];
// 初始化元信息下拉框
function initMetaUI() {
var sel = document.getElementById('metaModelTypeFilter');
sel.innerHTML = '<option value="">不限(显示全部型号)</option>';
productTypeOptions.forEach(function(t) {
if (!t) return;
sel.innerHTML += '<option value="' + t + '"' + (currentMeta.model_type_filter === t ? ' selected' : '') + '>' + t + '</option>';
});
document.getElementById('metaTypeFilter').value = currentMeta.type_filter || '';
document.getElementById('metaShellColors').checked = !!(currentMeta.extra_data_sources && currentMeta.extra_data_sources.shellColors);
// 渲染前缀映射表
renderPrefixFieldMap();
}
// ===== 前缀字段映射渲染 =====
// 从 currentMeta.prefix_field_map 读取映射,为每个 is_scan 的 text 字段渲染一行
function renderPrefixFieldMap() {
var tbody = document.getElementById('prefixFieldMapTbody');
if (!tbody) return;
var prefixMap = currentMeta.prefix_field_map || {};
// 用已知前缀规则展示预期前缀预览
var knownPrefixes = {};
try {
// productTypeOptions 中包含产品类型列表,前缀从服务器端已知规则获取
// 这里只做展示,实际规则由后端 SysConfig 动态读取
} catch(e) {}
var html = '';
currentFields.forEach(function(f, i) {
if (!f.enabled || !f.is_scan || f.type !== 'text') return;
var fieldName = f.name || '';
if (!fieldName) return;
var currentVal = prefixMap[fieldName] || '';
var label = f.label || fieldName;
// 构建产品类型下拉选项
var typeOpts = '<option value="">(自动推断)</option>';
typeOpts += '<option value="*dynamic*"' + (currentVal === '*dynamic*' ? ' selected' : '') + '>动态匹配(根据 product_type</option>';
productTypeOptions.forEach(function(t) {
if (!t) return;
typeOpts += '<option value="' + t + '"' + (currentVal === t ? ' selected' : '') + '>' + t + '</option>';
});
html += '<tr>';
html += '<td><strong>' + escHtml(label) + '</strong><br><small class="text-muted">' + escHtml(fieldName) + '</small></td>';
html += '<td><select class="form-control input-sm" data-prefix-field="' + escHtml(fieldName) + '" style="min-width:160px;">' + typeOpts + '</select></td>';
html += '<td><span class="text-muted" style="font-size:12px;">' + (currentVal && currentVal !== '*dynamic*' ? '系统配置中定义' : currentVal === '*dynamic*' ? '运行时动态匹配' : '自动推断') + '</span></td>';
html += '</tr>';
});
if (html === '') {
html = '<tr><td colspan="3" class="text-muted" style="text-align:center;padding:20px;">暂无扫描字段。勾选字段的「扫码」选项后,此处将显示前缀映射配置。</td></tr>';
}
tbody.innerHTML = html;
}
function updatePrefixMap(fieldName, productType) {
if (!currentMeta.prefix_field_map) currentMeta.prefix_field_map = {};
if (productType) {
currentMeta.prefix_field_map[fieldName] = productType;
} else {
delete currentMeta.prefix_field_map[fieldName];
}
updateFieldsConfig();
// 刷新前缀列显示
renderPrefixFieldMap();
}
// 更新隐藏域值(新格式:{"_meta":{...}, "fields":[...]}
function updateFieldsConfig() {
var config = {_meta: currentMeta, fields: currentFields};
document.getElementById('fieldsConfigInput').value = JSON.stringify(config);
}
function resetDefaults() {
if (confirm('确定恢复为默认配置吗?当前配置将丢失。')) {
currentFields = JSON.parse(JSON.stringify(defaultFields));
currentMeta = {};
initMetaUI();
renderFieldsTable();
renderPrefixFieldMap();
updateFieldsConfig();
}
}
function addField() {
currentFields.push({
name: '', label: '', type: 'text', source: '', required: false, unique: false,
enabled: true, placeholder: '', col_width: 2, is_scan: false,
show_in_table: true, table_label: ''
});
renderFieldsTable();
}
function removeField(idx) {
if (confirm('确定删除这个字段吗?')) {
currentFields = currentFields.filter(function(_, i) { return i !== idx; });
renderFieldsTable();
}
}
function moveField(idx, dir) {
var newIdx = idx + dir;
if (newIdx < 0 || newIdx >= currentFields.length) return;
var tmp = currentFields[idx];
currentFields[idx] = currentFields[newIdx];
currentFields[newIdx] = tmp;
renderFieldsTable();
}
function renderFieldsTable() {
var tbody = document.getElementById('fieldsTbody');
var html = '';
currentFields.forEach(function(f, i) {
var typeOpts = fieldTypes.map(function(t) {
return '<option value="' + t.v + '"' + (f.type === t.v ? ' selected' : '') + '>' + t.n + '</option>';
}).join('');
var sourceOpts = fieldSources.map(function(s) {
return '<option value="' + s.v + '"' + ((f.source||'') === s.v ? ' selected' : '') + '>' + s.n + '</option>';
}).join('');
html += '<tr>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="enabled" ' + (f.enabled !== false ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.name) + '" data-idx="'+i+'" data-prop="name" placeholder="字段名" style="min-width:90px;"></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.label) + '" data-idx="'+i+'" data-prop="label" placeholder="标签" style="min-width:90px;"></td>';
html += '<td><select class="form-control input-sm" data-idx="'+i+'" data-prop="type">' + typeOpts + '</select></td>';
html += '<td><select class="form-control input-sm" data-idx="'+i+'" data-prop="source">' + sourceOpts + '</select></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="required" ' + (f.required ? 'checked' : '') + '></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="unique" title="不允许重复录入" ' + (f.unique ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.placeholder) + '" data-idx="'+i+'" data-prop="placeholder" placeholder="提示" style="min-width:90px;"></td>';
html += '<td><input type="number" class="form-control input-sm" value="' + (f.col_width||2) + '" min="1" max="12" data-idx="'+i+'" data-prop="col_width" style="width:55px;"></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="is_scan" ' + (f.is_scan ? 'checked' : '') + '></td>';
html += '<td style="text-align:center"><input type="checkbox" data-idx="'+i+'" data-prop="show_in_table" ' + (f.show_in_table !== false ? 'checked' : '') + '></td>';
html += '<td><input type="text" class="form-control input-sm" value="' + escHtml(f.table_label||'') + '" data-idx="'+i+'" data-prop="table_label" placeholder="表头" style="min-width:90px;"></td>';
html += '<td style="text-align:center;white-space:nowrap">';
html += '<button type="button" class="btn btn-xs btn-default" data-idx="'+i+'" data-faction="moveUp" title="上移"><i class="fa fa-arrow-up"></i></button> ';
html += '<button type="button" class="btn btn-xs btn-default" data-idx="'+i+'" data-faction="moveDown" title="下移"><i class="fa fa-arrow-down"></i></button> ';
html += '<button type="button" class="btn btn-xs btn-danger" data-idx="'+i+'" data-faction="remove" title="删除"><i class="fa fa-trash"></i></button>';
html += '</td>';
html += '</tr>';
});
tbody.innerHTML = html;
updateFieldsConfig();
}
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ===== CSP 事件绑定(替代内联 onclick/onchange 处理器)=====
function bindFieldConfigEvents() {
// 顶部操作按钮
var addBtn = document.getElementById('addFieldBtn');
if (addBtn) addBtn.addEventListener('click', addField);
var resetBtn = document.getElementById('resetDefaultsBtn');
if (resetBtn) resetBtn.addEventListener('click', resetDefaults);
// 元信息筛选控件
var mmf = document.getElementById('metaModelTypeFilter');
if (mmf) mmf.addEventListener('change', function () {
currentMeta.model_type_filter = this.value; updateFieldsConfig();
});
var mtf = document.getElementById('metaTypeFilter');
if (mtf) mtf.addEventListener('change', function () {
currentMeta.type_filter = this.value; updateFieldsConfig();
});
var msc = document.getElementById('metaShellColors');
if (msc) msc.addEventListener('change', function () {
currentMeta.extra_data_sources = currentMeta.extra_data_sources || {};
currentMeta.extra_data_sources.shellColors = this.checked;
updateFieldsConfig();
});
// 字段表格:事件委托(绑定在稳定的 tbody 上,innerHTML 重渲染后依然有效)
var tbody = document.getElementById('fieldsTbody');
if (tbody) {
tbody.addEventListener('change', function (e) {
var el = e.target;
var idxAttr = el.getAttribute && el.getAttribute('data-idx');
var prop = el.getAttribute && el.getAttribute('data-prop');
if (idxAttr === null || idxAttr === undefined || !prop) return;
var idx = parseInt(idxAttr, 10);
var f = currentFields[idx];
if (!f) return;
if (el.type === 'checkbox') {
f[prop] = el.checked;
} else if (prop === 'col_width') {
f[prop] = parseInt(el.value, 10) || 2;
} else {
f[prop] = el.value;
}
updateFieldsConfig();
if (prop === 'type') renderFieldsTable();
if (prop === 'is_scan') renderPrefixFieldMap();
});
tbody.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('button[data-faction]') : null;
if (!btn) return;
var idx = parseInt(btn.getAttribute('data-idx'), 10);
var action = btn.getAttribute('data-faction');
if (action === 'moveUp') moveField(idx, -1);
else if (action === 'moveDown') moveField(idx, 1);
else if (action === 'remove') removeField(idx);
});
}
// 前缀映射表:事件委托
var prefixTbody = document.getElementById('prefixFieldMapTbody');
if (prefixTbody) {
prefixTbody.addEventListener('change', function (e) {
var el = e.target;
var fieldName = el.getAttribute && el.getAttribute('data-prefix-field');
if (!fieldName) return;
updatePrefixMap(fieldName, el.value);
});
}
}
// 表单提交前更新隐藏域
document.addEventListener('DOMContentLoaded', function() {
initMetaUI();
renderFieldsTable();
bindFieldConfigEvents();
var form = document.querySelector('form[method="post"]');
if (form) {
form.addEventListener('submit', function() {
updateFieldsConfig();
});
}
});
</script>
<?php
return ob_get_clean();
}
/**
* 生成扫描字段连环回车 JS 代码
*
* 行为:
* - 扫描字段按 Enter → 自动填入扫码值 → 焦点跳到下一个空扫描字段
* - 所有扫描字段填满后按 Enter → 触发提交
* - 非扫描字段按 Enter → 焦点自然跳下一个输入框(tabindex 顺序)
*
* @param string $formSelector 表单选择器,默认 '#mainForm'
* @param string $submitSelector 提交按钮选择器(可选),留空则直接 submit 表单
* @param bool $clearAfterSubmit 提交后是否清空扫描字段(默认true)
*/
function renderScanChainJS($formSelector = '#mainForm', $submitSelector = '', $clearAfterSubmit = true) {
ob_start();
?>
function initScanChain() {
var $form = $('<?php echo $formSelector; ?>');
if (!$form.length) return;
// 找到表单内所有输入元素(按DOM顺序)
function getInputsInOrder() {
var inputs = [];
$form.find('input:not([disabled]):not([type="hidden"]):not(:submit):not(:button), select:not([disabled]), textarea:not([disabled])').each(function() {
var $el = $(this);
if ($el.closest('#fieldConfigEditor').length) return; // 跳过配置编辑器
inputs.push(this);
});
// 按tabindex排序(如果有的话),否则按DOM顺序
inputs.sort(function(a, b) {
var ta = parseInt($(a).attr('tabindex')) || 0;
var tb = parseInt($(b).attr('tabindex')) || 0;
if (ta && tb) return ta - tb;
if (ta) return -1;
if (tb) return 1;
return 0;
});
return inputs;
}
// 检查是否所有扫描字段都已填值
function allScanFieldsFilled() {
var allFilled = true;
$form.find('.scan-submit').each(function() {
if ($(this).val().trim() === '') allFilled = false;
});
return allFilled;
}
function doSubmit() {
<?php if ($submitSelector): ?>
$('<?php echo $submitSelector; ?>').click();
<?php else: ?>
$form.submit();
<?php endif; ?>
}
// 回车事件:扫描字段 → 跳到下一个空位;最后一位 → 提交
$form.on('keypress', '.scan-submit', function(e) {
if (e.which !== 13) return;
e.preventDefault();
var $this = $(this);
var val = $this.val().trim();
if (val === '') return; // 空值不处理
// 找到所有扫描字段
var scanInputs = [];
$form.find('.scan-submit').each(function() { scanInputs.push(this); });
// 如果所有扫描字段都填满了 → 提交
if (allScanFieldsFilled()) {
doSubmit();
return;
}
// 找到下一个空的扫描字段
var found = false;
for (var i = 0; i < scanInputs.length; i++) {
if (scanInputs[i] === this) {
// 从当前之后找第一个空的
for (var j = 1; j < scanInputs.length; j++) {
var idx = (i + j) % scanInputs.length;
if ($(scanInputs[idx]).val().trim() === '') {
$(scanInputs[idx]).focus().select();
found = true;
break;
}
}
break;
}
}
if (!found) {
// 没找到空的,提交
doSubmit();
}
});
// 非扫描字段的回车 → 跳到下一个输入框
$form.on('keypress', 'input:not(.scan-submit):not([type="hidden"]):not(:submit):not(:button), select, textarea', function(e) {
if (e.which !== 13) return;
var $el = $(this);
if ($el.closest('#fieldConfigEditor').length) return;
e.preventDefault();
var inputs = getInputsInOrder();
var idx = inputs.indexOf(this);
if (idx >= 0 && idx < inputs.length - 1) {
$(inputs[idx + 1]).focus().select();
}
});
// 提交后清空扫描字段(重新聚焦第一个)
$form.on('submit', function() {
<?php if ($clearAfterSubmit): ?>
setTimeout(function() {
var $firstScan = $form.find('.scan-submit').first();
if ($firstScan.length) $firstScan.focus();
}, 100);
<?php endif; ?>
});
}
$(function() { initScanChain(); });
<?php
return ob_get_clean();
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* 工位业务配置辅助文件
*
* 将所有工位的特殊业务逻辑抽象为配置驱动。
* 每个内置工位在此定义其业务配置,FrontController 的 __call 统一读取并执行。
*
* 新增/修改工位只需:
* 1. 在 Admin 后台添加/编辑 workstation 记录
* 2. 如有特殊业务逻辑(如独立数据表、自定义提交),在此文件中添加配置
* 3. 无需修改 FrontController 或创建新视图文件
*/
/**
* 从 SysConfig 动态生成 codePrefix 验证规则
*
* 配置格式(在工位配置中):
* 'codePrefixFields' => [
* '字段名' => '产品类型', // 如 'serial_no' => 'PCBA'
* ]
*
* 运行时从 sys_config.code_prefix_rules 读取产品类型对应的前缀,
* 自动生成 codePrefix 验证规则。如果该产品类型未配置前缀(空字符串),则跳过验证。
*
* @param array|null $fieldMappings 字段到产品类型的映射
* @return array 格式化后的 codePrefix 规则
*/
function buildCodePrefixRules($fieldMappings, $prefixTypeMap = null) {
if (empty($fieldMappings)) return [];
// 如果未传入前缀映射,从 SysConfig 读取(兼容旧用法)
if ($prefixTypeMap === null) {
try {
$config = new \app\models\SysConfig();
$prefixTypeMap = $config->getCodePrefixRules();
} catch (\Throwable $e) {
error_log('[buildCodePrefixRules] getCodePrefixRules failed: ' . $e->getMessage());
$prefixTypeMap = [];
}
}
if (!is_array($prefixTypeMap)) $prefixTypeMap = [];
$rules = [];
foreach ($fieldMappings as $fieldName => $productType) {
// 动态前缀:*dynamic* 标记表示根据提交时的 product_type 动态匹配
if ($productType === '*dynamic*') {
$rules[$fieldName] = ['*dynamic*', '序列号前缀与产品类型不匹配'];
continue;
}
$prefix = $prefixTypeMap[$productType] ?? '';
// 原则:所有工位序列号都受前缀管控。类型已配置但前缀为空时,
// 自动回退为产品类型名拼音首字母(如 外壳→WK、包装→BZ、半成品→BCP、配件→PJ)。
if ($prefix === '') {
try {
$sc = new \app\models\SysConfig();
$prefix = $sc->pinyinInitials($productType);
} catch (\Throwable $e) {
error_log('[buildCodePrefixRules] pinyinInitials failed: ' . $e->getMessage());
$prefix = '';
}
}
if ($prefix === '') continue; // 实在无法解析(如类型根本未配置)才跳过
$rules[$fieldName] = [$prefix, "{$productType}序列号必须以 {$prefix} 开头"];
}
return $rules;
}
/**
* 获取指定工位的业务配置
* @param string $stationType 工位类型标识
* @return array|null 业务配置数组,null 表示使用通用 station_records 存储
*/
function getStationBusinessConfig($stationType) {
// ========== 所有工位统一走自动生成路径 ==========
//
// 之前的内置工位配置(inbound, pcb_test, battery, assembly, finished_test,
// delivery, warehouse)已全部移除。
//
// 现在所有工位(包括内置和动态)都通过 FrontController::buildGenericBizConfig()
// 自动生成业务配置,确保与 aging/semi_finished 等已验证正常的工位使用完全相同的代码路径。
//
// 特殊业务逻辑(如 delivery 的箱序列号、inbound 的成品入库切换等)
// 将逐步在 FrontController::handleBuiltinSubmit() 中按 stationType 判断实现。
//
// 如需恢复某个工位的特殊配置,请以 aging/semi_finished 的自动生成配置为基准,
// 在此基础上增量添加,避免重写整个配置导致不一致。
$configs = [
// ========== 成品入库工位(已合并到入库) ==========
// warehouse 重定向到 inbound,保留此逻辑
'warehouse' => [
'model' => 'StationDefinition',
'redirect' => 'inbound',
],
];
if (!isset($configs[$stationType])) {
return null;
}
$bizConfig = $configs[$stationType];
// 动态构建 codePrefix:从 codePrefixFields 映射到实际前缀规则
if (!empty($bizConfig['codePrefixFields'])) {
$bizConfig['codePrefix'] = buildCodePrefixRules($bizConfig['codePrefixFields']);
}
return $bizConfig;
}
/**
* 获取工位的测试状态颜色映射(用于 labelFields 动态颜色)
*/
function getTestStatusColor($status) {
$map = [
'passed' => 'success',
'failed' => 'danger',
'pending' => 'warning',
'通过' => 'success',
'失败' => 'danger',
'待测试' => 'warning',
];
return $map[$status] ?? 'default';
}
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 昂盛达设备模型
*/
class AsdDevice extends Model
{
protected $table = 'asd_device';
/**
* 获取 PDO 实例
*/
private function db()
{
return \core\db\Db::pdo();
}
/**
* 根据设备编码查找
*/
public function findByCode($device_code)
{
return $this->where(['device_code = :code'], [':code' => $device_code])->fetch();
}
/**
* 检查设备是否存在且启用
*/
public function isActive($device_code)
{
$device = $this->where([
'device_code = :code',
'status = 1'
], [':code' => $device_code])->fetch();
return $device ? $device : false;
}
/**
* 根据设备编码和组别校验
*/
public function validate($device_code, $group_name = '')
{
$conditions = ['device_code = :code', 'status = 1'];
$params = [':code' => $device_code];
if (!empty($group_name)) {
$conditions[] = 'group_name = :group';
$params[':group'] = $group_name;
}
return $this->where($conditions, $params)->fetch();
}
/**
* 获取所有启用设备
*/
public function getAllActive()
{
return $this->where(['status = 1'])->order(['group_name ASC', 'device_code ASC'])->fetchAll();
}
/**
* 添加设备
*/
public function addDevice($data)
{
return $this->add($data);
}
/**
* 更新设备
*/
public function updateDevice($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
/**
* 根据 IP 查找设备
*/
public function findByIp($device_ip)
{
return $this->where(['device_ip = :ip'], [':ip' => $device_ip])->fetch();
}
/**
* 根据 ID 查找设备
*/
public function findById($id)
{
return $this->where(['id = :id'], [':id' => intval($id)])->fetch();
}
/**
* 获取全部设备(调试页用,含禁用设备)
*/
public function getAll()
{
return $this->order(['group_name ASC', 'device_code ASC'])->fetchAll();
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 昂盛达上位机测试数据模型
*/
class AsdTestData extends Model
{
protected $table = 'asd_test_record';
/**
* 获取 PDO 实例
*/
private function db()
{
return \core\db\Db::pdo();
}
/**
* 获取所有测试记录(含统计信息)
*/
public function getAllWithStats($keyword = '', $limit = 50)
{
$sql = "SELECT r.*,
COUNT(d.id) AS step_count,
SUM(CASE WHEN d.test_result = 'FAIL' THEN 1 ELSE 0 END) AS fail_count
FROM DGZXY_asd_test_record r
LEFT JOIN DGZXY_asd_test_detail d ON r.id = d.record_id";
$params = [];
if (!empty($keyword)) {
$sql .= " WHERE r.qr_code LIKE :keyword";
$params[':keyword'] = '%' . $keyword . '%';
}
$sql .= " GROUP BY r.id ORDER BY r.id DESC LIMIT " . intval($limit);
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 新增测试记录主表,返回插入ID
*
* 兼容接口字段名(v1.5)与数据库实际列名的差异:
* status -> test_result (整体测试结果 PASS/NG
* device -> device_code (设备序列号)
* test_time -> test_date (测试时间)
* project_name -> product_model (产品/项目名称)
* 其余接口字段(tester/mo/site/station/work_order/group_name/running_time
* 已通过迁移脚本 upgrade_asd_api_20260718.sql 直接补齐为独立列。
*/
public function addRecord($data)
{
$map = [
'status' => 'test_result',
'device' => 'device_code',
'test_time' => 'test_date',
'project_name' => 'product_model',
];
foreach ($map as $from => $to) {
if (array_key_exists($from, $data)) {
// 仅当目标列尚未显式传入时才用接口值填充,避免覆盖
if (!array_key_exists($to, $data) || $data[$to] === '' || $data[$to] === null) {
$data[$to] = $data[$from];
}
unset($data[$from]);
}
}
$result = $this->add($data);
if ($result) {
return $this->db()->lastInsertId();
}
return false;
}
/**
* 根据ID获取测试记录
*/
public function findById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
/**
* 新增测试工步详情
*/
public function addDetail($data)
{
$sql = "INSERT INTO DGZXY_asd_test_detail
(record_id, seq, test_name, test_item, test_units, data_value, lower_limit, upper_limit, test_value, test_limit, test_result)
VALUES
(:record_id, :seq, :test_name, :test_item, :test_units, :data_value, :lower_limit, :upper_limit, :test_value, :test_limit, :test_result)";
$stmt = $this->db()->prepare($sql);
return $stmt->execute([
':record_id' => $data['record_id'],
':seq' => $data['seq'] ?? 1,
':test_name' => $data['test_name'] ?? '',
':test_item' => $data['test_item'] ?? '',
':test_units' => $data['test_units'] ?? '',
':data_value' => $data['data_value'] ?? null,
':lower_limit' => $data['lower_limit'] ?? null,
':upper_limit' => $data['upper_limit'] ?? null,
':test_value' => $data['test_value'] ?? '',
':test_limit' => $data['test_limit'] ?? '',
':test_result' => $data['test_result'] ?? 'PASS',
]);
}
/**
* 根据记录ID获取所有工步详情
*/
public function getDetails($recordId)
{
$stmt = $this->db()->prepare("SELECT * FROM DGZXY_asd_test_detail WHERE record_id = :rid ORDER BY seq ASC");
$stmt->execute([':rid' => $recordId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 删除测试记录(级联删除详情)
*/
public function deleteRecord($id)
{
// 先删详情(FOREIGN KEY ON DELETE CASCADE 会自动处理,这里显式处理更安全)
$this->db()->prepare("DELETE FROM DGZXY_asd_test_detail WHERE record_id = :rid")->execute([':rid' => $id]);
// 再删主表
return $this->delete($id);
}
/**
* 统计记录数
*/
public function countRecords($keyword = '')
{
if (!empty($keyword)) {
$stmt = $this->db()->prepare("SELECT COUNT(*) AS cnt FROM DGZXY_asd_test_record WHERE qr_code LIKE :kw");
$stmt->execute([':kw' => '%' . $keyword . '%']);
return $stmt->fetch(\PDO::FETCH_ASSOC)['cnt'];
}
$stmt = $this->db()->prepare("SELECT COUNT(*) AS cnt FROM DGZXY_asd_test_record");
$stmt->execute();
return $stmt->fetch(\PDO::FETCH_ASSOC)['cnt'];
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 产品组装模型
* @deprecated
*/
class Assembly extends Model
{
protected $table = 'product_assembly';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findByFinishedSerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
// 根据产品型号筛选记录(assembly 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(assembly_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 固定资产模型
*/
class Asset extends Model
{
protected $table = 'asset';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt, SUM(purchase_price * quantity) AS total_value FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $stmt->fetch();
}
public function getByStatus($status)
{
return $this->where(['status = :st'], [':st' => $status])->order(['id DESC'])->fetchAll();
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 电池状态模型
* @deprecated
*/
class Battery extends Model
{
protected $table = 'battery_status';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 客户模型
*/
class Customer extends Model
{
protected $table = 'customer';
// 获取所有客户
public function getAll()
{
return $this->order(['id ASC'])->fetchAll();
}
// 添加客户
public function addCustomer($data)
{
return $this->add($data);
}
// 更新客户
public function updateCustomer($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除客户
public function deleteCustomer($id)
{
return $this->delete($id);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 出货登记模型
* @deprecated
*/
class Delivery extends Model
{
protected $table = 'delivery';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
/**
* 批量添加出货记录
* @param array $records 记录数组
*/
public function batchAdd($records)
{
foreach ($records as $record) {
// 跳过已出货的序列号
$existing = $this->findBySerial($record['finished_serial']);
if ($existing) {
continue;
}
$this->add($record);
}
return true;
}
/**
* 从 warehouse_in 表查询成品序列号对应的产品型号
* @param string $finished_serial 成品序列号
* @return array|null ['product_model' => ..., 'box_serial' => ...] 或 null
*/
public function lookupFinishedSerial($finished_serial)
{
$table = self::TABLE_PREFIX . 'warehouse_in';
$sql = "SELECT finished_serial, product_model, box_serial FROM {$table} WHERE finished_serial = :serial LIMIT 1";
$result = $this->query($sql, [':serial' => $finished_serial]);
return !empty($result) ? $result[0] : null;
}
// 根据产品型号筛选记录(delivery 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(delivery_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
/**
* 根据箱序列号查询该箱下所有成品
* @param string $box_serial 箱序列号
* @return array 该箱下所有成品记录数组
*/
public function getBoxItems($box_serial)
{
$table = self::TABLE_PREFIX . 'warehouse_in';
$sql = "SELECT finished_serial, product_model FROM {$table} WHERE box_serial = :box ORDER BY id ASC";
return $this->query($sql, [':box' => $box_serial]);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 文档管理模型
*/
class Document extends Model
{
protected $table = 'document';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByCategory($category)
{
return $this->where(['category = :cat'], [':cat' => $category])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
public function getCategories()
{
$sql = 'SELECT DISTINCT category FROM ' . $this->getTable() . " WHERE category != '' ORDER BY category";
$stmt = $this->pdo->query($sql);
return $stmt->fetchAll();
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 员工模型
*/
class Employee extends Model
{
protected $table = 'employee';
// 根据员工编号查找
public function findByEmpNo($emp_no)
{
return $this->where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch();
}
/**
* 验证登录(支持 bcrypt 和旧版 MD5 兼容)
*/
public function validateLogin($emp_no, $password)
{
// 先按工号查找用户
$user = $this->where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch();
if (!$user) {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
error_log("[LOGIN_DIAG] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=0 ua={$ua} sess_hash={$sess}");
return false;
}
// 判断密码是否为 bcrypt 格式(bcrypt 哈希总是以 $2y$ 开头)
$isBcrypt = (substr($user['password'], 0, 4) === '$2y$');
$pwdFmt = $isBcrypt ? 'bcrypt' : (strlen($user['password']) === 32 ? 'md5' : 'other');
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
$pwdLen = strlen($password);
error_log("[LOGIN_DIAG] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=1 pwd_fmt={$pwdFmt} pwd_len={$pwdLen} ua={$ua} sess_hash={$sess}");
if ($isBcrypt) {
if (password_verify($password, $user['password'])) {
// 如果算法需要升级,自动更新哈希
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT)) {
$this->updatePasswordHash($user['id'], $password);
}
return $user;
}
error_log("[LOGIN_DIAG] PWD_VERIFY_FAIL pwd_len={$pwdLen} pwd_hash_len=" . strlen($user['password']) . " ua={$ua} sess_hash={$sess}");
return false;
}
// 兼容旧版 MD5 密码
if (md5($password) === $user['password']) {
// 自动升级为 bcrypt
$this->updatePasswordHash($user['id'], $password);
return $user;
}
return false;
}
/**
* 更新密码哈希为 bcrypt
*/
private function updatePasswordHash($id, $password)
{
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]);
}
/**
* 生成密码哈希
*/
public function hashPassword($password)
{
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
/**
* 密码复杂度校验(P1:强制强密码策略)
* 要求:≥12 位,且同时包含大写字母、小写字母、数字、特殊字符
* @throws \RuntimeException 不满足时抛出异常
*/
public function validatePasswordStrength($password)
{
if (!is_string($password) || $password === '') {
throw new \RuntimeException('密码不能为空');
}
if (mb_strlen($password) < 12) {
throw new \RuntimeException('密码长度至少 12 位');
}
if (!preg_match('/[A-Z]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个大写字母');
}
if (!preg_match('/[a-z]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个小写字母');
}
if (!preg_match('/[0-9]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个数字');
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个特殊字符(如 !@#$%^&*');
}
// 拒绝常见弱密码
$weak = ['123456', 'password', 'admin123', 'qwerty', '111111', '12345678', 'abc123'];
if (in_array(strtolower($password), $weak, true)) {
throw new \RuntimeException('密码过于常见,请更换');
}
}
// 重置密码
public function resetPassword($id, $new_password)
{
$this->validatePasswordStrength($new_password);
$hash = $this->hashPassword($new_password);
return $this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]);
}
// 获取所有员工
public function getAll()
{
return $this->order(['id ASC'])->fetchAll();
}
// 添加员工
public function addEmployee($data)
{
if (!isset($data['password']) || empty(trim($data['password']))) {
// 生成随机强密码
$randomPwd = bin2hex(random_bytes(8)); // 16位随机密码
$data['password'] = $this->hashPassword($randomPwd);
// 返回生成的密码供管理员告知用户
$data['_generated_password'] = $randomPwd;
} else {
// P1:密码复杂度校验(≥12位,含大小写+数字+特殊字符)
$this->validatePasswordStrength($data['password']);
$data['password'] = $this->hashPassword($data['password']);
}
// 移除 _generated_password 再写入
$generatedPwd = $data['_generated_password'] ?? null;
unset($data['_generated_password']);
$id = $this->add($data);
if ($id && $generatedPwd) {
// 返回生成的密码
$this->_lastGeneratedPwd = $generatedPwd;
}
return $id;
}
// 获取最后生成的密码
public function getLastGeneratedPwd()
{
return $this->_lastGeneratedPwd ?? null;
}
private $_lastGeneratedPwd = null;
// 更新员工
public function updateEmployee($id, $data)
{
unset($data['password']); // 不允许直接更新密码
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 根据类目获取员工列表
public function getByCategory($category)
{
if ($category === 'all') {
return $this->order(['id ASC'])->fetchAll();
}
return $this->where(['category = :cat OR role = :super'], [
':cat' => $category,
':super' => 'super_admin'
])->order(['id ASC'])->fetchAll();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 员工工位权限模型
*/
class EmployeePermission extends Model
{
protected $table = 'employee_station_permission';
/**
* 获取某员工所有工位权限(返回 station_type => is_allowed 的键值对)
*/
public function getByEmployee($employeeId)
{
$rows = $this->where(['employee_id = :eid'], [':eid' => $employeeId])->fetchAll();
$permissions = [];
foreach ($rows as $row) {
$permissions[$row['station_type']] = (int)$row['is_allowed'];
}
return $permissions;
}
/**
* 检查员工是否有某个工位的访问权限
* 超级管理员和管理员始终返回 true
*/
public function canAccess($employeeId, $role, $stationType)
{
// 超级管理员和管理员拥有所有工位权限
if ($role === \core\base\Controller::ROLE_SUPER_ADMIN || $role === \core\base\Controller::ROLE_ADMIN) {
return true;
}
$row = $this->where([
'employee_id = :eid AND station_type = :stype'
], [
':eid' => $employeeId,
':stype' => $stationType
])->fetch();
// 如果没配置过权限,默认允许访问
if (!$row) {
return true;
}
return (int)$row['is_allowed'] === 1;
}
/**
* 获取某员工允许访问的工位类型列表
*/
public function getAllowedStations($employeeId)
{
$rows = $this->where([
'employee_id = :eid AND is_allowed = 1'
], [':eid' => $employeeId])->fetchAll();
return array_column($rows, 'station_type');
}
/**
* 批量设置员工工位权限
* @param int $employeeId 员工ID
* @param array $permissions 格式:['station_type' => 1/0, ...]
*/
public function savePermissions($employeeId, $permissions)
{
// 先删除旧权限
$this->where(['employee_id = :eid'], [':eid' => $employeeId])->deleteWhere();
// 批量插入新权限
foreach ($permissions as $stationType => $isAllowed) {
$this->add([
'employee_id' => $employeeId,
'station_type' => $stationType,
'is_allowed' => $isAllowed ? 1 : 0,
]);
}
return true;
}
/**
* 删除某员工所有权限记录
*/
public function deleteByEmployee($employeeId)
{
return $this->where(['employee_id = :eid'], [':eid' => $employeeId])->deleteWhere();
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 财务账务记录模型
*/
class FinanceRecord extends Model
{
protected $table = 'finance_record';
public function getAll()
{
return $this->order(['record_date DESC', 'id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByDateRange($startDate, $endDate, $type = '')
{
$where = 'record_date >= :start AND record_date <= :end';
$params = [':start' => $startDate, ':end' => $endDate];
if ($type) {
$where .= ' AND type = :type';
$params[':type'] = $type;
}
return $this->where([$where], $params)->order(['record_date DESC', 'id DESC'])->fetchAll();
}
public function getSummary($startDate, $endDate)
{
$sql = "SELECT
type,
SUM(amount) AS total,
COUNT(*) AS cnt
FROM " . $this->getTable() . "
WHERE record_date >= :start AND record_date <= :end
GROUP BY type";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':start' => $startDate, ':end' => $endDate]);
return $stmt->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 成品测试模型
* @deprecated
*/
class FinishedTest extends Model
{
protected $table = 'finished_test';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetchAll();
}
// 根据产品型号筛选记录(finished_test 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(test_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 人事档案模型
*/
class HrEmployee extends Model
{
protected $table = 'hr_employee';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByDepartment($department)
{
return $this->where(['department = :dept'], [':dept' => $department])->order(['id DESC'])->fetchAll();
}
public function getByStatus($status)
{
return $this->where(['status = :st'], [':st' => $status])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
* 所有工位数据操作统一通过 StationDefinition 模型处理。
* 此文件保留仅用于向后兼容,后续版本将删除。
*/
namespace app\models;
use core\base\Model;
/**
* 入库工位模型(成品入库)
* @deprecated
*/
class Inbound extends Model
{
protected $table = 'station_inbound';
// 获取所有记录
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
// 添加记录
public function addRecord($data)
{
return $this->add($data);
}
// 根据序列号查找
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 批量导入
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 库存模型
*/
class Inventory extends Model
{
protected $table = 'inventory';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getLowStock()
{
return $this->where(['quantity <= min_stock AND min_stock > 0'])->order(['quantity ASC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt, SUM(quantity) AS total_qty FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $stmt->fetch();
}
public function search($keyword)
{
return $this->where(
['product_name LIKE :kw OR product_model LIKE :kw2 OR category LIKE :kw3'],
[':kw' => "%{$keyword}%", ':kw2' => "%{$keyword}%", ':kw3' => "%{$keyword}%"]
)->order(['id DESC'])->fetchAll();
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* PCB 测试模型
* @deprecated
*/
class PcbTest extends Model
{
protected $table = 'pcb_test';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 产品型号模型
*/
class ProductModel extends Model
{
protected $table = 'product_model';
/** @var bool 迁移是否已检查(进程级缓存,避免重复检查) */
private static $migrated = false;
/** @var bool type 列是否存在 */
private static $typeColumnExists = false;
/**
* 确保 type 列存在(自动迁移)
* @return bool type 列是否存在(true=可用,false=不存在且无法创建)
*/
private function ensureTypeColumn()
{
if (self::$migrated) {
return self::$typeColumnExists;
}
$table = $this->getTable();
try {
$this->query("SELECT type FROM {$table} LIMIT 1");
self::$typeColumnExists = true;
self::$migrated = true;
} catch (\Throwable $e) {
// 列不存在 → 尝试自动迁移
try {
$this->execute("ALTER TABLE {$table}
ADD COLUMN `type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '类型(成品/半成品/配件)'
AFTER `id`");
$this->execute("UPDATE {$table} SET `type` = `product_type` WHERE `type` = ''");
self::$typeColumnExists = true;
} catch (\Throwable $migErr) {
error_log('[ProductModel] Auto-migration failed: ' . $migErr->getMessage());
self::$typeColumnExists = false;
}
self::$migrated = true;
}
return self::$typeColumnExists;
}
// 获取所有产品型号
public function getAll()
{
$this->ensureTypeColumn();
return $this->order(['id ASC'])->fetchAll();
}
// 添加产品型号
public function addModel($data)
{
if (!$this->ensureTypeColumn()) {
unset($data['type']); // type 列不存在,移除该字段
}
return $this->add($data);
}
// 更新产品型号
public function updateModel($id, $data)
{
if (!$this->ensureTypeColumn()) {
unset($data['type']);
}
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除产品型号
public function deleteModel($id)
{
return $this->delete($id);
}
// 根据型号代码查找
public function findByCode($model_code)
{
return $this->where(['model_code = :code'], [':code' => $model_code])->fetch();
}
// 根据类型+产品类型和型号代码联合查找(用于检测同类型下是否重复)
public function findByTypeAndCode($type, $product_type, $model_code)
{
if ($this->ensureTypeColumn()) {
return $this->where(
['type = :t', 'product_type = :pt', 'model_code = :mc'],
[':t' => $type, ':pt' => $product_type, ':mc' => $model_code]
)->fetch();
}
// Fallback: type 列不存在,仅在 product_type 和 model_code 上查重
return $this->where(
['product_type = :pt', 'model_code = :mc'],
[':pt' => $product_type, ':mc' => $model_code]
)->fetch();
}
// 根据类型获取该类型下的所有型号(用于前端联动)
public function getByType($type)
{
if (!$this->ensureTypeColumn()) {
return [];
}
return $this->where(['type = :t'], [':t' => $type])
->order(['model_code ASC'])
->fetchAll();
}
// 获取类型列表(去重)
public function getTypes()
{
if (!$this->ensureTypeColumn()) {
return [];
}
$sql = 'SELECT DISTINCT type FROM ' . $this->getTable() . ' WHERE type != \'\' ORDER BY type';
return $this->query($sql);
}
/**
* 获取所有产品类型(product_type 去重列表,用于下拉筛选)
* 注意:这是 product_type 字段(自由文本),不是 type(成品/半成品/配件)
*/
public function getProductTypes()
{
$table = $this->getTable();
return $this->query(
"SELECT DISTINCT product_type FROM {$table} WHERE product_type != '' ORDER BY product_type"
);
}
/**
* 根据类型获取产品型号
* 优先通过 type 字段查询(大分类:成品/半成品/配件),
* 如果 type 查不到则通过 product_type 字段查询(子分类:PCBA/电池/外壳等)
* 若 type 列不可用,回退到 product_type 字段查询
*/
public function getByProductType($productType)
{
if ($this->ensureTypeColumn()) {
// 先尝试 type 字段
$result = $this->getByType($productType);
if (!empty($result)) {
return $result;
}
// type 查不到,回退到 product_type 字段
return $this->where(['product_type = :pt'], [':pt' => $productType])
->order(['model_code ASC'])
->fetchAll();
}
// Fallback: type 列不存在,使用 product_type 字段
return $this->where(['product_type = :pt'], [':pt' => $productType])
->order(['model_code ASC'])
->fetchAll();
}
}
+390
View File
@@ -0,0 +1,390 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 产品类型配置模型
*/
class ProductTypeConfig extends Model
{
protected $table = 'DGZXY_product_type_config';
/**
* 安全执行查询:表不存在时返回空数组
*/
private function safeQuery(callable $query, $default = [])
{
try {
return $query();
} catch (\Throwable $e) {
error_log('[ProductTypeConfig] DB error: ' . $e->getMessage());
return $default;
}
}
/**
* 获取所有启用的类型(按 sort_order 排序)
*/
public function getEnabled()
{
return $this->safeQuery(function () {
return $this->where(['enabled = 1'])
->order(['sort_order ASC'])
->fetchAll();
}, []);
}
/**
* 获取所有类型(含未启用,按 sort_order 排序)
*/
public function getAll()
{
return $this->safeQuery(function () {
return $this->order(['sort_order ASC'])->fetchAll();
}, []);
}
/**
* 根据类型名称获取配置
*/
public function getByTypeName($typeName)
{
return $this->safeQuery(function () use ($typeName) {
return $this->where(['type_name = :name'], [':name' => $typeName])->fetch();
}, null);
}
/**
* 获取类型名称列表(用于下拉选择)
*/
public function getTypeNames()
{
$rows = $this->getEnabled();
return array_column($rows, 'type_name');
}
/**
* 获取入库工位可见的类型(enabled=1 且 show_in_inbound=1
*/
public function getInboundVisible()
{
return $this->safeQuery(function () {
return $this->where(['enabled = 1', 'show_in_inbound = 1'])
->order(['sort_order ASC'])
->fetchAll();
}, []);
}
/**
* 获取类型的 display_columns(解析 JSON
*/
public function getDisplayColumns($typeName)
{
$config = $this->getByTypeName($typeName);
if ($config && !empty($config['display_columns'])) {
$columns = json_decode($config['display_columns'], true);
if (is_array($columns)) {
return $columns;
}
}
// 默认列
return [
['key' => 'type', 'label' => '类型'],
['key' => 'model_code', 'label' => '产品型号'],
['key' => 'model_desc', 'label' => '产品说明'],
];
}
/**
* 获取所有类型的 display_columnskeyed by type_name
*/
public function getAllDisplayColumns()
{
$all = $this->getAll();
$result = [];
foreach ($all as $row) {
$cols = [];
if (!empty($row['display_columns'])) {
$cols = json_decode($row['display_columns'], true);
}
if (!is_array($cols)) {
$cols = [
['key' => 'type', 'label' => '类型'],
['key' => 'model_code', 'label' => '产品型号'],
['key' => 'model_desc', 'label' => '产品说明'],
];
}
$result[$row['type_name']] = $cols;
}
return $result;
}
/**
* 添加类型配置(表不存在时自动创建)
*/
public function addConfig($data)
{
try {
return $this->add($data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] addConfig error: ' . $msg);
// 表不存在时自动创建
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
// 重试添加
try {
return $this->add($data);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] addConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 更新类型配置
*/
public function updateConfig($id, $data)
{
try {
return $this->where(['id = :id'], [':id' => $id])->update($data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] updateConfig error: ' . $msg);
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
try {
return $this->where(['id = :id'], [':id' => $id])->update($data);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] updateConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 删除类型配置
*/
public function deleteConfig($id)
{
try {
return $this->delete($id);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] deleteConfig error: ' . $msg);
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
try {
return $this->delete($id);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] deleteConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 自动创建 product_type_config 表并初始化默认数据
*/
public function autoCreateTable()
{
try {
$tableName = $this->getTable();
$sql = "CREATE TABLE IF NOT EXISTS `{$tableName}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`type_name` VARCHAR(50) NOT NULL COMMENT '类型名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`display_columns` TEXT COMMENT '列表页额外列(JSON)',
`product_type_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '产品类型输入框状态:hidden/readonly/editable',
`product_type_label` VARCHAR(50) DEFAULT '产品类型' COMMENT '产品类型输入框标签',
`product_type_field` VARCHAR(50) DEFAULT 'product_type' COMMENT '提交字段名',
`product_type_placeholder` VARCHAR(100) DEFAULT '' COMMENT '输入框占位提示',
`model_code_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '产品型号输入框状态:hidden/readonly/editable',
`color_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '颜色输入框状态:hidden/readonly/editable',
`color` VARCHAR(30) DEFAULT 'default' COMMENT '列表页box颜色',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
`show_in_inbound` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '入库页是否显示',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品类型配置'";
$this->pdo->exec($sql);
// 检查是否已有数据
$count = $this->pdo->query("SELECT COUNT(*) FROM `{$tableName}`")->fetchColumn();
if ($count == 0) {
$defaults = self::getDefaultConfigs();
$insertSql = "INSERT INTO `{$tableName}`
(`type_name`, `sort_order`, `display_columns`, `product_type_state`,
`product_type_label`, `product_type_field`, `product_type_placeholder`,
`model_code_state`, `color_state`, `color`, `enabled`, `show_in_inbound`)
VALUES (:type_name, :sort_order, :display_columns, :product_type_state,
:product_type_label, :product_type_field, :product_type_placeholder,
:model_code_state, :color_state, :color, :enabled, :show_in_inbound)";
$stmt = $this->pdo->prepare($insertSql);
foreach ($defaults as $row) {
$stmt->execute([
':type_name' => $row['type_name'],
':sort_order' => $row['sort_order'],
':display_columns' => $row['display_columns'],
':product_type_state' => $row['product_type_state'],
':product_type_label' => $row['product_type_label'],
':product_type_field' => $row['product_type_field'],
':product_type_placeholder' => $row['product_type_placeholder'],
':model_code_state' => $row['model_code_state'] ?? 'editable',
':color_state' => $row['color_state'] ?? 'editable',
':color' => $row['color'],
':enabled' => $row['enabled'],
':show_in_inbound' => $row['show_in_inbound'] ?? 1,
]);
}
}
error_log('[ProductTypeConfig] Table auto-created successfully');
return true;
} catch (\Throwable $e) {
error_log('[ProductTypeConfig] autoCreateTable error: ' . $e->getMessage());
return false;
}
}
/**
* 获取硬编码默认配置(数据库表不存在时的 fallback)
*/
/**
* 三态值常量
* hidden: 隐藏输入框
* readonly: 显示但只读
* editable: 可编辑
*/
const STATE_HIDDEN = 'hidden';
const STATE_READONLY = 'readonly';
const STATE_EDITABLE = 'editable';
public static function getDefaultConfigs()
{
return [
[
'type_name' => 'PCBA',
'sort_order' => 0,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'primary',
'enabled' => 1,
'show_in_inbound' => 0, // PCBA 默认不在入库页显示
],
[
'type_name' => '成品',
'sort_order' => 1,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'success',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '电池',
'sort_order' => 2,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"capacity","label":"容量"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '容量',
'product_type_field' => 'capacity',
'product_type_placeholder' => '如:2000mAh',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'primary',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '外壳',
'sort_order' => 3,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"color","label":"颜色"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_EDITABLE,
'color' => 'info',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '外壳颜色',
'sort_order' => 4,
'display_columns' => '[{"key":"color","label":"颜色"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '颜色',
'product_type_field' => 'color',
'product_type_placeholder' => '如:黑色',
'model_code_state' => self::STATE_HIDDEN,
'color_state' => self::STATE_EDITABLE,
'color' => 'default',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '包装',
'sort_order' => 5,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:通用包装',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'warning',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '半成品',
'sort_order' => 6,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:电池、外壳',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'warning',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '配件',
'sort_order' => 7,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:电池、外壳',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'info',
'enabled' => 1,
'show_in_inbound' => 1,
],
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 采购明细模型
*/
class PurchaseItem extends Model
{
protected $table = 'purchase_item';
public function getByOrder($orderId)
{
return $this->where(['order_id = :oid'], [':oid' => $orderId])->fetchAll();
}
public function deleteByOrder($orderId)
{
$sql = 'DELETE FROM ' . $this->getTable() . ' WHERE order_id = :oid';
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([':oid' => $orderId]);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 采购订单模型
*/
class PurchaseOrder extends Model
{
protected $table = 'purchase_order';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getBySupplier($supplierId)
{
return $this->where(['supplier_id = :sid'], [':sid' => $supplierId])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 应收应付账款模型
*/
class ReceivablePayable extends Model
{
protected $table = 'receivable_payable';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByType($type)
{
return $this->where(['type = :type'], [':type' => $type])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 销售明细模型
*/
class SalesItem extends Model
{
protected $table = 'sales_item';
public function getByOrder($orderId)
{
return $this->where(['order_id = :oid'], [':oid' => $orderId])->fetchAll();
}
public function deleteByOrder($orderId)
{
$sql = 'DELETE FROM ' . $this->getTable() . ' WHERE order_id = :oid';
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([':oid' => $orderId]);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 销售订单模型
*/
class SalesOrder extends Model
{
protected $table = 'sales_order';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+603
View File
@@ -0,0 +1,603 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 工位定义模型 - 统一管理所有内置工位的元数据
*
* 替代之前分散在 7 个独立 Model 文件中的硬编码配置。
* 所有内置工位的数据表名、时间列名、筛选开关等统一在此管理。
*
* 数据库表:DGZXY_station_definitions
*/
class StationDefinition extends Model
{
protected $table = 'station_definitions';
/** @var array 缓存已加载的定义 */
private static $cache = [];
/**
* 获取工位定义(表不存在时自动创建并初始化)
* @param string $stationType
* @return array|null
*/
public function getByType($stationType)
{
if (isset(self::$cache[$stationType])) {
return self::$cache[$stationType];
}
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e) {
error_log('[StationDefinition] getByType(' . $stationType . ') failed: ' . $e->getMessage());
// 表不存在,自动创建并初始化
if ($this->autoCreateTable()) {
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e2) {
error_log('[StationDefinition] getByType retry failed: ' . $e2->getMessage());
$row = null;
}
} else {
$row = null;
}
}
if ($row && !empty($row['extra_config'])) {
$row['extra'] = json_decode($row['extra_config'], true) ?: [];
} elseif ($row) {
$row['extra'] = [];
}
self::$cache[$stationType] = $row ?: null;
return self::$cache[$stationType];
}
/**
* 获取完整表名(带前缀)
*/
public function getFullTableName($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
return self::TABLE_PREFIX . $def['db_table'];
}
/**
* 获取时间列名
*/
public function getTimeColumn($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
}
return $def ? $def['time_column'] : 'created_at';
}
/**
* 获取记录列表(支持按类型和型号筛选)
*
* 如果 station_definitions 记录缺失,自动尝试重建。
*/
public function getRecords($stationType, $filterType = '', $filterModel = '', $limit = 100)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] getRecords: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return [];
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
$timeCol = $def['time_column'];
$sql = "SELECT * FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$sql .= " ORDER BY `{$timeCol}` DESC LIMIT " . intval($limit);
try {
return $this->query($sql, $params);
} catch (\Throwable $e) {
error_log("[StationDefinition] getRecords query failed for {$stationType}: " . $e->getMessage());
// 表可能不存在,尝试创建
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
try {
return $this->query($sql, $params);
} catch (\Throwable $e2) {
error_log("[StationDefinition] getRecords retry failed for {$stationType}: " . $e2->getMessage());
}
}
return [];
}
}
/**
* 获取所有记录
*/
public function getAllRecords($stationType, $limit = 100)
{
return $this->getRecords($stationType, '', '', $limit);
}
/**
* 统计今日某操作员的录入数量
*/
public function countTodayByOperator($stationType, $operator)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTodayByOperator: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$timeCol = $def['time_column'];
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}` WHERE operator = :op AND DATE(`{$timeCol}`) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTodayByOperator failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 统计总记录数(可按型号筛选)
*/
public function countTotal($stationType, $filterType = '', $filterModel = '')
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTotal: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$result = $this->query($sql, $params);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTotal failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 添加记录到工位专用表
*
* 如果 station_definitions 中没有该工位记录,自动尝试重建。
* 如果数据表不存在,自动创建后再插入。
*/
public function addRecord($stationType, $data)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建 station_definitions 数据
error_log("[StationDefinition] addRecord: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] addRecord: still no definition for '{$stationType}' after re-init");
return false;
}
}
$table = self::TABLE_PREFIX . $def['db_table'];
// 确保数据表存在(不存在则创建)
try {
$fields = implode('`, `', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO `{$table}` (`{$fields}`) VALUES ({$placeholders})";
return $this->execute($sql, $data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log("[StationDefinition] addRecord failed for {$stationType}: {$msg}");
// 表不存在时自动创建
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
// 需要 fieldConfigs 来创建表,从全局获取
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
// 重试插入
try {
return $this->execute($sql, $data);
} catch (\Throwable $e2) {
error_log("[StationDefinition] addRecord retry failed for {$stationType}: " . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 检查记录是否已存在(防重复)
*/
public function findByField($stationType, $fieldName, $value)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] findByField: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
$table = self::TABLE_PREFIX . $def['db_table'];
try {
$sql = "SELECT * FROM `{$table}` WHERE `{$fieldName}` = :val LIMIT 1";
$result = $this->query($sql, [':val' => $value]);
return $result[0] ?? null;
} catch (\Throwable $e) {
error_log("[StationDefinition] findByField failed for {$stationType}: " . $e->getMessage());
return null;
}
}
/**
* 检查工位是否启用筛选
*/
public function isFilterEnabled($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['filter_enabled'] : false;
}
/**
* 检查是否显示今日计数
*/
public function showTodayCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_today_count'] : false;
}
/**
* 检查是否显示总计数
*/
public function showTotalCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_total_count'] : false;
}
/**
* 确保工位数据表存在,不存在则自动创建
*
* 通用工位表结构(参考 station_inbound):
* id, product_type, product_model, operator, created_at + 动态字段
*
* @param string $stationType 工位类型标识
* @param array $fieldConfigs 字段配置(从 fields_config 解析)
* @return bool 表是否已存在/创建成功
*/
public function ensureTable($stationType, $fieldConfigs = [])
{
// 先尝试获取工位定义,如果失败则尝试重建 station_definitions 表数据
$def = $this->getByType($stationType);
if (!$def) {
// station_definitions 表中没有该工位的记录,尝试重新插入
error_log("[StationDefinition] ensureTable: no definition for '{$stationType}', trying to re-initialize built-in data");
$this->autoCreateTable();
// 清除缓存后重试
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] ensureTable: still no definition for '{$stationType}' after re-init");
return false;
}
}
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否已存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (!empty($exists)) return true;
// 基础列:所有通用工位表都有的字段
$columns = [
"`id` INT AUTO_INCREMENT PRIMARY KEY",
"`product_type` VARCHAR(100) DEFAULT NULL COMMENT '产品类型'",
"`product_model` VARCHAR(100) DEFAULT NULL COMMENT '产品型号'",
"`operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人'",
"`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间'",
];
// 索引
$indexes = [
"KEY `idx_product_type` (`product_type`)",
"KEY `idx_product_model` (`product_model`)",
"KEY `idx_created_at` (`created_at`)",
];
// 从 fields_config 派生额外字段(排除已存在的基础字段和隐藏字段)
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
$addedFields = [];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
// 跳过纯虚拟字段(没有实际存储意义的)
if (in_array($name, ['_meta'])) continue;
// 根据字段类型确定 MySQL 列类型
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
$columns[] = "`{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'";
// 为序列号类字段添加索引
if (!empty($field['is_scan']) || stripos($name, 'serial') !== false) {
$indexes[] = "KEY `idx_{$name}` (`{$name}`)";
}
$addedFields[] = $name;
}
// 从 station_business_helper 的 dataMapping 补充可能缺失的列
// 确保 helper 文件已加载
if (!function_exists('getStationBusinessConfig')) {
@include_once APP_PATH . 'app/helpers/station_business_helper.php';
}
if (function_exists('getStationBusinessConfig')) {
$bizConfig = getStationBusinessConfig($stationType);
if ($bizConfig && !empty($bizConfig['dataMapping'])) {
foreach ($bizConfig['dataMapping'] as $dbField => $postKey) {
if ($postKey === '__operator__') continue;
if (in_array($dbField, $baseFields) || in_array($dbField, $addedFields)) continue;
$columns[] = "`{$dbField}` VARCHAR(200) DEFAULT NULL COMMENT '{$dbField}'";
$addedFields[] = $dbField;
}
}
}
$allColumns = implode(",\n ", $columns);
$allIndexes = !empty($indexes) ? ",\n " . implode(",\n ", $indexes) : '';
$sql = "CREATE TABLE `{$fullTable}` (\n {$allColumns}{$allIndexes}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{$stationType} 工位数据表'";
try {
$this->execute($sql);
error_log("[StationDefinition] Auto-created table: {$fullTable} for station_type={$stationType}, extra fields: " . implode(', ', $addedFields));
return true;
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to create table {$fullTable}: " . $e->getMessage());
return false;
}
}
/**
* 确保工位列存在(表已存在时补充缺失的列)
* 当 fields_config 新增字段后,自动 ALTER TABLE 添加
*/
public function ensureColumns($stationType, $fieldConfigs = [])
{
$def = $this->getByType($stationType);
if (!$def) return;
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 表不存在,走建表流程
return $this->ensureTable($stationType, $fieldConfigs);
}
// 获取现有列
$existingCols = $this->query("SHOW COLUMNS FROM `{$fullTable}`");
$existingNames = [];
foreach ($existingCols as $col) {
$existingNames[] = strtolower($col['Field']);
}
// 基础字段名
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
if (in_array($name, ['_meta'])) continue;
if (in_array(strtolower($name), $existingNames)) continue;
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
try {
$this->execute("ALTER TABLE `{$fullTable}` ADD COLUMN `{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'");
error_log("[StationDefinition] Added column `{$name}` to {$fullTable}");
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to add column `{$name}` to {$fullTable}: " . $e->getMessage());
}
}
}
/**
* 自动创建 station_definitions 表并初始化内置工位数据
* @return bool 是否成功
*/
/**
* 获取所有内置工位的默认定义数据
*/
private function getBuiltinDefaults()
{
return [
['station_type' => 'inbound', 'db_table' => 'station_inbound', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => '{"alt_table":"warehouse_in","alt_time_column":"in_time","alt_condition_field":"product_type","alt_condition_value":"成品"}'],
['station_type' => 'pcb_test', 'db_table' => 'pcb_test', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'battery', 'db_table' => 'battery_status', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'assembly', 'db_table' => 'product_assembly', 'time_column' => 'assembly_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'finished_test','db_table' => 'finished_test', 'time_column' => 'test_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'delivery', 'db_table' => 'delivery', 'time_column' => 'delivery_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'warehouse', 'db_table' => 'warehouse_in', 'time_column' => 'in_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 0, 'show_today_count' => 0, 'show_total_count' => 0, 'extra_config' => ''],
];
}
/**
* 插入/更新内置工位默认数据(ON DUPLICATE KEY UPDATE
*
* 使用 ON DUPLICATE KEY UPDATE 而非 INSERT IGNORE,确保即使表中有旧数据,
* 也能更新为正确的默认值(修复数据损坏/不完整的情况)。
*
* @return bool
*/
private function insertBuiltinDefaults()
{
$fullTable = $this->getTable();
$defaults = $this->getBuiltinDefaults();
$insertSql = "INSERT INTO `{$fullTable}`
(`station_type`, `db_table`, `time_column`, `has_product_type`, `has_product_model`,
`filter_enabled`, `show_today_count`, `show_total_count`, `extra_config`)
VALUES (:st, :dt, :tc, :hpt, :hpm, :fe, :stc, :sttc, :ec)
ON DUPLICATE KEY UPDATE
`db_table` = VALUES(`db_table`),
`time_column` = VALUES(`time_column`),
`has_product_type` = VALUES(`has_product_type`),
`has_product_model` = VALUES(`has_product_model`),
`filter_enabled` = VALUES(`filter_enabled`),
`show_today_count` = VALUES(`show_today_count`),
`show_total_count` = VALUES(`show_total_count`),
`extra_config` = VALUES(`extra_config`)";
try {
// 使用 $this->execute() 而非直接访问 $this->pdo
// 确保在 Model 构造链完整的情况下执行
$affected = 0;
foreach ($defaults as $d) {
$params = [
':st' => $d['station_type'],
':dt' => $d['db_table'],
':tc' => $d['time_column'],
':hpt' => $d['has_product_type'],
':hpm' => $d['has_product_model'],
':fe' => $d['filter_enabled'],
':stc' => $d['show_today_count'],
':sttc' => $d['show_total_count'],
':ec' => $d['extra_config'],
];
// execute() 返回 boolrowCount() 需要通过 pdo 获取
if ($this->execute($insertSql, $params)) {
$affected++;
}
}
if ($affected > 0) {
error_log('[StationDefinition] Upserted ' . $affected . ' built-in station definitions');
}
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] insertBuiltinDefaults failed: ' . $e->getMessage());
return false;
}
}
public function autoCreateTable()
{
$fullTable = $this->getTable();
try {
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 创建表
$sql = "CREATE TABLE IF NOT EXISTS `{$fullTable}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`station_type` VARCHAR(50) NOT NULL UNIQUE COMMENT '工位类型标识',
`db_table` VARCHAR(100) NOT NULL COMMENT '数据表名(不含前缀)',
`time_column` VARCHAR(50) NOT NULL DEFAULT 'created_at' COMMENT '时间列名',
`has_product_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_type 列',
`has_product_model` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_model 列',
`filter_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用筛选',
`show_today_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示今日计数',
`show_total_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示总计数',
`extra_config` TEXT COMMENT '额外配置(JSON',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_station_type` (`station_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工位定义统一配置表'";
$this->execute($sql);
error_log('[StationDefinition] Auto-created table: ' . $fullTable);
}
// 无论表是否刚创建,确保内置工位数据存在
// (表可能已存在但数据不完整,例如之前创建失败只建了表没插入数据)
$this->insertBuiltinDefaults();
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] autoCreateTable failed: ' . $e->getMessage());
return false;
}
}
/**
* 清除缓存(数据更新后调用)
*/
public static function clearCache()
{
self::$cache = [];
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 通用工位记录模型
* 用于动态添加的工位类型的数据存储
* 通过 station_type 字段区分不同工位
*/
class StationRecord extends Model
{
protected $table = 'station_records';
/**
* 根据工位类型获取记录
*/
public function getByStationType($stationType, $limit = 100)
{
return $this->where(['station_type = :type'], [':type' => $stationType])
->order(['created_at DESC'])
->limit($limit)
->fetchAll();
}
/**
* 添加记录
* @param string $stationType 工位类型
* @param array $data 字段名→值的键值对
* @param string $operator 操作人
*/
public function addRecord($stationType, $data, $operator = '')
{
return $this->add([
'station_type' => $stationType,
'record_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'operator' => $operator,
'created_at' => date('Y-m-d H:i:s'),
]);
}
/**
* 统计今日某操作人的记录数
*/
public function countTodayByOperator($operator, $stationType = null)
{
$conditions = [];
$params = [];
$conditions[] = 'operator = :operator';
$params[':operator'] = $operator;
$conditions[] = 'DATE(created_at) = CURDATE()';
if ($stationType) {
$conditions[] = 'station_type = :type';
$params[':type'] = $stationType;
}
$result = $this->where($conditions, $params)
->field('COUNT(*) as cnt')
->fetch();
return $result['cnt'] ?? 0;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 供应商模型
*/
class Supplier extends Model
{
protected $table = 'supplier';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getActiveList()
{
return $this->where(['status = 1'])->order(['name ASC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function search($keyword)
{
return $this->where(
['name LIKE :kw OR contact_person LIKE :kw2 OR phone LIKE :kw3'],
[':kw' => "%{$keyword}%", ':kw2' => "%{$keyword}%", ':kw3' => "%{$keyword}%"]
)->order(['id DESC'])->fetchAll();
}
}
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 系统配置模型
*/
class SysConfig extends Model
{
protected $table = 'sys_config';
// 获取配置
public function getConfig()
{
return $this->where(['id = :id'], [':id' => 1])->fetch();
}
// 更新配置
public function updateConfig($data)
{
return $this->where(['id = :id'], [':id' => 1])->update($data);
}
/**
* 获取代码前缀规则
* 格式: ['成品' => 'CP', '电芯' => 'DX', 'PCBA' => 'XLB']
*/
public function getCodePrefixRules()
{
$config = $this->getConfig();
$raw = $config['code_prefix_rules'] ?? '';
if (empty($raw)) {
// 返回默认规则
$rules = $this->getDefaultCodePrefixRules();
} else {
$rules = json_decode($raw, true);
if (!is_array($rules)) {
$rules = $this->getDefaultCodePrefixRules();
}
}
// 产品类型别名对齐:确保同义类型能匹配到同一前缀规则
// 例如配置表中使用「电池」,而前缀规则可能使用旧名「电芯」
$aliases = [
'电池' => '电芯',
'电芯' => '电池',
'底壳' => '外壳',
'外壳' => '底壳',
'半成品' => '半成',
'半成' => '半成品',
];
foreach ($aliases as $from => $to) {
if (!isset($rules[$from]) && isset($rules[$to])) {
$rules[$from] = $rules[$to];
}
}
return $rules;
}
/**
* 获取默认代码前缀规则
*/
public function getDefaultCodePrefixRules()
{
return [
'成品' => 'CP',
'电池' => 'dc',
'PCBA' => 'xlb',
'半成品' => '',
'外壳' => '',
'包装' => '',
'配件' => '',
];
}
/**
* 根据产品类型获取代码前缀
* @param string $productType 产品类型(如:成品、电芯、PCBA)
* @return string 前缀,无规则返回空字符串
*/
public function getPrefixByType($productType)
{
$rules = $this->getCodePrefixRules();
$prefix = $rules[$productType] ?? '';
// 原则:所有工位序列号都受前缀管控。配置了该类型但前缀为空时,
// 自动回退为「产品类型名拼音首字母」,避免无前缀类型被放行或卡死。
if ($prefix === '' && isset($rules[$productType])) {
$prefix = $this->pinyinInitials($productType);
}
return $prefix;
}
/**
* 取得字符串的拼音首字母(中文取首字拼音首字母,英文/数字原样保留)。
* 兼容「外壳」「包装」「半成品」「配件」等常见类型,英文部分(如 PCBA)原样输出。
* @param string $str
* @return string
*/
public function pinyinInitials($str)
{
$str = trim($str);
if ($str === '') return '';
// 常见产品类型精确映射(避免多音字/误判)
$map = [
'外壳' => 'WK',
'底壳' => 'WK',
'包装' => 'BZ',
'半成品' => 'BCP',
'半成' => 'BCP',
'配件' => 'PJ',
'成品' => 'CP',
'电池' => 'DC',
'电芯' => 'DC',
'PCBA' => 'XLB',
];
if (isset($map[$str])) return $map[$str];
$first = mb_substr($str, 0, 1, 'UTF-8');
// 英文字母/数字:直接原样(大写)
if (preg_match('/^[A-Za-z0-9]$/', $first)) {
return strtoupper($first) . (mb_strlen($str, 'UTF-8') > 1 ? strtoupper(mb_substr($str, 1, 1, 'UTF-8')) : '');
}
// 中文:取首字拼音首字母
return $this->chineseInitial($first);
}
/**
* 单汉字 → 拼音首字母(GB2312 区位区间法,覆盖常用汉字)
*/
private function chineseInitial($char)
{
$py = 'A';
try {
$gbk = iconv('UTF-8', 'GB2312//IGNORE', $char);
} catch (\Throwable $e) {
$gbk = '';
}
if ($gbk === '' || strlen($gbk) < 2) {
return $py; // 生僻字/无法转换时回退
}
$hi = ord($gbk[0]);
$lo = ord($gbk[1]);
$code = $hi * 256 + $lo;
// GB2312 拼音首字母分区(常用字)
$ranges = [
[0xB0A1, 0xB0C4, 'A'], [0xB0C5, 0xB2C0, 'B'], [0xB2C1, 0xB4ED, 'C'],
[0xB4EE, 0xB6E9, 'D'], [0xB6EA, 0xB7A1, 'E'], [0xB7A2, 0xB8C0, 'F'],
[0xB8C1, 0xB9FD, 'G'], [0xB9FE, 0xBBF6, 'H'], [0xBBF7, 0xBFA5, 'J'],
[0xBFA6, 0xC0AB, 'K'], [0xC0AC, 0xC2E7, 'L'], [0xC2E8, 0xC4C2, 'M'],
[0xC4C3, 0xC5B5, 'N'], [0xC5B6, 0xC5BD, 'O'], [0xC5BE, 0xC6DA, 'P'],
[0xC6DB, 0xC8BA, 'Q'], [0xC8BB, 0xC8F5, 'R'], [0xC8F6, 0xCBF0, 'S'],
[0xCBF1, 0xCDDA, 'T'], [0xCDDB, 0xCEF3, 'W'], [0xCEF4, 0xD1B8, 'X'],
[0xD1B9, 0xD4D0, 'Y'], [0xD4D1, 0xD7F9, 'Z'],
];
foreach ($ranges as $r) {
if ($code >= $r[0] && $code <= $r[1]) { $py = $r[2]; break; }
}
return $py;
}
/**
* 获取所有已配置的非空前缀规则(用于验证)
* @return array [前缀 => 产品类型名]
*/
public function getActivePrefixMap()
{
$rules = $this->getCodePrefixRules();
$active = [];
foreach ($rules as $type => $prefix) {
if (!empty($prefix)) {
$active[$prefix] = $type;
}
}
return $active;
}
/**
* 更新代码前缀规则(JSON 存储)
* @param array $rules ['成品' => 'CP', '电池' => 'dc', ...]
*/
public function updateCodePrefixRules($rules)
{
$json = json_encode($rules, JSON_UNESCAPED_UNICODE);
// 使用原生 PDO 直写,避免 Model::update 链式调用在二次写入时的潜在问题
$pdo = \core\db\Db::pdo();
$table = $this->getTable();
$stmt = $pdo->prepare("UPDATE {$table} SET code_prefix_rules = :v WHERE id = 1");
$stmt->execute([':v' => $json]);
return true;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 入库模型
* @deprecated
*/
class WarehouseIn extends Model
{
protected $table = 'warehouse_in';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
// 根据产品型号筛选记录(warehouse_in 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(in_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 工位模型
*/
class Workstation extends Model
{
protected $table = 'workstation';
// 获取所有工位(按排序)
public function getAll()
{
return $this->order(['sort_order ASC'])->fetchAll();
}
// 添加工位
public function addStation($data)
{
return $this->add($data);
}
// 更新工位
public function updateStation($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除工位
public function deleteStation($id)
{
return $this->delete($id);
}
// 更新排序
public function updateSort($id, $sort_order)
{
return $this->where(['id = :id'], [':id' => $id])->update(['sort_order' => $sort_order]);
}
// 根据类型获取工位
public function getByType($station_type)
{
return $this->where(['station_type = :type'], [':type' => $station_type])->fetch();
}
}
+244
View File
@@ -0,0 +1,244 @@
<?php
/**
* 认证服务
* 杰柏特半导体后台管理系统
*/
namespace app\service;
use app\model\Admin;
class AuthService
{
private $adminModel;
private $config;
public function __construct()
{
$this->adminModel = new Admin();
$this->config = require __DIR__ . '/../config/app.php';
}
/**
* 用户登录
*/
public function login($username, $password)
{
if (empty($username) || empty($password)) {
return ['code' => 400, 'msg' => '用户名和密码不能为空'];
}
$admin = $this->adminModel->findByUsername($username);
if (!$admin) {
return ['code' => 400, 'msg' => '用户名或密码错误'];
}
if ($admin['status'] != 1) {
return ['code' => 403, 'msg' => '账号已被禁用,请联系管理员'];
}
if (!password_verify($password, $admin['password'])) {
return ['code' => 400, 'msg' => '用户名或密码错误'];
}
// 更新登录信息
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$this->adminModel->updateLoginInfo($admin['id'], $ip);
// 生成Token
$token = $this->generateToken($admin);
return [
'code' => 200,
'msg' => '登录成功',
'data' => [
'token' => $token,
'userinfo' => [
'id' => $admin['id'],
'username' => $admin['username'],
'nickname' => $admin['nickname'] ?: $admin['username'],
'avatar' => $admin['avatar'] ?: '',
'category' => $admin['category'] ?? 'mes',
'role' => ($admin['category'] === 'all') ? 'super_admin' : 'admin',
],
],
];
}
/**
* 生成JWT Token
*/
private function generateToken($admin)
{
$header = base64_encode(json_encode([
'alg' => 'HS256',
'typ' => 'JWT',
]));
$payload = base64_encode(json_encode([
'id' => $admin['id'],
'username' => $admin['username'],
'iat' => time(),
'exp' => time() + $this->config['token_expire'],
]));
$signature = base64_encode(hash_hmac(
'sha256',
"{$header}.{$payload}",
$this->config['token_secret'],
true
));
return "{$header}.{$payload}.{$signature}";
}
/**
* 验证Token
*/
public function verifyToken($token)
{
if (empty($token)) {
return false;
}
$parts = explode('.', $token);
if (count($parts) !== 3) {
return false;
}
[$header, $payload, $signature] = $parts;
// 验证签名
$expectedSig = base64_encode(hash_hmac(
'sha256',
"{$header}.{$payload}",
$this->config['token_secret'],
true
));
if (!hash_equals($expectedSig, $signature)) {
return false;
}
// 解析payload
$data = json_decode(base64_decode($payload), true);
if (!$data) {
return false;
}
// 检查过期
if (isset($data['exp']) && $data['exp'] < time()) {
return false;
}
return $data;
}
/**
* 获取当前登录用户
*/
public function getCurrentUser()
{
$token = $this->getBearerToken();
if (!$token) {
return null;
}
$payload = $this->verifyToken($token);
if (!$payload) {
return null;
}
return $this->adminModel->find($payload['id']);
}
/**
* 从请求头获取Token
* 优先级: Authorization > X-Auth-Token (Nginx兼容)
*/
public function getBearerToken()
{
// 1. 标准 Authorization header
$headers = $this->getAuthorizationHeader();
if ($headers && preg_match('/Bearer\s+(.+)$/i', $headers, $matches)) {
return $matches[1];
}
// 2. 自定义 header X-Auth-Token (Nginx 默认不丢弃自定义 header)
// 前端发送: X-Auth-Token: Bearer <token>
$xAuthToken = $_SERVER['HTTP_X_AUTH_TOKEN'] ?? null;
if ($xAuthToken) {
if (preg_match('/^Bearer\s+(.+)$/i', $xAuthToken, $m)) {
return $m[1];
}
// 也支持直接传 token 值
return $xAuthToken;
}
// 3. URL 参数 token(用于 window.open / <a href> 下载等场景)
$urlToken = $_GET['token'] ?? null;
if ($urlToken) {
return $urlToken;
}
return null;
}
/**
* 获取Authorization头
* 兼容 Apache / Nginx / IIS / PHP-FPM 等多种环境
*/
private function getAuthorizationHeader()
{
// 1. Apache / Nginx (with pass) 标准
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
return $_SERVER['HTTP_AUTHORIZATION'];
}
// 2. Apache rewrite 后
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
// 3. Nginx + PHP-FPM: Authorization 可能被放在其他变量中
if (isset($_SERVER['AUTHORIZATION'])) {
return $_SERVER['AUTHORIZATION'];
}
// 4. apache_request_headers (Apache/FastCGI)
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (isset($headers['Authorization'])) {
return $headers['Authorization'];
}
// 有些环境下 header 名是小写的
if (isset($headers['authorization'])) {
return $headers['authorization'];
}
}
// 5. Nginx + PHP-FPM 终极兜底: 从原始 HTTP 头中提取
// 有些 Nginx 配置下,Authorization 可能根本不在 $_SERVER 中
// 尝试从 getallheaders() 获取 (PHP-FPM built-in)
if (function_exists('getallheaders')) {
$allHeaders = getallheaders();
if (isset($allHeaders['Authorization'])) {
return $allHeaders['Authorization'];
}
if (isset($allHeaders['authorization'])) {
return $allHeaders['authorization'];
}
}
return null;
}
/**
* 生成密码哈希
*/
public function hashPassword($password)
{
return password_hash($password, PASSWORD_BCRYPT);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
/**
* 批量删除测试数据
*/
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($title); ?> - MES系统</title>
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/font-awesome.min.css">
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/admin.css">
</head>
<body>
<?php include APP_PATH . 'app/views/layouts/admin_header.php'; ?>
<div class="content-wrapper">
<section class="content-header">
<h1><i class="fa fa-trash"></i> <?php echo htmlspecialchars($title); ?></h1>
</section>
<section class="content">
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
<?php endif; ?>
<!-- 按设备删除 -->
<div class="box">
<div class="box-header">
<h3 class="box-title"><i class="fa fa-laptop"></i> 按设备编码删除</h3>
</div>
<div class="box-body">
<form method="post" action="" class="confirm-form" data-confirm="确定删除该设备的所有测试数据?此操作不可恢复!">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="by_device">
<div class="form-group">
<label>设备编码</label>
<select name="device_code" class="form-control">
<option value="">请选择设备</option>
<?php foreach ($devices as $dev): ?>
<option value="<?php echo htmlspecialchars($dev['device_code']); ?>">
<?php echo htmlspecialchars($dev['device_code'] . ' - ' . ($dev['device_name'] ?? '')); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-danger">
<i class="fa fa-trash"></i> 按设备删除
</button>
</form>
</div>
</div>
<!-- 按ID批量删除 -->
<div class="box">
<div class="box-header">
<h3 class="box-title"><i class="fa fa-list-ol"></i> 按记录ID批量删除</h3>
</div>
<div class="box-body">
<form method="post" action="" class="confirm-form" data-confirm="确定删除这些记录?此操作不可恢复!">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="by_ids">
<div class="form-group">
<label>记录ID列表(多个ID用逗号分隔,如:1,2,3</label>
<input type="text" name="ids" class="form-control" placeholder="例如:1,2,3">
</div>
<button type="submit" class="btn btn-danger">
<i class="fa fa-trash"></i> 按ID删除
</button>
</form>
</div>
</div>
<!-- 清空全部 -->
<div class="box box-danger">
<div class="box-header">
<h3 class="box-title"><i class="fa fa-exclamation-triangle"></i> 危险操作</h3>
</div>
<div class="box-body">
<form method="post" action="" class="confirm-form" data-confirm="确定清空全部测试数据?此操作不可恢复!请确认您已备份数据。">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="clear_all">
<button type="submit" class="btn btn-danger btn-lg">
<i class="fa fa-bomb"></i> 清空全部测试数据
</button>
</form>
</div>
</div>
<a href="<?php echo BASE_URL; ?>/Admin/asdTestData" class="btn btn-default">
<i class="fa fa-arrow-left"></i> 返回测试数据列表
</a>
</section>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
</body>
</html>
+221
View File
@@ -0,0 +1,221 @@
<?php
$activeMenu = 'asdDeviceDebug';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plug"></i> 昂盛达设备调试</h3>
<div class="box-tools">
<span class="badge bg-blue">共 <?php echo count($devices ?? []); ?> 台设备</span>
</div>
</div>
<div class="box-body">
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdDeviceDebug" class="form-inline">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group" style="margin-right:8px;">
<label class="sr-only">查找设备</label>
<input type="text" name="keyword" class="form-control" style="width:260px;"
placeholder="按名称 / IP / 编号查找" value="<?php echo htmlspecialchars($keyword ?? ''); ?>">
</div>
<button type="submit" name="action" value="search" class="btn btn-default">
<i class="fa fa-search"></i> 查找设备
</button>
<span class="text-muted" style="margin-left:10px;">共 <?php echo count($devices ?? []); ?> 台</span>
</form>
<hr>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdDeviceDebug">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group">
<label>选择设备</label>
<select name="device_id" class="form-control" style="max-width:480px;">
<option value="0">— 请选择设备 —</option>
<?php foreach ($devices ?? [] as $dev): ?>
<option value="<?php echo intval($dev['id']); ?>" <?php echo (isset($selDeviceId) && $selDeviceId == $dev['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($dev['device_name'] ?? '设备' . $dev['id']); ?>
(<?php echo htmlspecialchars($dev['device_ip'] ?? '无IP'); ?>:<?php echo intval($dev['device_port'] ?? 502); ?>)
- <?php echo ($dev['status'] == 1) ? '启用' : '禁用'; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="btn-group" style="margin-left:8px;">
<button type="submit" name="action" value="testLink" class="btn btn-info">
<i class="fa fa-wifi"></i> 测试链接状态
</button>
<button type="submit" name="action" value="readData" class="btn btn-success">
<i class="fa fa-download"></i> 数据读取
</button>
<button type="submit" name="action" value="writeData" class="btn btn-warning">
<i class="fa fa-upload"></i> 数据写入(探针)
</button>
</div>
</form>
<hr>
<div class="row">
<div class="col-md-6">
<div class="well" style="margin-bottom:0;">
<h4><i class="fa fa-plug"></i> 手动连接(IP:端口)</h4>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdDeviceDebug" class="form-inline">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group" style="margin-right:6px;">
<input type="text" name="manual_ip" class="form-control" style="width:160px;"
placeholder="设备 IP,如 192.168.1.10" value="<?php echo htmlspecialchars($_POST['manual_ip'] ?? ''); ?>">
</div>
<div class="form-group" style="margin-right:6px;">
<input type="number" name="manual_port" class="form-control" style="width:90px;"
placeholder="端口" value="<?php echo htmlspecialchars($_POST['manual_port'] ?? '502'); ?>">
</div>
<button type="submit" name="action" value="manualConnect" class="btn btn-primary">
<i class="fa fa-wifi"></i> 测试连接
</button>
</form>
<p class="text-muted" style="margin-top:8px;margin-bottom:0;">不依赖数据库记录,直接对任意 IP:端口做 TCP 连通测试。</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($result)): ?>
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-terminal"></i>
<?php
$titles = [
'testLink' => '链接状态测试结果',
'readData' => '数据读取结果',
'writeData' => '数据写入(探针)结果',
'manualConnect' => '手动连接测试结果',
];
echo htmlspecialchars($titles[$result['action']] ?? '测试结果');
?>
</h3>
</div>
<div class="box-body">
<?php if (!empty($result['error'])): ?>
<div class="alert alert-danger">
<i class="fa fa-times-circle"></i> <?php echo htmlspecialchars($result['error']); ?>
</div>
<?php endif; ?>
<?php if ($result['action'] === 'testLink'): ?>
<table class="table table-bordered" style="max-width:600px;">
<tr><th style="width:160px;">设备</th><td><?php echo htmlspecialchars(($result['device']['device_code'] ?? '') . ' / ' . ($result['device']['device_name'] ?? '')); ?></td></tr>
<tr><th>目标地址</th><td><?php echo htmlspecialchars($result['ip']); ?> : <?php echo intval($result['port']); ?></td></tr>
<tr><th>连接结果</th>
<td>
<?php if ($result['reachable']): ?>
<span class="label label-success"><i class="fa fa-check"></i> 可达</span>
<?php else: ?>
<span class="label label-danger"><i class="fa fa-times"></i> 不可达</span>
<?php endif; ?>
</td>
</tr>
<tr><th>延迟</th><td><?php echo $result['latency']; ?> ms</td></tr>
<?php if ($result['error']): ?>
<tr><th>错误信息</th><td class="text-danger"><?php echo htmlspecialchars($result['error']); ?></td></tr>
<?php endif; ?>
</table>
<?php endif; ?>
<?php if ($result['action'] === 'readData'): ?>
<p>
设备编号:<code><?php echo htmlspecialchars($result['device_code']); ?></code>
&nbsp;|&nbsp; 测试记录总数:<b><?php echo intval($result['counts']['rec_cnt'] ?? 0); ?></b>
&nbsp;|&nbsp; 测试数据总数:<b><?php echo intval($result['counts']['data_cnt'] ?? 0); ?></b>
&nbsp;|&nbsp; 设备总数:<b><?php echo intval($result['counts']['dev_cnt'] ?? 0); ?></b>
</p>
<p class="text-muted">最近 10 条该设备的测试记录(device_code 匹配):</p>
<?php if (empty($result['rows'])): ?>
<div class="alert alert-info">该设备暂无测试记录。</div>
<?php else: ?>
<table class="table table-bordered table-hover">
<thead>
<tr><th>ID</th><th>二维码/条码</th><th>产品型号</th><th>测试结果</th><th>测试时间</th></tr>
</thead>
<tbody>
<?php foreach ($result['rows'] as $r): ?>
<tr>
<td><?php echo intval($r['id']); ?></td>
<td><?php echo htmlspecialchars($r['qr_code'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($r['product_model'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($r['test_result'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($r['test_date'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php endif; ?>
<?php if ($result['action'] === 'manualConnect'): ?>
<table class="table table-bordered" style="max-width:600px;">
<tr><th style="width:160px;">目标</th><td><?php echo htmlspecialchars($result['device']['device_name'] ?? ''); ?></td></tr>
<tr><th>地址</th><td><?php echo htmlspecialchars($result['ip']); ?> : <?php echo intval($result['port']); ?></td></tr>
<tr><th>连接结果</th>
<td>
<?php if ($result['reachable']): ?>
<span class="label label-success"><i class="fa fa-check"></i> 可达</span>
<?php else: ?>
<span class="label label-danger"><i class="fa fa-times"></i> 不可达</span>
<?php endif; ?>
</td>
</tr>
<tr><th>延迟</th><td><?php echo $result['latency']; ?> ms</td></tr>
<?php if ($result['error']): ?>
<tr><th>错误信息</th><td class="text-danger"><?php echo htmlspecialchars($result['error']); ?></td></tr>
<?php endif; ?>
</table>
<?php endif; ?>
<?php if ($result['action'] === 'writeData'): ?>
<?php if (empty($result['error'])): ?>
<div class="alert alert-success">
<i class="fa fa-check-circle"></i> 数据读写链路正常(探针已写入并回读、清理)
</div>
<?php endif; ?>
<table class="table table-bordered" style="max-width:700px;">
<tr><th style="width:180px;">设备编号</th><td><?php echo htmlspecialchars($result['device_code']); ?></td></tr>
<tr><th>探针二维码</th><td><code><?php echo htmlspecialchars($result['probe_code']); ?></code></td></tr>
<tr><th>写入 ID</th><td><?php echo intval($result['written_id']); ?></td></tr>
<tr><th>回读一致</th>
<td>
<?php if ($result['read_back_match']): ?>
<span class="label label-success">一致</span>
<?php else: ?>
<span class="label label-danger">不一致</span>
<?php endif; ?>
</td>
</tr>
<tr><th>探针记录已清理</th>
<td>
<?php if ($result['deleted']): ?>
<span class="label label-success">已删除</span>
<?php else: ?>
<span class="label label-warning">未删除(请检查)</span>
<?php endif; ?>
</td>
</tr>
<?php if ($result['error']): ?>
<tr><th>错误信息</th><td class="text-danger"><?php echo htmlspecialchars($result['error']); ?></td></tr>
<?php endif; ?>
</table>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+92
View File
@@ -0,0 +1,92 @@
<?php
$activeMenu = 'asdTestData';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-flask"></i> 测试数据管理</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/asdTestDataImport" class="btn btn-success btn-sm">
<i class="fa fa-plus"></i> 导入测试数据
</a>
<span class="badge bg-blue" style="margin-left:10px;">共 <?php echo $totalCount; ?> 条记录</span>
</div>
</div>
<div class="box-header">
<form method="get" action="<?php echo BASE_URL; ?>/Admin/asdTestData" class="form-inline">
<div class="input-group input-group-sm" style="width:300px;">
<input type="text" name="keyword" class="form-control" placeholder="输入条码搜索..." value="<?php echo htmlspecialchars($keyword ?? ''); ?>">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
</span>
</div>
</form>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>条码</th>
<th>项目名称</th>
<th>测试员</th>
<th>工位</th>
<th>设备</th>
<th>状态</th>
<th>工步数</th>
<th>失败数</th>
<th>测试时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php if (empty($records)): ?>
<tr><td colspan="11" class="text-center text-muted">暂无测试数据</td></tr>
<?php else: ?>
<?php foreach ($records as $r): ?>
<tr>
<td><?php echo $r['id']; ?></td>
<td><code><?php echo htmlspecialchars($r['qr_code']); ?></code></td>
<td><?php echo htmlspecialchars($r['project_name']); ?></td>
<td><?php echo htmlspecialchars($r['tester']); ?></td>
<td><?php echo htmlspecialchars($r['site']); ?></td>
<td><?php echo htmlspecialchars($r['device']); ?></td>
<td>
<?php if ($r['status'] === 'PASS'): ?>
<span class="label label-success">PASS</span>
<?php else: ?>
<span class="label label-danger"><?php echo htmlspecialchars($r['status']); ?></span>
<?php endif; ?>
</td>
<td><span class="badge bg-light-blue"><?php echo $r['step_count']; ?></span></td>
<td>
<?php if ($r['fail_count'] > 0): ?>
<span class="badge bg-red"><?php echo $r['fail_count']; ?></span>
<?php else: ?>
<span class="text-muted">0</span>
<?php endif; ?>
</td>
<td><?php echo $r['test_time'] ?? '-'; ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/asdTestDataView/<?php echo $r['id']; ?>" class="btn btn-xs btn-info" title="查看详情">
<i class="fa fa-eye"></i>
</a>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdTestDataDelete/<?php echo $r['id']; ?>" style="display:inline;" onsubmit="return confirm('确定删除该测试记录及所有工步数据吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit" class="btn btn-xs btn-danger" title="删除"><i class="fa fa-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+805
View File
@@ -0,0 +1,805 @@
<?php
$activeMenu = 'asdTestData';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<style>
/* 设备连接操作区样式 */
.device-card { border: 1px solid #d2d6de; border-radius: 4px; padding: 15px; margin-bottom: 15px; background: #f9f9f9; }
.device-card .device-info { margin-bottom: 10px; }
.device-card .device-info .info-row { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; margin-bottom: 8px; }
.device-card .device-info .info-label { font-weight: 600; color: #555; min-width: 70px; }
.device-card .device-info .info-value { color: #333; }
.status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
.status-dot.online { background: #00a65a; }
.status-dot.offline { background: #dd4b39; }
.status-dot.unknown { background: #999; }
.device-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
#scanInput { font-size: 16px; letter-spacing: 1px; }
.btn-connect { min-width: 100px; }
.console-log { background: #1e1e1e; color: #d4d4d4; font-family: Consolas, monospace; font-size: 13px; padding: 12px; border-radius: 4px; max-height: 350px; overflow-y: auto; line-height: 1.6; }
.console-log .log-line { margin: 2px 0; }
.console-log .log-success { color: #4ec9b0; }
.console-log .log-error { color: #f44747; }
.console-log .log-warn { color: #e6a23c; }
.console-log .log-info { color: #9cdcfe; }
.console-log .log-time { color: #808080; }
.batch-result-table { max-height: 400px; overflow-y: auto; margin-top: 10px; }
.batch-result-table table { font-size: 12px; }
.device-batch-item { border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 10px; }
.device-batch-item .panel-heading { background: #f5f5f5; padding: 8px 12px; cursor: pointer; }
.device-batch-item .panel-body { display: none; padding: 10px; }
.device-batch-item .panel-body.open { display: block; }
</style>
<div class="row">
<div class="col-md-12">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-upload"></i> 导入测试数据</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/asdTestData" class="btn btn-default btn-sm">
<i class="fa fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<?php if (isset($success)): ?>
<div class="alert alert-success alert-dismissible" style="margin:10px;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fa fa-check-circle"></i> <?php echo h($success); ?>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible" style="margin:10px;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fa fa-exclamation-circle"></i> <?php echo h($error); ?>
</div>
<?php endif; ?>
<!-- Tab 切换 -->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li class="<?php echo (empty($activeTab) || $activeTab === 'form') ? 'active' : ''; ?>"><a href="#tab-form" data-toggle="tab"><i class="fa fa-edit"></i> 表单导入</a></li>
<li class="<?php echo (isset($activeTab) && $activeTab === 'json') ? 'active' : ''; ?>"><a href="#tab-json" data-toggle="tab"><i class="fa fa-code"></i> JSON 导入</a></li>
<li class="<?php echo (isset($activeTab) && $activeTab === 'device') ? 'active' : ''; ?>"><a href="#tab-device" data-toggle="tab"><i class="fa fa-plug"></i> 设备连接与批量操作</a></li>
</ul>
<div class="tab-content">
<!-- ==================== 表单导入 ==================== -->
<div class="tab-pane <?php echo (empty($activeTab) || $activeTab === 'form') ? 'active' : ''; ?>" id="tab-form" style="display:<?php echo (empty($activeTab) || $activeTab === 'form') ? 'block' : 'none'; ?>;">
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdTestDataImport">
<div class="box-body">
<legend>基本信息</legend>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>条码/二维码 <span class="text-red">*</span></label>
<input type="text" name="qr_code" class="form-control" placeholder="SN6977187913642" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>项目名称</label>
<input type="text" name="project_name" class="form-control" placeholder="充电器测试">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>工位</label>
<select name="site" class="form-control">
<option value="">-- 选择工位 --</option>
<?php foreach ($stations as $st): ?>
<option value="<?php echo htmlspecialchars($st['station_name']); ?>"><?php echo htmlspecialchars($st['station_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>站点</label>
<input type="text" name="station" class="form-control" placeholder="A区-1线">
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>单号</label>
<input type="text" name="mo" class="form-control" placeholder="MO20240525001">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>工单</label>
<input type="text" name="work_order" class="form-control" placeholder="WO-20240525-01">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>组别</label>
<input type="text" name="group_name" class="form-control" placeholder="A组">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>设备号</label>
<input type="text" name="device" class="form-control" placeholder="ASD-989A-001">
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>运行时间</label>
<input type="text" name="running_time" class="form-control" placeholder="28S">
</div>
</div>
</div>
<legend>测试工步数据</legend>
<div id="stepContainer">
<?php
$defaultSteps = [
['name' => '输出测试', 'item' => '电压', 'units' => 'V', 'value' => '5.12', 'lower' => '4.75', 'upper' => '5.25'],
['name' => '输出测试', 'item' => '电流', 'units' => 'A', 'value' => '2.10', 'lower' => '1.90', 'upper' => '2.20'],
['name' => '空载功耗', 'item' => '功率', 'units' => 'W', 'value' => '0.08', 'lower' => '0', 'upper' => '0.3'],
['name' => '效率测试', 'item' => '效率', 'units' => '%', 'value' => '88.5', 'lower' => '85', 'upper' => '95'],
];
foreach ($defaultSteps as $idx => $ds):
?>
<div class="row step-row" style="margin-bottom:8px;">
<div class="col-md-2">
<input type="text" name="step_name[]" class="form-control input-sm" placeholder="工步名称" value="<?php echo $ds['name']; ?>">
</div>
<div class="col-md-2">
<input type="text" name="step_item[]" class="form-control input-sm" placeholder="测试项目" value="<?php echo $ds['item']; ?>">
</div>
<div class="col-md-2">
<input type="text" name="step_value[]" class="form-control input-sm" placeholder="测试值" value="<?php echo $ds['value']; ?>">
</div>
<div class="col-md-1">
<input type="text" name="step_units[]" class="form-control input-sm" placeholder="单位" value="<?php echo $ds['units']; ?>">
</div>
<div class="col-md-1">
<input type="text" name="step_lower[]" class="form-control input-sm" placeholder="下限" value="<?php echo $ds['lower']; ?>">
</div>
<div class="col-md-1">
<input type="text" name="step_upper[]" class="form-control input-sm" placeholder="上限" value="<?php echo $ds['upper']; ?>">
</div>
<div class="col-md-2">
<select name="step_result[]" class="form-control input-sm">
<option value="PASS" selected>PASS</option>
<option value="FAIL">FAIL</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-xs btn-danger remove-step" title="删除此行">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" id="addStep" class="btn btn-sm btn-info" style="margin-top:5px;">
<i class="fa fa-plus"></i> 添加工步
</button>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary">
<i class="fa fa-check"></i> 提交导入
</button>
</div>
</form>
</div>
<!-- ==================== JSON 导入 ==================== -->
<div class="tab-pane <?php echo (isset($activeTab) && $activeTab === 'json') ? 'active' : ''; ?>" id="tab-json" style="display:<?php echo (isset($activeTab) && $activeTab === 'json') ? 'block' : 'none'; ?>;">
<form method="post" action="<?php echo BASE_URL; ?>/Admin/asdTestDataImport">
<input type="hidden" name="action" value="import_json">
<div class="box-body">
<div class="form-group">
<label>粘贴昂盛达设备上报的 JSON 数据:</label>
<textarea name="json_data" class="form-control" rows="18"
placeholder='{"Tester":"张三","QrCode":"SN6977187913642","Mo":"MO20240525","Site":"成品测试","Status":"PASS","DataList":[{"Seq":1,"TestName":"输出测试","TestItem":"电压","TestUnits":"V","DataValue":5.12,"LowerLimit":4.75,"UpperLimit":5.25,"TestResult":"PASS"}]}'><?php echo isset($_POST['json_data']) ? htmlspecialchars($_POST['json_data']) : ''; ?></textarea>
</div>
<p class="text-muted">
<i class="fa fa-info-circle"></i>
JSON 格式请参考接口文档。必填字段:<code>QrCode</code>、<code>Status</code>、<code>DataList</code>
</p>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary">
<i class="fa fa-upload"></i> 导入 JSON
</button>
</div>
</form>
</div>
<!-- ==================== 设备连接与批量操作 ==================== -->
<div class="tab-pane <?php echo (isset($activeTab) && $activeTab === 'device') ? 'active' : ''; ?>" id="tab-device" style="display:<?php echo (isset($activeTab) && $activeTab === 'device') ? 'block' : 'none'; ?>;">
<div class="box-body">
<div class="row">
<!-- 左侧:设备选择与连接 -->
<div class="col-md-6">
<div class="device-card">
<h4 style="margin-top:0;"><i class="fa fa-plug"></i> 设备连接</h4>
<!-- 扫码 / 手动输入设备编码 -->
<div class="form-group">
<label>设备编码(扫码枪 / 手动输入) <span class="text-red">*</span></label>
<div class="input-group">
<input type="text" id="scanInput" class="form-control" placeholder="扫描设备二维码 或 输入设备编码...">
<span class="input-group-btn">
<button type="button" id="btnScanLookup" class="btn btn-info btn-connect">
<i class="fa fa-search"></i> 查找
</button>
</span>
</div>
</div>
<!-- 设备选择下拉框 -->
<div class="form-group">
<label>或从列表选择已注册设备:</label>
<select id="deviceSelect" class="form-control">
<option value="">-- 选择设备 --</option>
<?php foreach ($devices as $dv): ?>
<option value="<?php echo htmlspecialchars($dv['device_code']); ?>"
data-ip="<?php echo htmlspecialchars($dv['device_ip'] ?? ''); ?>"
data-name="<?php echo htmlspecialchars($dv['device_name'] ?? ''); ?>"
data-group="<?php echo htmlspecialchars($dv['group_name'] ?? ''); ?>"
data-status="<?php echo $dv['status']; ?>">
<?php echo htmlspecialchars($dv['device_code']); ?>
[<?php echo htmlspecialchars($dv['device_name'] ?? $dv['group_name'] ?? '-'); ?>]
<?php echo !empty($dv['device_ip']) ? ' - IP:' . htmlspecialchars($dv['device_ip']) : ''; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<hr>
<!-- 设备信息展示区 -->
<div id="deviceDetail" class="device-info" style="display:none;">
<div class="info-row">
<span class="info-label">设备编码:</span>
<span class="info-value" id="dispDeviceCode">-</span>
</div>
<div class="info-row">
<span class="info-label">设备名称:</span>
<span class="info-value" id="dispDeviceName">-</span>
</div>
<div class="info-row">
<span class="info-label">所属组别:</span>
<span class="info-value" id="dispDeviceGroup">-</span>
</div>
<div class="info-row">
<span class="info-label">设备IP</span>
<span class="info-value">
<input type="text" id="deviceIpInput" class="form-control input-sm" style="width:160px; display:inline-block;" placeholder="192.168.1.101">
</span>
</div>
<div class="info-row">
<span class="info-label">连接状态:</span>
<span class="info-value" id="dispConnStatus">
<span class="status-dot unknown"></span> 未连接
</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="device-actions">
<button type="button" id="btnConnect" class="btn btn-success btn-connect" disabled>
<i class="fa fa-link"></i> 连接设备
</button>
<button type="button" id="btnDisconnect" class="btn btn-default btn-connect" disabled>
<i class="fa fa-unlink"></i> 断开
</button>
</div>
<div class="device-actions">
<button type="button" id="btnBatchRead" class="btn btn-primary" disabled>
<i class="fa fa-download"></i> 读取设备数据
</button>
<button type="button" id="btnBatchImport" class="btn btn-warning">
<i class="fa fa-cloud-upload"></i> 批量导入选中设备数据
</button>
<button type="button" id="btnDeleteDeviceData" class="btn btn-danger" disabled>
<i class="fa fa-trash"></i> 删除该设备数据
</button>
</div>
</div>
<!-- 已连接设备列表 -->
<div class="box box-solid box-default collapsed-box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 已选中设备列表(用于批量读取)</h3>
<div class="box-tools">
<span class="badge bg-light-blue" id="selectedCount">0</span>
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-plus"></i></button>
</div>
</div>
<div class="box-body">
<div id="selectedDevicesList" class="text-muted" style="padding:5px;">
尚未选择设备,请先扫码或从列表选择并连接设备
</div>
</div>
</div>
</div>
<!-- 右侧:操作日志和结果 -->
<div class="col-md-6">
<div class="box box-solid box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-terminal"></i> 操作日志</h3>
<div class="box-tools">
<button type="button" id="btnClearLog" class="btn btn-box-tool btn-xs"><i class="fa fa-trash-o"></i> 清空</button>
</div>
</div>
<div class="box-body" style="padding:0;">
<div id="consoleLog" class="console-log">
<div class="log-line log-info">系统就绪,请选择设备进行操作...</div>
</div>
</div>
</div>
<!-- 批量读取结果区 -->
<div id="batchResultArea" style="display:none;">
<div class="box box-solid box-success">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-table"></i> 批量读取结果</h3>
<span class="pull-right" id="batchResultSummary"></span>
</div>
<div class="box-body" style="padding:10px;">
<div id="batchResultContent" class="batch-result-table"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /.tab-content -->
</div><!-- /.nav-tabs-custom -->
</div><!-- /.box -->
</div>
</div>
<!-- 删除确认弹窗 -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"><i class="fa fa-exclamation-triangle text-red"></i> 确认删除</h4>
</div>
<div class="modal-body">
<p id="deleteConfirmMsg">确定要删除该设备的所有测试数据吗?此操作不可撤销!</p>
<div class="form-group">
<label>请输入 <strong>DELETE</strong> 确认:</label>
<input type="text" id="deleteConfirmInput" class="form-control" placeholder="输入 DELETE">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="btnConfirmDelete" disabled>
<i class="fa fa-trash"></i> 确认删除
</button>
</div>
</div>
</div>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
// ==================== 全局状态 ====================
var connectedDevices = {}; // { deviceCode: { device info, connected: bool } }
var currentDevice = null; // 当前选中的设备编码
var BASE_URL = '<?php echo BASE_URL; ?>';
// CSP:设备批次面板标题点击展开(替代动态生成 HTML 的内联 onclick
$(document).on('click', '.device-batch-header', function () {
$(this).next().toggleClass('open');
});
// ==================== 控制台日志 ====================
function log(msg, type) {
type = type || 'info';
var now = new Date();
var time = now.toTimeString().substring(0, 8);
var $log = $('#consoleLog');
var cls = 'log-' + type;
$log.append('<div class="log-line ' + cls + '"><span class="log-time">[' + time + ']</span> ' + msg + '</div>');
$log.scrollTop($log[0].scrollHeight);
}
// ==================== 设备选择处理 ====================
$('#deviceSelect').on('change', function() {
var code = $(this).val();
if (!code) {
$('#deviceDetail').hide();
$('#btnConnect').prop('disabled', true);
currentDevice = null;
return;
}
currentDevice = code;
$('#scanInput').val(code);
var opt = $(this).find('option:selected');
$('#dispDeviceCode').text(code);
$('#dispDeviceName').text(opt.data('name') || '-');
$('#dispDeviceGroup').text(opt.data('group') || '-');
$('#deviceIpInput').val(opt.data('ip') || '');
$('#deviceDetail').show();
$('#btnConnect').prop('disabled', false);
updateConnectStatus(code);
});
// 扫码查找
$('#btnScanLookup').on('click', function() {
var code = $('#scanInput').val().trim();
if (!code) { alert('请输入设备编码'); return; }
// 尝试从下拉框匹配
var found = false;
$('#deviceSelect option').each(function() {
if ($(this).val().toUpperCase() === code.toUpperCase()) {
$('#deviceSelect').val($(this).val()).trigger('change');
found = true;
log('扫码识别成功: ' + code, 'success');
return false;
}
});
if (!found) {
log('设备编码 ' + code + ' 未在系统中注册,请先录入设备信息', 'warn');
// 仍允许手动连接
currentDevice = code;
$('#dispDeviceCode').text(code);
$('#dispDeviceName').text('(未注册)');
$('#dispDeviceGroup').text('-');
$('#deviceIpInput').val('');
$('#deviceDetail').show();
$('#btnConnect').prop('disabled', false);
}
});
// 更新连接状态显示
function updateConnectStatus(code) {
if (connectedDevices[code] && connectedDevices[code].connected) {
$('#dispConnStatus').html('<span class="status-dot online"></span> 已连接');
} else {
$('#dispConnStatus').html('<span class="status-dot unknown"></span> 未连接');
}
}
// ==================== 连接设备 ====================
$('#btnConnect').on('click', function() {
if (!currentDevice) { alert('请先选择设备'); return; }
var ip = $('#deviceIpInput').val().trim();
if (!ip) { alert('请输入设备 IP 地址'); return; }
var $btn = $(this);
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 连接中...');
log('正在连接设备 ' + currentDevice + ' (' + ip + ')...', 'info');
$.ajax({
url: BASE_URL + '/Api/asdDeviceConnect',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ DeviceCode: currentDevice, DeviceIp: ip }),
success: function(resp) {
if (resp.Result == 1) {
log('连接成功! 设备: ' + currentDevice + ', IP: ' + ip
+ (resp.ConnectionInfo.reachable ? ', 延迟: ' + resp.ConnectionInfo.latency_ms + 'ms' : ', 网络不可达(数据库校验通过)'), 'success');
connectedDevices[currentDevice] = {
info: resp.Device,
connected: true
};
$('#dispConnStatus').html('<span class="status-dot online"></span> 已连接');
$('#btnDisconnect').prop('disabled', false);
$('#btnBatchRead').prop('disabled', false);
$('#btnDeleteDeviceData').prop('disabled', false);
updateSelectedList();
} else {
log('连接失败: ' + resp.Message, 'error');
}
},
error: function(xhr, status, err) {
log('连接异常: ' + err, 'error');
},
complete: function() {
$btn.prop('disabled', false).html('<i class="fa fa-link"></i> 连接设备');
}
});
});
// 断开连接
$('#btnDisconnect').on('click', function() {
if (!currentDevice) return;
if (connectedDevices[currentDevice]) {
connectedDevices[currentDevice].connected = false;
}
$('#dispConnStatus').html('<span class="status-dot online" style="background:#999;"></span> 已断开');
log('已断开设备: ' + currentDevice, 'warn');
updateConnectStatus(currentDevice);
updateSelectedList();
});
// ==================== 更新已选设备列表 ====================
function updateSelectedList() {
var $list = $('#selectedDevicesList');
var count = 0;
var html = '';
for (var code in connectedDevices) {
if (connectedDevices[code].connected) {
count++;
var info = connectedDevices[code].info;
html += '<span class="label label-success" style="margin:2px;" title="' + (info.device_ip || '') + '">'
+ '<span class="status-dot online"></span> ' + code + '</span> ';
}
}
if (count === 0) {
$list.html('尚未选择设备,请先扫码或从列表选择并连接设备');
} else {
$list.html(html);
}
$('#selectedCount').text(count);
}
// ==================== 批量读取设备数据 ====================
$('#btnBatchRead').on('click', function() {
if (!currentDevice) { alert('请先选择设备'); return; }
var $btn = $(this);
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 读取中...');
log('正在读取设备 ' + currentDevice + ' 的测试数据...', 'info');
$.ajax({
url: BASE_URL + '/Api/asdBatchRead',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ DeviceCodes: [currentDevice], Limit: 20 }),
success: function(resp) {
if (resp.Result == 1) {
var deviceData = resp.DevicesData[currentDevice];
if (!deviceData) {
log('设备 ' + currentDevice + ' 无数据', 'warn');
return;
}
log('读取成功! 设备 ' + currentDevice + ' 共 ' + deviceData.total + ' 条记录,本次返回 ' + deviceData.records.length + ' 条', 'success');
renderBatchResult(deviceData, currentDevice);
} else {
log('读取失败: ' + resp.Message, 'error');
}
},
error: function(xhr, status, err) {
log('读取异常: ' + err, 'error');
},
complete: function() {
$btn.prop('disabled', false).html('<i class="fa fa-download"></i> 读取设备数据');
}
});
});
// 渲染批量读取结果
function renderBatchResult(deviceData, deviceCode) {
var $area = $('#batchResultArea');
var $content = $('#batchResultContent');
var records = deviceData.records;
var html = '<table class="table table-bordered table-condensed table-striped">';
html += '<thead><tr><th>ID</th><th>条码</th><th>项目名称</th><th>测试员</th><th>状态</th><th>工步数</th><th>失败</th><th>测试时间</th><th>操作</th></tr></thead>';
html += '<tbody>';
if (records.length === 0) {
html += '<tr><td colspan="9" class="text-center text-muted">该设备暂无测试数据</td></tr>';
} else {
for (var i = 0; i < records.length; i++) {
var r = records[i];
var statusLabel = r.status === 'PASS' ? 'label-success' : 'label-danger';
var failBadge = r.fail_count > 0 ? '<span class="badge bg-red">' + r.fail_count + '</span>' : '<span class="text-muted">0</span>';
html += '<tr>';
html += '<td>' + r.id + '</td>';
html += '<td><code>' + escapeHtml(r.qr_code) + '</code></td>';
html += '<td>' + escapeHtml(r.project_name) + '</td>';
html += '<td>' + escapeHtml(r.tester) + '</td>';
html += '<td><span class="label ' + statusLabel + '">' + r.status + '</span></td>';
html += '<td><span class="badge bg-light-blue">' + r.step_count + '</span></td>';
html += '<td>' + failBadge + '</td>';
html += '<td>' + (r.test_time || '-') + '</td>';
html += '<td><a href="' + BASE_URL + '/Admin/asdTestDataView/' + r.id + '" class="btn btn-xs btn-info" target="_blank"><i class="fa fa-eye"></i></a></td>';
html += '</tr>';
}
}
html += '</tbody></table>';
$('#batchResultSummary').html('设备: <strong>' + deviceCode + '</strong> | 总数: ' + deviceData.total + ' | 显示: ' + records.length + ' 条');
$content.html(html);
$area.show();
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ==================== 删除设备数据 ====================
$('#btnDeleteDeviceData').on('click', function() {
if (!currentDevice) { alert('请先选择设备'); return; }
$('#deleteConfirmMsg').text('确定要删除设备 [' + currentDevice + '] 的所有测试数据吗?此操作不可撤销!');
$('#deleteConfirmInput').val('');
$('#btnConfirmDelete').prop('disabled', true);
$('#deleteConfirmModal').modal('show');
});
$('#deleteConfirmInput').on('input', function() {
$('#btnConfirmDelete').prop('disabled', $(this).val() !== 'DELETE');
});
$('#btnConfirmDelete').on('click', function() {
var $btn = $(this);
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 删除中...');
log('正在删除设备 ' + currentDevice + ' 的所有测试数据...', 'warn');
$.ajax({
url: BASE_URL + '/Api/asdDeviceDataDelete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ DeviceCode: currentDevice, Confirm: 'YES' }),
success: function(resp) {
if (resp.Result == 1) {
log('删除成功! 共删除 ' + resp.DeletedCount + ' 条记录', 'success');
$('#batchResultArea').hide();
} else {
log('删除失败: ' + resp.Message, 'error');
}
},
error: function(xhr, status, err) {
log('删除异常: ' + err, 'error');
},
complete: function() {
$btn.prop('disabled', false).html('<i class="fa fa-trash"></i> 确认删除');
$('#deleteConfirmModal').modal('hide');
}
});
});
// ==================== 批量导入选中设备数据 ====================
$('#btnBatchImport').on('click', function() {
var codes = [];
for (var code in connectedDevices) {
if (connectedDevices[code].connected) {
codes.push(code);
}
}
if (codes.length === 0) {
alert('请先连接至少一台设备');
return;
}
var $btn = $(this);
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 读取中...');
log('批量读取 ' + codes.length + ' 台设备数据...', 'info');
$.ajax({
url: BASE_URL + '/Api/asdBatchRead',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ DeviceCodes: codes, Limit: 50 }),
success: function(resp) {
if (resp.Result == 1) {
var totalRecords = 0;
var html = '';
for (var code in resp.DevicesData) {
var dd = resp.DevicesData[code];
totalRecords += dd.total;
html += '<div class="device-batch-item">';
html += '<div class="panel-heading device-batch-header">';
html += '<strong>' + code + '</strong> ';
html += '<span class="badge bg-light-blue pull-right">' + dd.records.length + ' / ' + dd.total + ' 条</span>';
html += '<small class="text-muted">' + (dd.device_info ? dd.device_info.device_name + ' | IP:' + dd.device_info.device_ip : '') + '</small>';
html += '</div>';
html += '<div class="panel-body">';
html += '<table class="table table-condensed table-bordered"><thead><tr><th>ID</th><th>条码</th><th>状态</th><th>工步</th><th>时间</th></tr></thead><tbody>';
for (var i = 0; i < dd.records.length; i++) {
var r = dd.records[i];
html += '<tr><td>' + r.id + '</td><td><code>' + escapeHtml(r.qr_code) + '</code></td>';
html += '<td><span class="label ' + (r.status === 'PASS' ? 'label-success' : 'label-danger') + '">' + r.status + '</span></td>';
html += '<td>' + r.step_count + '</td><td>' + (r.test_time || '-') + '</td></tr>';
}
html += '</tbody></table></div></div>';
}
$('#batchResultContent').html(html);
$('#batchResultSummary').html('设备数: <strong>' + codes.length + '</strong> | 总记录: ' + totalRecords + ' 条');
$('#batchResultArea').show();
log('批量读取完成! ' + codes.length + ' 台设备, 共 ' + totalRecords + ' 条记录', 'success');
} else {
log('批量读取失败: ' + resp.Message, 'error');
}
},
error: function(xhr, status, err) {
log('批量读取异常: ' + err, 'error');
},
complete: function() {
$btn.prop('disabled', false).html('<i class="fa fa-cloud-upload"></i> 批量导入选中设备数据');
}
});
});
// ==================== 清空日志 ====================
$('#btnClearLog').on('click', function() {
$('#consoleLog').html('<div class="log-line log-info">日志已清空</div>');
});
// ==================== 表单工步操作 ====================
document.getElementById('addStep').addEventListener('click', function() {
var container = document.getElementById('stepContainer');
var row = document.createElement('div');
row.className = 'row step-row';
row.style.marginBottom = '8px';
row.innerHTML =
'<div class="col-md-2"><input type="text" name="step_name[]" class="form-control input-sm" placeholder="工步名称"></div>' +
'<div class="col-md-2"><input type="text" name="step_item[]" class="form-control input-sm" placeholder="测试项目"></div>' +
'<div class="col-md-2"><input type="text" name="step_value[]" class="form-control input-sm" placeholder="测试值"></div>' +
'<div class="col-md-1"><input type="text" name="step_units[]" class="form-control input-sm" placeholder="单位"></div>' +
'<div class="col-md-1"><input type="text" name="step_lower[]" class="form-control input-sm" placeholder="下限"></div>' +
'<div class="col-md-1"><input type="text" name="step_upper[]" class="form-control input-sm" placeholder="上限"></div>' +
'<div class="col-md-2"><select name="step_result[]" class="form-control input-sm"><option value="PASS" selected>PASS</option><option value="FAIL">FAIL</option></select></div>' +
'<div class="col-md-1"><button type="button" class="btn btn-xs btn-danger remove-step"><i class="fa fa-times"></i></button></div>';
container.appendChild(row);
row.querySelector('.remove-step').addEventListener('click', function() {
container.removeChild(row);
});
});
document.querySelectorAll('.remove-step').forEach(function(btn) {
btn.addEventListener('click', function() {
var rows = document.querySelectorAll('.step-row');
if (rows.length > 1) {
this.closest('.step-row').remove();
}
});
});
// 扫码输入框回车自动查找
$('#scanInput').on('keypress', function(e) {
if (e.which === 13) {
e.preventDefault();
$('#btnScanLookup').trigger('click');
}
});
// ==================== Tab 切换(直接操作 display,不依赖 Bootstrap JS ====================
$(function() {
// 切换 tab:显示目标,隐藏其余
function switchTab(hash) {
if (!hash) return;
// 隐藏所有 pane,显示目标
$('.tab-content > .tab-pane').hide();
$(hash).show();
// 切换 nav 高亮
$('.nav-tabs li').removeClass('active');
$('.nav-tabs a[href="' + hash + '"]').parent('li').addClass('active');
}
// 点击 nav tab
$('.nav-tabs a').on('click', function(e) {
e.preventDefault();
var hash = $(this).attr('href');
switchTab(hash);
// 更新 URL hash
if (history.replaceState) {
history.replaceState(null, null, hash);
}
});
// 页面加载时根据 URL hash 切换
var hash = window.location.hash;
if (hash && '#tab-form,#tab-json,#tab-device'.indexOf(hash) !== -1) {
switchTab(hash);
}
});
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+126
View File
@@ -0,0 +1,126 @@
<?php
$activeMenu = 'asdTestData';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-12">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-info-circle"></i> 测试数据详情</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/asdTestData" class="btn btn-default btn-sm">
<i class="fa fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<div class="box-body">
<table class="table table-bordered table-striped" style="width:auto;">
<tr>
<th style="width:120px;">记录ID</th>
<td><?php echo $record['id']; ?></td>
<th style="width:120px;">条码</th>
<td><code><?php echo htmlspecialchars($record['qr_code']); ?></code></td>
</tr>
<tr>
<th>测试员</th>
<td><?php echo htmlspecialchars($record['tester']); ?></td>
<th>项目名称</th>
<td><?php echo htmlspecialchars($record['project_name']); ?></td>
</tr>
<tr>
<th>工位</th>
<td><?php echo htmlspecialchars($record['site']); ?></td>
<th>站点</th>
<td><?php echo htmlspecialchars($record['station']); ?></td>
</tr>
<tr>
<th>单号</th>
<td><?php echo htmlspecialchars($record['mo']); ?></td>
<th>工单</th>
<td><?php echo htmlspecialchars($record['work_order']); ?></td>
</tr>
<tr>
<th>组别</th>
<td><?php echo htmlspecialchars($record['group_name']); ?></td>
<th>设备</th>
<td><?php echo htmlspecialchars($record['device']); ?></td>
</tr>
<tr>
<th>运行时间</th>
<td><?php echo htmlspecialchars($record['running_time']); ?></td>
<th>测试时间</th>
<td><?php echo $record['test_time']; ?></td>
</tr>
<tr>
<th>整体状态</th>
<td colspan="3">
<?php if ($record['status'] === 'PASS'): ?>
<span class="label label-success">PASS</span>
<?php else: ?>
<span class="label label-danger"><?php echo htmlspecialchars($record['status']); ?></span>
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
<!-- 工步详情 -->
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list-ol"></i> 测试工步详情(共 <?php echo count($details); ?> 步)</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:50px;">序号</th>
<th>工步名称</th>
<th>测试项目</th>
<th>测试值</th>
<th>单位</th>
<th>下限</th>
<th>上限</th>
<th>字符串值</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<?php if (empty($details)): ?>
<tr><td colspan="9" class="text-center text-muted">无工步数据</td></tr>
<?php else: ?>
<?php foreach ($details as $d): ?>
<tr>
<td><?php echo $d['seq']; ?></td>
<td><?php echo htmlspecialchars($d['test_name']); ?></td>
<td><?php echo htmlspecialchars($d['test_item']); ?></td>
<td>
<?php if ($d['data_value'] !== null): ?>
<strong><?php echo rtrim(rtrim($d['data_value'], '0'), '.'); ?></strong>
<?php else: ?>
-
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($d['test_units']); ?></td>
<td><?php echo $d['lower_limit'] !== null ? htmlspecialchars($d['lower_limit']) : '-'; ?></td>
<td><?php echo $d['upper_limit'] !== null ? htmlspecialchars($d['upper_limit']) : '-'; ?></td>
<td><code><?php echo htmlspecialchars($d['test_value']); ?></code></td>
<td>
<?php if ($d['test_result'] === 'PASS'): ?>
<span class="label label-success">PASS</span>
<?php else: ?>
<span class="label label-danger"><?php echo htmlspecialchars($d['test_result']); ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+133
View File
@@ -0,0 +1,133 @@
<?php
$activeMenu = 'config';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<?php if (!empty($msg)): ?>
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fa fa-check-circle"></i> <?php echo htmlspecialchars($msg); ?>
</div>
<?php endif; ?>
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fa fa-exclamation-circle"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-cog"></i> 系统配置</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>公司名称</label>
<input type="text" name="company_name" class="form-control" value="<?php echo htmlspecialchars($sysConfig['company_name'] ?? ''); ?>">
</div>
<div class="form-group">
<label>MES 展示名称</label>
<input type="text" name="mes_name" class="form-control" value="<?php echo htmlspecialchars($sysConfig['mes_name'] ?? ''); ?>">
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存配置</button>
</div>
</div>
<!-- 代码前缀规则配置 -->
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-barcode"></i> 序列号前缀规则</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
</div>
</div>
<div class="box-body">
<p class="text-muted" style="margin-bottom:12px;">
<i class="fa fa-info-circle"></i>
设置各产品类型的序列号前缀。工位录入时,序列号必须匹配对应前缀,否则将报错。
留空表示不检查前缀。
</p>
<div id="prefixRulesContainer">
<?php
$ruleIndex = 0;
if (!empty($codePrefixRules)):
foreach ($codePrefixRules as $typeName => $prefixVal):
?>
<div class="prefix-rule-row" style="display:flex; gap:10px; margin-bottom:8px; align-items:center;">
<input type="text" name="prefix_types[]" class="form-control" style="flex:1;"
placeholder="产品类型(如:成品、电芯、PCBA)"
value="<?php echo htmlspecialchars($typeName); ?>">
<input type="text" name="prefix_values[]" class="form-control" style="width:120px;"
placeholder="前缀(如:CP"
value="<?php echo htmlspecialchars($prefixVal); ?>">
<button type="button" class="btn btn-danger btn-sm remove-prefix-row" title="删除">
<i class="fa fa-trash"></i>
</button>
</div>
<?php
$ruleIndex++;
endforeach;
endif;
?>
</div>
<button type="button" class="btn btn-success btn-sm" id="addPrefixRow" style="margin-top:8px;">
<i class="fa fa-plus"></i> 添加规则
</button>
</div>
</form>
</div>
</div>
<div class="col-md-6">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-info-circle"></i> 当前信息</h3>
</div>
<div class="box-body">
<dl class="dl-horizontal">
<dt>公司名称</dt>
<dd><?php echo htmlspecialchars($sysConfig['company_name'] ?? ''); ?></dd>
<dt>系统名称</dt>
<dd><?php echo htmlspecialchars($sysConfig['mes_name'] ?? ''); ?></dd>
<dt>前缀规则</dt>
<dd>
<?php if (!empty($codePrefixRules)): ?>
<table class="table table-condensed table-bordered" style="margin-bottom:0;">
<thead><tr><th>产品类型</th><th>前缀</th></tr></thead>
<tbody>
<?php foreach ($codePrefixRules as $typeName => $prefixVal): ?>
<tr>
<td><?php echo htmlspecialchars($typeName); ?></td>
<td>
<?php if ($prefixVal !== ''): ?>
<code><?php echo htmlspecialchars($prefixVal); ?></code>
<?php else: ?>
<span class="text-muted">无限制</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<span class="text-muted">未配置</span>
<?php endif; ?>
</dd>
<dt>PHP 版本</dt>
<dd><?php echo phpversion(); ?></dd>
</dl>
</div>
</div>
</div>
</div>
<?php // v3-P2:原内联 <script> 已抽离为 external JS,并以 nonce 加载,配合 CSP script-src 'nonce-***' ?>
<script src="<?php echo BASE_URL; ?>/static/js/admin_config.js" nonce="<?php echo csp_nonce(); ?>"></script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+55
View File
@@ -0,0 +1,55 @@
<?php
$activeMenu = 'customer';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 客户列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/customerAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加客户</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:60px">序号</th>
<th>公司名称</th>
<th>联系人1</th>
<th>联系方式1</th>
<th>联系人2</th>
<th>联系方式2</th>
<th>操作人</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($customers)): ?>
<?php foreach ($customers as $cust): ?>
<tr>
<td><?php echo $cust['id']; ?></td>
<td><strong><?php echo htmlspecialchars($cust['company_name']); ?></strong></td>
<td><?php echo htmlspecialchars($cust['contact1'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($cust['phone1'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($cust['contact2'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($cust['phone2'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($cust['created_by'] ?? '-'); ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/customerEdit/<?php echo $cust['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/customerDelete/<?php echo $cust['id']; ?>" style="display:inline;" onsubmit="return confirm('确定删除该客户?')">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="8" class="text-center text-muted">暂无客户数据</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+45
View File
@@ -0,0 +1,45 @@
<?php
$activeMenu = 'customer';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加客户</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>公司名称 <span class="text-danger">*</span></label>
<input type="text" name="company_name" class="form-control" placeholder="公司全称" required>
</div>
<div class="form-group">
<label>联系人1</label>
<input type="text" name="contact1" class="form-control" placeholder="主要联系人">
</div>
<div class="form-group">
<label>联系方式1</label>
<input type="text" name="phone1" class="form-control" placeholder="电话/邮箱">
</div>
<div class="form-group">
<label>联系人2</label>
<input type="text" name="contact2" class="form-control" placeholder="备用联系人">
</div>
<div class="form-group">
<label>联系方式2</label>
<input type="text" name="phone2" class="form-control" placeholder="电话/邮箱">
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Admin/customer" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+45
View File
@@ -0,0 +1,45 @@
<?php
$activeMenu = 'customer';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑客户</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>公司名称 <span class="text-danger">*</span></label>
<input type="text" name="company_name" class="form-control" value="<?php echo htmlspecialchars($cust['company_name']); ?>" required>
</div>
<div class="form-group">
<label>联系人1</label>
<input type="text" name="contact1" class="form-control" value="<?php echo htmlspecialchars($cust['contact1'] ?? ''); ?>">
</div>
<div class="form-group">
<label>联系方式1</label>
<input type="text" name="phone1" class="form-control" value="<?php echo htmlspecialchars($cust['phone1'] ?? ''); ?>">
</div>
<div class="form-group">
<label>联系人2</label>
<input type="text" name="contact2" class="form-control" value="<?php echo htmlspecialchars($cust['contact2'] ?? ''); ?>">
</div>
<div class="form-group">
<label>联系方式2</label>
<input type="text" name="phone2" class="form-control" value="<?php echo htmlspecialchars($cust['phone2'] ?? ''); ?>">
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-save"></i> 更新</button>
<a href="<?php echo BASE_URL; ?>/Admin/customer" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
$activeMenu = 'database';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-database"></i> 数据库表管理</h3>
<div class="box-tools">
<span class="badge bg-blue">共 <?php echo count($tables); ?> 张表</span>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<thead>
<tr>
<th>序号</th>
<th>表名</th>
<th>记录数</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($tables as $idx => $tbl): ?>
<tr>
<td><?php echo $idx + 1; ?></td>
<td>
<strong><?php echo htmlspecialchars($tbl['business_name']); ?></strong>
<br><code style="color:#999;"><?php echo htmlspecialchars($tbl['name']); ?></code>
</td>
<td><span class="badge bg-light-blue"><?php echo $tbl['count']; ?></span></td>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>" class="btn btn-sm btn-primary">
<i class="fa fa-eye"></i> 查看/编辑
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+130
View File
@@ -0,0 +1,130 @@
<?php
$activeMenu = 'databaseBackup';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<?php if (!empty($message)): ?>
<div class="alert alert-<?php echo $messageType; ?> alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa <?php echo $messageType === 'success' ? 'fa-check' : ($messageType === 'danger' ? 'fa-ban' : 'fa-warning'); ?>"></i> 操作结果</h4>
<?php echo nl2br(htmlspecialchars($message)); ?>
</div>
<?php endif; ?>
<div class="row">
<!-- 备份操作区 -->
<div class="col-md-5">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-cloud-download"></i> 创建备份</h3>
</div>
<div class="box-body">
<p class="text-muted">将当前数据库导出为 SQL 文件,保存到服务器 <code>backups/</code> 目录。</p>
<p>数据库:<strong><?php echo htmlspecialchars(DB_NAME); ?></strong></p>
<p>主机:<strong><?php echo htmlspecialchars(DB_HOST); ?></strong></p>
<form method="post" onsubmit="return confirm('确定要创建数据库备份吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="backup">
<button type="submit" class="btn btn-success btn-lg btn-block">
<i class="fa fa-database"></i> 立即备份数据库
</button>
</form>
</div>
<div class="box-footer text-muted">
<small><i class="fa fa-info-circle"></i> 备份将包含所有表结构和数据(排除 jp_ 前缀的表)</small>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-info-circle"></i> 说明</h3>
</div>
<div class="box-body">
<ul>
<li><strong>备份</strong>:将数据库完整导出为 .sql 文件保存到服务器本地</li>
<li><strong>恢复</strong>:选择一个备份文件,将其数据恢复到当前数据库</li>
<li><strong>下载</strong>:将备份文件下载到本地电脑保存</li>
<li><strong>删除</strong>:删除服务器上的备份文件</li>
</ul>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> 注意</h4>
<p>恢复操作会<strong>覆盖当前数据库</strong>中已存在的同名表!请在恢复前确认已做好最新备份。</p>
</div>
</div>
</div>
</div>
<!-- 备份文件列表 -->
<div class="col-md-7">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-files-o"></i> 备份文件列表</h3>
<div class="box-tools">
<span class="badge bg-blue">共 <?php echo count($backupFiles); ?> 个备份</span>
</div>
</div>
<div class="box-body table-responsive no-padding">
<?php if (empty($backupFiles)): ?>
<div class="text-center" style="padding:40px;">
<i class="fa fa-inbox" style="font-size:48px; color:#ddd;"></i>
<p class="text-muted" style="margin-top:10px;">暂无备份文件,请先创建备份</p>
</div>
<?php else: ?>
<table class="table table-hover">
<thead>
<tr>
<th>文件名</th>
<th>大小</th>
<th>备份时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($backupFiles as $file): ?>
<tr>
<td>
<i class="fa fa-file-code-o text-primary"></i>
<code><?php echo htmlspecialchars($file['name']); ?></code>
</td>
<td><span class="badge bg-light-blue"><?php echo $file['size']; ?></span></td>
<td><?php echo $file['time']; ?></td>
<td>
<!-- 恢复 -->
<form method="post" style="display:inline;" onsubmit="return confirm('确定要恢复此备份吗?\n\n⚠ 此操作会覆盖当前数据库中的数据!\n建议先创建最新备份。');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="restore">
<input type="hidden" name="file" value="<?php echo htmlspecialchars($file['name']); ?>">
<button type="submit" class="btn btn-sm btn-warning" title="恢复此备份">
<i class="fa fa-cloud-upload"></i> 恢复
</button>
</form>
<!-- 下载 -->
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="download">
<input type="hidden" name="file" value="<?php echo htmlspecialchars($file['name']); ?>">
<button type="submit" class="btn btn-sm btn-primary" title="下载到本地">
<i class="fa fa-download"></i> 下载
</button>
</form>
<!-- 删除 -->
<form method="post" style="display:inline;" onsubmit="return confirm('确定要删除备份文件 <?php echo htmlspecialchars($file['name']); ?> 吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="file" value="<?php echo htmlspecialchars($file['name']); ?>">
<button type="submit" class="btn btn-sm btn-danger" title="删除备份文件">
<i class="fa fa-trash"></i> 删除
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+74
View File
@@ -0,0 +1,74 @@
<?php
$activeMenu = 'database';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-edit"></i> 编辑记录 — 表:<code><?php echo htmlspecialchars($table); ?></code> — ID<?php echo $row['id']; ?>
</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>" class="btn btn-default btn-sm">
<i class="fa fa-arrow-left"></i> 返回
</a>
</div>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php foreach ($columns as $col): ?>
<?php $field = $col['Field']; ?>
<?php $isSensitive = in_array($field, $sensitiveFields ?? [], true); ?>
<div class="form-group">
<label>
<strong><?php echo htmlspecialchars($field); ?></strong>
<small class="text-muted">(<?php echo htmlspecialchars($col['Type']); ?><?php echo $col['Key'] === 'PRI' ? ', 主键' : ''; ?><?php echo $isSensitive ? ', 已脱敏不可编辑' : ''; ?>)</small>
</label>
<?php if ($field === 'id'): ?>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($row[$field] ?? ''); ?>" disabled>
<?php elseif ($isSensitive): ?>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($row[$field] ?? ''); ?>" disabled>
<p class="text-warning" style="margin-top:4px;"><i class="fa fa-lock"></i> 敏感字段已脱敏,无法在此查看或修改</p>
<?php elseif (stripos($col['Type'], 'text') !== false): ?>
<textarea name="<?php echo htmlspecialchars($field); ?>" class="form-control" rows="4"><?php echo htmlspecialchars($row[$field] ?? ''); ?></textarea>
<?php elseif (stripos($col['Type'], 'enum') !== false): ?>
<?php
// 解析 ENUM 值
preg_match("/^enum\((.*)\)$/i", $col['Type'], $m);
$enumVals = [];
if (!empty($m[1])) {
foreach (explode(',', $m[1]) as $ev) {
$enumVals[] = trim($ev, "'\"");
}
}
?>
<?php if (!empty($enumVals)): ?>
<select name="<?php echo htmlspecialchars($field); ?>" class="form-control">
<?php foreach ($enumVals as $ev): ?>
<option value="<?php echo htmlspecialchars($ev); ?>" <?php echo ($row[$field] ?? '') === $ev ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($ev); ?>
</option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="text" name="<?php echo htmlspecialchars($field); ?>" class="form-control" value="<?php echo htmlspecialchars($row[$field] ?? ''); ?>">
<?php endif; ?>
<?php else: ?>
<input type="text" name="<?php echo htmlspecialchars($field); ?>" class="form-control" value="<?php echo htmlspecialchars($row[$field] ?? ''); ?>">
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-save"></i> 保存修改</button>
<a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>" class="btn btn-default">取消</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+173
View File
@@ -0,0 +1,173 @@
<?php
$activeMenu = 'databaseUpgrade';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<style>
.sql-preview {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
padding: 10px 15px;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
margin: 0;
}
.sql-preview::-webkit-scrollbar { width: 6px; }
.sql-preview::-webkit-scrollbar-track { background: #2d2d2d; }
.sql-preview::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
.badge-status { font-size: 12px; }
.btn-execute-all { margin-left: 10px; }
</style>
<?php if (!empty($message)): ?>
<div class="row">
<div class="col-md-12">
<div class="alert alert-<?php echo $messageType; ?> alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-<?php echo $messageType === 'success' ? 'check' : ($messageType === 'danger' ? 'ban' : 'warning'); ?>"></i> 提示</h4>
<?php echo nl2br(htmlspecialchars($message)); ?>
</div>
</div>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-12">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-wrench"></i> 数据库升级/迁移</h3>
<div class="box-tools">
<?php if ($pendingCount > 0): ?>
<span class="badge bg-yellow" style="font-size:13px;"><?php echo $pendingCount; ?> 个待执行</span>
<form method="post" style="display:inline;" class="confirm-form" data-confirm="确定要一键执行所有未执行的迁移脚本吗?此操作将按文件名顺序依次执行所有未执行过的 .sql 文件。">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="execute_all">
<button type="submit" class="btn btn-sm btn-success btn-execute-all">
<i class="fa fa-bolt"></i> 一键全部执行
</button>
</form>
<?php else: ?>
<span class="badge bg-green" style="font-size:13px;">全部已执行</span>
<?php endif; ?>
</div>
</div>
<div class="box-body">
<div class="callout callout-info" style="margin-bottom: 15px;">
<h4><i class="icon fa fa-info-circle"></i> 使用说明</h4>
<p style="margin-bottom:0;">
1. 系统会自动扫描 <code>sql/</code> 目录下的所有 <code>.sql</code> 文件。<br>
2. 已执行过的脚本会标记为 <span class="label label-success">已执行</span>,不会重复执行。<br>
3. 每个脚本在事务中执行,失败会自动回滚。<br>
4. 点击文件名可展开查看 SQL 内容预览。
</p>
</div>
<?php if (empty($fileList)): ?>
<div class="text-center" style="padding: 40px;">
<i class="fa fa-inbox" style="font-size: 48px; color: #ccc;"></i>
<p style="color: #999; margin-top: 10px;">sql/ 目录下没有找到 .sql 迁移文件</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;">#</th>
<th>文件名</th>
<th style="width: 100px;">状态</th>
<th style="width: 160px;">执行时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<?php $idx = 1; foreach ($fileList as $file): ?>
<tr>
<td><?php echo $idx++; ?></td>
<td>
<a href="#" class="sql-preview-link" data-preview="preview-<?php echo $idx; ?>" style="font-family: monospace;">
<i class="fa fa-file-code-o"></i> <?php echo htmlspecialchars($file['name']); ?>
</a>
<div id="preview-<?php echo $idx; ?>" class="sql-preview" style="display:none; margin-top:8px;">
<?php echo $file['preview']; ?>
</div>
</td>
<td>
<?php if ($file['is_migrated']): ?>
<?php if ($file['status'] == 1): ?>
<span class="label label-success">已执行</span>
<?php else: ?>
<span class="label label-danger" title="<?php echo htmlspecialchars($file['error_msg']); ?>">执行失败</span>
<?php endif; ?>
<?php else: ?>
<span class="label label-warning">待执行</span>
<?php endif; ?>
</td>
<td>
<?php if ($file['is_migrated'] && $file['executed_at']): ?>
<small><?php echo htmlspecialchars($file['executed_at']); ?></small>
<?php else: ?>
<small style="color:#999;">—</small>
<?php endif; ?>
</td>
<td>
<?php if (!$file['is_migrated'] || $file['status'] == 0): ?>
<form method="post" style="display:inline;" class="confirm-form" data-confirm="确定要执行该迁移脚本吗?">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="action" value="execute">
<input type="hidden" name="file" value="<?php echo htmlspecialchars($file['name']); ?>">
<button type="submit" class="btn btn-sm btn-primary">
<i class="fa fa-play"></i> 执行
</button>
</form>
<?php else: ?>
<button class="btn btn-sm btn-default" disabled>
<i class="fa fa-check"></i> 已完成
</button>
<?php endif; ?>
<?php if ($file['is_migrated'] && $file['status'] == 0 && !empty($file['error_msg'])): ?>
<br><small class="text-danger" style="display:inline-block;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?php echo htmlspecialchars($file['error_msg']); ?>"><?php echo htmlspecialchars($file['error_msg']); ?></small>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
function togglePreview(link, previewId) {
var preview = document.getElementById(previewId);
var icon = link.querySelector('i');
if (preview.style.display === 'none' || preview.style.display === '') {
preview.style.display = 'block';
if (icon) {
icon.className = 'fa fa-file-code-o';
}
} else {
preview.style.display = 'none';
if (icon) {
icon.className = 'fa fa-file-code-o';
}
}
}
// CSP:文件名预览切换改为事件委托(替代内联 onclick / javascript:
document.addEventListener('click', function (e) {
var link = e.target.closest ? e.target.closest('.sql-preview-link') : null;
if (!link) return;
e.preventDefault();
togglePreview(link, link.getAttribute('data-preview'));
});
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+98
View File
@@ -0,0 +1,98 @@
<?php
$activeMenu = 'database';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-12">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-table"></i> 表:<code><?php echo htmlspecialchars($table); ?></code>
</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/database" class="btn btn-default btn-sm"><i class="fa fa-arrow-left"></i> 返回列表</a>
<span class="badge bg-blue" style="margin-left:10px;">共 <?php echo $totalCount; ?> 条</span>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-bordered">
<thead>
<tr>
<?php foreach ($columns as $col): ?>
<th title="<?php echo htmlspecialchars($col['Type']); ?>">
<?php echo htmlspecialchars($col['Field']); ?>
</th>
<?php endforeach; ?>
<th style="width:140px;">操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<?php foreach ($columns as $col): ?>
<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
<?php
$val = $row[$col['Field']] ?? '';
if ($val === null) {
echo '<span class="text-muted">NULL</span>';
} elseif ($val === '') {
echo '<span class="text-muted">-</span>';
} else {
echo htmlspecialchars($val);
}
?>
</td>
<?php endforeach; ?>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/databaseEdit/<?php echo $idx; ?>/<?php echo $row['id']; ?>"
class="btn btn-xs btn-warning"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/databaseDelete/<?php echo $idx; ?>/<?php echo $row['id']; ?>" style="display:inline;" onsubmit="return confirm('确定删除 ID=<?php echo $row['id']; ?> 的记录吗?此操作不可恢复!');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr>
<td colspan="<?php echo count($columns) + 1; ?>" class="text-center text-muted">
暂无数据
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div class="box-footer clearfix">
<ul class="pagination pagination-sm no-margin pull-right">
<?php if ($page > 1): ?>
<li><a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>?page=<?php echo $page - 1; ?>">&laquo;</a></li>
<?php endif; ?>
<?php
$range = 2;
$start = max(1, $page - $range);
$end = min($totalPages, $page + $range);
if ($start > 1) { echo '<li><a href="<?php echo BASE_URL; ?>/Admin/databaseView/'.$idx.'?page=1">1</a></li>'; if ($start > 2) echo '<li><span>...</span></li>'; }
for ($p = $start; $p <= $end; $p++):
?>
<li class="<?php echo $p == $page ? 'active' : ''; ?>">
<a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>?page=<?php echo $p; ?>"><?php echo $p; ?></a>
</li>
<?php endfor;
if ($end < $totalPages) { if ($end < $totalPages - 1) echo '<li><span>...</span></li>'; echo '<li><a href="<?php echo BASE_URL; ?>/Admin/databaseView/'.$idx.'?page='.$totalPages.'">'.$totalPages.'</a></li>'; }
?>
<?php if ($page < $totalPages): ?>
<li><a href="<?php echo BASE_URL; ?>/Admin/databaseView/<?php echo $idx; ?>?page=<?php echo $page + 1; ?>">&raquo;</a></li>
<?php endif; ?>
</ul>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+90
View File
@@ -0,0 +1,90 @@
<?php
$activeMenu = 'employee';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 员工列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/employeeAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加员工</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:60px">序号</th>
<th>员工编号</th>
<th>姓名</th>
<th>权限</th>
<th>管理类目</th>
<th>操作人</th>
<th style="width:200px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($employees)): ?>
<?php foreach ($employees as $emp): ?>
<tr>
<td><?php echo $emp['id']; ?></td>
<td><code><?php echo htmlspecialchars($emp['emp_no']); ?></code></td>
<td><?php echo htmlspecialchars($emp['emp_name']); ?></td>
<td>
<?php
$roleNames = \core\base\Controller::ROLE_NAMES;
$roleClass = [
\core\base\Controller::ROLE_SUPER_ADMIN => 'danger',
\core\base\Controller::ROLE_ADMIN => 'warning',
\core\base\Controller::ROLE_OPERATOR => 'info',
];
$cls = $roleClass[$emp['role']] ?? 'default';
?>
<span class="label label-<?php echo $cls; ?>">
<?php echo htmlspecialchars($roleNames[$emp['role']] ?? $emp['role']); ?>
</span>
</td>
<td>
<?php
$catNames = \core\base\Controller::CATEGORY_NAMES;
$cat = $emp['category'] ?? 'mes';
if ($emp['role'] === \core\base\Controller::ROLE_SUPER_ADMIN) {
echo '<span class="label label-danger">全部类目</span>';
} else {
echo '<span class="label label-primary">' . htmlspecialchars($catNames[$cat] ?? $cat) . '</span>';
}
?>
</td>
<td><?php echo htmlspecialchars($emp['created_by'] ?? '-'); ?></td>
<td>
<?php
// 只有超级管理员可以操作
$canOperate = ($user['role'] === \core\base\Controller::ROLE_SUPER_ADMIN);
// 不能操作自己
if ($canOperate && (int)$emp['id'] === (int)$user['id']) {
$canOperate = false;
}
?>
<?php if ($canOperate): ?>
<a href="<?php echo BASE_URL; ?>/Admin/employeePermission/<?php echo $emp['id']; ?>" class="btn btn-xs btn-success"><i class="fa fa-shield"></i> 工位权限</a>
<a href="<?php echo BASE_URL; ?>/Admin/employeeEdit/<?php echo $emp['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<a href="<?php echo BASE_URL; ?>/Admin/employeeResetPwd/<?php echo $emp['id']; ?>" class="btn btn-xs btn-warning"><i class="fa fa-key"></i> 重置密码</a>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/employeeDelete/<?php echo $emp['id']; ?>" style="display:inline;" onsubmit="return confirm('确定删除该员工?')">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
<?php else: ?>
<span class="text-muted">无操作权限</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-muted">暂无员工数据</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+79
View File
@@ -0,0 +1,79 @@
<?php
$activeMenu = 'employee';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-user-plus"></i> 添加员工</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="form-group">
<label>员工编号 <small class="text-muted">(自动生成)</small></label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($nextEmpNo); ?>" readonly style="background:#eee;">
</div>
<div class="form-group">
<label>姓名 <span class="text-danger">*</span></label>
<input type="text" name="emp_name" class="form-control" placeholder="如:张三" required>
</div>
<div class="form-group">
<label>密码 <span class="text-danger">*</span></label>
<input type="password" name="password" class="form-control" placeholder="默认密码 123456" value="123456" required>
</div>
<div class="form-group">
<label>权限 <span class="text-danger">*</span></label>
<select name="role" id="roleSelect" class="form-control" onchange="toggleCategory()">
<?php
$roleNames = \core\base\Controller::ROLE_NAMES;
$manageableRoles = $manageableRoles ?? [\core\base\Controller::ROLE_OPERATOR, \core\base\Controller::ROLE_ADMIN];
foreach ($manageableRoles as $r):
?>
<option value="<?php echo $r; ?>"><?php echo htmlspecialchars($roleNames[$r] ?? $r); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="categoryGroup">
<label>管理类目 <span class="text-danger">*</span> <small class="text-muted">(管理员/操作员必须选择单一类目)</small></label>
<select name="category" class="form-control">
<?php
$catNames = \core\base\Controller::CATEGORY_NAMES;
foreach ($catNames as $key => $name):
?>
<option value="<?php echo $key; ?>"><?php echo htmlspecialchars($name); ?></option>
<?php endforeach; ?>
</select>
</div>
<script>
function toggleCategory() {
var role = document.getElementById('roleSelect').value;
var catGroup = document.getElementById('categoryGroup');
if (role === 'super_admin') {
catGroup.style.display = 'none';
} else {
catGroup.style.display = 'block';
}
}
// 页面加载时执行一次
toggleCategory();
</script>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Admin/employee" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+68
View File
@@ -0,0 +1,68 @@
<?php
$activeMenu = 'employee';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑员工</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>员工编号</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($emp['emp_no']); ?>" disabled>
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" name="emp_name" class="form-control" value="<?php echo htmlspecialchars($emp['emp_name']); ?>" required>
</div>
<div class="form-group">
<label>权限 <span class="text-danger">*</span></label>
<select name="role" id="roleSelect" class="form-control" onchange="toggleCategory()">
<?php
$roleNames = \core\base\Controller::ROLE_NAMES;
$manageableRoles = $manageableRoles ?? [\core\base\Controller::ROLE_OPERATOR, \core\base\Controller::ROLE_ADMIN];
foreach ($manageableRoles as $r):
?>
<option value="<?php echo $r; ?>" <?php echo ($emp['role'] ?? '') === $r ? 'selected' : ''; ?>><?php echo htmlspecialchars($roleNames[$r] ?? $r); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="categoryGroup">
<label>管理类目 <span class="text-danger">*</span></label>
<select name="category" class="form-control">
<?php
$catNames = \core\base\Controller::CATEGORY_NAMES;
foreach ($catNames as $key => $name):
?>
<option value="<?php echo $key; ?>" <?php echo ($emp['category'] ?? 'mes') === $key ? 'selected' : ''; ?>><?php echo htmlspecialchars($name); ?></option>
<?php endforeach; ?>
</select>
</div>
<script>
function toggleCategory() {
var role = document.getElementById('roleSelect').value;
var catGroup = document.getElementById('categoryGroup');
if (role === 'super_admin') {
catGroup.style.display = 'none';
} else {
catGroup.style.display = 'block';
}
}
toggleCategory();
</script>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-save"></i> 更新</button>
<a href="<?php echo BASE_URL; ?>/Admin/employee" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+129
View File
@@ -0,0 +1,129 @@
<?php
$activeMenu = 'employee';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<?php if ($saved): ?>
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-check"></i> 保存成功!</h4>
员工 <strong><?php echo htmlspecialchars($emp['emp_name']); ?></strong> 的工位权限已更新。
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-lock"></i>
工位权限设置 -
<strong><?php echo htmlspecialchars($emp['emp_name']); ?></strong>
<small class="text-muted"><?php echo htmlspecialchars($emp['emp_no']); ?></small>
</h3>
<div class="box-tools">
<?php
$roleNames = \core\base\Controller::ROLE_NAMES;
$isAdminRole = ($emp['role'] === \core\base\Controller::ROLE_SUPER_ADMIN || $emp['role'] === \core\base\Controller::ROLE_ADMIN);
$labelClass = $emp['role'] === \core\base\Controller::ROLE_SUPER_ADMIN ? 'danger' : ($emp['role'] === \core\base\Controller::ROLE_ADMIN ? 'warning' : 'info');
?>
<span class="label label-<?php echo $labelClass; ?>">
<?php echo htmlspecialchars($roleNames[$emp['role']] ?? $emp['role']); ?>
<?php echo $isAdminRole ? '(默认拥有全部权限)' : ''; ?>
</span>
</div>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if ($isAdminRole): ?>
<div class="callout callout-info">
<h4><i class="icon fa fa-info"></i> 提示</h4>
<p>超级管理员和管理员默认拥有所有工位的访问权限,无需单独设置。</p>
</div>
<?php endif; ?>
<div class="callout callout-warning">
<h4><i class="icon fa fa-exclamation-triangle"></i> 权限说明</h4>
<ul style="margin-bottom:0;">
<li><strong>勾选</strong>:允许该员工访问对应工位</li>
<li><strong>不勾选</strong>:该员工无法进入对应工位(前台左侧菜单不显示,直接访问会提示无权限)</li>
<li>如果员工没有任何工位被勾选,则默认允许访问所有<strong>已启用</strong>工位</li>
<li><span class="label label-danger">禁用</span> 状态的工位<strong>所有人都无法访问</strong>,即使勾选了也无效</li>
</ul>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:60px;text-align:center;">
<input type="checkbox" id="checkAll" onclick="toggleAll(this)">
</th>
<th style="width:60px;">#</th>
<th>工位编码</th>
<th>工位名称</th>
<th>工位类型</th>
<th style="width:80px;">状态</th>
</tr>
</thead>
<tbody>
<?php if (!empty($stations)): ?>
<?php foreach ($stations as $i => $station):
$stype = $station['station_type'];
$isDisabled = $station['status'] == 0;
$isChecked = isset($currentPermissions[$stype])
? ($currentPermissions[$stype] === 1)
: true; // 未配置时默认允许
?>
<tr class="<?php echo $isDisabled ? 'text-muted' : ''; ?>" style="<?php echo $isDisabled ? 'background:#f5f5f5;' : ''; ?>">
<td style="text-align:center;">
<input type="checkbox" name="allow[]" value="<?php echo htmlspecialchars($stype); ?>"
class="station-check" <?php echo $isChecked ? 'checked' : ''; ?>>
</td>
<td><?php echo $i + 1; ?></td>
<td><code><?php echo htmlspecialchars($station['station_code']); ?></code></td>
<td>
<?php echo htmlspecialchars($station['station_name']); ?>
<?php if ($isDisabled): ?>
<small class="text-danger"><i class="fa fa-ban"></i> 已禁用,权限无效</small>
<?php endif; ?>
</td>
<td><span class="label label-default"><?php echo htmlspecialchars($stype); ?></span></td>
<td>
<?php if ($station['status'] == 1): ?>
<span class="label label-success">启用</span>
<?php else: ?>
<span class="label label-danger">禁用</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="6" class="text-center text-muted">暂无工位数据,请先在「工位管理」中添加</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fa fa-save"></i> 保存权限设置
</button>
<a href="<?php echo BASE_URL; ?>/Admin/employee" class="btn btn-default btn-lg">
<i class="fa fa-arrow-left"></i> 返回员工列表
</a>
</div>
</form>
</div>
</div>
</div>
<script>
function toggleAll(source) {
var checkboxes = document.querySelectorAll('.station-check');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = source.checked;
}
}
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+41
View File
@@ -0,0 +1,41 @@
<?php
$activeMenu = 'employee';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-key"></i> 重置密码</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>员工编号</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($emp['emp_no']); ?>" disabled>
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($emp['emp_name']); ?>" disabled>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" name="new_password" class="form-control" placeholder="请输入新密码" required>
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" name="confirm_password" class="form-control" placeholder="请再次输入新密码" required>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-key"></i> 重置密码</button>
<a href="<?php echo BASE_URL; ?>/Admin/employee" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title><?php echo $title; ?> - MES</title>
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/bootstrap.min.css">
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/font-awesome.min.css">
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/AdminLTE.min.css">
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/_all-skins.min.css">
</head>
<body class="hold-transition skin-blue sidebar-mini">
<div class="wrapper">
<?php include 'header.php'; ?>
<div class="content-wrapper">
<section class="content-header">
<h1><?php echo $title; ?></h1>
</section>
<section class="content">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">员工列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/employeeAdd" class="btn btn-primary btn-sm">
<i class="fa fa-plus"></i> 添加员工
</a>
</div>
</div>
<div class="box-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>序号</th>
<th>员工编号</th>
<th>姓名</th>
<th>权限</th>
<th>操作人</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($employees as $i => $emp): ?>
<tr>
<td><?php echo $emp['id']; ?></td>
<td><?php echo htmlspecialchars($emp['emp_no']); ?></td>
<td><?php echo htmlspecialchars($emp['emp_name']); ?></td>
<td>
<span class="label label-<?php echo $emp['role'] === 'admin' ? 'danger' : 'info'; ?>">
<?php echo $emp['role'] === 'admin' ? '管理员' : '操作员'; ?>
</span>
</td>
<td><?php echo htmlspecialchars($emp['created_by']); ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/employeeEdit/<?php echo $emp['id']; ?>" class="btn btn-xs btn-info">编辑</a>
<a href="<?php echo BASE_URL; ?>/Admin/employeeResetPwd/<?php echo $emp['id']; ?>" class="btn btn-xs btn-warning">重置密码</a>
<a href="<?php echo BASE_URL; ?>/Admin/employeeDelete/<?php echo $emp['id']; ?>" class="btn btn-xs btn-danger confirm-link" data-confirm="确定删除?">删除</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
</div>
<?php include 'footer.php'; ?>
</div>
<script src="../static/js/jquery-3.7.1.min.js"></script>
<script src="../static/js/bootstrap.min.js"></script>
<script src="../static/js/app.min.js"></script>
<?php // v3-P2 CSP:全局事件委托(替代内联 onclick 等) ?>
<script src="<?php echo BASE_URL; ?>/static/js/csp_global.js" nonce="<?php echo csp_nonce(); ?>"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
<footer class="main-footer">
<div class="pull-right hidden-xs"><b>Version</b> 1.0.0</div>
<strong>Copyright &copy; 2024 MES.</strong> All rights reserved.
</footer>
<script src="<?php echo BASE_URL; ?>/static/js/jquery-3.7.1.min.js"></script>
<script src="<?php echo BASE_URL; ?>/static/js/bootstrap.min.js"></script>
<script src="<?php echo BASE_URL; ?>/static/js/app.min.js"></script>
+39
View File
@@ -0,0 +1,39 @@
<header class="main-header">
<a href="<?php echo BASE_URL; ?>/Admin/index" class="logo">
<span class="logo-mini"><b>MES</b></span>
<span class="logo-lg"><b>MES</b> 后台管理</span>
</a>
<nav class="navbar navbar-static-top">
<a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button">
<span class="sr-only">切换导航</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="hidden-xs"><?php echo htmlspecialchars($user['emp_name']); ?></span>
</a>
<ul class="dropdown-menu">
<li><a href="<?php echo BASE_URL; ?>/Login/logout"><i class="fa fa-power-off"></i> 退出登录</a></li>
</ul>
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu">
<li class="header">导航菜单</li>
<li><a href="<?php echo BASE_URL; ?>/Admin/index"><i class="fa fa-dashboard"></i> <span>控制台</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/employee"><i class="fa fa-users"></i> <span>员工管理</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/customer"><i class="fa fa-building"></i> <span>客户管理</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/productModel"><i class="fa fa-cube"></i> <span>产品型号</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/workstation"><i class="fa fa-th"></i> <span>工位管理</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/config"><i class="fa fa-cog"></i> <span>系统配置</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Admin/import"><i class="fa fa-upload"></i> <span>批量导入</span></a></li>
<li><a href="<?php echo BASE_URL; ?>/Front/index"><i class="fa fa-desktop"></i> <span>前台工位</span></a></li>
</ul>
</section>
</aside>
+81
View File
@@ -0,0 +1,81 @@
<?php
$activeMenu = 'import';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<?php if (!empty($msg)): ?>
<div class="alert alert-<?php echo strpos($msg, '成功') !== false ? 'success' : 'danger'; ?> alert-dismissible">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fa fa-info-circle"></i> <?php echo htmlspecialchars($msg); ?>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-upload"></i> 批量导入</h3>
</div>
<form method="post" action="" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>数据表类型</label>
<select name="table_type" class="form-control" required>
<option value="">请选择</option>
<option value="employee">员工数据</option>
<option value="customer">客户数据</option>
<option value="product_model">产品型号</option>
<option value="inbound">半成品入库工位数据</option>
<option value="pcb_test">PCB 测试数据</option>
<option value="battery">电池状态数据</option>
<option value="assembly">产品组装数据</option>
<option value="finished_test">成品测试数据</option>
<option value="warehouse">成品入库数据</option>
<option value="delivery">出货登记数据</option>
</select>
</div>
<div class="form-group">
<label>选择文件 (CSV)</label>
<input type="file" name="import_file" class="form-control" accept=".csv" required>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-upload"></i> 开始导入</button>
</div>
</form>
</div>
</div>
<div class="col-md-6">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-download"></i> 导入模板下载</h3>
</div>
<div class="box-body">
<div class="list-group">
<?php
$templates = [
'employee' => '员工数据模板',
'customer' => '客户数据模板',
'product_model' => '产品型号模板',
'inbound' => '入库工位数据模板',
'pcb_test' => 'PCB 测试数据模板',
'battery' => '电池状态数据模板',
'assembly' => '产品组装数据模板',
'finished_test' => '成品测试数据模板',
'warehouse' => '入库数据模板',
'delivery' => '出货登记数据模板',
];
foreach ($templates as $key => $label):
?>
<a href="<?php echo BASE_URL; ?>/Admin/importTemplate/<?php echo $key; ?>" class="list-group-item">
<i class="fa fa-download text-primary"></i> <?php echo $label; ?> (CSV)
</a>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+80
View File
@@ -0,0 +1,80 @@
<?php
$activeMenu = 'index';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-aqua">
<div class="inner">
<h3><?php echo isset($employeeCount) ? $employeeCount : 0; ?></h3>
<p>员工总数</p>
</div>
<div class="icon"><i class="fa fa-users"></i></div>
<a href="<?php echo BASE_URL; ?>/Admin/employee" class="small-box-footer">进入管理 <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-green">
<div class="inner">
<h3><?php echo isset($customerCount) ? $customerCount : 0; ?></h3>
<p>客户数量</p>
</div>
<div class="icon"><i class="fa fa-building"></i></div>
<a href="<?php echo BASE_URL; ?>/Admin/customer" class="small-box-footer">进入管理 <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-yellow">
<div class="inner">
<h3><?php echo isset($modelCount) ? $modelCount : 0; ?></h3>
<p>产品型号</p>
</div>
<div class="icon"><i class="fa fa-cube"></i></div>
<a href="<?php echo BASE_URL; ?>/Admin/productModel" class="small-box-footer">进入管理 <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-red">
<div class="inner">
<h3><?php echo isset($stationCount) ? $stationCount : 0; ?></h3>
<p>工位数量</p>
</div>
<div class="icon"><i class="fa fa-th"></i></div>
<a href="<?php echo BASE_URL; ?>/Admin/workstation" class="small-box-footer">进入管理 <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-info-circle"></i> 系统信息</h3>
</div>
<div class="box-body">
<dl class="dl-horizontal">
<dt>公司名称</dt><dd><?php echo isset($sysConfig) ? htmlspecialchars($sysConfig['company_name']) : '-'; ?></dd>
<dt>系统名称</dt><dd><?php echo isset($sysConfig) ? htmlspecialchars($sysConfig['mes_name']) : '-'; ?></dd>
<dt>当前用户</dt><dd><?php echo htmlspecialchars($user['emp_name']); ?></dd>
<dt>用户角色</dt><dd><span class="label label-info"><?php echo $user['role'] === 'admin' ? '管理员' : '操作员'; ?></span></dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-rocket"></i> 快捷操作</h3>
</div>
<div class="box-body">
<a href="<?php echo BASE_URL; ?>/Admin/employeeAdd" class="btn btn-primary"><i class="fa fa-user-plus"></i> 添加员工</a>
<a href="<?php echo BASE_URL; ?>/Admin/workstationAdd" class="btn btn-success"><i class="fa fa-plus-square"></i> 添加工位</a>
<a href="<?php echo BASE_URL; ?>/Admin/import" class="btn btn-warning"><i class="fa fa-upload"></i> 批量导入</a>
<a href="<?php echo BASE_URL; ?>/Front/index" class="btn btn-info"><i class="fa fa-desktop"></i> 进入前台</a>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+158
View File
@@ -0,0 +1,158 @@
<?php
$activeMenu = 'productModel';
include APP_PATH . 'app/views/layouts/admin_header.php';
// 构建类型颜色和列定义(统一从 typeConfigs 或 getDefaultConfigs 获取)
$typeColors = [];
$typeColumns = [];
// 统一数据源:typeConfigs 为空时回退到默认配置
$allConfigs = !empty($typeConfigs) ? $typeConfigs : \app\models\ProductTypeConfig::getDefaultConfigs();
foreach ($allConfigs as $cfg) {
$typeColors[$cfg['type_name']] = $cfg['color'] ?? 'default';
$cols = [];
if (!empty($cfg['display_columns'])) {
$decoded = json_decode($cfg['display_columns'], true);
if (is_array($decoded)) {
$cols = $decoded;
}
}
// 确保有序号列在最前
$hasId = false;
foreach ($cols as $c) {
if (($c['key'] ?? '') === 'id') { $hasId = true; break; }
}
if (!$hasId) {
array_unshift($cols, ['key'=>'id','label'=>'序号','width'=>'60px']);
}
// 补充 width 字段
foreach ($cols as &$c) {
if (!isset($c['width'])) $c['width'] = '';
}
unset($c);
$typeColumns[$cfg['type_name']] = $cols;
}
// 通用 fallback 列(所有类型都可用的字段集合)
$defaultColumns = [
['key'=>'id','label'=>'序号','width'=>'60px'],
['key'=>'type','label'=>'类型','width'=>'80px'],
['key'=>'model_code','label'=>'产品型号','width'=>''],
['key'=>'model_desc','label'=>'产品说明','width'=>''],
];
function renderModelTableRow($model, $columns, $typeName, $idx, $csrfToken) {
$html = '<tr>';
foreach ($columns as $col) {
$key = $col['key'];
$val = '';
if ($key === 'id') {
$val = $idx;
} elseif ($key === 'type') {
$val = htmlspecialchars($typeName);
} elseif (isset($model[$key])) {
$val = htmlspecialchars($model[$key]);
} else {
$val = '-';
}
$html .= '<td>' . $val . '</td>';
}
$html .= '<td style="width:150px">
<a href="' . BASE_URL . '/Admin/productModelEdit/' . $model['id'] . '" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="POST" action="' . BASE_URL . '/Admin/productModelDelete/' . $model['id'] . '" style="display:inline" onsubmit="return confirm(\'确定删除该型号?\')">
<input type="hidden" name="csrf_token" value="' . ($csrfToken ?? '') . '">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>';
$html .= '</tr>';
return $html;
}
?>
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 产品型号列表</h3>
<div class="box-tools" style="display:flex; align-items:center; gap:8px;">
<span class="column-adjuster" title="调整类型卡片列数" style="display:inline-flex; gap:4px;">
<button type="button" class="btn btn-default btn-xs col-btn" data-cols="1" title="1列"><i class="fa fa-align-justify"></i></button>
<button type="button" class="btn btn-default btn-xs col-btn active" data-cols="2" title="2列"><i class="fa fa-columns"></i> 2</button>
<button type="button" class="btn btn-default btn-xs col-btn" data-cols="3" title="3列"><i class="fa fa-columns"></i> 3</button>
<button type="button" class="btn btn-default btn-xs col-btn" data-cols="auto" title="自适应"><i class="fa fa-arrows-h"></i> 自动</button>
</span>
<a href="<?php echo BASE_URL; ?>/Admin/productTypeConfig" class="btn btn-warning btn-sm"><i class="fa fa-cogs"></i> 类型配置</a>
<a href="<?php echo BASE_URL; ?>/Admin/productModelAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加产品型号</a>
</div>
</div>
</div>
<?php if (!empty($groupedModels)): ?>
<div class="type-grid" id="typeGrid">
<?php foreach ($groupedModels as $typeName => $models): ?>
<div class="box box-<?php echo $typeColors[$typeName] ?? 'default'; ?> type-grid-item">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-tags"></i>
<?php echo htmlspecialchars($typeName); ?>
<span class="badge bg-<?php echo $typeColors[$typeName] ?? 'default'; ?>"><?php echo count($models); ?></span>
</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<?php $cols = $typeColumns[$typeName] ?? $defaultColumns; ?>
<?php foreach ($cols as $col): ?>
<th<?php if ($col['width']) echo ' style="width:' . $col['width'] . '"'; ?>><?php echo $col['label']; ?></th>
<?php endforeach; ?>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php $idx = 0; ?>
<?php foreach ($models as $model): ?>
<?php echo renderModelTableRow($model, $cols, $typeName, ++$idx, $csrfToken); ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="box">
<div class="box-body text-center text-muted" style="padding:40px;">
<i class="fa fa-inbox fa-3x"></i>
<p style="margin-top:10px;">暂无产品型号数据</p>
<a href="<?php echo BASE_URL; ?>/Admin/productModelAdd" class="btn btn-primary"><i class="fa fa-plus"></i> 添加产品型号</a>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
// ========== 类型卡片列数调节 ==========
$(function() {
var $grid = $('#typeGrid');
if (!$grid.length) return;
var COL_KEY = 'admin_type_grid_cols';
function applyColumns(cols) {
$grid.attr('data-columns', cols);
$('.column-adjuster .col-btn').removeClass('active');
$('.column-adjuster .col-btn[data-cols="' + cols + '"]').addClass('active');
try { localStorage.setItem(COL_KEY, cols); } catch(e) {}
}
var savedCols = localStorage.getItem(COL_KEY) || '2';
applyColumns(savedCols);
$('.column-adjuster .col-btn').click(function() {
applyColumns($(this).data('cols'));
});
});
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+151
View File
@@ -0,0 +1,151 @@
<?php
$activeMenu = 'productModel';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加产品型号</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="icon fa fa-ban"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="form-group">
<label>类型 <span class="text-danger">*</span></label>
<select name="type" id="typeSelect" class="form-control" required>
<option value="">-- 请选择类型 --</option>
<?php if (!empty($typeConfigs)): ?>
<?php foreach ($typeConfigs as $cfg): ?>
<option value="<?php echo htmlspecialchars($cfg['type_name']); ?>"><?php echo htmlspecialchars($cfg['type_name']); ?></option>
<?php endforeach; ?>
<?php else: ?>
<option value="成品">成品</option>
<option value="电池">电池</option>
<option value="外壳">外壳</option>
<option value="外壳颜色">外壳颜色</option>
<option value="包装">包装</option>
<option value="半成品">半成品</option>
<option value="配件">配件</option>
<?php endif; ?>
</select>
</div>
<div class="form-group" id="productTypeGroup">
<label id="productTypeLabel">产品类型 <span class="text-danger">*</span></label>
<input type="text" name="product_type" id="productTypeInput" class="form-control" placeholder="如:电池、外壳,成品" required>
</div>
<div class="form-group" id="modelCodeGroup">
<label>产品型号 <span class="text-danger">*</span></label>
<input type="text" name="model_code" id="modelCodeInput" class="form-control" placeholder="如:T8" required>
</div>
<div class="form-group" id="colorGroup">
<label>颜色</label>
<input type="text" name="color" id="colorInput" class="form-control" placeholder="如:珍珠白、深空灰">
</div>
<div class="form-group">
<label>产品说明</label>
<textarea name="description" class="form-control" rows="3" placeholder="产品描述信息"></textarea>
</div>
<script>
(function(){
var $typeSel = $('#typeSelect');
var $ptGroup = $('#productTypeGroup');
var $ptLabel = $('#productTypeLabel');
var $ptInput = $('#productTypeInput');
var $modelCodeGroup = $('#modelCodeGroup');
var $modelCodeInput = $('#modelCodeInput');
var $colorGroup = $('#colorGroup');
var $colorInput = $('#colorInput');
// 类型配置数据(从服务端传入)
var typeConfigs = <?php echo json_encode($typeConfigs ?? [], JSON_UNESCAPED_UNICODE); ?>;
// 构建类型名→配置映射
var typeConfigMap = {};
if (Array.isArray(typeConfigs)) {
typeConfigs.forEach(function(cfg) {
typeConfigMap[cfg.type_name] = cfg;
});
}
function updateProductTypeField() {
var t = $typeSel.val();
var cfg = typeConfigMap[t] || {};
// 产品类型输入框(三态:hidden/readonly/editable
var ptState = cfg.product_type_state || 'editable';
var field = cfg.product_type_field || 'product_type';
// 当 product_type_field === 'color' 且颜色框可见时,
// 隐藏产品类型输入框,避免与颜色输入框 name="color" 冲突
var cState = cfg.color_state || 'editable';
var hidePtForColor = (field === 'color' && cState !== 'hidden');
if (ptState === 'hidden' || hidePtForColor) {
$ptGroup.hide();
$ptInput.removeAttr('required');
} else {
$ptGroup.show();
var label = cfg.product_type_label || '产品类型';
var placeholder = cfg.product_type_placeholder || '';
if (!placeholder) {
placeholder = '如:' + label;
}
$ptLabel.html(label + (ptState === 'editable' ? ' <span class="text-danger">*</span>' : ''));
$ptInput.attr({name: field, placeholder: placeholder});
if (ptState === 'readonly') {
$ptInput.prop({readonly: true, required: false});
} else {
$ptInput.prop({readonly: false, required: true});
}
}
// 产品型号输入框(三态:hidden/readonly/editable
var mcState = cfg.model_code_state || 'editable';
if (mcState === 'hidden') {
$modelCodeGroup.hide();
$modelCodeInput.removeAttr('required').val('');
} else {
$modelCodeGroup.show();
if (mcState === 'readonly') {
$modelCodeInput.prop({readonly: true, required: false});
} else {
$modelCodeInput.prop({readonly: false, required: true});
}
}
// 颜色输入框(三态:hidden/readonly/editable
var cState = cfg.color_state || 'editable';
if (cState === 'hidden') {
$colorGroup.hide();
$colorInput.val('');
} else {
$colorGroup.show();
if (cState === 'readonly') {
$colorInput.prop('readonly', true);
} else {
$colorInput.prop('readonly', false);
}
}
}
$typeSel.on('change', updateProductTypeField);
updateProductTypeField();
})();
</script>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Admin/productModel" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+162
View File
@@ -0,0 +1,162 @@
<?php
$activeMenu = 'productModel';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑产品型号</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="icon fa fa-ban"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="form-group">
<label>类型 <span class="text-danger">*</span></label>
<select name="type" id="typeSelect" class="form-control" required>
<option value="">-- 请选择类型 --</option>
<?php if (!empty($typeConfigs)): ?>
<?php foreach ($typeConfigs as $cfg): ?>
<option value="<?php echo htmlspecialchars($cfg['type_name']); ?>" <?php echo (($model['type'] ?? '') === $cfg['type_name']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($cfg['type_name']); ?>
</option>
<?php endforeach; ?>
<?php else: ?>
<?php foreach (['成品', '电池', '外壳', '外壳颜色', '包装', '半成品', '配件'] as $t): ?>
<option value="<?php echo $t; ?>" <?php echo (($model['type'] ?? '') === $t) ? 'selected' : ''; ?>>
<?php echo $t; ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="form-group" id="productTypeGroup">
<label id="productTypeLabel">产品类型 <span class="text-danger">*</span></label>
<input type="text" name="product_type" id="productTypeInput" class="form-control" value="<?php echo htmlspecialchars($model['product_type'] ?? ''); ?>" placeholder="如:电池、外壳,成品" required>
</div>
<div class="form-group" id="modelCodeGroup">
<label>产品型号 <span class="text-danger">*</span></label>
<input type="text" name="model_code" id="modelCodeInput" class="form-control" value="<?php echo htmlspecialchars($model['model_code']); ?>" required>
</div>
<div class="form-group" id="colorGroup">
<label>颜色</label>
<input type="text" name="color" id="colorInput" class="form-control" value="<?php echo htmlspecialchars($model['color'] ?? ''); ?>" placeholder="如:珍珠白、深空灰">
</div>
<div class="form-group">
<label>产品说明</label>
<textarea name="description" class="form-control" rows="3"><?php echo htmlspecialchars($model['model_desc'] ?? ''); ?></textarea>
</div>
<script>
(function(){
var $typeSel = $('#typeSelect');
var $ptGroup = $('#productTypeGroup');
var $ptLabel = $('#productTypeLabel');
var $ptInput = $('#productTypeInput');
var $modelCodeGroup = $('#modelCodeGroup');
var $modelCodeInput = $('#modelCodeInput');
var $colorGroup = $('#colorGroup');
var $colorInput = $('#colorInput');
// 编辑时原始值
var editProductType = <?php echo json_encode($model['product_type'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
var editModelCode = <?php echo json_encode($model['model_code'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
var editColor = <?php echo json_encode($model['color'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
// 类型配置数据(从服务端传入)
var typeConfigs = <?php echo json_encode($typeConfigs ?? [], JSON_UNESCAPED_UNICODE); ?>;
// 构建类型名→配置映射
var typeConfigMap = {};
if (Array.isArray(typeConfigs)) {
typeConfigs.forEach(function(cfg) {
typeConfigMap[cfg.type_name] = cfg;
});
}
function updateProductTypeField() {
var t = $typeSel.val();
var cfg = typeConfigMap[t] || {};
// 产品类型输入框(三态:hidden/readonly/editable
var ptState = cfg.product_type_state || 'editable';
if (ptState === 'hidden') {
$ptGroup.hide();
$ptInput.removeAttr('required').val('');
} else {
$ptGroup.show();
var label = cfg.product_type_label || '产品类型';
var field = cfg.product_type_field || 'product_type';
var placeholder = cfg.product_type_placeholder || '';
if (!placeholder) {
placeholder = '如:' + label;
}
$ptLabel.html(label + (ptState === 'editable' ? ' <span class="text-danger">*</span>' : ''));
$ptInput.attr({name: field, placeholder: placeholder});
if (ptState === 'readonly') {
$ptInput.prop({readonly: true, required: false});
} else {
$ptInput.prop({readonly: false, required: true});
}
// 编辑时恢复原始值
if (editProductType) {
$ptInput.val(editProductType);
}
}
// 产品型号输入框(三态:hidden/readonly/editable
var mcState = cfg.model_code_state || 'editable';
if (mcState === 'hidden') {
$modelCodeGroup.hide();
$modelCodeInput.removeAttr('required').val('');
} else {
$modelCodeGroup.show();
if (mcState === 'readonly') {
$modelCodeInput.prop({readonly: true, required: false});
} else {
$modelCodeInput.prop({readonly: false, required: true});
}
if (editModelCode) {
$modelCodeInput.val(editModelCode);
}
}
// 颜色输入框(三态:hidden/readonly/editable
var cState = cfg.color_state || 'editable';
if (cState === 'hidden') {
$colorGroup.hide();
$colorInput.val('');
} else {
$colorGroup.show();
if (cState === 'readonly') {
$colorInput.prop('readonly', true);
} else {
$colorInput.prop('readonly', false);
}
if (editColor) {
$colorInput.val(editColor);
}
}
}
$typeSel.on('change', updateProductTypeField);
updateProductTypeField();
})();
</script>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-save"></i> 更新</button>
<a href="<?php echo BASE_URL; ?>/Admin/productModel" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+128
View File
@@ -0,0 +1,128 @@
<?php
$activeMenu = 'productSearch';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<!-- 产品追溯搜索 -->
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-search"></i> 产品追溯查询</h3>
</div>
<div class="box-body">
<form method="GET" action="<?php echo BASE_URL; ?>/Admin/productSearch" class="form-inline">
<div class="form-group" style="width:70%;">
<div class="input-group" style="width:100%;">
<input type="text" name="keyword" class="form-control input-lg"
placeholder="输入成品序列号 / 电池序列号 / PCB序列号 / 产品型号 进行查询..."
value="<?php echo htmlspecialchars($keyword); ?>" autofocus>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary btn-lg"><i class="fa fa-search"></i> 查询</button>
</span>
</div>
</div>
<small class="help-block" style="margin-left:5px; color:#999;">
<i class="fa fa-info-circle"></i> 支持模糊搜索:成品序列号、电池序列号、PCB序列号、产品型号
</small>
</form>
</div>
</div>
</div>
</div>
<?php if ($searched): ?>
<div class="row">
<div class="col-md-12">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-list"></i> 查询结果
<small class="label label-primary">共 <?php echo count($results); ?> 条记录</small>
</h3>
</div>
<div class="box-body table-responsive no-padding">
<?php if (empty($results)): ?>
<div class="callout callout-warning" style="margin:15px;">
<h4><i class="fa fa-warning"></i> 未找到匹配记录</h4>
<p>关键词 "<?php echo htmlspecialchars($keyword); ?>" 未匹配到任何产品记录,请检查输入。</p>
</div>
<?php else: ?>
<table class="table table-bordered table-hover table-striped" style="font-size:13px;">
<thead>
<tr style="background:#3c8dbc; color:#fff;">
<th style="text-align:center;">#</th>
<th style="text-align:center;">成品序列号</th>
<th style="text-align:center;">产品型号</th>
<th style="text-align:center;">电池序列号</th>
<th style="text-align:center;">PCB序列号</th>
<th style="text-align:center;">操作员</th>
<th style="text-align:center;">组装时间</th>
<th style="text-align:center;">电池电压平台</th>
<th style="text-align:center;">PCB测试结果</th>
<th style="text-align:center;">成品测试结果</th>
<th style="text-align:center;">入库时间</th>
<th style="text-align:center;">出货客户</th>
</tr>
</thead>
<tbody>
<?php $i = 1; foreach ($results as $row): ?>
<tr>
<td style="text-align:center;"><?php echo $i++; ?></td>
<td style="text-align:center; font-weight:bold; color:#3c8dbc;">
<?php echo htmlspecialchars($row['finished_serial']); ?>
</td>
<td style="text-align:center;">
<span class="label label-info"><?php echo htmlspecialchars($row['product_model']); ?></span>
</td>
<td style="text-align:center;">
<?php echo htmlspecialchars($row['battery_serial']); ?>
</td>
<td style="text-align:center;">
<?php echo htmlspecialchars($row['pcb_serial']); ?>
</td>
<td style="text-align:center;">
<?php echo htmlspecialchars($row['operator']); ?>
</td>
<td style="text-align:center;">
<?php echo htmlspecialchars($row['assembly_time']); ?>
</td>
<td style="text-align:center;">
<?php echo htmlspecialchars($row['voltage_platform']); ?>
</td>
<td style="text-align:center;">
<?php if (!empty($row['pcb_test_status'])): ?>
<span class="label label-<?php echo stripos($row['pcb_test_status'], 'OK') !== false || stripos($row['pcb_test_status'], '合格') !== false || stripos($row['pcb_test_status'], 'pass') !== false ? 'success' : 'danger'; ?>">
<?php echo htmlspecialchars($row['pcb_test_status']); ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td style="text-align:center;">
<?php if (!empty($row['finished_test_result'])): ?>
<span class="label label-<?php echo stripos($row['finished_test_result'], 'OK') !== false || stripos($row['finished_test_result'], '合格') !== false || stripos($row['finished_test_result'], 'pass') !== false ? 'success' : 'danger'; ?>">
<?php echo htmlspecialchars($row['finished_test_result']); ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td style="text-align:center;">
<?php echo !empty($row['warehouse_time']) ? htmlspecialchars($row['warehouse_time']) : '<span class="text-muted">未入库</span>'; ?>
</td>
<td style="text-align:center;">
<?php echo !empty($row['delivery_customer']) ? htmlspecialchars($row['delivery_customer']) : '<span class="text-muted">未出货</span>'; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+568
View File
@@ -0,0 +1,568 @@
<?php
$activeMenu = 'productModel';
include APP_PATH . 'app/views/layouts/admin_header.php';
// 构建类型配置完整数据,包含解析后的 display_columns
$configsWithParsed = [];
if (!empty($configs)) {
foreach ($configs as $cfg) {
$cfg['_columns'] = [];
if (!empty($cfg['display_columns'])) {
$decoded = json_decode($cfg['display_columns'], true);
if (is_array($decoded)) {
$cfg['_columns'] = $decoded;
}
}
// 确保有序号列
$hasId = false;
foreach ($cfg['_columns'] as $c) {
if (($c['key'] ?? '') === 'id') { $hasId = true; break; }
}
if (!$hasId) {
array_unshift($cfg['_columns'], ['key' => 'id', 'label' => '序号', 'width' => '60px']);
}
$configsWithParsed[] = $cfg;
}
}
?>
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-cogs"></i> 产品类型配置</h3>
<div class="box-tools">
<button type="button" class="btn btn-success btn-sm" id="addTypeBtn"><i class="fa fa-plus"></i> 添加类型</button>
<a href="<?php echo BASE_URL; ?>/Admin/productModel" class="btn btn-default btn-sm"><i class="fa fa-arrow-left"></i> 返回型号列表</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:50px">排序</th>
<th>类型名称</th>
<th>列表显示列</th>
<th style="width:90px">产品类型输入框</th>
<th>输入框标签</th>
<th>提交字段</th>
<th style="width:70px">颜色</th>
<th style="width:60px">启用</th>
<th style="width:80px">入库显示</th>
<th style="width:120px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($configsWithParsed)): ?>
<?php foreach ($configsWithParsed as $cfg): ?>
<tr>
<td><?php echo $cfg['sort_order']; ?></td>
<td><strong><?php echo htmlspecialchars($cfg['type_name']); ?></strong></td>
<td>
<?php
$colLabels = [];
foreach ($cfg['_columns'] as $col) {
$colLabels[] = htmlspecialchars($col['label'] ?? $col['key']);
}
echo implode(' &rarr; ', $colLabels);
?>
</td>
<td>
<?php
$ptState = $cfg['product_type_state'] ?? 'editable';
if ($ptState === 'hidden'): ?>
<span class="label label-default">隐藏</span>
<?php elseif ($ptState === 'readonly'): ?>
<span class="label label-warning">只读</span>
<?php else: ?>
<span class="label label-success">可编辑</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($cfg['product_type_label'] ?? '产品类型'); ?></td>
<td><code><?php echo htmlspecialchars($cfg['product_type_field'] ?? 'product_type'); ?></code></td>
<td><span class="label label-<?php echo htmlspecialchars($cfg['color'] ?? 'default'); ?>"><?php echo htmlspecialchars($cfg['color'] ?? 'default'); ?></span></td>
<td><?php echo $cfg['enabled'] ? '<span class="label label-success">启用</span>' : '<span class="label label-danger">禁用</span>'; ?></td>
<td><?php echo ($cfg['show_in_inbound'] ?? 1) ? '<span class="label label-success">显示</span>' : '<span class="label label-default">隐藏</span>'; ?></td>
<td>
<button type="button" class="btn btn-xs btn-info pt-edit-btn" data-id="<?php echo $cfg['id']; ?>"><i class="fa fa-edit"></i> 编辑</button>
<button type="button" class="btn btn-xs btn-danger pt-del-btn" data-id="<?php echo $cfg['id']; ?>" data-name="<?php echo htmlspecialchars($cfg['type_name'], ENT_QUOTES); ?>"><i class="fa fa-trash"></i> 删除</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="10" class="text-center text-muted" style="padding:30px;">暂无配置,请先执行 SQL 初始化或点击"添加类型"</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 编辑/添加弹窗 -->
<div class="modal fade" id="configModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title" id="configModalTitle">编辑类型配置</h4>
</div>
<form id="configForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<input type="hidden" name="id" id="cfgId" value="0">
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>类型名称 <span class="text-danger">*</span></label>
<input type="text" name="type_name" id="cfgTypeName" class="form-control" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>排序</label>
<input type="number" name="sort_order" id="cfgSortOrder" class="form-control" value="0" min="0">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Box颜色</label>
<select name="color" id="cfgColor" class="form-control">
<option value="default">default(灰)</option>
<option value="success">success(绿)</option>
<option value="primary">primary(蓝)</option>
<option value="info">info(青)</option>
<option value="warning">warning(黄)</option>
<option value="danger">danger(红)</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>输入框标签</label>
<input type="text" name="product_type_label" id="cfgPtLabel" class="form-control" placeholder="如:容量、颜色">
<small class="text-muted">添加/编辑页面第二个输入框的标签文字</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>提交字段名</label>
<input type="text" name="product_type_field" id="cfgPtField" class="form-control" placeholder="product_type / capacity / color">
<small class="text-muted">表单提交时的字段名(后端据此识别)</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>输入框占位提示</label>
<input type="text" name="product_type_placeholder" id="cfgPtPlaceholder" class="form-control" placeholder="如:2000mAh">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>产品类型输入框</label>
<select name="product_type_state" id="cfgProductTypeState" class="form-control">
<option value="editable">可编辑</option>
<option value="readonly">只读</option>
<option value="hidden">隐藏</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>产品型号输入框</label>
<select name="model_code_state" id="cfgModelCodeState" class="form-control">
<option value="editable">可编辑</option>
<option value="readonly">只读</option>
<option value="hidden">隐藏</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>颜色输入框</label>
<select name="color_state" id="cfgColorState" class="form-control">
<option value="editable">可编辑</option>
<option value="readonly">只读</option>
<option value="hidden">隐藏</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>启用</label>
<select name="enabled" id="cfgEnabled" class="form-control">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>入库页显示</label>
<select name="show_in_inbound" id="cfgShowInInbound" class="form-control">
<option value="1">显示</option>
<option value="0">隐藏</option>
</select>
<small class="text-muted">控制该类型在入库工位的下拉选项中是否可见</small>
</div>
</div>
</div>
<!-- 列表显示列 - 复选框编辑器 -->
<div class="form-group">
<label>列表显示列</label>
<div class="well well-sm" style="margin-bottom:8px; padding:8px 10px;">
<small class="text-muted"><i class="fa fa-info-circle"></i> 勾选需要在列表页显示的列。序号列自动包含,无需选择。拖拽调整顺序。</small>
</div>
<!-- 可选字段区 -->
<div style="margin-bottom:8px;">
<label class="text-muted" style="font-weight:normal; margin-right:8px;">可选字段:</label>
<label class="checkbox-inline" style="margin-right:10px;">
<input type="checkbox" class="col-checkbox" data-key="type" data-label="类型" data-width="80px"> 类型
</label>
<label class="checkbox-inline" style="margin-right:10px;">
<input type="checkbox" class="col-checkbox" data-key="model_code" data-label="产品型号" data-width=""> 产品型号
</label>
<label class="checkbox-inline" style="margin-right:10px;">
<input type="checkbox" class="col-checkbox" data-key="color" data-label="颜色" data-width="120px"> 颜色
</label>
<label class="checkbox-inline" style="margin-right:10px;">
<input type="checkbox" class="col-checkbox" data-key="capacity" data-label="容量" data-width="120px"> 容量
</label>
<label class="checkbox-inline" style="margin-right:10px;">
<input type="checkbox" class="col-checkbox" data-key="model_desc" data-label="产品说明" data-width=""> 产品说明
</label>
</div>
<!-- 已选列预览 -->
<div id="selectedColumnsPreview" style="min-height:36px; padding:6px 8px; border:1px dashed #ccc; border-radius:4px; background:#fafafa;">
<span id="noColumnsHint" class="text-muted">暂未选择列</span>
</div>
<div style="margin-top:5px;">
<button type="button" class="btn btn-xs btn-warning" id="resetColsBtn"><i class="fa fa-refresh"></i> 恢复默认</button>
<button type="button" class="btn btn-xs btn-default" id="clearColsBtn"><i class="fa fa-times"></i> 清空</button>
</div>
</div>
<input type="hidden" name="display_columns" id="cfgDisplayCols" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
</div>
</form>
</div>
</div>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
var BASE_URL = '<?php echo BASE_URL; ?>';
var configsData = <?php echo json_encode($configs ?? [], JSON_UNESCAPED_UNICODE); ?>;
// 当前编辑中的列数据(按显示顺序)
var editColumns = [];
// 所有可用字段定义
var allFields = [
{key:'type', label:'类型', width:'80px'},
{key:'model_code', label:'产品型号', width:''},
{key:'color', label:'颜色', width:'120px'},
{key:'capacity', label:'容量', width:'120px'},
{key:'model_desc', label:'产品说明', width:''}
];
// 构建 key→field 映射
var fieldMap = {};
allFields.forEach(function(f) { fieldMap[f.key] = f; });
// 默认列模板(按类型名,返回 key 数组)
function getDefaultKeys(typeName) {
var tn = (typeName || '').trim();
if (tn === '电池') {
return ['model_code', 'capacity', 'model_desc'];
} else if (tn === '外壳') {
return ['model_code', 'color', 'model_desc'];
} else if (tn === '外壳颜色') {
return ['color', 'model_desc'];
} else {
return ['type', 'model_code', 'model_desc'];
}
}
function getDefaultColumns(typeName) {
var keys = getDefaultKeys(typeName);
return keys.map(function(k) {
var f = fieldMap[k] || {key:k, label:k, width:''};
return {key: f.key, label: f.label, width: f.width};
});
}
// 根据 editColumns 刷新复选框状态和预览
function syncCheckboxesFromColumns() {
var selectedKeys = {};
editColumns.forEach(function(c) { selectedKeys[c.key] = true; });
$('.col-checkbox').each(function() {
$(this).prop('checked', !!selectedKeys[$(this).data('key')]);
});
renderPreview();
updateHiddenInput();
}
// 根据复选框状态重建 editColumns(保持已有顺序,新增追加末尾,取消的移除)
function syncColumnsFromCheckboxes() {
var checkedKeys = {};
$('.col-checkbox:checked').each(function() {
checkedKeys[$(this).data('key')] = true;
});
// 移除已取消的
editColumns = editColumns.filter(function(c) { return checkedKeys[c.key]; });
// 追加新勾选的
$('.col-checkbox:checked').each(function() {
var key = $(this).data('key');
if (!editColumns.some(function(c) { return c.key === key; })) {
var label = $(this).data('label');
var width = $(this).data('width') || '';
editColumns.push({key: key, label: label, width: width});
}
});
renderPreview();
updateHiddenInput();
}
// 渲染已选列预览(可拖拽排序的标签)
function renderPreview() {
var container = document.getElementById('selectedColumnsPreview');
var hint = document.getElementById('noColumnsHint');
if (!editColumns.length) {
container.innerHTML = '';
container.appendChild(hint || createHint());
return;
}
var html = '';
editColumns.forEach(function(col, i) {
html += '<span class="label label-primary column-tag" draggable="true" data-idx="' + i + '" style="display:inline-block; margin:2px 4px; padding:6px 10px; font-size:13px; cursor:grab; user-select:none;">';
html += escHtml(col.label);
html += ' <a href="#" class="column-remove" data-idx="' + i + '" style="color:#fff; opacity:0.7; margin-left:4px;" title="移除">&times;</a>';
html += '</span>';
});
container.innerHTML = html;
// 拖拽排序
var tags = container.querySelectorAll('.column-tag');
var dragSrc = null;
tags.forEach(function(tag) {
tag.addEventListener('dragstart', function(e) {
dragSrc = this;
this.style.opacity = '0.4';
e.dataTransfer.effectAllowed = 'move';
});
tag.addEventListener('dragend', function(e) {
this.style.opacity = '1';
tags.forEach(function(t) { t.style.borderTop = ''; });
});
tag.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.style.borderTop = '2px solid #3c8dbc';
});
tag.addEventListener('dragleave', function(e) {
this.style.borderTop = '';
});
tag.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderTop = '';
if (dragSrc !== this) {
var fromIdx = parseInt(dragSrc.getAttribute('data-idx'));
var toIdx = parseInt(this.getAttribute('data-idx'));
var moved = editColumns.splice(fromIdx, 1)[0];
editColumns.splice(toIdx, 0, moved);
renderPreview();
updateHiddenInput();
}
});
});
}
function createHint() {
var span = document.createElement('span');
span.id = 'noColumnsHint';
span.className = 'text-muted';
span.textContent = '暂未选择列';
return span;
}
function removeColumn(idx) {
var key = editColumns[idx].key;
editColumns.splice(idx, 1);
// 同步取消复选框
$('.col-checkbox[data-key="' + key + '"]').prop('checked', false);
renderPreview();
updateHiddenInput();
}
function clearAllColumns() {
editColumns = [];
$('.col-checkbox').prop('checked', false);
renderPreview();
updateHiddenInput();
}
function resetColumnsToDefault() {
var typeName = $('#cfgTypeName').val().trim();
editColumns = JSON.parse(JSON.stringify(getDefaultColumns(typeName)));
syncCheckboxesFromColumns();
}
function updateHiddenInput() {
document.getElementById('cfgDisplayCols').value = JSON.stringify(editColumns);
}
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ===== 轻量 Modal Polyfill(不依赖 Bootstrap JS =====
var _modalPolyfill = {
show: function(modalId) {
var $modal = $(modalId);
if (!$modal.length) return;
if (!$('.modal-backdrop').length) {
$('body').append('<div class="modal-backdrop fade in" style="z-index:1040;"></div>');
}
$modal.css({display:'block', 'z-index':1050}).addClass('in');
$('body').addClass('modal-open');
$modal.find('[data-dismiss="modal"]').off('click.modalPolyfill').on('click.modalPolyfill', function(){
_modalPolyfill.hide(modalId);
});
$('.modal-backdrop').off('click.modalPolyfill').on('click.modalPolyfill', function(){
_modalPolyfill.hide(modalId);
});
$(document).off('keydown.modalPolyfill').on('keydown.modalPolyfill', function(e){
if (e.keyCode === 27) _modalPolyfill.hide(modalId);
});
},
hide: function(modalId) {
var $modal = $(modalId);
$modal.removeClass('in').css('display', '');
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
$(document).off('keydown.modalPolyfill');
}
};
function safeModal(action, modalId) {
if (typeof $.fn.modal === 'function') {
$(modalId).modal(action);
} else {
if (action === 'show') _modalPolyfill.show(modalId);
else if (action === 'hide') _modalPolyfill.hide(modalId);
}
}
function openEditModal(id) {
var cfg = {};
if (id > 0) {
for (var i = 0; i < configsData.length; i++) {
if (configsData[i].id == id) { cfg = configsData[i]; break; }
}
$('#configModalTitle').text('编辑类型配置');
} else {
$('#configModalTitle').text('添加类型配置');
cfg = {
id: 0, type_name: '', sort_order: configsData.length + 1,
display_columns: '', product_type_state: 'editable',
product_type_label: '产品类型', product_type_field: 'product_type',
product_type_placeholder: '', model_code_state: 'editable',
color_state: 'editable', color: 'default', enabled: 1,
show_in_inbound: 1
};
}
$('#cfgId').val(cfg.id || 0);
$('#cfgTypeName').val(cfg.type_name || '');
$('#cfgSortOrder').val(cfg.sort_order || 0);
$('#cfgProductTypeState').val(cfg.product_type_state || 'editable');
$('#cfgPtLabel').val(cfg.product_type_label || '产品类型');
$('#cfgPtField').val(cfg.product_type_field || 'product_type');
$('#cfgPtPlaceholder').val(cfg.product_type_placeholder || '');
$('#cfgModelCodeState').val(cfg.model_code_state || 'editable');
$('#cfgColorState').val(cfg.color_state || 'editable');
$('#cfgColor').val(cfg.color || 'default');
$('#cfgEnabled').val(cfg.enabled !== undefined ? cfg.enabled : 1);
$('#cfgShowInInbound').val(cfg.show_in_inbound !== undefined ? cfg.show_in_inbound : 1);
// 解析 display_columns JSON
editColumns = [];
if (cfg.display_columns) {
try {
var parsed = JSON.parse(cfg.display_columns);
if (Array.isArray(parsed)) {
editColumns = parsed.filter(function(c) { return c.key !== 'id'; });
}
} catch(e) {}
}
if (!editColumns.length) {
editColumns = JSON.parse(JSON.stringify(getDefaultColumns(cfg.type_name || '')));
}
syncCheckboxesFromColumns();
safeModal('show', '#configModal');
}
// 复选框变化时同步
$(document).on('change', '.col-checkbox', function() {
syncColumnsFromCheckboxes();
});
// ===== CSP:事件绑定(替代内联 onclick/javascript: =====
$('#addTypeBtn').on('click', function() { openEditModal(0); });
$('#resetColsBtn').on('click', resetColumnsToDefault);
$('#clearColsBtn').on('click', clearAllColumns);
$(document).on('click', '.pt-edit-btn', function() { openEditModal($(this).data('id')); });
$(document).on('click', '.pt-del-btn', function() { deleteConfig($(this).data('id'), $(this).data('name')); });
$('#selectedColumnsPreview').on('click', '.column-remove', function(e) {
e.preventDefault();
removeColumn(parseInt($(this).attr('data-idx'), 10));
});
function deleteConfig(id, name) {
if (!confirm('确定删除类型「' + name + '」吗?\n注意:已使用该类型的产品型号不会受影响,但添加新型号时不再显示此类型。')) return;
$.ajax({
url: window.location.href, type: 'POST',
data: { csrf_token: $('input[name="csrf_token"]').val(), action: 'delete', id: id },
dataType: 'json',
success: function(res) {
if (res.success) location.reload();
else alert('删除失败: ' + (res.error || '未知错误'));
},
error: function() { alert('网络错误'); }
});
}
$('#configForm').on('submit', function(e) {
e.preventDefault();
updateHiddenInput();
var data = $(this).serialize() + '&action=save';
$.ajax({
url: window.location.href, type: 'POST', data: data, dataType: 'json',
success: function(res) {
if (res.success) location.reload();
else alert('保存失败: ' + (res.error || '未知错误'));
},
error: function() { alert('网络错误'); }
});
});
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+57
View File
@@ -0,0 +1,57 @@
<?php
$activeMenu = 'workstation';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 工位列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Admin/workstationAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加工位</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width:60px">序号</th>
<th>工位名称</th>
<th>工位编码</th>
<th>工位类型</th>
<th>排序</th>
<th>状态</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($stations)): ?>
<?php foreach ($stations as $station): ?>
<tr>
<td><?php echo $station['id']; ?></td>
<td><?php echo htmlspecialchars($station['station_name']); ?></td>
<td><code><?php echo htmlspecialchars($station['station_code']); ?></code></td>
<td><?php echo htmlspecialchars($station['station_type']); ?></td>
<td><?php echo $station['sort_order']; ?></td>
<td>
<span class="label label-<?php echo $station['status'] == 1 ? 'success' : 'danger'; ?>">
<?php echo $station['status'] == 1 ? '启用' : '禁用'; ?>
</span>
</td>
<td>
<a href="<?php echo BASE_URL; ?>/Admin/workstationEdit/<?php echo $station['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Admin/workstationDelete/<?php echo $station['id']; ?>" style="display:inline;" onsubmit="return confirm('确定删除该工位?')">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-muted">暂无工位数据</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+100
View File
@@ -0,0 +1,100 @@
<?php
$activeMenu = 'workstation';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加工位</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-ban"></i> 操作失败</h4>
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="form-group">
<label>工位名称 <span class="text-danger">*</span></label>
<input type="text" name="station_name" class="form-control" placeholder="如:入库工位" required>
</div>
<div class="form-group">
<label>工位编码 <span class="text-danger">*</span></label>
<input type="text" name="station_code" class="form-control" placeholder="如:STATION_INBOUND" required>
</div>
<div class="form-group">
<label>工位类型 <span class="text-danger">*</span></label>
<input type="text" name="station_type" class="form-control" placeholder="英文标识,如:painting、welding" required
pattern="[a-zA-Z][a-zA-Z0-9_]*" title="必须以字母开头,只能包含字母、数字和下划线">
<small class="text-muted">用于系统路由和权限识别,必须以字母开头,建议使用英文小写+下划线</small>
</div>
<div class="form-group">
<label>字段配置模板 <small class="text-muted">(可选,从已有工位复制)</small></label>
<select name="template_station" class="form-control" onchange="handleTemplateChange(this.value)">
<option value="">不复制(空白工位)</option>
<option value="inbound">半成品入库工位</option>
<option value="pcb_test">PCB 测试</option>
<option value="battery">电池状态</option>
<option value="assembly">产品组装</option>
<option value="finished_test">成品测试</option>
<option value="warehouse">成品入库工位</option>
<option value="delivery">出货登记</option>
</select>
<small class="text-muted">选择后将复制该工位的字段配置,后续可在编辑中修改</small>
</div>
<div id="templatePreview" style="display:none; margin-top:10px; padding:10px; background:#f9f9f9; border-radius:4px;">
<strong><i class="fa fa-clone"></i> 将复制的字段:</strong>
<span id="templateFieldsPreview" style="margin-left:8px; color:#666;"></span>
</div>
<div class="form-group">
<label>排序</label>
<input type="number" name="sort_order" class="form-control" value="1" min="1">
</div>
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Admin/workstation" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<script>
// 工位类型模板预览
var templateFieldsConfig = <?php echo json_encode($templateFieldsMap ?? [], JSON_UNESCAPED_UNICODE); ?>;
function handleTemplateChange(templateType) {
var preview = document.getElementById('templatePreview');
var fieldsSpan = document.getElementById('templateFieldsPreview');
if (!templateType) {
preview.style.display = 'none';
return;
}
var fields = templateFieldsConfig[templateType] || [];
if (fields.length > 0) {
var names = fields.map(function(f) { return f.label || f.name; });
fieldsSpan.textContent = names.join('、');
preview.style.display = 'block';
} else {
fieldsSpan.textContent = '(该工位暂无字段配置)';
preview.style.display = 'block';
}
}
</script>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+74
View File
@@ -0,0 +1,74 @@
<?php
$activeMenu = 'workstation';
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑工位</h3>
</div>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-ban"></i> 操作失败</h4>
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>工位名称 <span class="text-danger">*</span></label>
<input type="text" name="station_name" class="form-control" value="<?php echo htmlspecialchars($station['station_name']); ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>工位编码</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($station['station_code']); ?>" disabled>
<small class="text-muted">编码不可修改</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>工位类型</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($station['station_type']); ?>" disabled>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>排序</label>
<input type="number" name="sort_order" class="form-control" value="<?php echo $station['sort_order']; ?>" min="1">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<option value="1" <?php echo $station['status'] == 1 ? 'selected' : ''; ?>>启用</option>
<option value="0" <?php echo $station['status'] == 0 ? 'selected' : ''; ?>>禁用</option>
</select>
</div>
</div>
</div>
<!-- 字段配置编辑器 -->
<?php echo $fieldEditor ?? ''; ?>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-warning"><i class="fa fa-save"></i> 更新</button>
<a href="<?php echo BASE_URL; ?>/Admin/workstation" class="btn btn-default"><i class="fa fa-arrow-left"></i> 返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+319
View File
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前台工位看板 - <?php echo htmlspecialchars($sysConfig['company_name'] ?? 'MES'); ?></title>
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/bootstrap.min.css">
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/static/css/font-awesome.min.css">
<style>
:root {
--bg: #0f1923;
--card-bg: #1a2736;
--text: #c8d6e5;
--text-light: #8395a7;
--primary: #0abde3;
--success: #10ac84;
--warning: #feca57;
--danger: #ee5253;
--border: #2d3a4a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
/* 顶部导航栏 */
.topbar {
background: var(--card-bg);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.topbar .logo {
font-size: 20px;
font-weight: bold;
color: var(--primary);
}
.topbar .logo i { margin-right: 8px; }
.topbar .user-info {
display: flex;
align-items: center;
gap: 16px;
}
.topbar .user-info .clock {
font-size: 18px;
font-weight: bold;
color: var(--text);
}
.topbar .user-info .user-name {
color: var(--primary);
}
.topbar .btn-back {
color: var(--text-light);
text-decoration: none;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 4px;
transition: all 0.3s;
}
.topbar .btn-back:hover {
color: var(--primary);
border-color: var(--primary);
text-decoration: none;
}
/* 内容区 */
.content {
padding: 20px 24px;
flex: 1;
min-height: 0;
}
/* 概览卡片 */
.overview-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.overview-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
text-align: center;
transition: transform 0.2s;
}
.overview-card:hover { transform: translateY(-2px); }
.overview-card .card-icon {
font-size: 32px;
margin-bottom: 8px;
}
.overview-card .card-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 4px;
}
.overview-card .card-label {
font-size: 13px;
color: var(--text-light);
}
.overview-card.total .card-value { color: var(--primary); }
.overview-card.total .card-icon { color: var(--primary); }
/* 工位网格 */
.station-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.station-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
transition: all 0.3s;
cursor: pointer;
position: relative;
overflow: hidden;
}
.station-card:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(10, 189, 227, 0.15);
}
.station-card.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.station-card .station-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.station-card .station-name {
font-size: 16px;
font-weight: bold;
}
.station-card .station-code {
font-size: 11px;
color: var(--text-light);
background: rgba(255,255,255,0.05);
padding: 2px 8px;
border-radius: 3px;
}
.station-card .station-count {
font-size: 36px;
font-weight: bold;
color: var(--primary);
margin-bottom: 4px;
}
.station-card .station-label {
font-size: 12px;
color: var(--text-light);
}
.station-card .status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
}
.status-active { background: rgba(16, 172, 132, 0.2); color: var(--success); }
.status-inactive { background: rgba(238, 82, 83, 0.2); color: var(--danger); }
/* 底部 */
.footer {
text-align: center;
padding: 20px;
color: var(--text-light);
font-size: 12px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
/* 响应式 */
@media (max-width: 768px) {
.overview-cards { grid-template-columns: repeat(2, 1fr); }
.station-grid { grid-template-columns: 1fr; }
.topbar { flex-direction: column; gap: 8px; }
}
/* 刷新动画 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.refreshing { animation: pulse 0.6s ease-in-out; }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="topbar">
<div>
<span class="logo">
<i class="fa fa-industry"></i>
<?php echo htmlspecialchars($sysConfig['company_name'] ?? 'MES'); ?> - 前台工位看板
</span>
</div>
<div class="user-info">
<span class="clock" id="liveClock"><?php echo date('Y-m-d H:i:s'); ?></span>
<span><i class="fa fa-user"></i> <span class="user-name"><?php echo htmlspecialchars($user['emp_name']); ?></span></span>
<a href="<?php echo BASE_URL; ?>/Front/index" class="btn-back"><i class="fa fa-th"></i> 进入工位</a>
<a href="<?php echo BASE_URL; ?>/Admin/index" class="btn-back"><i class="fa fa-cog"></i> 后台管理</a>
<a href="<?php echo BASE_URL; ?>/Login/logout" class="btn-back confirm-link" data-confirm="确定退出?"><i class="fa fa-sign-out"></i> 退出</a>
</div>
</div>
<div class="content">
<!-- 概览卡片 -->
<div class="overview-cards">
<div class="overview-card total">
<div class="card-icon"><i class="fa fa-list-alt"></i></div>
<div class="card-value"><?php echo $totalRecords; ?></div>
<div class="card-label">今日总记录</div>
</div>
<div class="overview-card total">
<div class="card-icon"><i class="fa fa-calendar"></i></div>
<div class="card-value" id="todayDisplay"><?php echo $today; ?></div>
<div class="card-label">当前日期</div>
</div>
<div class="overview-card total">
<div class="card-icon"><i class="fa fa-th-large"></i></div>
<div class="card-value"><?php echo count($stations); ?></div>
<div class="card-label">工位总数</div>
</div>
<div class="overview-card total">
<div class="card-icon"><i class="fa fa-check-circle"></i></div>
<?php
$activeCount = 0;
foreach ($stations as $s) { if ($s['status'] == 1) $activeCount++; }
?>
<div class="card-value"><?php echo $activeCount; ?></div>
<div class="card-label">启用工位</div>
</div>
</div>
<!-- 工位网格 -->
<div class="station-grid">
<?php
$icons = [
'inbound' => 'fa-sign-in',
'pcb_test' => 'fa-microchip',
'battery' => 'fa-battery-full',
'assembly' => 'fa-cogs',
'finished_test' => 'fa-check-circle',
'warehouse' => 'fa-warehouse',
'delivery' => 'fa-truck',
];
foreach ($stations as $station):
$type = $station['station_type'];
$icon = $icons[$type] ?? 'fa-circle-o';
$stat = $stats[$type] ?? ['count' => 0, 'name' => $station['station_name']];
?>
<div class="station-card <?php echo $station['status'] == 0 ? 'disabled' : ''; ?>"
<?php echo $station['status'] == 1 ? 'data-href="' . BASE_URL . '/Front/' . $type . '"' : ''; ?>>
<div class="station-header">
<span class="station-name">
<i class="fa <?php echo $icon; ?>"></i> <?php echo htmlspecialchars($stat['name']); ?>
</span>
<span class="station-code"><?php echo htmlspecialchars($station['station_code']); ?></span>
</div>
<div class="station-count"><?php echo $stat['count']; ?></div>
<div class="station-label">今日处理量</div>
<div style="margin-top: 12px;">
<span class="status-badge <?php echo $station['status'] == 1 ? 'status-active' : 'status-inactive'; ?>">
<?php echo $station['status'] == 1 ? '● 启用' : '● 禁用'; ?>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="footer">
&copy;2026深圳市云创芯电子有限公司版权所有
<br><span class="beian">备案号:<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">粤ICP备16118477号-1</a></span>
<br><span class="beian">公安网备:<a href="https://beian.mps.gov.cn/#/query/webSearch?code=44030002015390" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:4px;"><img src="<?php echo BASE_URL; ?>/static/images/gongan_badge.png" width="16" height="17" alt="公安网备" loading="lazy" style="vertical-align:middle;" onerror="this.onerror=null;this.outerHTML='<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;17&quot; viewBox=&quot;0 0 24 24&quot;><path d=&quot;M12 2l8 3v6c0 5-4 9-8 11-4-2-8-6-8-11V5l8-3z&quot; fill=&quot;#1a3a8f&quot; stroke=&quot;#d4af37&quot; stroke-width=&quot;1&quot;/><polygon points=&quot;12,7 13.5,10.5 17,10.5 14,12.8 15,16.5 12,14.5 9,16.5 10,12.8 7,10.5 10.5,10.5&quot; fill=&quot;#ffffff&quot;/></svg>'">粤公网安备44030002015390号</a></span>
</div>
<script src="<?php echo BASE_URL; ?>/static/js/jquery-3.7.1.min.js"></script>
<?php // v3-P2 CSP:全局事件委托(替代内联 onclick 等) ?>
<script src="<?php echo BASE_URL; ?>/static/js/csp_global.js" nonce="<?php echo csp_nonce(); ?>"></script>
<script nonce="<?php echo csp_nonce(); ?>">
// 实时时钟
function updateClock() {
var now = new Date();
var str = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
document.getElementById('liveClock').textContent = str;
}
setInterval(updateClock, 1000);
updateClock();
// 自动刷新(每30秒)
setTimeout(function() {
location.reload();
}, 30000);
</script>
</body>
</html>
+85
View File
@@ -0,0 +1,85 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 账务记录</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Finance/accountsAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加记录</a>
</div>
</div>
<div class="box-body">
<form method="get" class="form-inline" style="margin-bottom:15px;">
<div class="form-group">
<label>类型:</label>
<select name="type" class="form-control input-sm">
<option value="">全部</option>
<option value="income" <?php echo ($typeFilter ?? '') === 'income' ? 'selected' : ''; ?>>收入</option>
<option value="expense" <?php echo ($typeFilter ?? '') === 'expense' ? 'selected' : ''; ?>>支出</option>
</select>
</div>
<div class="form-group">
<label>起始日期:</label>
<input type="date" name="start_date" class="form-control input-sm" value="<?php echo htmlspecialchars($startDate ?? date('Y-m-01')); ?>">
</div>
<div class="form-group">
<label>截止日期:</label>
<input type="date" name="end_date" class="form-control input-sm" value="<?php echo htmlspecialchars($endDate ?? date('Y-m-d')); ?>">
</div>
<button type="submit" class="btn btn-info btn-sm"><i class="fa fa-search"></i> 查询</button>
</form>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>序号</th>
<th>凭证号</th>
<th>类型</th>
<th>类别</th>
<th>金额</th>
<th>日期</th>
<th>摘要</th>
<th>往来单位</th>
<th>支付方式</th>
<th>操作人</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($records)): ?>
<?php foreach ($records as $r): ?>
<tr>
<td><?php echo $r['id']; ?></td>
<td><?php echo htmlspecialchars($r['record_no']); ?></td>
<td>
<span class="label label-<?php echo $r['type'] === 'income' ? 'success' : 'danger'; ?>">
<?php echo $r['type'] === 'income' ? '收入' : '支出'; ?>
</span>
</td>
<td><?php echo htmlspecialchars($r['category']); ?></td>
<td>&yen;<?php echo number_format($r['amount'], 2); ?></td>
<td><?php echo htmlspecialchars($r['record_date']); ?></td>
<td><?php echo htmlspecialchars($r['description']); ?></td>
<td><?php echo htmlspecialchars($r['related_party']); ?></td>
<td><?php echo htmlspecialchars($r['payment_method']); ?></td>
<td><?php echo htmlspecialchars($r['operator']); ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Finance/accountsEdit?id=<?php echo $r['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Finance/accountsDelete" style="display:inline" onsubmit="return confirm('确定删除吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken ?? ''); ?>">
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="11" class="text-center text-muted">暂无账务记录</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+92
View File
@@ -0,0 +1,92 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加账务记录</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>凭证号</label>
<input type="text" name="record_no" class="form-control" value="CW<?php echo date('YmdHis'); ?>">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>类型 <span class="text-red">*</span></label>
<select name="type" class="form-control">
<option value="income">收入</option>
<option value="expense">支出</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>费用类别</label>
<input type="text" name="category" class="form-control" placeholder="如:原材料采购、办公费、销售回款">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>金额 <span class="text-red">*</span></label>
<input type="number" name="amount" class="form-control" value="0" step="0.01" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>日期</label>
<input type="date" name="record_date" class="form-control" value="<?php echo date('Y-m-d'); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>摘要 <span class="text-red">*</span></label>
<input type="text" name="description" class="form-control" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>往来单位</label>
<input type="text" name="related_party" class="form-control">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>支付方式</label>
<select name="payment_method" class="form-control">
<option value="">-- 请选择 --</option>
<option value="银行转账">银行转账</option>
<option value="现金">现金</option>
<option value="微信">微信</option>
<option value="支付宝">支付宝</option>
<option value="支票">支票</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/accounts" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+94
View File
@@ -0,0 +1,94 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑账务记录</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>凭证号</label>
<input type="text" name="record_no" class="form-control" value="<?php echo htmlspecialchars($record['record_no']); ?>">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>类型 <span class="text-red">*</span></label>
<select name="type" class="form-control">
<option value="income" <?php echo $record['type'] === 'income' ? 'selected' : ''; ?>>收入</option>
<option value="expense" <?php echo $record['type'] === 'expense' ? 'selected' : ''; ?>>支出</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>费用类别</label>
<input type="text" name="category" class="form-control" value="<?php echo htmlspecialchars($record['category']); ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>金额 <span class="text-red">*</span></label>
<input type="number" name="amount" class="form-control" value="<?php echo $record['amount']; ?>" step="0.01" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>日期</label>
<input type="date" name="record_date" class="form-control" value="<?php echo htmlspecialchars($record['record_date']); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>摘要 <span class="text-red">*</span></label>
<input type="text" name="description" class="form-control" value="<?php echo htmlspecialchars($record['description']); ?>" required>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>往来单位</label>
<input type="text" name="related_party" class="form-control" value="<?php echo htmlspecialchars($record['related_party']); ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>支付方式</label>
<select name="payment_method" class="form-control">
<option value="">-- 请选择 --</option>
<?php
$methods = ['银行转账','现金','微信','支付宝','支票'];
foreach ($methods as $m):
$sel = ($record['payment_method'] ?? '') === $m ? 'selected' : '';
?>
<option value="<?php echo $m; ?>" <?php echo $sel; ?>><?php echo $m; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"><?php echo htmlspecialchars($record['remark']); ?></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/accounts" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+90
View File
@@ -0,0 +1,90 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-green">
<div class="inner">
<h3>&yen;<?php echo number_format($incomeTotal, 2); ?></h3>
<p>本月收入</p>
</div>
<div class="icon"><i class="fa fa-arrow-up"></i></div>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-red">
<div class="inner">
<h3>&yen;<?php echo number_format($expenseTotal, 2); ?></h3>
<p>本月支出</p>
</div>
<div class="icon"><i class="fa fa-arrow-down"></i></div>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-<?php echo $profit >= 0 ? 'aqua' : 'yellow'; ?>">
<div class="inner">
<h3>&yen;<?php echo number_format($profit, 2); ?></h3>
<p>本月利润</p>
</div>
<div class="icon"><i class="fa fa-line-chart"></i></div>
</div>
</div>
<div class="col-lg-3 col-xs-6">
<div class="small-box bg-purple">
<div class="inner">
<h3><?php echo $recordCount ?? 0; ?></h3>
<p>账务记录</p>
</div>
<div class="icon"><i class="fa fa-book"></i></div>
<a href="<?php echo BASE_URL; ?>/Finance/accounts" class="small-box-footer">进入管理 <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-arrow-down"></i> 应收账款</h3>
</div>
<div class="box-body">
<h2 class="text-center text-info">&yen;<?php echo number_format($receivableTotal, 2); ?></h2>
<div class="text-center">
<a href="<?php echo BASE_URL; ?>/Finance/receivable" class="btn btn-info btn-sm">查看详情</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-arrow-up"></i> 应付账款</h3>
</div>
<div class="box-body">
<h2 class="text-center text-warning">&yen;<?php echo number_format($payableTotal, 2); ?></h2>
<div class="text-center">
<a href="<?php echo BASE_URL; ?>/Finance/payable" class="btn btn-warning btn-sm">查看详情</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-rocket"></i> 快捷操作</h3>
</div>
<div class="box-body">
<a href="<?php echo BASE_URL; ?>/Finance/accountsAdd" class="btn btn-primary"><i class="fa fa-plus"></i> 添加账务记录</a>
<a href="<?php echo BASE_URL; ?>/Finance/receivableAdd" class="btn btn-info"><i class="fa fa-arrow-down"></i> 添加应收账款</a>
<a href="<?php echo BASE_URL; ?>/Finance/payableAdd" class="btn btn-warning"><i class="fa fa-arrow-up"></i> 添加应付账款</a>
<a href="<?php echo BASE_URL; ?>/Finance/report" class="btn btn-success"><i class="fa fa-bar-chart"></i> 财务报表</a>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+73
View File
@@ -0,0 +1,73 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 应付账款列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Finance/payableAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加应付</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>序号</th>
<th>往来单位</th>
<th>应付金额</th>
<th>已付金额</th>
<th>未付金额</th>
<th>到期日期</th>
<th>状态</th>
<th>操作人</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($records)): ?>
<?php foreach ($records as $r): ?>
<?php
$unpaid = (float)$r['amount'] - (float)($r['paid_amount'] ?? 0);
?>
<tr>
<td><?php echo $r['id']; ?></td>
<td><?php echo htmlspecialchars($r['party_name']); ?></td>
<td>&yen;<?php echo number_format($r['amount'], 2); ?></td>
<td>&yen;<?php echo number_format($r['paid_amount'] ?? 0, 2); ?></td>
<td>
<span class="label label-<?php echo $unpaid > 0 ? 'warning' : 'success'; ?>">
&yen;<?php echo number_format($unpaid, 2); ?>
</span>
</td>
<td><?php echo htmlspecialchars($r['due_date']); ?></td>
<td>
<?php
$statusLabels = ['pending'=>'待付','partial'=>'部分付','paid'=>'已付','overdue'=>'逾期'];
$statusColors = ['pending'=>'warning','partial'=>'info','paid'=>'success','overdue'=>'danger'];
$st = $r['status'] ?? 'pending';
?>
<span class="label label-<?php echo $statusColors[$st] ?? 'default'; ?>">
<?php echo $statusLabels[$st] ?? $st; ?>
</span>
</td>
<td><?php echo htmlspecialchars($r['operator']); ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Finance/payableEdit?id=<?php echo $r['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Finance/payableDelete" style="display:inline" onsubmit="return confirm('确定删除吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken ?? ''); ?>">
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="9" class="text-center text-muted">暂无应付账款</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+70
View File
@@ -0,0 +1,70 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加应付账款</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>往来单位 <span class="text-red">*</span></label>
<input type="text" name="party_name" class="form-control" required>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>应付金额 <span class="text-red">*</span></label>
<input type="number" name="amount" class="form-control" value="0" step="0.01" required>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>已付金额</label>
<input type="number" name="paid_amount" class="form-control" value="0" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>到期日期</label>
<input type="date" name="due_date" class="form-control" value="<?php echo date('Y-m-d', strtotime('+30 days')); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<option value="pending">待付</option>
<option value="partial">部分付</option>
<option value="paid">已付</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>说明</label>
<input type="text" name="description" class="form-control">
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/payable" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+74
View File
@@ -0,0 +1,74 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑应付账款</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>往来单位 <span class="text-red">*</span></label>
<input type="text" name="party_name" class="form-control" value="<?php echo htmlspecialchars($record['party_name']); ?>" required>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>应付金额</label>
<input type="number" name="amount" class="form-control" value="<?php echo $record['amount']; ?>" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>已付金额</label>
<input type="number" name="paid_amount" class="form-control" value="<?php echo $record['paid_amount'] ?? 0; ?>" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>到期日期</label>
<input type="date" name="due_date" class="form-control" value="<?php echo htmlspecialchars($record['due_date']); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<?php
$statuses = ['pending'=>'待付','partial'=>'部分付','paid'=>'已付','overdue'=>'逾期'];
foreach ($statuses as $k => $v):
$sel = ($record['status'] ?? '') === $k ? 'selected' : '';
?>
<option value="<?php echo $k; ?>" <?php echo $sel; ?>><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>说明</label>
<input type="text" name="description" class="form-control" value="<?php echo htmlspecialchars($record['description']); ?>">
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"><?php echo htmlspecialchars($record['remark']); ?></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/payable" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+73
View File
@@ -0,0 +1,73 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 应收账款列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/Finance/receivableAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加应收</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>序号</th>
<th>往来单位</th>
<th>应收金额</th>
<th>已收金额</th>
<th>未收金额</th>
<th>到期日期</th>
<th>状态</th>
<th>操作人</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($records)): ?>
<?php foreach ($records as $r): ?>
<?php
$unpaid = (float)$r['amount'] - (float)($r['paid_amount'] ?? 0);
?>
<tr>
<td><?php echo $r['id']; ?></td>
<td><?php echo htmlspecialchars($r['party_name']); ?></td>
<td>&yen;<?php echo number_format($r['amount'], 2); ?></td>
<td>&yen;<?php echo number_format($r['paid_amount'] ?? 0, 2); ?></td>
<td>
<span class="label label-<?php echo $unpaid > 0 ? 'warning' : 'success'; ?>">
&yen;<?php echo number_format($unpaid, 2); ?>
</span>
</td>
<td><?php echo htmlspecialchars($r['due_date']); ?></td>
<td>
<?php
$statusLabels = ['pending'=>'待收','partial'=>'部分收','paid'=>'已收','overdue'=>'逾期'];
$statusColors = ['pending'=>'warning','partial'=>'info','paid'=>'success','overdue'=>'danger'];
$st = $r['status'] ?? 'pending';
?>
<span class="label label-<?php echo $statusColors[$st] ?? 'default'; ?>">
<?php echo $statusLabels[$st] ?? $st; ?>
</span>
</td>
<td><?php echo htmlspecialchars($r['operator']); ?></td>
<td>
<a href="<?php echo BASE_URL; ?>/Finance/receivableEdit?id=<?php echo $r['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/Finance/receivableDelete" style="display:inline" onsubmit="return confirm('确定删除吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken ?? ''); ?>">
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="9" class="text-center text-muted">暂无应收账款</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+70
View File
@@ -0,0 +1,70 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加应收账款</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>往来单位 <span class="text-red">*</span></label>
<input type="text" name="party_name" class="form-control" required>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>应收金额 <span class="text-red">*</span></label>
<input type="number" name="amount" class="form-control" value="0" step="0.01" required>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>已收金额</label>
<input type="number" name="paid_amount" class="form-control" value="0" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>到期日期</label>
<input type="date" name="due_date" class="form-control" value="<?php echo date('Y-m-d', strtotime('+30 days')); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<option value="pending">待收</option>
<option value="partial">部分收</option>
<option value="paid">已收</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>说明</label>
<input type="text" name="description" class="form-control">
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/receivable" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+74
View File
@@ -0,0 +1,74 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑应收账款</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="form-group">
<label>往来单位 <span class="text-red">*</span></label>
<input type="text" name="party_name" class="form-control" value="<?php echo htmlspecialchars($record['party_name']); ?>" required>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>应收金额</label>
<input type="number" name="amount" class="form-control" value="<?php echo $record['amount']; ?>" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>已收金额</label>
<input type="number" name="paid_amount" class="form-control" value="<?php echo $record['paid_amount'] ?? 0; ?>" step="0.01">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>到期日期</label>
<input type="date" name="due_date" class="form-control" value="<?php echo htmlspecialchars($record['due_date']); ?>">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>状态</label>
<select name="status" class="form-control">
<?php
$statuses = ['pending'=>'待收','partial'=>'部分收','paid'=>'已收','overdue'=>'逾期'];
foreach ($statuses as $k => $v):
$sel = ($record['status'] ?? '') === $k ? 'selected' : '';
?>
<option value="<?php echo $k; ?>" <?php echo $sel; ?>><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>说明</label>
<input type="text" name="description" class="form-control" value="<?php echo htmlspecialchars($record['description']); ?>">
</div>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"><?php echo htmlspecialchars($record['remark']); ?></textarea>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/Finance/receivable" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+79
View File
@@ -0,0 +1,79 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-bar-chart"></i> 财务报表</h3>
<div class="box-tools">
<form method="get" class="form-inline">
<div class="form-group">
<input type="month" name="month" class="form-control input-sm auto-submit" value="<?php echo htmlspecialchars($month); ?>">
</div>
</form>
</div>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-4">
<div class="info-box bg-green">
<span class="info-box-icon"><i class="fa fa-arrow-up"></i></span>
<div class="info-box-content">
<span class="info-box-text">收入合计</span>
<span class="info-box-number">&yen;<?php echo number_format($incomeTotal, 2); ?></span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="info-box bg-red">
<span class="info-box-icon"><i class="fa fa-arrow-down"></i></span>
<div class="info-box-content">
<span class="info-box-text">支出合计</span>
<span class="info-box-number">&yen;<?php echo number_format($expenseTotal, 2); ?></span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="info-box bg-<?php echo $profit >= 0 ? 'aqua' : 'yellow'; ?>">
<span class="info-box-icon"><i class="fa fa-line-chart"></i></span>
<div class="info-box-content">
<span class="info-box-text">利润</span>
<span class="info-box-number">&yen;<?php echo number_format($profit, 2); ?></span>
</div>
</div>
</div>
</div>
<?php if (!empty($categorySummary)): ?>
<h4>费用类别明细</h4>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>类别</th>
<th>收入</th>
<th>支出</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<?php foreach ($categorySummary as $cat => $vals): ?>
<tr>
<td><?php echo htmlspecialchars($cat); ?></td>
<td>&yen;<?php echo number_format($vals['income'], 2); ?></td>
<td>&yen;<?php echo number_format($vals['expense'], 2); ?></td>
<td>&yen;<?php echo number_format($vals['income'] - $vals['expense'], 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="text-center text-muted">该月份暂无财务数据</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+75
View File
@@ -0,0 +1,75 @@
<?php
$activeStation = '';
// 确保 sysConfig 可用
if (!isset($sysConfig)) {
$cfgModel = new app\models\SysConfig();
$sysConfig = $cfgModel->getConfig();
}
include APP_PATH . 'app/views/layouts/station_header.php';
?>
<div class="content">
<!-- 用户信息提示 -->
<div class="alert alert-info" style="margin-bottom:16px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<i class="fa fa-info-circle"></i>
<span>当前登录:<strong><?php echo htmlspecialchars($user['emp_name']); ?></strong></span>
<span style="color:var(--text-light);">| 角色:<?php echo htmlspecialchars(\core\base\Controller::ROLE_NAMES[$user['role']] ?? $user['role']); ?></span>
<span style="color:var(--text-light);">| 选择下方工位开始操作</span>
<span style="margin-left:auto;" class="column-adjuster" title="调整工位卡片列数">
<button type="button" class="col-btn" data-cols="1" title="1列"><i class="fa fa-align-justify"></i></button>
<button type="button" class="col-btn" data-cols="2" title="2列"><i class="fa fa-columns"></i>2</button>
<button type="button" class="col-btn" data-cols="3" title="3列"><i class="fa fa-columns"></i>3</button>
<button type="button" class="col-btn active" data-cols="auto" title="自适应"><i class="fa fa-arrows-h"></i> 自动</button>
</span>
</div>
<!-- 工位网格 -->
<div class="station-grid" id="stationGrid">
<?php
$stationIcons = [
'inbound' => 'fa-sign-in',
'pcb_test' => 'fa-microchip',
'battery' => 'fa-battery-full',
'assembly' => 'fa-cogs',
'finished_test' => 'fa-check-circle',
'warehouse' => 'fa-warehouse',
'delivery' => 'fa-truck',
'document_manage' => 'fa-folder-open',
];
$iconColors = [
'inbound' => ['#10ac84', 'rgba(16,172,132,0.2)'],
'pcb_test' => ['#0abde3', 'rgba(10,189,227,0.2)'],
'battery' => ['#feca57', 'rgba(254,202,87,0.2)'],
'assembly' => ['#a29bfe', 'rgba(162,155,254,0.2)'],
'finished_test' => ['#55efc4', 'rgba(85,239,196,0.2)'],
'warehouse' => ['#fd79a8', 'rgba(253,121,168,0.2)'],
'delivery' => ['#74b9ff', 'rgba(116,185,255,0.2)'],
'document_manage' => ['#e17055', 'rgba(225,112,85,0.2)'],
];
foreach ($stations as $station):
$type = $station['station_type'];
$icon = $stationIcons[$type] ?? 'fa-circle-o';
$color = $iconColors[$type] ?? ['#0abde3', 'rgba(10,189,227,0.2)'];
$disabled = $station['status'] == 0;
?>
<a href="<?php echo $disabled ? '#' : BASE_URL . '/Front/' . $type; ?>"
class="station-card"
style="<?php echo $disabled ? 'opacity:0.5; cursor:not-allowed;' : ''; ?>"
<?php echo $disabled ? 'onclick="return false;"' : ''; ?>>
<div class="station-icon" style="background:<?php echo $color[1]; ?>; color:<?php echo $color[0]; ?>;">
<i class="fa <?php echo $icon; ?>"></i>
</div>
<div class="station-name"><?php echo htmlspecialchars($station['station_name']); ?></div>
<div class="station-code"><?php echo htmlspecialchars($station['station_code']); ?></div>
<div class="station-status">
<span style="color:<?php echo $station['status'] == 1 ? 'var(--success)' : 'var(--danger)'; ?>">
<?php echo $station['status'] == 1 ? '● 启用' : '● 禁用'; ?>
</span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/station_footer.php'; ?>
+30
View File
@@ -0,0 +1,30 @@
<?php
if (!isset($sysConfig)) {
$cfgModel = new app\models\SysConfig();
$sysConfig = $cfgModel->getConfig();
}
include APP_PATH . 'app/views/layouts/station_header.php';
?>
<div class="content">
<div class="card" style="max-width:600px; margin:60px auto;">
<div class="card-body" style="text-align:center; padding:60px 20px;">
<i class="fa fa-lock" style="font-size:80px; color:var(--danger);"></i>
<h2 style="margin-top:20px; color:var(--danger);">抱歉,您没有访问该工位的权限</h2>
<p style="color:var(--text-light); font-size:16px; margin-top:12px;">
工位类型:<code><?php echo htmlspecialchars($stationType ?? '未知'); ?></code>
</p>
<?php if (!empty($message)): ?>
<p style="color:var(--text-light); font-size:16px;"><?php echo htmlspecialchars($message); ?></p>
<?php else: ?>
<p style="color:var(--text-light);">请联系管理员为您开通对应工位的访问权限。</p>
<?php endif; ?>
<br>
<a href="<?php echo BASE_URL; ?>/Front/index" class="btn btn-primary btn-lg">
<i class="fa fa-home"></i> 返回工位首页
</a>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/station_footer.php'; ?>
+350
View File
@@ -0,0 +1,350 @@
<?php
/**
* 文件管理工位视图
*
* 管理员:可上传、删除、打开文件
* 操作员:只能查看和打开文件
*
* 所需变量:
* $user - 当前用户
* $sysConfig - 系统配置
* $stations - 所有工位列表
* $documents - 文档列表
* $categories - 文档分类列表
* $csrfToken - CSRF token
* $isAdmin - 是否为管理员
*/
$activeStation = 'document_manage';
$title = '文件管理';
include APP_PATH . 'app/views/layouts/station_header.php';
// 权限判断
$isAdmin = ($user['role'] === \core\base\Controller::ROLE_SUPER_ADMIN
|| $user['role'] === \core\base\Controller::ROLE_ADMIN);
?>
<div class="content">
<a href="<?php echo BASE_URL; ?>/Front/index" class="back-link"><i class="fa fa-arrow-left"></i> 返回工位选择</a>
<div class="page-title-bar">
<div>
<h1><i class="fa fa-folder-open"></i> 文件管理</h1>
</div>
<div class="today-count-badge">
<i class="fa fa-user"></i> <?php echo htmlspecialchars($user['emp_name'] ?? ''); ?>
&nbsp;共:<span class="count-num" id="docCount"><?php echo count($documents); ?></span> 个文件
</div>
</div>
<!-- 搜索和筛选区 -->
<div class="card" style="margin-bottom:12px;">
<div class="card-body" style="padding:12px 16px;">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<input type="text" id="searchInput" class="form-control" placeholder="搜索文件名称..."
style="max-width:300px;">
<select id="categoryFilter" class="form-control" style="max-width:200px;">
<option value="">全部分类</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo htmlspecialchars($cat['category']); ?>">
<?php echo htmlspecialchars($cat['category']); ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($isAdmin): ?>
<button type="button" class="btn btn-primary" id="uploadBtn" style="margin-left:auto;">
<i class="fa fa-upload"></i> 上传文件
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php if ($isAdmin): ?>
<!-- 上传表单(默认隐藏) -->
<div class="card" id="uploadCard" style="display:none;margin-bottom:12px;">
<div class="card-header">
<h3><i class="fa fa-upload"></i> 上传文件</h3>
<button type="button" class="btn btn-sm btn-warning" id="cancelUploadBtn">
<i class="fa fa-times"></i> 取消
</button>
</div>
<div class="card-body">
<form id="uploadForm" method="post" enctype="multipart/form-data" action="<?php echo BASE_URL; ?>/Front/documentManageUpload">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="row" style="display:flex;flex-wrap:wrap;gap:12px;">
<div class="station-field-item" style="flex:1;min-width:200px;">
<label>文档标题 <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control" placeholder="输入文档标题" required>
</div>
<div class="station-field-item" style="flex:1;min-width:150px;">
<label>文档编号</label>
<input type="text" name="doc_no" class="form-control" placeholder="如:SOP-001">
</div>
<div class="station-field-item" style="flex:1;min-width:150px;">
<label>分类</label>
<input type="text" name="category" class="form-control" placeholder="如:操作规程" list="categoryList">
<datalist id="categoryList">
<?php foreach ($categories as $cat): ?>
<option value="<?php echo htmlspecialchars($cat['category']); ?>">
<?php endforeach; ?>
</datalist>
</div>
<div class="station-field-item" style="flex:1;min-width:100px;">
<label>版本</label>
<input type="text" name="version" class="form-control" value="1.0" placeholder="版本号">
</div>
</div>
<div class="row" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:12px;">
<div class="station-field-item" style="flex:1;min-width:200px;">
<label>选择文件 <span class="text-danger">*</span></label>
<input type="file" name="doc_file" class="form-control" required
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.zip,.rar,.jpg,.jpeg,.png,.gif">
</div>
<div class="station-field-item" style="flex:1;min-width:200px;">
<label>描述</label>
<input type="text" name="description" class="form-control" placeholder="简要描述">
</div>
</div>
<div style="margin-top:12px;">
<button type="submit" class="btn btn-success btn-lg">
<i class="fa fa-cloud-upload"></i> 确认上传
</button>
<span id="uploadStatus" style="margin-left:10px;color:var(--primary);display:none;">
<i class="fa fa-spinner fa-spin"></i> 上传中...
</span>
</div>
</form>
</div>
</div>
<?php endif; ?>
<!-- 文件列表 -->
<div class="card">
<div class="card-header">
<h3><i class="fa fa-list"></i> 文件列表</h3>
</div>
<div class="card-body no-padding">
<div class="table-wrap">
<table class="table" id="docTable">
<thead>
<tr>
<th data-col="title" style="text-align:left;padding-left:16px;">文件名称</th>
<th data-col="category">分类</th>
<th data-col="version">版本</th>
<th data-col="file_size">大小</th>
<th data-col="operator">上传人</th>
<th data-col="created_at">上传时间</th>
<th data-col="actions">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($documents)): ?>
<?php foreach ($documents as $doc):
$sizeStr = '';
$fs = (int)($doc['file_size'] ?? 0);
if ($fs > 1048576) $sizeStr = round($fs / 1048576, 1) . ' MB';
elseif ($fs > 1024) $sizeStr = round($fs / 1024, 1) . ' KB';
else $sizeStr = $fs . ' B';
$fileExt = strtolower($doc['file_type'] ?? '');
$fileIcon = 'fa-file-o';
if (in_array($fileExt, ['pdf'])) $fileIcon = 'fa-file-pdf-o';
elseif (in_array($fileExt, ['doc','docx'])) $fileIcon = 'fa-file-word-o';
elseif (in_array($fileExt, ['xls','xlsx','csv'])) $fileIcon = 'fa-file-excel-o';
elseif (in_array($fileExt, ['ppt','pptx'])) $fileIcon = 'fa-file-powerpoint-o';
elseif (in_array($fileExt, ['jpg','jpeg','png','gif'])) $fileIcon = 'fa-file-image-o';
elseif (in_array($fileExt, ['zip','rar'])) $fileIcon = 'fa-file-archive-o';
elseif (in_array($fileExt, ['txt'])) $fileIcon = 'fa-file-text-o';
?>
<tr class="doc-row" data-title="<?php echo htmlspecialchars(mb_strtolower($doc['title'] ?? '')); ?>"
data-category="<?php echo htmlspecialchars($doc['category'] ?? ''); ?>">
<td data-col="title" style="text-align:left;padding-left:16px;">
<i class="fa <?php echo $fileIcon; ?>" style="color:var(--primary);margin-right:6px;"></i>
<strong><?php echo htmlspecialchars($doc['title']); ?></strong>
<?php if (!empty($doc['description'])): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_substr($doc['description'], 0, 60)); ?><?php echo mb_strlen($doc['description']) > 60 ? '...' : ''; ?></small>
<?php endif; ?>
</td>
<td data-col="category">
<?php if (!empty($doc['category'])): ?>
<span class="label label-info"><?php echo htmlspecialchars($doc['category']); ?></span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td data-col="version"><?php echo htmlspecialchars($doc['version'] ?? '1.0'); ?></td>
<td data-col="file_size"><?php echo $sizeStr; ?></td>
<td data-col="operator"><?php echo htmlspecialchars($doc['operator'] ?? '-'); ?></td>
<td data-col="created_at"><?php echo htmlspecialchars($doc['created_at'] ?? '-'); ?></td>
<td data-col="actions">
<?php if (!empty($doc['file_path'])): ?>
<a href="<?php echo BASE_URL . htmlspecialchars($doc['file_path']); ?>"
class="btn btn-xs btn-info" target="_blank" title="打开文件">
<i class="fa fa-external-link"></i> 打开
</a>
<a href="<?php echo BASE_URL . htmlspecialchars($doc['file_path']); ?>"
class="btn btn-xs btn-info" download title="下载文件">
<i class="fa fa-download"></i> 下载
</a>
<?php else: ?>
<span class="text-muted">无文件</span>
<?php endif; ?>
<?php if ($isAdmin): ?>
<button type="button" class="btn btn-xs btn-danger doc-del-btn"
data-id="<?php echo $doc['id']; ?>" data-title="<?php echo htmlspecialchars($doc['title'], ENT_QUOTES); ?>"
title="删除文件">
<i class="fa fa-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="7">
<div class="empty-state">
<i class="fa fa-folder-open-o"></i>
<p>暂无文件,<?php echo $isAdmin ? '请点击「上传文件」添加' : '请联系管理员上传'; ?></p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script nonce="<?php echo csp_nonce(); ?>">
var BASE_URL = '<?php echo BASE_URL; ?>';
// ========== 搜索和筛选 ==========
function filterDocuments() {
var searchText = ($('#searchInput').val() || '').toLowerCase();
var category = $('#categoryFilter').val() || '';
var visibleCount = 0;
$('#docTable .doc-row').each(function() {
var title = $(this).data('title') || '';
var rowCat = $(this).data('category') || '';
var match = true;
if (searchText && title.indexOf(searchText) < 0) match = false;
if (category && rowCat !== category) match = false;
if (match) {
$(this).show();
visibleCount++;
} else {
$(this).hide();
}
});
$('#docCount').text(visibleCount);
}
// CSP:搜索/筛选事件绑定(替代内联 oninput/onchange
$('#searchInput').on('input', filterDocuments);
$('#categoryFilter').on('change', filterDocuments);
<?php if ($isAdmin): ?>
// ========== 上传表单 ==========
function showUploadForm() {
$('#uploadCard').slideDown(300);
}
function hideUploadForm() {
$('#uploadCard').slideUp(300);
}
function updateFileName(input) {
// 如果标题为空,自动用文件名填充
var fileName = input.files[0] ? input.files[0].name.replace(/\.[^/.]+$/, '') : '';
if (fileName && !$('#uploadForm input[name="title"]').val()) {
$('#uploadForm input[name="title"]').val(fileName);
}
}
// 上传表单提交
$('#uploadForm').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var title = $form.find('input[name="title"]').val().trim();
if (!title) {
alert('请输入文档标题');
return;
}
if (!$form.find('input[name="doc_file"]').val()) {
alert('请选择要上传的文件');
return;
}
$('#uploadStatus').show();
var formData = new FormData($form[0]);
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
success: function(res) {
$('#uploadStatus').hide();
if (res.success) {
alert('文件上传成功!');
location.reload();
} else {
alert('上传失败: ' + (res.error || '未知错误'));
}
},
error: function(xhr) {
$('#uploadStatus').hide();
var msg = '网络错误,请重试';
try {
var r = JSON.parse(xhr.responseText);
if (r && r.error) msg = r.error;
else if (xhr.responseText) msg = xhr.responseText;
} catch (e) {
if (xhr.responseText) msg = xhr.responseText;
}
alert(msg);
}
});
});
// ========== 删除文件 ==========
function deleteDocument(id, title) {
if (!confirm('确定删除文件「' + title + '」吗?\n此操作不可撤销。')) return;
$.ajax({
url: BASE_URL + '/Front/documentManageDelete',
type: 'POST',
data: {
id: id,
csrf_token: '<?php echo htmlspecialchars($csrfToken); ?>'
},
dataType: 'json',
success: function(res) {
if (res.success) {
alert('文件已删除');
location.reload();
} else {
alert('删除失败: ' + (res.error || '未知错误'));
}
},
error: function() {
alert('网络错误,请重试');
}
});
}
// CSP:上传/删除相关事件绑定(替代内联 onclick/onchange
$('#uploadBtn').on('click', showUploadForm);
$('#cancelUploadBtn').on('click', hideUploadForm);
$('#uploadForm input[name="doc_file"]').on('change', function () { updateFileName(this); });
$('#docTable').on('click', '.doc-del-btn', function () {
deleteDocument($(this).attr('data-id'), $(this).attr('data-title'));
});
<?php endif; ?>
</script>
<?php include APP_PATH . 'app/views/layouts/station_footer.php'; ?>
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* 通用工位操作视图(唯一工位视图文件)
*
* 所有工位(内置和动态添加)都通过此视图渲染。
*
* 所需变量(由 FrontController::handleStation 传入):
* $stationType - 工位类型标识
* $currentStation - 当前工位的数据库记录
* $bizConfig - 业务配置(内置工位有,动态工位为 null)
* $fieldConfigs - 字段配置数组
* $models - 产品型号列表
* $types - 产品类型列表
* $customers - 客户列表
* $records - 操作记录
* $filterType - 筛选类型
* $filterModel - 筛选型号
* $todayCount - 今日计数
* $shellColors - 外壳颜色(assembly 工位)
* $user - 当前用户
* $sysConfig - 系统配置
* $stations - 所有工位列表
*/
require_once APP_PATH . 'app/helpers/station_template_helper.php';
// 获取工位名称
$stationName = $currentStation['station_name'] ?? $stationType;
$stationCode = $currentStation['station_code'] ?? $stationType;
// ========== 内置工位:使用 bizConfig ==========
if ($bizConfig) {
// 解析记录(内置工位使用专用表,记录结构已完整,无需 JSON 解析)
$parsedRecords = $records;
// 构建 stationConfig(从 bizConfig.view 继承)
$stationConfig = array_merge([
'stationKey' => $stationType,
'pageTitle' => $stationName,
'pageIcon' => 'cube',
'cardTitle' => '快速录入',
'tableTitle' => '操作记录',
'dataSources' => [
'types' => $types ?? [],
'models' => $models ?? [],
'customers' => $customers ?? [],
],
'submitMode' => 'auto',
'codeFields' => ['serial_no'],
'labelFields' => [],
'emptyText' => '暂无记录,请在扫描框中录入第一条数据',
'reloadAfterSubmit' => true,
'enableFilter' => false,
], $bizConfig['view'] ?? []);
// 注入全局变量
$GLOBALS['records'] = $parsedRecords;
$GLOBALS['fieldConfigs'] = $fieldConfigs;
$GLOBALS['types'] = $types ?? [];
$GLOBALS['models'] = $models ?? [];
$GLOBALS['customers'] = $customers ?? [];
$GLOBALS['user'] = $user;
$GLOBALS['sysConfig'] = $sysConfig;
$GLOBALS['stationType'] = $stationType;
$GLOBALS['bizConfig'] = $bizConfig;
$GLOBALS['filterType'] = $filterType ?? '';
$GLOBALS['filterModel'] = $filterModel ?? '';
$GLOBALS['todayCount'] = $todayCount ?? 0;
$GLOBALS['totalCount'] = $totalCount ?? 0;
$GLOBALS['shellColors'] = $shellColors ?? [];
$GLOBALS['csrfToken'] = $csrfToken;
$GLOBALS['stationError'] = $stationError ?? '';
renderStationTemplate($stationConfig);
} else {
// ========== 动态工位:使用 station_records 表(JSON 存储) ==========
// 解析记录中的 JSON 数据
$parsedRecords = [];
if (!empty($records)) {
foreach ($records as $rec) {
$row = ['id' => $rec['id'], 'operator' => $rec['operator'], 'created_at' => $rec['created_at']];
$jsonData = json_decode($rec['record_data'], true);
if (is_array($jsonData)) {
$row = array_merge($row, $jsonData);
}
$parsedRecords[] = $row;
}
}
// 配置工位模板
$stationConfig = [
'stationKey' => $stationType,
'pageTitle' => $stationName,
'pageIcon' => 'cube',
'cardTitle' => '快速录入',
'tableTitle' => '操作记录',
'dataSources' => [
'types' => $types ?? [],
'models' => $models ?? [],
'customers' => $customers ?? [],
],
'submitMode' => 'auto', // 扫描回车直接提交
'codeFields' => ['serial_no'], // 序列号类字段 code 样式
'labelFields' => [],
'emptyText' => '暂无记录,请在扫描框中录入第一条数据',
'reloadAfterSubmit' => true, // 提交后刷新页面
'enableFilter' => false, // 不启用联动筛选
];
// 将解析后的记录和字段配置注入全局,供 renderStationTemplate 使用
$GLOBALS['records'] = $parsedRecords;
$GLOBALS['fieldConfigs'] = $fieldConfigs;
$GLOBALS['types'] = $types ?? [];
$GLOBALS['models'] = $models ?? [];
$GLOBALS['customers'] = $customers ?? [];
$GLOBALS['user'] = $user;
$GLOBALS['sysConfig'] = $sysConfig;
$GLOBALS['stationType'] = $stationType;
$GLOBALS['bizConfig'] = null;
$GLOBALS['filterType'] = '';
$GLOBALS['filterModel'] = '';
$GLOBALS['todayCount'] = 0;
$GLOBALS['totalCount'] = $totalCount ?? count($parsedRecords);
$GLOBALS['shellColors'] = [];
$GLOBALS['csrfToken'] = $csrfToken;
$GLOBALS['stationError'] = '';
renderStationTemplate($stationConfig);
}
+70
View File
@@ -0,0 +1,70 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-list"></i> 资产列表</h3>
<div class="box-tools">
<a href="<?php echo BASE_URL; ?>/General/assetAdd" class="btn btn-primary btn-sm"><i class="fa fa-plus"></i> 添加资产</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>序号</th>
<th>资产编号</th>
<th>资产名称</th>
<th>类别</th>
<th>规格型号</th>
<th>数量</th>
<th>购置价格</th>
<th>使用部门</th>
<th>使用人</th>
<th>状态</th>
<th style="width:150px">操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($assets)): ?>
<?php foreach ($assets as $a): ?>
<tr>
<td><?php echo $a['id']; ?></td>
<td><?php echo htmlspecialchars($a['asset_no']); ?></td>
<td><?php echo htmlspecialchars($a['name']); ?></td>
<td><?php echo htmlspecialchars($a['category']); ?></td>
<td><?php echo htmlspecialchars($a['model']); ?></td>
<td><?php echo $a['quantity']; ?></td>
<td>&yen;<?php echo number_format($a['purchase_price'], 2); ?></td>
<td><?php echo htmlspecialchars($a['department']); ?></td>
<td><?php echo htmlspecialchars($a['user_name']); ?></td>
<td>
<?php
$statusLabels = ['normal'=>'正常','scrapped'=>'报废','repairing'=>'维修中','borrowed'=>'借用中'];
$statusColors = ['normal'=>'success','scrapped'=>'default','repairing'=>'warning','borrowed'=>'info'];
$st = $a['status'] ?? 'normal';
?>
<span class="label label-<?php echo $statusColors[$st] ?? 'default'; ?>">
<?php echo $statusLabels[$st] ?? $st; ?>
</span>
</td>
<td>
<a href="<?php echo BASE_URL; ?>/General/assetEdit?id=<?php echo $a['id']; ?>" class="btn btn-xs btn-info"><i class="fa fa-edit"></i> 编辑</a>
<form method="post" action="<?php echo BASE_URL; ?>/General/assetDelete" style="display:inline" onsubmit="return confirm('确定删除该资产吗?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken ?? ''); ?>">
<input type="hidden" name="id" value="<?php echo $a['id']; ?>">
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-trash"></i> 删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="11" class="text-center text-muted">暂无资产数据</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+77
View File
@@ -0,0 +1,77 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-plus"></i> 添加资产</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>资产编号</label><input type="text" name="asset_no" class="form-control"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>资产名称 <span class="text-red">*</span></label><input type="text" name="name" class="form-control" required></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>类别</label><input type="text" name="category" class="form-control" placeholder="如:电子设备、办公家具"></div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group"><label>规格型号</label><input type="text" name="model" class="form-control"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>数量</label><input type="number" name="quantity" class="form-control" value="1"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>单位</label><input type="text" name="unit" class="form-control" value="台"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>购置价格</label><input type="number" name="purchase_price" class="form-control" value="0" step="0.01"></div>
</div>
<div class="col-md-3">
<div class="form-group"><label>购置日期</label><input type="date" name="purchase_date" class="form-control" value="<?php echo date('Y-m-d'); ?>"></div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>供应商</label><input type="text" name="supplier" class="form-control"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>使用部门</label><input type="text" name="department" class="form-control"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>使用人</label><input type="text" name="user_name" class="form-control"></div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>存放位置</label><input type="text" name="location" class="form-control"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>状态</label>
<select name="status" class="form-control"><option value="normal">正常</option><option value="repairing">维修中</option><option value="borrowed">借用中</option></select>
</div>
</div>
<div class="col-md-4">
<div class="form-group"><label>保修到期</label><input type="date" name="warranty_expire" class="form-control"></div>
</div>
</div>
<div class="form-group"><label>备注</label><textarea name="remark" class="form-control" rows="2"></textarea></div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/General/asset" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
+85
View File
@@ -0,0 +1,85 @@
<?php
include APP_PATH . 'app/views/layouts/admin_header.php';
?>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-edit"></i> 编辑资产</h3>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="box-body">
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>资产编号</label><input type="text" name="asset_no" class="form-control" value="<?php echo htmlspecialchars($record['asset_no']); ?>"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>资产名称 <span class="text-red">*</span></label><input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($record['name']); ?>" required></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>类别</label><input type="text" name="category" class="form-control" value="<?php echo htmlspecialchars($record['category']); ?>"></div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group"><label>规格型号</label><input type="text" name="model" class="form-control" value="<?php echo htmlspecialchars($record['model']); ?>"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>数量</label><input type="number" name="quantity" class="form-control" value="<?php echo $record['quantity']; ?>"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>单位</label><input type="text" name="unit" class="form-control" value="<?php echo htmlspecialchars($record['unit']); ?>"></div>
</div>
<div class="col-md-2">
<div class="form-group"><label>购置价格</label><input type="number" name="purchase_price" class="form-control" value="<?php echo $record['purchase_price']; ?>" step="0.01"></div>
</div>
<div class="col-md-3">
<div class="form-group"><label>购置日期</label><input type="date" name="purchase_date" class="form-control" value="<?php echo htmlspecialchars($record['purchase_date']); ?>"></div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>供应商</label><input type="text" name="supplier" class="form-control" value="<?php echo htmlspecialchars($record['supplier']); ?>"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>使用部门</label><input type="text" name="department" class="form-control" value="<?php echo htmlspecialchars($record['department']); ?>"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>使用人</label><input type="text" name="user_name" class="form-control" value="<?php echo htmlspecialchars($record['user_name']); ?>"></div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group"><label>存放位置</label><input type="text" name="location" class="form-control" value="<?php echo htmlspecialchars($record['location']); ?>"></div>
</div>
<div class="col-md-4">
<div class="form-group"><label>状态</label>
<select name="status" class="form-control">
<?php
$statuses = ['normal'=>'正常','scrapped'=>'报废','repairing'=>'维修中','borrowed'=>'借用中'];
foreach ($statuses as $k => $v):
$sel = ($record['status'] ?? '') === $k ? 'selected' : '';
?>
<option value="<?php echo $k; ?>" <?php echo $sel; ?>><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group"><label>保修到期</label><input type="date" name="warranty_expire" class="form-control" value="<?php echo htmlspecialchars($record['warranty_expire']); ?>"></div>
</div>
</div>
<div class="form-group"><label>备注</label><textarea name="remark" class="form-control" rows="2"><?php echo htmlspecialchars($record['remark']); ?></textarea></div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
<a href="<?php echo BASE_URL; ?>/General/asset" class="btn btn-default">返回</a>
</div>
</form>
</div>
</div>
</div>
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>

Some files were not shown because too many files have changed in this diff Show More