m0_70156489 10 時間 前
コミット
2b3e886f59

+ 68 - 10
application/admin/controller/Index.php

@@ -2,6 +2,7 @@
 
 
 namespace app\admin\controller;
 namespace app\admin\controller;
 
 
+use app\admin\library\SystemLogin;
 use app\admin\model\AdminLog;
 use app\admin\model\AdminLog;
 use app\common\controller\Backend;
 use app\common\controller\Backend;
 use think\Config;
 use think\Config;
@@ -16,7 +17,7 @@ use think\Validate;
 class Index extends Backend
 class Index extends Backend
 {
 {
 
 
-    protected $noNeedLogin = ['login'];
+    protected $noNeedLogin = ['login', 'cockpitlogin'];
     protected $noNeedRight = ['index', 'logout'];
     protected $noNeedRight = ['index', 'logout'];
     protected $layout = '';
     protected $layout = '';
 
 
@@ -36,9 +37,17 @@ class Index extends Backend
         foreach ($cookieArr as $key => $regex) {
         foreach ($cookieArr as $key => $regex) {
             $cookieValue = $this->request->cookie($key);
             $cookieValue = $this->request->cookie($key);
             if (!is_null($cookieValue) && preg_match($regex, $cookieValue)) {
             if (!is_null($cookieValue) && preg_match($regex, $cookieValue)) {
+                // 皮肤由系统风格强制,忽略 cookie 覆盖
+                if ($key === 'adminskin') {
+                    continue;
+                }
                 config('fastadmin.' . $key, $cookieValue);
                 config('fastadmin.' . $key, $cookieValue);
             }
             }
         }
         }
+        // 按登录系统再次套用风格(覆盖 cookie)
+        $theme = SystemLogin::applyTheme($this->auth);
+        $this->view->assign($theme);
+
         //左侧菜单
         //左侧菜单
         list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
         list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
             'dashboard' => 'hot',
             'dashboard' => 'hot',
@@ -61,15 +70,48 @@ class Index extends Backend
     }
     }
 
 
     /**
     /**
-     * 管理员登录
+     * 生产协同系统登录(login.html)— 仅角色组根 10 树可登录
      */
      */
     public function login()
     public function login()
     {
     {
+        return $this->doSystemLogin('collab', 'login');
+    }
+
+    /**
+     * 生产经营驾驶舱登录(生产经营驾驶舱系统.html)— 仅角色组根 2 树可登录
+     */
+    public function cockpitlogin()
+    {
+        return $this->doSystemLogin('cockpit', '生产经营驾驶舱系统');
+    }
+
+    /**
+     * 按登录页入口锁定系统:页面是哪个系统,就只允许该系统账号
+     *
+     * @param string $systemKey collab|cockpit
+     * @param string $viewName  模板名
+     */
+    protected function doSystemLogin(string $systemKey, string $viewName)
+    {
+        SystemLogin::loadConfig();
+        $systemKey = SystemLogin::normalizeKey($systemKey);
         $url = $this->request->get('url', '', 'url_clean');
         $url = $this->request->get('url', '', 'url_clean');
         $url = $url ?: 'index/index';
         $url = $url ?: 'index/index';
+        $sysName = (string)SystemLogin::option($systemKey, 'name', '');
+        if ($sysName === '') {
+            $sysName = $systemKey === 'cockpit' ? '生产经营驾驶舱系统' : '生产协同系统';
+        }
+
         if ($this->auth->isLogin()) {
         if ($this->auth->isLogin()) {
-            $this->success(__("You've logged in, do not login again"), $url);
+            if (!SystemLogin::belongsTo($this->auth, (int)$this->auth->id, $systemKey)) {
+                $this->auth->logout();
+                Session::delete('login_system');
+            } else {
+                Session::set('login_system', $systemKey);
+                $this->success(__("You've logged in, do not login again"), $url);
+            }
         }
         }
+
         //保持会话有效时长,单位:小时(与 config fastadmin.keeplogin_hours 一致)
         //保持会话有效时长,单位:小时(与 config fastadmin.keeplogin_hours 一致)
         $keeyloginhours = (int)Config::get('fastadmin.keeplogin_hours');
         $keeyloginhours = (int)Config::get('fastadmin.keeplogin_hours');
         if ($keeyloginhours <= 0) {
         if ($keeyloginhours <= 0) {
@@ -98,11 +140,18 @@ class Index extends Backend
             if (!$result) {
             if (!$result) {
                 $this->error($validate->getError(), $url, ['token' => $this->request->token()]);
                 $this->error($validate->getError(), $url, ['token' => $this->request->token()]);
             }
             }
-            AdminLog::setTitle(__('Login'));
+            AdminLog::setTitle(__('Login') . '-' . $sysName);
             // 默认保持登录 72 小时(登录页无「记住我」勾选时也生效,避免频繁掉线)
             // 默认保持登录 72 小时(登录页无「记住我」勾选时也生效,避免频繁掉线)
             $keeptime = $keeyloginhours * 3600;
             $keeptime = $keeyloginhours * 3600;
             $result = $this->auth->login($username, $password, $keeptime);
             $result = $this->auth->login($username, $password, $keeptime);
             if ($result === true) {
             if ($result === true) {
+                // 以当前登录页对应系统为准(与页面标题一致),不属于则拒绝
+                if (!SystemLogin::belongsTo($this->auth, (int)$this->auth->id, $systemKey)) {
+                    $this->auth->logout();
+                    Session::delete('login_system');
+                    $this->error('登录账号不存在', $url, ['token' => $this->request->token()]);
+                }
+                Session::set('login_system', $systemKey);
                 Hook::listen("admin_login_after", $this->request);
                 Hook::listen("admin_login_after", $this->request);
                 $this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => resolve_admin_avatar($this->auth->avatar)]);
                 $this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => resolve_admin_avatar($this->auth->avatar)]);
             } else {
             } else {
@@ -114,27 +163,36 @@ class Index extends Backend
 
 
         // 根据客户端的cookie,判断是否可以自动登录
         // 根据客户端的cookie,判断是否可以自动登录
         if ($this->auth->autologin()) {
         if ($this->auth->autologin()) {
-            Session::delete("referer");
-            $this->redirect($url);
+            if (!SystemLogin::belongsTo($this->auth, (int)$this->auth->id, $systemKey)) {
+                $this->auth->logout();
+                Session::delete('login_system');
+            } else {
+                Session::set('login_system', $systemKey);
+                Session::delete("referer");
+                $this->redirect($url);
+            }
         }
         }
         $background = Config::get('fastadmin.login_background');
         $background = Config::get('fastadmin.login_background');
         $background = $background ? (stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background) : '';
         $background = $background ? (stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background) : '';
         $this->view->assign('keeyloginhours', $keeyloginhours);
         $this->view->assign('keeyloginhours', $keeyloginhours);
         $this->view->assign('background', $background);
         $this->view->assign('background', $background);
-        $this->view->assign('title', __('Login'));
+        $this->view->assign('login_system', $systemKey);
+        $this->view->assign('title', $sysName . ' - ' . __('Login'));
         Hook::listen("admin_login_init", $this->request);
         Hook::listen("admin_login_init", $this->request);
-        return $this->view->fetch();
+        return $this->view->fetch($viewName);
     }
     }
 
 
     /**
     /**
-     * 退出登录
+     * 退出登录:回到对应系统登录页
      */
      */
     public function logout()
     public function logout()
     {
     {
+        $system = Session::get('login_system') ?: 'collab';
         if ($this->request->isPost()) {
         if ($this->request->isPost()) {
             $this->auth->logout();
             $this->auth->logout();
+            Session::delete('login_system');
             Hook::listen("admin_logout_after", $this->request);
             Hook::listen("admin_logout_after", $this->request);
-            $this->redirect('index/login');
+            $this->redirect($system === 'cockpit' ? 'index/cockpitlogin' : 'index/login');
         }
         }
         $html = "<form id='logout_submit' name='logout_submit' action='' method='post'>" . token() . "<input type='submit' value='ok' style='display:none;'></form>";
         $html = "<form id='logout_submit' name='logout_submit' action='' method='post'>" . token() . "<input type='submit' value='ok' style='display:none;'></form>";
         $html .= "<script>document.forms['logout_submit'].submit();</script>";
         $html .= "<script>document.forms['logout_submit'].submit();</script>";

+ 22 - 2
application/admin/controller/Procuremen.php

@@ -1911,6 +1911,19 @@ class Procuremen extends Backend
         return time() >= $ts;
         return time() >= $ts;
     }
     }
 
 
+    /**
+     * 开标前校验:必须已过报价截止时间
+     *
+     * @param array{ccydh?:string,pos?:array,merge_rows?:array} $bundle
+     */
+    protected function assertProcuremenBidOpenAfterDeadline(array $bundle): void
+    {
+        $sysRq = $this->resolveBundleSysRq($bundle);
+        if (!$this->isProcuremenQuoteDeadlineReached($sysRq)) {
+            $this->error('未到截止时间');
+        }
+    }
+
     /**
     /**
      * 截止时间前:隐藏单价/交期真实值,统一展示文案
      * 截止时间前:隐藏单价/交期真实值,统一展示文案
      *
      *
@@ -7037,9 +7050,13 @@ class Procuremen extends Backend
         $this->view->assign('quoteGroups', $this->maskSupplierContactList($quoteGroups));
         $this->view->assign('quoteGroups', $this->maskSupplierContactList($quoteGroups));
         $this->view->assign('quoteGroupsJson', json_encode($this->maskSupplierContactList($quoteGroups), JSON_UNESCAPED_UNICODE));
         $this->view->assign('quoteGroupsJson', json_encode($this->maskSupplierContactList($quoteGroups), JSON_UNESCAPED_UNICODE));
         $this->view->assign('pickedCompanyName', $pickedName);
         $this->view->assign('pickedCompanyName', $pickedName);
-        $this->view->assign('sysRq', $this->formatProcuremenSysRqForInput($po['sys_rq'] ?? null));
+        $sysRqForView = $this->formatProcuremenSysRqForInput($po['sys_rq'] ?? null);
+        if ($sysRqForView === '') {
+            $sysRqForView = $this->formatProcuremenSysRqForInput($this->resolveBundleSysRq($bundle));
+        }
+        $this->view->assign('sysRq', $sysRqForView);
         $this->view->assign('bidOpenVerified', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
         $this->view->assign('bidOpenVerified', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
-        $this->view->assign('quoteDeadlineReached', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
+        $this->view->assign('quoteDeadlineReached', $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0);
 
 
         return $this->view->fetch('procuremen/audit_issue');
         return $this->view->fetch('procuremen/audit_issue');
     }
     }
@@ -7061,6 +7078,7 @@ class Procuremen extends Backend
                     'users'    => [],
                     'users'    => [],
                 ]);
                 ]);
             }
             }
+            $this->assertProcuremenBidOpenAfterDeadline($bundle);
         }
         }
         $this->success('', null, [
         $this->success('', null, [
             'verified' => 0,
             'verified' => 0,
@@ -7091,6 +7109,7 @@ class Procuremen extends Backend
         if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
         if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
             $this->success('已完成开标验证,无需重复发送');
             $this->success('已完成开标验证,无需重复发送');
         }
         }
+        $this->assertProcuremenBidOpenAfterDeadline($bundle);
         $ccydh = trim((string)($bundle['ccydh'] ?? ''));
         $ccydh = trim((string)($bundle['ccydh'] ?? ''));
         if ($ccydh === '') {
         if ($ccydh === '') {
             $this->error('订单号无效');
             $this->error('订单号无效');
@@ -7158,6 +7177,7 @@ class Procuremen extends Backend
         if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
         if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
             $this->success('已完成开标验证');
             $this->success('已完成开标验证');
         }
         }
+        $this->assertProcuremenBidOpenAfterDeadline($bundle);
         $verifier1 = $this->loadBidOpenAdminUser($verifier1Id);
         $verifier1 = $this->loadBidOpenAdminUser($verifier1Id);
         $verifier2 = $this->loadBidOpenAdminUser($verifier2Id);
         $verifier2 = $this->loadBidOpenAdminUser($verifier2Id);
         if ($verifier1 === null || $verifier2 === null) {
         if ($verifier1 === null || $verifier2 === null) {

+ 303 - 0
application/admin/library/SystemLogin.php

@@ -0,0 +1,303 @@
+<?php
+
+namespace app\admin\library;
+
+use app\admin\model\AuthGroup;
+use fast\Tree;
+use think\Config;
+use think\Session;
+
+/**
+ * 双系统登录隔离与后台风格(统一走 index/login,按角色组识别系统)
+ */
+class SystemLogin
+{
+    /**
+     * 加载 systems.php / mproc.php
+     */
+    public static function loadConfig(): void
+    {
+        static $loaded = false;
+        if ($loaded) {
+            return;
+        }
+        $loaded = true;
+        if (is_file(APP_PATH . 'extra/systems.php')) {
+            Config::load(APP_PATH . 'extra/systems.php', 'systems');
+        }
+        if (is_file(APP_PATH . 'extra/mproc.php')) {
+            Config::load(APP_PATH . 'extra/mproc.php', 'mproc');
+        }
+    }
+
+    public static function normalizeKey(string $key): string
+    {
+        $key = strtolower(trim($key));
+
+        return $key === 'cockpit' ? 'cockpit' : 'collab';
+    }
+
+    /**
+     * @param mixed $default
+     * @return mixed
+     */
+    public static function option(string $systemKey, string $key, $default = null)
+    {
+        self::loadConfig();
+        $systemKey = self::normalizeKey($systemKey);
+        $val = Config::get('systems.' . $systemKey . '.' . $key);
+        if ($val !== null && $val !== '') {
+            return $val;
+        }
+        if ($systemKey === 'collab') {
+            if ($key === 'allowed_group_roots') {
+                $root = (int)Config::get('mproc.collab_login_group_root_id');
+                if ($root <= 0) {
+                    $root = (int)Config::get('mproc.bid_open_auth_group_root_id');
+                }
+
+                return $root > 0 ? [$root] : $default;
+            }
+            if ($key === 'name' || $key === 'brand') {
+                if ($key === 'brand') {
+                    $brand = Config::get('mproc.collab_system_brand');
+                    if (!$brand) {
+                        $brand = Config::get('mproc.system_brand.collab');
+                    }
+
+                    return $brand ?: '生产协同系统';
+                }
+
+                return '生产协同系统';
+            }
+            if ($key === 'allow_rule_super') {
+                return false;
+            }
+        }
+        if ($systemKey === 'cockpit') {
+            if ($key === 'allowed_group_roots') {
+                $roots = Config::get('mproc.cockpit_login_group_root_ids');
+
+                return is_array($roots) ? $roots : $default;
+            }
+            if ($key === 'name') {
+                return '生产经营驾驶舱系统';
+            }
+            if ($key === 'brand') {
+                $brand = Config::get('mproc.cockpit_system_brand');
+                if (!$brand) {
+                    $brand = Config::get('mproc.system_brand.cockpit');
+                }
+
+                return $brand ?: '浙江印刷集团有限公司';
+            }
+            if ($key === 'allow_rule_super') {
+                return (bool)Config::get('mproc.cockpit_allow_rule_super');
+            }
+        }
+
+        return $default;
+    }
+
+    /**
+     * @return array<int, true>
+     */
+    public static function allowedGroupIds(string $systemKey): array
+    {
+        self::loadConfig();
+        $systemKey = self::normalizeKey($systemKey);
+        $roots = self::option($systemKey, 'allowed_group_roots', []);
+        if (!is_array($roots) || $roots === []) {
+            return [];
+        }
+        $rootIds = [];
+        foreach ($roots as $rid) {
+            $rid = (int)$rid;
+            if ($rid > 0) {
+                $rootIds[$rid] = true;
+            }
+        }
+        if ($rootIds === []) {
+            return [];
+        }
+
+        static $cache = [];
+        $cacheKey = $systemKey . ':' . implode(',', array_keys($rootIds));
+        if (isset($cache[$cacheKey])) {
+            return $cache[$cacheKey];
+        }
+
+        $groupList = AuthGroup::where('status', 'normal')->field('id,pid')->select();
+        $rows = [];
+        if (is_array($groupList) || $groupList instanceof \Traversable) {
+            foreach ($groupList as $g) {
+                if (is_object($g) && method_exists($g, 'toArray')) {
+                    $g = $g->toArray();
+                }
+                if (!is_array($g)) {
+                    continue;
+                }
+                $rows[] = [
+                    'id'  => (int)($g['id'] ?? 0),
+                    'pid' => (int)($g['pid'] ?? 0),
+                ];
+            }
+        }
+
+        $allowed = [];
+        foreach (array_keys($rootIds) as $rootId) {
+            $allowed[$rootId] = true;
+            $children = Tree::instance()->init($rows, 'pid')->getChildrenIds($rootId, false);
+            if (!is_array($children)) {
+                continue;
+            }
+            foreach ($children as $cid) {
+                $cid = (int)$cid;
+                if ($cid > 0) {
+                    $allowed[$cid] = true;
+                }
+            }
+        }
+
+        $cache[$cacheKey] = $allowed;
+
+        return $allowed;
+    }
+
+    public static function belongsTo(Auth $auth, int $adminId, string $systemKey): bool
+    {
+        if ($adminId <= 0) {
+            return false;
+        }
+        $systemKey = self::normalizeKey($systemKey);
+        if (self::option($systemKey, 'allow_rule_super', false)) {
+            $ruleIds = $auth->getRuleIds($adminId);
+            if (is_array($ruleIds) && in_array('*', $ruleIds, true)) {
+                return true;
+            }
+        }
+        $allowed = self::allowedGroupIds($systemKey);
+        if ($allowed === []) {
+            return false;
+        }
+        $userGroupIds = $auth->getGroupIds($adminId);
+        if ($userGroupIds === []) {
+            return false;
+        }
+        foreach ($userGroupIds as $gid) {
+            if (isset($allowed[(int)$gid])) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 按角色组识别所属系统:先生产协同,再驾驶舱
+     */
+    public static function resolveSystem(Auth $auth, int $adminId): ?string
+    {
+        if ($adminId <= 0) {
+            return null;
+        }
+        if (self::belongsTo($auth, $adminId, 'collab')) {
+            return 'collab';
+        }
+        if (self::belongsTo($auth, $adminId, 'cockpit')) {
+            return 'cockpit';
+        }
+
+        return null;
+    }
+
+    /**
+     * 会话中的系统;缺失时按角色组补全
+     */
+    public static function currentSystem(Auth $auth = null): string
+    {
+        $system = Session::get('login_system');
+        if ($system === 'collab' || $system === 'cockpit') {
+            return $system;
+        }
+        if ($auth && $auth->isLogin()) {
+            $resolved = self::resolveSystem($auth, (int)$auth->id);
+            if ($resolved) {
+                Session::set('login_system', $resolved);
+
+                return $resolved;
+            }
+        }
+
+        return 'collab';
+    }
+
+    /**
+     * 读取风格码:1=白色 2=黑色原版
+     */
+    public static function styleCode(string $systemKey): int
+    {
+        self::loadConfig();
+        $systemKey = self::normalizeKey($systemKey);
+        // 优先:collab_system_style / cockpit_system_style(只改 1 或 2)
+        $flatKey = $systemKey === 'cockpit' ? 'mproc.cockpit_system_style' : 'mproc.collab_system_style';
+        $flat = Config::get($flatKey);
+        if ($flat === 1 || $flat === 2 || $flat === '1' || $flat === '2') {
+            return (int)$flat === 2 ? 2 : 1;
+        }
+        // 兼容旧数组 / 单数字写法
+        $map = Config::get('mproc.system_style');
+        if (is_array($map) && isset($map[$systemKey])) {
+            $code = (int)$map[$systemKey];
+        } elseif (!is_array($map) && ((int)$map === 1 || (int)$map === 2)) {
+            $code = (int)$map;
+        } else {
+            $code = $systemKey === 'cockpit' ? 2 : 1;
+        }
+
+        return $code === 2 ? 2 : 1;
+    }
+
+    /**
+     * 应用后台皮肤到 Config,并返回模板变量
+     *
+     * @return array{login_system:string,system_style:int,adminskin:string,load_xinhua_theme:bool,system_brand:string}
+     */
+    public static function applyTheme(Auth $auth = null): array
+    {
+        $system = self::currentSystem($auth);
+        $style = self::styleCode($system);
+        // 1 白色:skin-blue-light + xinhua-theme(生产协同)
+        // 2 黑色原版:skin-black(驾驶舱;本站 skin-blue 已改成亮蓝,不能当原版用)
+        if ($style === 2) {
+            $skin = 'skin-black';
+            $loadXinhua = false;
+        } else {
+            $skin = 'skin-blue-light';
+            $loadXinhua = true;
+        }
+        Config::set('fastadmin.adminskin', $skin);
+
+        $brand = (string)self::option($system, 'brand', '');
+        if ($brand === '') {
+            $brand = (string)self::option($system, 'name', '生产协同系统');
+        }
+        $flatBrand = Config::get($system === 'cockpit' ? 'mproc.cockpit_system_brand' : 'mproc.collab_system_brand');
+        if (is_string($flatBrand) && $flatBrand !== '') {
+            $brand = $flatBrand;
+        } else {
+            $fromMproc = Config::get('mproc.system_brand.' . $system);
+            if (is_string($fromMproc) && $fromMproc !== '') {
+                $brand = $fromMproc;
+            }
+        }
+
+        return [
+            'login_system'      => $system,
+            'system_style'      => $style,
+            'adminskin'         => $skin,
+            'load_xinhua_theme' => $loadXinhua,
+            'system_brand'      => $brand,
+        ];
+    }
+}

+ 3 - 4
application/admin/view/common/header.html

@@ -1,10 +1,9 @@
 <!-- Logo -->
 <!-- Logo -->
 <a href="javascript:;" class="logo">
 <a href="javascript:;" class="logo">
     <!-- 迷你模式下Logo的大小为50X50 -->
     <!-- 迷你模式下Logo的大小为50X50 -->
-    <span class="logo-mini">{$site.name|mb_substr=0,4,'utf-8'|mb_strtoupper='utf-8'|htmlentities}</span>
-    <!-- 普通模式下Logo -->
-    <!-- <span class="logo-lg" style=" font-size: 16px;">{$site.name|htmlentities}</span> -->
-    <span class="logo-lg" style=" font-size: 16px;">大数据协同系统</span>
+    <span class="logo-mini">{$system_brand|default='生产协同'|mb_substr=0,4,'utf-8'|htmlentities}</span>
+    <!-- 普通模式下Logo:按登录系统品牌显示 -->
+    <span class="logo-lg" style=" font-size: 16px;">{$system_brand|default='生产协同系统'|htmlentities}</span>
 
 
 </a>
 </a>
 
 

+ 5 - 1
application/admin/view/common/meta.html

@@ -10,12 +10,16 @@
 <!-- Loading Bootstrap -->
 <!-- Loading Bootstrap -->
 <link href="__CDN__/assets/css/backend{$Think.config.app_debug?'':'.min'}.css?v={$Think.config.site.version}" rel="stylesheet">
 <link href="__CDN__/assets/css/backend{$Think.config.app_debug?'':'.min'}.css?v={$Think.config.site.version}" rel="stylesheet">
 
 
-{if $Think.config.fastadmin.adminskin}
+{if isset($adminskin) && $adminskin}
+<link href="__CDN__/assets/css/skins/{$adminskin}.css?v={$Think.config.site.version}" rel="stylesheet">
+{elseif $Think.config.fastadmin.adminskin /}
 <link href="__CDN__/assets/css/skins/{$Think.config.fastadmin.adminskin}.css?v={$Think.config.site.version}" rel="stylesheet">
 <link href="__CDN__/assets/css/skins/{$Think.config.fastadmin.adminskin}.css?v={$Think.config.site.version}" rel="stylesheet">
 {else /}
 {else /}
 <link href="__CDN__/assets/css/skins/skin-blue-light.css?v={$Think.config.site.version}" rel="stylesheet">
 <link href="__CDN__/assets/css/skins/skin-blue-light.css?v={$Think.config.site.version}" rel="stylesheet">
 {/if}
 {/if}
+{if !isset($load_xinhua_theme) || $load_xinhua_theme}
 <link href="__CDN__/assets/css/xinhua-theme.css?v={$Think.config.site.version}" rel="stylesheet">
 <link href="__CDN__/assets/css/xinhua-theme.css?v={$Think.config.site.version}" rel="stylesheet">
+{/if}
 
 
 <!-- HTML5 shim, for IE6-8 support of HTML5 elements. All other JS at the end of file. -->
 <!-- HTML5 shim, for IE6-8 support of HTML5 elements. All other JS at the end of file. -->
 <!--[if lt IE 9]>
 <!--[if lt IE 9]>

+ 1 - 1
application/admin/view/index/index.html

@@ -4,7 +4,7 @@
         <!-- 加载样式及META信息 -->
         <!-- 加载样式及META信息 -->
         {include file="common/meta" /}
         {include file="common/meta" /}
     </head>
     </head>
-    <body class="hold-transition {$Think.config.fastadmin.adminskin|default='skin-blue-light'} sidebar-mini {:$Think.cookie.sidebar_collapse?'sidebar-collapse':''} fixed {:$Think.config.fastadmin.multipletab?'multipletab':''} {:$Think.config.fastadmin.multiplenav?'multiplenav':''}" id="tabs">
+    <body class="hold-transition {$adminskin|default='skin-blue-light'} sidebar-mini {:$Think.cookie.sidebar_collapse?'sidebar-collapse':''} fixed {:$Think.config.fastadmin.multipletab?'multipletab':''} {:$Think.config.fastadmin.multiplenav?'multiplenav':''}" id="tabs">
 
 
         <div class="wrapper">
         <div class="wrapper">
 
 

+ 6 - 6
application/admin/view/index/login.html

@@ -610,8 +610,8 @@
                     <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/tb.png?v={$site.version|default='1.0.1'|htmlentities}" alt="大数据协同系统"/>
                     <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/tb.png?v={$site.version|default='1.0.1'|htmlentities}" alt="大数据协同系统"/>
                 </div>
                 </div>
                 <div class="login-brand-text">
                 <div class="login-brand-text">
-                    <h2 class="login-brand-title">大数据协同系统</h2>
-                    <p class="login-brand-sub">Big Data Collaborative System</p>
+                    <h2 class="login-brand-title">生产协同系统</h2>
+                    <p class="login-brand-sub">Production Collaboration System</p>
                 </div>
                 </div>
             </div>
             </div>
 
 
@@ -621,8 +621,8 @@
 
 
             <ul class="login-brand-features">
             <ul class="login-brand-features">
                 <li>
                 <li>
-                    <span class="feat-icon"><i class="glyphicon glyphicon-dashboard"></i></span>
-                    <span>数据看板</span>
+                    <span class="feat-icon"><i class="glyphicon glyphicon-shopping-cart"></i></span>
+                    <span>协助采购</span>
                 </li>
                 </li>
                 <li>
                 <li>
                     <span class="feat-icon"><i class="glyphicon glyphicon-time"></i></span>
                     <span class="feat-icon"><i class="glyphicon glyphicon-time"></i></span>
@@ -645,14 +645,14 @@
         <div class="login-side-main">
         <div class="login-side-main">
             <div class="login-side-head">
             <div class="login-side-head">
                 <h1>欢迎回来</h1>
                 <h1>欢迎回来</h1>
-                <p>请输入账户登录大数据协同平台</p>
+                <p>请输入账户登录生产协同平台</p>
             </div>
             </div>
 
 
             <form action="" method="post" id="login-form">
             <form action="" method="post" id="login-form">
                 <!--@AdminLoginFormBegin-->
                 <!--@AdminLoginFormBegin-->
                 <div id="errtips" class="hide"></div>
                 <div id="errtips" class="hide"></div>
                 {:token()}
                 {:token()}
-
+                <input type="hidden" name="system" value="collab"/>
                 <div class="login-field">
                 <div class="login-field">
                     <div class="login-input">
                     <div class="login-input">
                         <span class="login-input-icon">
                         <span class="login-input-icon">

+ 801 - 0
application/admin/view/index/生产经营驾驶舱系统.html

@@ -0,0 +1,801 @@
+<!DOCTYPE html>
+<html>
+<head>
+    {include file="common/meta" /}
+    <style type="text/css">
+        * {
+            box-sizing: border-box;
+        }
+
+        html, body {
+            width: 100%;
+            height: 100%;
+            margin: 0;
+            padding: 0;
+            overflow: hidden;
+        }
+
+        body.login-page {
+            height: 100vh;
+            min-height: 100vh;
+            max-height: 100vh;
+            overflow: hidden;
+            background: #eef2f8;
+            color: #1f2937;
+            font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
+            display: flex;
+        }
+
+        /* ========== 左侧(背景图 / 插画图后续替换下方 url) ========== */
+        .login-brand {
+            position: relative;
+            z-index: 1;
+            flex: 1 1 68%;
+            min-width: 0;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: space-between;
+            color: #fff;
+            /* 背景图 bj.png */
+            background-color: #1a3a6e;
+            background-image: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/bj.png?v={$site.version|default='1.0.1'|htmlentities}');
+            background-repeat: no-repeat;
+            background-position: center center;
+            background-size: cover;
+            padding: clamp(28px, 4vh, 48px) clamp(28px, 4vw, 56px) clamp(20px, 3vh, 36px);
+        }
+
+        .login-brand::before {
+            content: "";
+            position: absolute;
+            inset: 0;
+            background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 48%, rgba(15, 35, 70, 0.08) 100%);
+            pointer-events: none;
+        }
+
+        /* 中间内容:图标标题在上,插画在下(参考图二) */
+        .login-brand-stage {
+            position: relative;
+            z-index: 2;
+            flex: 1 1 auto;
+            min-height: 0;
+            width: 100%;
+            max-width: 720px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            gap: 14px;
+            margin: 8px 0;
+        }
+
+        .login-brand-header {
+            position: relative;
+            z-index: 2;
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            justify-content: center;
+            text-align: left;
+            gap: 16px;
+        }
+
+        .login-brand-logo {
+            flex: 0 0 auto;
+            width: 64px;
+            height: 64px;
+            border-radius: 14px;
+            background: transparent;
+            box-shadow: none;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            overflow: hidden;
+        }
+
+        .login-brand-logo img {
+            width: 100%;
+            height: 100%;
+            object-fit: contain;
+        }
+
+        .login-brand-text {
+            min-width: 0;
+            text-align: left;
+        }
+
+        .login-brand-title {
+            margin: 0 0 6px;
+            font-size: clamp(24px, 2.6vw, 34px);
+            font-weight: 700;
+            letter-spacing: 3px;
+            line-height: 1.25;
+        }
+
+        .login-brand-sub {
+            margin: 0;
+            font-size: 12px;
+            letter-spacing: 3px;
+            color: rgba(210, 228, 255, 0.72);
+            text-transform: uppercase;
+        }
+
+        /* 插画区域 ch.png:缩小并放在标题下方 */
+        .login-brand-illust {
+            position: relative;
+            z-index: 1;
+            flex: 0 1 auto;
+            width: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        .login-brand-illust img {
+            display: block;
+            max-width: min(360px, 46%);
+            max-height: min(26vh, 240px);
+            width: auto;
+            height: auto;
+            object-fit: contain;
+            pointer-events: none;
+            user-select: none;
+            opacity: 0.96;
+        }
+
+        .login-brand-features {
+            position: relative;
+            z-index: 2;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 0;
+            margin: 6px 0 0;
+            padding: 0;
+            list-style: none;
+            flex-shrink: 0;
+        }
+
+        .login-brand-features li {
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            gap: 10px;
+            min-width: 0;
+            padding: 0 22px;
+            font-size: 13px;
+            color: rgba(235, 244, 255, 0.92);
+            position: relative;
+            white-space: nowrap;
+        }
+
+        .login-brand-features li + li::before {
+            content: "";
+            position: absolute;
+            left: 0;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 1px;
+            height: 22px;
+            background: rgba(180, 210, 255, 0.28);
+        }
+
+        .login-brand-features .feat-icon {
+            width: 30px;
+            height: 30px;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.1);
+            border: 1px solid rgba(210, 228, 255, 0.35);
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            color: #eef4ff;
+            font-size: 13px;
+            flex-shrink: 0;
+        }
+
+        .login-brand-footer {
+            position: relative;
+            z-index: 2;
+            text-align: center;
+            font-size: 12px;
+            color: rgba(235, 244, 255, 0.72);
+        }
+
+        .login-brand-footer a {
+            color: rgba(220, 235, 255, 0.58);
+        }
+
+        .login-brand-footer a:hover {
+            color: #fff;
+        }
+
+        /* ========== 右侧登录(按设计稿红框区域) ========== */
+        body.login-page .login-side {
+            position: relative;
+            z-index: 2;
+            flex: 0 0 40% !important;
+            width: 40% !important;
+            min-width: 380px !important;
+            max-width: 500px !important;
+            height: 100vh !important;
+            background: linear-gradient(180deg, #f8fbff 0%, #f1f5fb 100%) !important;
+            display: flex !important;
+            flex-direction: column !important;
+            justify-content: stretch !important;
+            align-items: stretch !important;
+            padding: 0 48px !important;
+            margin: 0 !important;
+            box-shadow: -4px 0 24px rgba(15, 23, 42, 0.06);
+            border-left: 1px solid #e2e8f0;
+        }
+
+        body.login-page .login-side-main {
+            width: 100%;
+            max-width: 340px;
+            margin: 0 auto;
+            flex: 1 1 auto;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            padding: 64px 0 28px;
+        }
+
+        body.login-page .login-side-head {
+            margin-bottom: 36px;
+            margin-top: 12px;
+            text-align: center;
+        }
+
+        body.login-page .login-side-head h1 {
+            margin: 0 0 10px;
+            font-size: 32px !important;
+            font-weight: 700 !important;
+            color: #111827 !important;
+            letter-spacing: 0.5px;
+            line-height: 1.25;
+            text-align: center;
+        }
+
+        body.login-page .login-side-head p {
+            margin: 0;
+            font-size: 13px !important;
+            color: #9ca3af !important;
+            line-height: 1.6;
+            text-align: center;
+        }
+
+        body.login-page #login-form {
+            width: 100%;
+            margin: 0;
+            padding: 0;
+            background: transparent !important;
+            box-shadow: none !important;
+            border: none !important;
+        }
+
+        body.login-page #login-form .login-field {
+            margin-bottom: 18px;
+        }
+
+        body.login-page #login-form .login-field-label {
+            display: block;
+            margin: 0 0 8px;
+            font-size: 14px;
+            font-weight: 500;
+            color: #111827;
+            line-height: 1.2;
+            text-align: left;
+        }
+
+        body.login-page #login-form .login-input {
+            position: relative;
+        }
+
+        body.login-page #login-form .login-input-icon {
+            position: absolute;
+            left: 12px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 16px;
+            height: 16px;
+            pointer-events: none;
+            z-index: 2;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        body.login-page #login-form .login-input-icon img {
+            display: block;
+            width: 16px;
+            height: 16px;
+            object-fit: contain;
+        }
+
+        body.login-page #login-form .form-control {
+            height: 40px !important;
+            border: 1px solid #e5e7eb !important;
+            border-radius: 8px !important;
+            box-shadow: none !important;
+            font-size: 13px !important;
+            color: #111827 !important;
+            background: #ffffff !important;
+            padding: 8px 14px 8px 36px !important;
+            transition: border-color .2s ease, box-shadow .2s ease;
+        }
+
+        body.login-page #login-form .login-password-wrap .form-control {
+            padding-right: 40px !important;
+        }
+
+        body.login-page #login-form .form-control:hover {
+            border-color: #d1d5db !important;
+        }
+
+        body.login-page #login-form .form-control:focus {
+            border-color: #818cf8 !important;
+            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12) !important;
+            background: #ffffff !important;
+            outline: none !important;
+        }
+
+        body.login-page #login-form .form-control::placeholder {
+            color: #c0c6d0 !important;
+        }
+
+        body.login-page #login-form .btn-toggle-password {
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+            border: none;
+            background: transparent;
+            padding: 2px;
+            cursor: pointer;
+            line-height: 1;
+            z-index: 2;
+            width: 24px;
+            height: 24px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        body.login-page #login-form .btn-toggle-password img {
+            display: block;
+            width: 16px;
+            height: 16px;
+            object-fit: contain;
+        }
+
+        body.login-page #login-form .btn-toggle-password:hover {
+            opacity: 0.8;
+        }
+
+        body.login-page #login-form .login-captcha-row {
+            display: flex;
+            gap: 10px;
+            align-items: stretch;
+        }
+
+        body.login-page #login-form .login-captcha-row .login-input {
+            flex: 1 1 auto;
+            min-width: 0;
+        }
+
+        body.login-page #login-form .login-captcha-img {
+            flex: 0 0 auto;
+            height: 40px;
+            border-radius: 8px;
+            border: 1px solid #e5e7eb;
+            overflow: hidden;
+            background: #fff;
+            cursor: pointer;
+        }
+
+        body.login-page #login-form .login-captcha-img img {
+            display: block;
+            height: 100%;
+            width: auto;
+            min-width: 110px;
+        }
+
+        body.login-page .login-form-extra {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin: 4px 0 22px;
+            font-size: 13px;
+        }
+
+        body.login-page .login-form-extra label {
+            margin: 0 !important;
+            font-weight: normal !important;
+            color: #6b7280 !important;
+            cursor: pointer;
+            display: inline-flex;
+            align-items: center;
+            gap: 8px;
+            user-select: none;
+        }
+
+        body.login-page .login-form-extra input[type="checkbox"] {
+            margin: 0;
+            width: 15px;
+            height: 15px;
+            accent-color: #4f46e5;
+            vertical-align: middle;
+        }
+
+        body.login-page .login-form-extra .login-forgot {
+            color: #9ca3af !important;
+            text-decoration: none !important;
+        }
+
+        body.login-page .login-form-extra .login-forgot:hover {
+            color: #6b7280 !important;
+            text-decoration: underline !important;
+        }
+
+        body.login-page #login-form .form-group {
+            margin-bottom: 0 !important;
+        }
+
+        body.login-page #login-form .btn-login-submit {
+            height: 42px !important;
+            width: 100% !important;
+            border: none !important;
+            border-radius: 8px !important;
+            background: linear-gradient(90deg, #3b82f6 0%, #6366f1 45%, #8b5cf6 100%) !important;
+            color: #fff !important;
+            font-size: 16px !important;
+            font-weight: 600 !important;
+            letter-spacing: 3px;
+            box-shadow: 0 12px 28px rgba(99, 102, 241, 0.32) !important;
+            transition: transform .15s ease, box-shadow .15s ease, filter .15s ease;
+            display: inline-flex !important;
+            align-items: center;
+            justify-content: center;
+            gap: 10px;
+            text-shadow: none !important;
+            line-height: 1 !important;
+        }
+
+        body.login-page #login-form .btn-login-submit:hover,
+        body.login-page #login-form .btn-login-submit:focus {
+            color: #fff !important;
+            filter: brightness(1.05);
+            box-shadow: 0 14px 32px rgba(99, 102, 241, 0.4) !important;
+            transform: translateY(-1px);
+            background: linear-gradient(90deg, #3b82f6 0%, #6366f1 45%, #8b5cf6 100%) !important;
+        }
+
+        body.login-page #login-form .btn-login-submit:active {
+            transform: translateY(0);
+        }
+
+        body.login-page #errtips {
+            margin-bottom: 14px;
+        }
+
+        body.login-page .login-side-register {
+            margin-top: 22px;
+            text-align: center;
+            font-size: 13px;
+        }
+
+        body.login-page .login-side-register a {
+            color: #374151 !important;
+            text-decoration: none !important;
+        }
+
+        body.login-page .login-side-register a:hover {
+            color: #4f46e5 !important;
+            text-decoration: none !important;
+        }
+
+        body.login-page .login-side-company {
+            flex: 0 0 auto;
+            padding: 0 0 28px;
+            text-align: center;
+            font-size: 12px;
+            color: #9ca3af;
+            letter-spacing: 0.2px;
+        }
+
+        /* 覆盖 FastAdmin 默认灰色登录底 */
+        body.login-page {
+            background: #0a1630 !important;
+        }
+
+        body.login-page .wrapper,
+        body.login-page .content-wrapper,
+        body.login-page .login-box,
+        body.login-page .login-box-body {
+            background: transparent !important;
+            box-shadow: none !important;
+            margin: 0 !important;
+            padding: 0 !important;
+            width: auto !important;
+        }
+
+        @media (max-width: 960px) {
+            html, body {
+                overflow-x: hidden;
+                overflow-y: auto;
+            }
+
+            body.login-page {
+                height: auto;
+                min-height: 100vh;
+                max-height: none;
+                flex-direction: column;
+                overflow-x: hidden;
+                overflow-y: auto;
+            }
+
+            .login-brand {
+                flex: 0 0 auto;
+                min-height: 40vh;
+                padding: 24px 20px 16px;
+            }
+
+            .login-brand-illust {
+                min-height: 110px;
+            }
+
+            .login-brand-illust img {
+                max-width: min(280px, 72%);
+                max-height: 180px;
+            }
+
+            .login-brand-features li {
+                min-width: 88px;
+                padding: 0 14px;
+                font-size: 12px;
+            }
+
+            body.login-page .login-side {
+                flex: 1 1 auto !important;
+                width: 100% !important;
+                max-width: none !important;
+                min-width: 0 !important;
+                height: auto !important;
+                min-height: 60vh;
+                padding: 0 24px !important;
+                border-radius: 20px 20px 0 0;
+            }
+
+            body.login-page .login-side-main {
+                padding: 36px 0 20px;
+            }
+
+            body.login-page .login-side-company {
+                padding-bottom: 24px;
+            }
+        }
+
+        @media (max-width: 480px) {
+            .login-brand-features {
+                display: none;
+            }
+
+            .login-side {
+                padding: 28px 18px 24px;
+            }
+
+            .login-side-head h1 {
+                font-size: 24px;
+            }
+        }
+    </style>
+    <script>
+        document.addEventListener('wheel', function (e) {
+            if (e.ctrlKey) {
+                e.preventDefault();
+            }
+        }, {passive: false});
+        document.addEventListener('gesturestart', function (e) { e.preventDefault(); });
+        document.addEventListener('gesturechange', function (e) { e.preventDefault(); });
+        document.addEventListener('gestureend', function (e) { e.preventDefault(); });
+    </script>
+</head>
+<body class="login-page">
+    <section class="login-brand">
+        <div class="login-brand-stage">
+            <div class="login-brand-header">
+                <div class="login-brand-logo">
+                    <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/tb.png?v={$site.version|default='1.0.1'|htmlentities}" alt="大数据协同系统"/>
+                </div>
+                <div class="login-brand-text">
+                    <h2 class="login-brand-title">生产经营驾驶舱系统</h2>
+                    <p class="login-brand-sub">Production & Operation Cockpit System</p>
+                </div>
+            </div>
+
+            <div class="login-brand-illust">
+                <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/ch.png?v={$site.version|default='1.0.1'|htmlentities}" alt=""/>
+            </div>
+
+            <ul class="login-brand-features">
+                <li>
+                    <span class="feat-icon"><i class="glyphicon glyphicon-dashboard"></i></span>
+                    <span>业务总览</span>
+                </li>
+                <li>
+                    <span class="feat-icon"><i class="glyphicon glyphicon-bullhorn"></i></span>
+                    <span>营销管理</span>
+                </li>
+                <li>
+                    <span class="feat-icon"><i class="glyphicon glyphicon-wrench"></i></span>
+                    <span>生产管理</span>
+                </li>
+                <li>
+                    <span class="feat-icon"><i class="glyphicon glyphicon-shopping-cart"></i></span>
+                    <span>采购管理</span>
+                </li>
+            </ul>
+        </div>
+
+        <div class="login-brand-footer">
+            Copyright @ 杭州可集达科技有限公司 2026-{:date('Y',time())} 版权所有
+            {if $site.beian}<a href="https://beian.miit.gov.cn" target="_blank"> {$site.beian|htmlentities}</a>{/if}
+        </div>
+    </section>
+
+    <aside class="login-side">
+        <div class="login-side-main">
+            <div class="login-side-head">
+                <h1>欢迎回来</h1>
+                <p>请输入账户登录生产经营驾驶舱系统</p>
+            </div>
+
+            <form action="" method="post" id="login-form">
+                <!--@AdminLoginFormBegin-->
+                <div id="errtips" class="hide"></div>
+                {:token()}
+                <input type="hidden" name="system" value="cockpit"/>
+
+                <div class="login-field">
+                    <div class="login-input">
+                        <span class="login-input-icon">
+                            <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/5.png?v={$site.version|default='1.0.1'|htmlentities}" alt=""/>
+                        </span>
+                        <input type="text" class="form-control" id="pd-form-username" placeholder="请输入账号" name="username" autocomplete="username" value="" data-rule="{:__('Username')}:required;username"/>
+                    </div>
+                </div>
+
+                <div class="login-field">
+                    <div class="login-input login-password-wrap">
+                        <span class="login-input-icon">
+                            <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/6.png?v={$site.version|default='1.0.1'|htmlentities}" alt=""/>
+                        </span>
+                        <input type="password" class="form-control" id="pd-form-password" placeholder="请输入密码" name="password" autocomplete="current-password" value="" data-rule="{:__('Password')}:required;length(4~30)"/>
+                        <button type="button" class="btn-toggle-password" id="btn-toggle-password" title="显示/隐藏密码" aria-label="显示/隐藏密码"
+                                data-eye-open="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/11.png?v={$site.version|default='1.0.1'|htmlentities}"
+                                data-eye-close="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/10.png?v={$site.version|default='1.0.1'|htmlentities}">
+                            <img src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/11.png?v={$site.version|default='1.0.1'|htmlentities}" alt="显示密码"/>
+                        </button>
+                    </div>
+                </div>
+
+                {if $Think.config.fastadmin.login_captcha}
+                <div class="login-field">
+                    <div class="login-captcha-row">
+                        <div class="login-input">
+                            <span class="login-input-icon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></span>
+                            <input type="text" id="pd-form-captcha" name="captcha" class="form-control" placeholder="请输入验证码" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length})" autocomplete="off"/>
+                        </div>
+                        <span class="login-captcha-img" title="点击刷新验证码">
+                            <img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" height="40" alt="captcha" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
+                        </span>
+                    </div>
+                </div>
+                {/if}
+
+                <div class="login-form-extra">
+                    <label>
+                        <input type="checkbox" id="remember-password" name="rememberpwd" value="1"/>
+                        记住密码
+                    </label>
+                    <a href="javascript:;" class="login-forgot" onclick="Layer && Layer.msg ? Layer.msg('请联系管理员重置密码') : alert('请联系管理员重置密码'); return false;">忘记密码?</a>
+                </div>
+
+                <div class="form-group" style="margin-bottom:0;">
+                    <button type="submit" class="btn btn-login-submit btn-lg btn-block">
+                        登录
+                        <span aria-hidden="true">→</span>
+                    </button>
+                </div>
+                <!--@AdminLoginFormEnd-->
+            </form>
+        </div>
+
+        <div class="login-side-company">浙江新华数码印务有限公司</div>
+    </aside>
+
+{include file="common/script" /}
+<script>
+(function () {
+    var REMEMBER_KEY = 'xh_admin_remember_pwd';
+
+    function loadRemember() {
+        var $user = document.getElementById('pd-form-username');
+        var $pass = document.getElementById('pd-form-password');
+        var $cb = document.getElementById('remember-password');
+        if (!$user || !$pass || !$cb) {
+            return;
+        }
+        try {
+            var raw = localStorage.getItem(REMEMBER_KEY);
+            if (!raw) {
+                return;
+            }
+            var data = JSON.parse(raw);
+            if (data && data.username) {
+                $user.value = data.username;
+            }
+            if (data && data.password) {
+                $pass.value = data.password;
+            }
+            $cb.checked = true;
+        } catch (e) {
+        }
+    }
+
+    function saveRemember() {
+        var $user = document.getElementById('pd-form-username');
+        var $pass = document.getElementById('pd-form-password');
+        var $cb = document.getElementById('remember-password');
+        if (!$user || !$pass || !$cb) {
+            return;
+        }
+        if ($cb.checked) {
+            localStorage.setItem(REMEMBER_KEY, JSON.stringify({
+                username: $user.value || '',
+                password: $pass.value || ''
+            }));
+        } else {
+            localStorage.removeItem(REMEMBER_KEY);
+        }
+    }
+
+    loadRemember();
+
+    var form = document.getElementById('login-form');
+    if (form) {
+        form.addEventListener('submit', function () {
+            saveRemember();
+        });
+    }
+    var cb = document.getElementById('remember-password');
+    if (cb) {
+        cb.addEventListener('change', function () {
+            if (!cb.checked) {
+                localStorage.removeItem(REMEMBER_KEY);
+            }
+        });
+    }
+
+    var btn = document.getElementById('btn-toggle-password');
+    var input = document.getElementById('pd-form-password');
+    if (!btn || !input) {
+        return;
+    }
+    var eyeOpen = btn.getAttribute('data-eye-open') || '';
+    var eyeClose = btn.getAttribute('data-eye-close') || '';
+    btn.addEventListener('click', function () {
+        var show = input.getAttribute('type') === 'password';
+        input.setAttribute('type', show ? 'text' : 'password');
+        var img = btn.querySelector('img');
+        if (img) {
+            img.src = show ? eyeClose : eyeOpen;
+            img.alt = show ? '隐藏密码' : '显示密码';
+        }
+    });
+})();
+</script>
+</body>
+</html>

+ 0 - 247
application/admin/view/index/生产经营驾驶舱系统登录页面.html

@@ -1,247 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    {include file="common/meta" /}
-    <style type="text/css">
-        body {
-            width: 100%;
-            height: 100vh;
-            background-image: url("http://xh-erp.7in6.com/img/bg1.jpg");
-            background-size: cover;
-            background-repeat: no-repeat;
-            display: flex;
-            flex-direction: column;
-            justify-content: center;
-            align-items: center;
-            margin: 0;
-            position: relative;
-            /*background-position: center;*/
-            filter: brightness(1.2); /* 调整亮度 */
-        }
-
-        a {
-            color: #444;
-        }
-
-        .login-wrapper {
-            display: flex;
-            flex-direction: column;
-            align-items: flex-end;
-            flex: 1 10 1;
-            width: 100%;
-            padding: 20px;
-            box-sizing: border-box;
-            margin-right: 20%;margin-top: 10%;
-            position: relative;
-        }
-
-
-        .login-screen {
-            width: 100%;
-            max-width: 475px;
-            /* display: flex; */
-            flex-direction: column;
-            align-items: end;
-            border-radius: 3px;
-            box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
-            padding: 15px;
-            box-sizing: border-box;
-
-            background-color: rgba(115, 162, 229, 0.1);
-        }
-
-        .profile-img-card {
-            width: 100px;
-            height: 100px;
-            border-radius: 50%;
-            margin: -50px auto 30px;
-            border: 5px solid #fff;
-        }
-
-        .profile-name-card {
-            text-align: center;
-        }
-
-        .login-form {
-            width: 100%;
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            padding: 20px;
-            box-sizing: border-box;
-        }
-
-        #login-form {
-            width: 100%;
-        }
-
-        #login-form .btn {
-            background-color: rgb(34, 60, 212);
-            height: 50px;
-            width: 100%;
-        }
-
-        #login-form .input-group {
-            margin-bottom: 15px;
-            width: 100%;
-        }
-
-        #login-form .form-control {
-            font-size: 13px;
-            width: 100%;
-        }
-
-        .head {
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            width: 100%;
-            padding: 10px;
-            box-sizing: border-box;
-            position: absolute;
-            top: 0;
-            left: 0;
-        }
-
-        .head .head-title {
-            font-size: 24px;
-            margin: 0;
-            margin-top: 30px;
-            text-align: center;
-            color: whitesmoke;
-            font-family: 'Songti', '宋体', serif;
-        }
-
-        .head-title-logo {
-            text-align: center;
-            color: whitesmoke;
-            font-family: 'Songti', '宋体', serif;
-            font-size: 36px;
-            font-weight: bold;
-            margin: 20px 0;
-        }
-
-        .title {
-            color: white;
-        }
-
-        .footer {
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            width: 100%;
-            padding: 10px;
-            color: white;
-            text-align: center;
-            font-family: 'Songti', '宋体', serif;
-            position: absolute;
-            bottom: 0;
-            left: 0;
-        }
-
-        @media (max-width: 768px) {
-            .head .head-title {
-                font-size: 16px;
-            }
-        }
-
-        @media (max-width: 480px) {
-            .head .head-title {
-                font-size: 12px;
-            }
-        }
-
-        @media (max-width: 1920px) {
-            .head .head-title {
-                font-size: 30px;
-            }
-        }
-    </style>
-    <!--@formatter:off-->
-    {if $background}
-    <style type="text/css">
-        body {
-            background-image: url('{$background}');
-            background-image: url("./img/bg.jpg");
-            filter: brightness(1.2); /* 调整亮度 */
-        }
-    </style>
-    {/if}
-    <!--@formatter:on-->
-    <script>
-        // 禁止页面缩放
-        document.addEventListener('wheel', function(e) {
-            if (e.ctrlKey) {
-                e.preventDefault();
-            }
-        }, { passive: false });
-
-        // 禁止触摸缩放
-        document.addEventListener('gesturestart', function(e) {
-            e.preventDefault();
-        });
-        document.addEventListener('gesturechange', function(e) {
-            e.preventDefault();
-        });
-        document.addEventListener('gestureend', function(e) {
-            e.preventDefault();
-        });
-    </script>
-</head>
-<body>
-<div class="head">
-    <p class="head-title">浙江新华数码印务有限公司</p>
-</div>
-<div class="login-wrapper">
-    <div class="tmbg">
-    </div>
-    <div class="login-screen">
-        <p class="head-title-logo">生产经营驾驶舱系统</p>
-        <div>
-            <div class="login-form">
-                <p id="profile-name" class="profile-name-card"></p>
-                <form action="" method="post" id="login-form">
-                    <!--@AdminLoginFormBegin-->
-                    <div id="errtips" class="hide"></div>
-                    {:token()}
-                    <div class="input-group">
-                        <div class="input-group-addon">
-                            <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
-                        </div>
-                        <input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}" name="username" autocomplete="off" value="" data-rule="{:__('Username')}:required;username"/>
-                    </div>
-
-                    <div class="input-group">
-                        <div class="input-group-addon">
-                            <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
-                        </div>
-                        <input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;length(4~30)"/>
-                    </div>
-
-                    {if $Think.config.fastadmin.login_captcha}
-                    <div class="input-group">
-                        <div class="input-group-addon">
-                            <span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span>
-                        </div>
-                        <input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length})" autocomplete="off"/>
-                        <span class="input-group-addon" style="padding:0;border:none;cursor:pointer;">
-                            <img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="100" height="30" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
-                        </span>
-                    </div>
-                    {/if}
-
-                    <div class="form-group">
-                        <button type="submit" class="btn btn-success btn-lg btn-block" style="margin-top: 6px;">登录</button>
-                    </div>
-                    <!--@AdminLoginFormEnd-->
-                </form>
-            </div>
-        </div>
-    </div>
-</div>
-<div class="footer">
-    <p>Copyright @ 杭州可集达科技有限公司 2023-{:date('Y',time())} 版权所有 <a href="https://beian.miit.gov.cn" target="_blank">{$site.beian|htmlentities}</a></p>
-</div>
-{include file="common/script" /}
-</body>
-</html>

+ 13 - 0
application/common/controller/Backend.php

@@ -3,6 +3,7 @@
 namespace app\common\controller;
 namespace app\common\controller;
 
 
 use app\admin\library\Auth;
 use app\admin\library\Auth;
+use app\admin\library\SystemLogin;
 use think\Config;
 use think\Config;
 use think\Controller;
 use think\Controller;
 use think\Hook;
 use think\Hook;
@@ -245,6 +246,18 @@ class Backend extends Controller
             $admin['avatar'] = resolve_admin_avatar($admin['avatar_raw']);
             $admin['avatar'] = resolve_admin_avatar($admin['avatar_raw']);
         }
         }
         $this->assign('admin', $admin);
         $this->assign('admin', $admin);
+        // 按登录所属系统套用后台风格(1白/2黑)
+        if ($this->auth && $this->auth->isLogin()) {
+            $this->view->assign(SystemLogin::applyTheme($this->auth));
+        } else {
+            $this->view->assign([
+                'login_system'      => 'collab',
+                'system_style'      => 1,
+                'adminskin'         => Config::get('fastadmin.adminskin') ?: 'skin-blue-light',
+                'load_xinhua_theme' => true,
+                'system_brand'      => '生产协同系统',
+            ]);
+        }
     }
     }
 
 
     /**
     /**

+ 15 - 1
application/extra/mproc.php

@@ -22,6 +22,20 @@ return [
     // 'smsbao_pass' => '明文密码',
     // 'smsbao_pass' => '明文密码',
     // 仅本地调试:与登录页输入的 6 位验证码一致时跳过短信缓存校验;上线请注释或删除
     // 仅本地调试:与登录页输入的 6 位验证码一致时跳过短信缓存校验;上线请注释或删除
     'mock_sms_code' => '',
     'mock_sms_code' => '',
-    // 开标双重验证:可选验证人所属角色组根 id(含全部子组),默认 10(管理员/业务员/审批员/采购员等)
+    // 开标双重验证:可选验证人所属角色组根 id(含全部子组)
     'bid_open_auth_group_root_id' => 10,
     'bid_open_auth_group_root_id' => 10,
+    // 登录入口与角色组(页面标题对应哪个系统,就只允许该组账号)
+    // index/login          → 生产协同系统 → 根组 10
+    // index/cockpitlogin   → 生产经营驾驶舱 → 根组 2(及子组;如 xinhua=组3)
+    'collab_login_group_root_id' => 10,
+    'cockpit_login_group_root_ids' => [2],
+    // 驾驶舱是否允许规则为 * 的超管账号
+    'cockpit_allow_rule_super' => true,
+    // 系统风格:数字 1=白色 2=黑色原版侧栏
+    'collab_system_style'  => 1, // 生产协同
+    'cockpit_system_style' => 2, // 生产经营驾驶舱系统
+    // 左侧栏名
+    'collab_system_brand'  => '生产协同系统',
+    'cockpit_system_brand' => '浙江印刷集团有限公司',
 ];
 ];
+

+ 1 - 1
application/extra/site.php

@@ -5,7 +5,7 @@ return array (
   'brand_logo' => 'https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/logo1.png',
   'brand_logo' => 'https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/logo1.png',
   'beian' => '',
   'beian' => '',
   'cdnurl' => '',
   'cdnurl' => '',
-  'version' => '1.4.1',
+  'version' => '1.4.5',
   'timezone' => 'Asia/Shanghai',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'forbiddenip' => '',
   'languages' => 
   'languages' => 

+ 29 - 0
application/extra/systems.php

@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * 双系统登录隔离(按角色组根节点)
+ *
+ * 入口与页面一一对应:
+ * collab  = 生产协同系统         → index/login          → login.html
+ * cockpit = 生产经营驾驶舱系统   → index/cockpitlogin   → 生产经营驾驶舱系统.html
+ *
+ * allowed_group_roots:用户属于「该根组或其任意子组」才可登录该入口。
+ * allow_rule_super:是否允许后台规则为 * 的超管(通常仅驾驶舱)。
+ *
+ * 注意:不要把驾驶舱根配成 1,否则生产协同账号也会被放行。
+ * 后台风格见 mproc.collab_system_style / cockpit_system_style(1白 / 2黑)。
+ */
+return [
+    'collab' => [
+        'name'                => '生产协同系统',
+        'brand'               => '生产协同系统',
+        'allowed_group_roots' => [10],
+        'allow_rule_super'    => false,
+    ],
+    'cockpit' => [
+        'name'                => '生产经营驾驶舱系统',
+        'brand'               => '浙江印刷集团有限公司',
+        'allowed_group_roots' => [2],
+        'allow_rule_super'    => true,
+    ],
+];

+ 79 - 4
public/assets/js/backend/procuremen.js

@@ -1107,11 +1107,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return;
                     return;
                 }
                 }
                 var deleteMsg = procuremenBuildConfirmMsg(
                 var deleteMsg = procuremenBuildConfirmMsg(
-                    '从初选列表移除以下 <strong>' + selDel.length + '</strong> 条工序:',
+                    '除以下 <strong>' + selDel.length + '</strong> 条工序,确认后将无法恢复:',
                     procuremenBuildConfirmRowsTable(selDel)
                     procuremenBuildConfirmRowsTable(selDel)
                 );
                 );
                 Layer.confirm(deleteMsg, $.extend({}, procuremenConfirmLayerBase, {
                 Layer.confirm(deleteMsg, $.extend({}, procuremenConfirmLayerBase, {
-                    title: '删除确认',
+                    title: '删除',
                     btn: ['确定删除', '取消']
                     btn: ['确定删除', '取消']
                 }), function (idxDel) {
                 }), function (idxDel) {
                     Layer.close(idxDel);
                     Layer.close(idxDel);
@@ -1140,7 +1140,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return;
                     return;
                 }
                 }
                 var finishBatchMsg = procuremenBuildConfirmMsg(
                 var finishBatchMsg = procuremenBuildConfirmMsg(
-                    '将对以下 <strong>' + selFin.length + '</strong> 条工序标记为<strong>已完结</strong>:',
+                    '将对以下 <strong>' + selFin.length + '</strong> 条工序标记为<strong>已完结</strong>:',
                     procuremenBuildConfirmRowsTable(selFin)
                     procuremenBuildConfirmRowsTable(selFin)
                 );
                 );
                 Layer.confirm(finishBatchMsg, $.extend({}, procuremenConfirmLayerBase, {
                 Layer.confirm(finishBatchMsg, $.extend({}, procuremenConfirmLayerBase, {
@@ -2941,6 +2941,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 return ($('#audit-sys-rq').val() || '').trim();
                 return ($('#audit-sys-rq').val() || '').trim();
             }
             }
 
 
+            /** 是否已过报价截止时间(无截止时间时按已到处理,与后端一致) */
+            function auditIssueQuoteDeadlineReached() {
+                var sysRq = auditIssueSysRq();
+                if (!sysRq) {
+                    return true;
+                }
+                var normalized = String(sysRq).replace(/T/, ' ').replace(/-/g, '/');
+                var ts = Date.parse(normalized);
+                if (!ts || isNaN(ts)) {
+                    return true;
+                }
+                return Date.now() >= ts;
+            }
+
             function auditIssueValidateSysRq() {
             function auditIssueValidateSysRq() {
                 $('#audit-sys-rq-wrap').find('.procuremen-sys-rq-date').trigger('change');
                 $('#audit-sys-rq-wrap').find('.procuremen-sys-rq-date').trigger('change');
                 var sysRq = auditIssueSysRq();
                 var sysRq = auditIssueSysRq();
@@ -3034,7 +3048,55 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     Toastr.info('本单已完成开标验证');
                     Toastr.info('本单已完成开标验证');
                     return;
                     return;
                 }
                 }
+                if (!auditIssueQuoteDeadlineReached()) {
+                    Toastr.warning('未到截止时间');
+                    return;
+                }
                 var users = [];
                 var users = [];
+                var BID_OPEN_LAST_USERS_KEY = 'xh_procuremen_bid_open_last_users';
+
+                function auditIssueLoadLastBidOpenUsers() {
+                    try {
+                        var raw = localStorage.getItem(BID_OPEN_LAST_USERS_KEY);
+                        if (!raw) {
+                            return {v1: 0, v2: 0};
+                        }
+                        var parsed = JSON.parse(raw);
+                        return {
+                            v1: parseInt(parsed && parsed.v1, 10) || 0,
+                            v2: parseInt(parsed && parsed.v2, 10) || 0
+                        };
+                    } catch (e) {
+                        return {v1: 0, v2: 0};
+                    }
+                }
+
+                function auditIssueSaveLastBidOpenUsers(v1, v2) {
+                    v1 = parseInt(v1, 10) || 0;
+                    v2 = parseInt(v2, 10) || 0;
+                    if (!v1 && !v2) {
+                        return;
+                    }
+                    try {
+                        localStorage.setItem(BID_OPEN_LAST_USERS_KEY, JSON.stringify({v1: v1, v2: v2}));
+                    } catch (e) {
+                    }
+                }
+
+                function auditIssueUserExists(uid) {
+                    uid = parseInt(uid, 10) || 0;
+                    if (!uid) {
+                        return false;
+                    }
+                    var found = false;
+                    $.each(users, function (i, u) {
+                        if ((parseInt(u.id, 10) || 0) === uid) {
+                            found = true;
+                            return false;
+                        }
+                    });
+                    return found;
+                }
 
 
                 function auditIssueRenderBidOpenUserOptions(excludeId, selectedId) {
                 function auditIssueRenderBidOpenUserOptions(excludeId, selectedId) {
                     var opts = '<option value="">请选择</option>';
                     var opts = '<option value="">请选择</option>';
@@ -3082,6 +3144,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }
                     }
                     $('#bid-open-user-1').html(auditIssueRenderBidOpenUserOptions(v2, v1));
                     $('#bid-open-user-1').html(auditIssueRenderBidOpenUserOptions(v2, v1));
                     $('#bid-open-user-2').html(auditIssueRenderBidOpenUserOptions(v1, v2));
                     $('#bid-open-user-2').html(auditIssueRenderBidOpenUserOptions(v1, v2));
+                    auditIssueSaveLastBidOpenUsers(v1, v2);
+                }
+
+                function auditIssueApplyLastBidOpenUsers() {
+                    var last = auditIssueLoadLastBidOpenUsers();
+                    var v1 = auditIssueUserExists(last.v1) ? last.v1 : 0;
+                    var v2 = auditIssueUserExists(last.v2) ? last.v2 : 0;
+                    if (v1 && v2 && v1 === v2) {
+                        v2 = 0;
+                    }
+                    $('#bid-open-user-1').html(auditIssueRenderBidOpenUserOptions(v2, v1));
+                    $('#bid-open-user-2').html(auditIssueRenderBidOpenUserOptions(v1, v2));
                 }
                 }
 
 
                 var html = [
                 var html = [
@@ -3134,6 +3208,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 code2: c2
                                 code2: c2
                             })
                             })
                         }, function (data, ret) {
                         }, function (data, ret) {
+                            auditIssueSaveLastBidOpenUsers(v1, v2);
                             Layer.close(idx);
                             Layer.close(idx);
                             var msg = (ret && ret.msg) ? ret.msg : '开标验证成功';
                             var msg = (ret && ret.msg) ? ret.msg : '开标验证成功';
                             Toastr.success(msg);
                             Toastr.success(msg);
@@ -3159,7 +3234,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         return false;
                         return false;
                     }
                     }
                     users = payload.users || [];
                     users = payload.users || [];
-                    auditIssueSyncBidOpenUserSelects();
+                    auditIssueApplyLastBidOpenUsers();
                     $(document).off('change.bidOpenUser', '#bid-open-user-1, #bid-open-user-2')
                     $(document).off('change.bidOpenUser', '#bid-open-user-1, #bid-open-user-2')
                         .on('change.bidOpenUser', '#bid-open-user-1', function () {
                         .on('change.bidOpenUser', '#bid-open-user-1', function () {
                             auditIssueResetBidOpenSendBtn($('.btn-send-code[data-slot="1"]'));
                             auditIssueResetBidOpenSendBtn($('.btn-send-code[data-slot="1"]'));