初始化
This commit is contained in:
@@ -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
@@ -1,13 +1,40 @@
|
||||
# ---> AL
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
# 环境配置(含数据库密码)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
*.app
|
||||
.snapshots/*
|
||||
# 数据库备份
|
||||
backups/
|
||||
*.sql
|
||||
!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/
|
||||
|
||||
@@ -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-P2:CSP 已迁移至 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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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、隐私模式等)
|
||||
// 导致每次请求创建新 Session,CSRF 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
@@ -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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// ===== 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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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_columns(keyed 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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() 返回 bool,rowCount() 需要通过 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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
| 测试记录总数:<b><?php echo intval($result['counts']['rec_cnt'] ?? 0); ?></b>
|
||||
| 测试数据总数:<b><?php echo intval($result['counts']['data_cnt'] ?? 0); ?></b>
|
||||
| 设备总数:<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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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">×</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">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ==================== 删除设备数据 ====================
|
||||
$('#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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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; ?>">«</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; ?>">»</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include APP_PATH . 'app/views/layouts/admin_footer.php'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<footer class="main-footer">
|
||||
<div class="pull-right hidden-xs"><b>Version</b> 1.0.0</div>
|
||||
<strong>Copyright © 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>
|
||||
@@ -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>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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'; ?>
|
||||
|
||||
@@ -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(' → ', $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">×</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="移除">×</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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// ===== 轻量 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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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">
|
||||
©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="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 24 24"><path d="M12 2l8 3v6c0 5-4 9-8 11-4-2-8-6-8-11V5l8-3z" fill="#1a3a8f" stroke="#d4af37" stroke-width="1"/><polygon points="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" fill="#ffffff"/></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>
|
||||
@@ -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>¥<?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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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>¥<?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>¥<?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>¥<?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">¥<?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">¥<?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'; ?>
|
||||
@@ -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>¥<?php echo number_format($r['amount'], 2); ?></td>
|
||||
<td>¥<?php echo number_format($r['paid_amount'] ?? 0, 2); ?></td>
|
||||
<td>
|
||||
<span class="label label-<?php echo $unpaid > 0 ? 'warning' : 'success'; ?>">
|
||||
¥<?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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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>¥<?php echo number_format($r['amount'], 2); ?></td>
|
||||
<td>¥<?php echo number_format($r['paid_amount'] ?? 0, 2); ?></td>
|
||||
<td>
|
||||
<span class="label label-<?php echo $unpaid > 0 ? 'warning' : 'success'; ?>">
|
||||
¥<?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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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">¥<?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">¥<?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">¥<?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>¥<?php echo number_format($vals['income'], 2); ?></td>
|
||||
<td>¥<?php echo number_format($vals['expense'], 2); ?></td>
|
||||
<td>¥<?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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'] ?? ''); ?>
|
||||
共:<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'; ?>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>¥<?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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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
Reference in New Issue
Block a user