liuhairui пре 2 недеља
родитељ
комит
2792dc2f47

+ 5 - 6
application/admin/controller/Procuremen.php

@@ -3576,11 +3576,9 @@ class Procuremen extends Backend
             } catch (\Throwable $e) {
             }
             try {
-                $detRows = Db::table('purchase_order_detail')
-                    ->where('scydgy_id', 'in', $idList)
-                    ->field('scydgy_id, COUNT(*) AS cnt')
-                    ->group('scydgy_id')
-                    ->select();
+                $detQuery = Db::table('purchase_order_detail')->where('scydgy_id', 'in', $idList);
+                $this->applyActivePurchaseOrderDetailWhere($detQuery);
+                $detRows = $detQuery->field('scydgy_id, COUNT(*) AS cnt')->group('scydgy_id')->select();
                 if (is_array($detRows)) {
                     foreach ($detRows as $dr) {
                         if (!is_array($dr)) {
@@ -5837,10 +5835,11 @@ class Procuremen extends Backend
             ->where('scydgy_id', 'in', array_keys($sids))
             ->update([
                 'wflow_status'      => 0,
-                'status'            => '',
+                'status'            => 0,
                 'pick_company_name' => '',
                 'pick_time'         => null,
                 'sys_rq'            => null,
+                'mod_rq'            => null,
             ]);
         $this->voidPurchaseOrderDetailsForScydgyIds(array_keys($sids));
 

+ 1 - 1
application/api/controller/Procuremen.php

@@ -73,7 +73,7 @@ class Procuremen extends Controller
                 ->where([
                     'a.bwjg'    => 1,
                     'a.iEndBz'  => 0,
-                    'a.iType'   => 0,
+                    'a.iType'   => 0,   
                     'a.iStatus' => 10,
                 ])
                 ->where('a.dStamp', '>=', $startTime)

+ 241 - 14
application/index/controller/Index.php

@@ -42,37 +42,236 @@ class Index extends Frontend
     }
 
     /**
-     * 当前手机端登录用户;未登录返回 null
+     * 当前手机端登录用户;未登录返回 null(支持 Cookie 令牌 + 7 天记住登录)
      */
     protected function mprocGetUser()
+    {
+        $token = $this->mprocReadTokenFromRequest();
+        if ($token !== '') {
+            $user = $this->mprocLoadUserByToken($token);
+            if ($user) {
+                $this->mprocTouchLoginState($user, $token);
+
+                return $user;
+            }
+        }
+
+        $user = $this->mprocUserFromRememberCookie();
+        if (!$user) {
+            return null;
+        }
+        $token = bin2hex(random_bytes(16));
+        $this->mprocTouchLoginState($user, $token);
+        $user['token'] = $token;
+
+        return $user;
+    }
+
+    protected function mprocReadTokenFromRequest(): string
     {
         $token = Session::get('mproc_token');
         if ($token === null || $token === '') {
             $token = Cookie::get('mproc_token');
         }
         if ($token === null || $token === '') {
-            return null;
+            $token = $this->request->header('X-Mproc-Token');
         }
-        $token = preg_replace('/[^a-f0-9]/i', '', (string)$token);
-        if (strlen($token) < 16) {
-            return null;
+        if ($token === null || $token === '') {
+            $token = $this->request->request('mproc_token', '');
         }
+        $token = preg_replace('/[^a-f0-9]/i', '', (string)$token);
+
+        return strlen($token) >= 16 ? $token : '';
+    }
+
+    /**
+     * @return array<string, mixed>|null
+     */
+    protected function mprocLoadUserByToken(string $token): ?array
+    {
         $user = Cache::get('mproc_u_' . $token);
         if (!is_array($user)) {
-            return null;
+            $user = $this->mprocUserFromRememberCookie();
+            if (!$user) {
+                return null;
+            }
         }
-        // 手机登录必有 phone;账号密码登录必有 username
         if (empty($user['phone']) && empty($user['account']) && empty($user['username'])) {
             return null;
         }
         if (time() - (int)($user['login_time'] ?? 0) > $this->mprocTtlSeconds) {
             $this->mprocClearLogin($token);
+
             return null;
         }
         $user['token'] = $token;
+
         return $user;
     }
 
+    /**
+     * 滑动续期:刷新 Cache / Session / Cookie(保留 7 天)
+     *
+     * @param array<string, mixed> $user
+     */
+    protected function mprocTouchLoginState(array $user, string $token): void
+    {
+        $user['login_time'] = time();
+        Cache::set('mproc_u_' . $token, $user, $this->mprocTtlSeconds + 86400);
+        Session::set('mproc_token', $token);
+        $this->mprocSetTokenCookie($token);
+        $this->mprocSetRememberCookie($user);
+    }
+
+    protected function mprocSetTokenCookie(string $token): void
+    {
+        Cookie::set('mproc_token', $token, [
+            'expire'   => $this->mprocTtlSeconds,
+            'path'     => '/',
+            'httponly' => true,
+            'samesite' => 'Lax',
+        ]);
+    }
+
+    /**
+     * @param array<string, mixed> $user
+     */
+    protected function mprocSetRememberCookie(array $user): void
+    {
+        Cookie::set('mproc_remember', $this->mprocPackRememberCookie($user), [
+            'expire'   => $this->mprocTtlSeconds,
+            'path'     => '/',
+            'httponly' => true,
+            'samesite' => 'Lax',
+        ]);
+    }
+
+    protected function mprocAuthSignSecret(): string
+    {
+        $key = trim((string)Config::get('mproc.auth_sign_key'));
+        if ($key === '') {
+            $key = (string)Config::get('database.database') . '|' . (string)Config::get('database.hostname') . '|mproc';
+        }
+
+        return hash('sha256', $key);
+    }
+
+    /**
+     * @param array<string, mixed> $user
+     */
+    protected function mprocPackRememberCookie(array $user): string
+    {
+        $payload = [
+            'uid'   => (int)($user['customer_user_id'] ?? $user['customer_id'] ?? 0),
+            'phone' => trim((string)($user['phone'] ?? '')),
+            'uname' => trim((string)($user['username'] ?? $user['account'] ?? '')),
+            'admin' => !empty($user['is_admin']) ? 1 : 0,
+            'lt'    => time(),
+        ];
+        $b64 = rtrim(strtr(base64_encode(json_encode($payload, JSON_UNESCAPED_UNICODE)), '+/', '-_'), '=');
+        $sig = hash_hmac('sha256', $b64, $this->mprocAuthSignSecret());
+
+        return $b64 . '.' . $sig;
+    }
+
+    /**
+     * @return array<string, mixed>|null
+     */
+    protected function mprocUserFromRememberCookie(): ?array
+    {
+        $raw = Cookie::get('mproc_remember');
+        if ($raw === null || $raw === '') {
+            return null;
+        }
+        $parts = explode('.', (string)$raw, 2);
+        if (count($parts) !== 2) {
+            return null;
+        }
+        $b64 = $parts[0];
+        $sig = $parts[1];
+        if (!hash_equals(hash_hmac('sha256', $b64, $this->mprocAuthSignSecret()), $sig)) {
+            return null;
+        }
+        $pad = strlen($b64) % 4;
+        if ($pad > 0) {
+            $b64 .= str_repeat('=', 4 - $pad);
+        }
+        $json = base64_decode(strtr($b64, '-_', '+/'), true);
+        if ($json === false || $json === '') {
+            return null;
+        }
+        $payload = json_decode($json, true);
+        if (!is_array($payload)) {
+            return null;
+        }
+        $lt = (int)($payload['lt'] ?? 0);
+        if ($lt <= 0 || time() - $lt > $this->mprocTtlSeconds) {
+            return null;
+        }
+
+        return $this->mprocRebuildUserFromRememberPayload($payload, $lt);
+    }
+
+    /**
+     * @param array<string, mixed> $payload
+     * @return array<string, mixed>|null
+     */
+    protected function mprocRebuildUserFromRememberPayload(array $payload, int $loginTime): ?array
+    {
+        if (!empty($payload['admin'])) {
+            $uname = trim((string)($payload['uname'] ?? ''));
+            if ($uname === '') {
+                return null;
+            }
+            try {
+                $row = Db::name('admin')->where('username', $uname)->find();
+            } catch (\Throwable $e) {
+                $row = null;
+            }
+            if (!is_array($row) || ($row['status'] ?? '') === 'hidden') {
+                return null;
+            }
+
+            return [
+                'phone'            => trim((string)($row['mobile'] ?? '')),
+                'company_name'     => '',
+                'username'         => $uname,
+                'customer_user_id' => 0,
+                'login_type'       => 'remember',
+                'is_admin'         => 1,
+                'login_time'       => $loginTime,
+            ];
+        }
+
+        $cid = (int)($payload['uid'] ?? 0);
+        if ($cid > 0) {
+            try {
+                $cu = Db::table('customer')->where('id', $cid)->find();
+            } catch (\Throwable $e) {
+                $cu = null;
+            }
+            if (is_array($cu) && $cu !== [] && $this->mprocCustomerUserActive($cu)) {
+                $user = $this->mprocLoginPayloadFromCustomer($cu, 'remember');
+                $user['login_time'] = $loginTime;
+
+                return $user;
+            }
+        }
+
+        $phone = trim((string)($payload['phone'] ?? ''));
+        if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) {
+            $cu = $this->mprocFindCustomerUserByMobile($phone);
+            if ($cu) {
+                $user = $this->mprocLoginPayloadFromCustomer($cu, 'remember');
+                $user['login_time'] = $loginTime;
+
+                return $user;
+            }
+        }
+
+        return null;
+    }
+
     protected function mprocClearLogin($token)
     {
         if ($token !== null && $token !== '') {
@@ -80,6 +279,7 @@ class Index extends Frontend
         }
         Session::delete('mproc_token');
         Cookie::delete('mproc_token');
+        Cookie::delete('mproc_remember');
     }
 
     /**
@@ -379,16 +579,17 @@ class Index extends Frontend
         }
         $token = bin2hex(random_bytes(16));
         $userData['login_time'] = time();
-        Cache::set('mproc_u_' . $token, $userData, $this->mprocTtlSeconds + 86400);
-        Session::set('mproc_token', $token);
-        Cookie::set('mproc_token', $token, $this->mprocTtlSeconds);
+        $this->mprocTouchLoginState($userData, $token);
 
         $postR = $this->mprocSanitizeRedirectUrl($this->request->post('redirect', ''));
         $sessR = $this->mprocSanitizeRedirectUrl((string)Session::get('mproc_intended_url', ''));
         Session::delete('mproc_intended_url');
         $raw = $postR !== '' ? $postR : $sessR;
         $jump = $this->mprocBuildAfterLoginIndexUrl($raw);
-        $this->success('登录成功', $jump);
+        $this->success('登录成功', $jump, [
+            'mproc_token' => $token,
+            'keep_days'   => max(1, (int)round($this->mprocTtlSeconds / 86400)),
+        ]);
     }
 
     /**
@@ -1183,7 +1384,7 @@ class Index extends Frontend
             if ($safe !== '') {
                 Session::set('mproc_intended_url', $safe);
             }
-            $this->redirect($this->mprocBuildLoginUrl(''));
+            $this->redirect($this->mprocBuildLoginUrl($safe));
 
             return;
         }
@@ -1307,10 +1508,10 @@ class Index extends Frontend
      */
     public function login()
     {
+        $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', ''));
         if ($this->mprocGetUser()) {
-            $this->redirect(url('index/index/index'));
+            $this->redirect($this->mprocBuildAfterLoginIndexUrl($redirect));
         }
-        $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', ''));
         if ($redirect !== '') {
             Session::set('mproc_intended_url', $redirect);
         }
@@ -1414,6 +1615,32 @@ class Index extends Frontend
         $this->mprocFinishLogin($this->mprocLoginPayloadFromCustomer($cu, 'sms'));
     }
 
+    /**
+     * 用本地保存的 token 恢复登录态(POST:mproc_token)
+     */
+    public function mprocRestore()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $token = $this->mprocReadTokenFromRequest();
+        if ($token === '') {
+            $this->error('登录已过期,请重新登录', url('index/index/login'));
+        }
+        $user = $this->mprocLoadUserByToken($token);
+        if (!$user) {
+            $user = $this->mprocUserFromRememberCookie();
+            if ($user) {
+                $token = bin2hex(random_bytes(16));
+            }
+        }
+        if (!$user) {
+            $this->error('登录已过期,请重新登录', url('index/index/login'));
+        }
+        $this->mprocTouchLoginState($user, $token);
+        $this->success('ok', '', ['mproc_token' => $token]);
+    }
+
     /**
      * 账号密码登录(POST:username、password)
      * 先 customer(account),未命中再 admin;admin 密码规则同 FastAdmin Auth::login

+ 150 - 9
application/index/view/index/index.html

@@ -204,11 +204,37 @@
             cursor: pointer;
             -webkit-tap-highlight-color: transparent;
             overflow: hidden;
+            min-height: 44px;
         }
         .date-field-shell:focus-within { border-color: #3c8dbc; }
         .date-field-shell:active { background: #fafcfd; }
+        .date-placeholder {
+            position: absolute;
+            left: 12px;
+            right: 36px;
+            top: 0;
+            bottom: 0;
+            display: flex;
+            align-items: center;
+            color: #aaa;
+            font-size: 16px;
+            line-height: 1.4;
+            pointer-events: none;
+            z-index: 1;
+        }
+        .date-field-shell.has-value .date-placeholder { display: none; }
+        .date-display-text {
+            display: none;
+            margin: 6px 0 0;
+            font-size: 13px;
+            color: #3c8dbc;
+            line-height: 1.4;
+        }
+        .date-field-shell.has-value + .date-display-text { display: block; }
         .inp-date {
             display: block;
+            position: relative;
+            z-index: 2;
             width: 100%;
             margin: 0;
             padding: 11px 36px 11px 12px;
@@ -216,10 +242,13 @@
             border-radius: 8px;
             font-size: 16px;
             line-height: 1.4;
-            min-height: 0;
+            min-height: 44px;
+            color: #222;
             background: transparent;
             cursor: pointer;
             box-sizing: border-box;
+            -webkit-appearance: none;
+            appearance: none;
         }
         .inp-date:focus { outline: none; }
         .inp-date::-webkit-calendar-picker-indicator {
@@ -240,7 +269,7 @@
 <body class="{eq name='mprocMainTab' value='me'}mproc-layout-me{else/}mproc-layout-orders{/eq}">
 <div class="bar">
     <h1></h1>
-    <a href="{:url('index/index/logout')}" class="mproc-bar-logout">退出</a>
+    <a href="{:url('index/index/logout')}" class="mproc-bar-logout" id="mproc-bar-logout">退出</a>
 </div>
 
 <div id="mproc-pane-orders" style="{eq name='mprocMainTab' value='me'}display:none{else/}display:flex{/eq}">
@@ -347,8 +376,10 @@
         <div class="modal-field">
             <label class="modal-field-label" for="inp-delivery" id="lbl-delivery">交货日期</label>
             <div class="date-field-shell" id="date-field-shell" role="button" tabindex="0" title="点击选择年月日">
+                <span class="date-placeholder" id="delivery-placeholder">请选择年月日</span>
                 <input type="date" id="inp-delivery" class="inp-date" lang="zh-CN" autocomplete="off" enterkeyhint="done" aria-labelledby="lbl-delivery">
             </div>
+            <p class="date-display-text" id="delivery-display-text" aria-live="polite"></p>
         </div>
         <div class="modal-actions">
             <button type="button" class="btn-cancel" id="edit-cancel">取消</button>
@@ -374,6 +405,86 @@
     }
     var listUrl = mprocEndpointUrl('mprocList.html');
     var saveUrl = mprocEndpointUrl('mprocSave.html');
+    var restoreUrl = mprocEndpointUrl('mprocRestore.html');
+    function mprocPersistLoginToken(ret) {
+        var d = ret && ret.data;
+        var tok = d && d.mproc_token;
+        if (!tok) {
+            return;
+        }
+        try {
+            var days = parseInt(d.keep_days, 10) || 7;
+            localStorage.setItem('mproc_token', tok);
+            localStorage.setItem('mproc_token_exp', String(Date.now() + days * 86400000));
+        } catch (ignoreStore) {
+        }
+    }
+
+    function mprocClearLoginToken() {
+        try {
+            localStorage.removeItem('mproc_token');
+            localStorage.removeItem('mproc_token_exp');
+        } catch (ignoreClr) {
+        }
+    }
+
+    function mprocAuthHeaders(extra) {
+        var h = {
+            'X-Requested-With': 'XMLHttpRequest'
+        };
+        if (extra) {
+            for (var k in extra) {
+                if (Object.prototype.hasOwnProperty.call(extra, k)) {
+                    h[k] = extra[k];
+                }
+            }
+        }
+        try {
+            var tok = localStorage.getItem('mproc_token');
+            var exp = parseInt(localStorage.getItem('mproc_token_exp'), 10) || 0;
+            if (tok && exp && Date.now() <= exp) {
+                h['X-Mproc-Token'] = tok;
+            }
+        } catch (ignoreTok) {
+        }
+        return h;
+    }
+
+    function mprocTryRestoreLoginFromStorage() {
+        var tok = '';
+        var exp = 0;
+        try {
+            tok = localStorage.getItem('mproc_token') || '';
+            exp = parseInt(localStorage.getItem('mproc_token_exp'), 10) || 0;
+        } catch (ignoreRead) {
+            return Promise.resolve(false);
+        }
+        if (!tok || !exp || Date.now() > exp) {
+            mprocClearLoginToken();
+            return Promise.resolve(false);
+        }
+        return fetch(restoreUrl, {
+            method: 'POST',
+            credentials: 'same-origin',
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+                'X-Requested-With': 'XMLHttpRequest'
+            },
+            body: 'mproc_token=' + encodeURIComponent(tok)
+        }).then(function (res) {
+            return res.json();
+        }).then(function (ret) {
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                mprocPersistLoginToken(ret);
+                return true;
+            }
+            mprocClearLoginToken();
+            return false;
+        }).catch(function () {
+            return false;
+        });
+    }
+
     var boot = {:json_encode(['main_tab' => $mprocMainTab, 'tab' => $mprocTab, 'q' => $mprocSearchQ, 'is_admin' => $mprocIsAdmin, 'can_change_pwd' => isset($mprocCanChangePwd) ? (int)$mprocCanChangePwd : 0, 'focus_eid' => isset($mprocFocusEid) ? (int)$mprocFocusEid : 0], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
     var changePwdUrl = mprocEndpointUrl('mprocChangePwd.html');
     var currentMain = boot.main_tab === 'me' ? 'me' : 'orders';
@@ -604,10 +715,9 @@
         var fetchOpts = {
             method: 'GET',
             credentials: 'same-origin',
-            headers: {
-                'X-Requested-With': 'XMLHttpRequest',
+            headers: mprocAuthHeaders({
                 'Accept': 'application/json'
-            }
+            })
         };
         if (myAbort) {
             fetchOpts.signal = myAbort.signal;
@@ -681,6 +791,7 @@
     var inpAmount = document.getElementById('inp-amount');
     var inpDelivery = document.getElementById('inp-delivery');
     var dateFieldShell = document.getElementById('date-field-shell');
+    var deliveryDisplayText = document.getElementById('delivery-display-text');
     var btnSave = document.getElementById('edit-save');
     var titleEl = document.getElementById('edit-sheet-title');
     var editMetaQty = document.getElementById('edit-meta-qty');
@@ -867,6 +978,26 @@
     }
 
     // 点交期区域弹出系统日期选择
+    function syncDeliveryFieldUi() {
+        if (!inpDelivery || !dateFieldShell) {
+            return;
+        }
+        var val = (inpDelivery.value || '').trim();
+        dateFieldShell.classList.toggle('has-value', !!val);
+        if (deliveryDisplayText) {
+            if (val) {
+                var parts = val.split('-');
+                if (parts.length === 3) {
+                    deliveryDisplayText.textContent = '已选:' + parts[0] + '年' + parseInt(parts[1], 10) + '月' + parseInt(parts[2], 10) + '日';
+                } else {
+                    deliveryDisplayText.textContent = '已选:' + val;
+                }
+            } else {
+                deliveryDisplayText.textContent = '';
+            }
+        }
+    }
+
     function openDeliveryPicker() {
         if (!inpDelivery) return;
         if (typeof inpDelivery.showPicker === 'function') {
@@ -890,6 +1021,8 @@
                 openDeliveryPicker();
             }
         });
+        inpDelivery.addEventListener('change', syncDeliveryFieldUi);
+        inpDelivery.addEventListener('input', syncDeliveryFieldUi);
     }
 
     function mprocMetaDisplayText(s) {
@@ -913,6 +1046,7 @@
         }
         inpAmount.value = mprocSanitizeAmountValue((card.getAttribute('data-amount') || '').replace(/&quot;/g, '"'));
         inpDelivery.value = deliveryToDateVal(card.getAttribute('data-delivery') || '');
+        syncDeliveryFieldUi();
         mprocRefreshAmountCeilHint();
         var no = String(card.getAttribute('data-ccydh') || '').trim();
         var name = String(card.getAttribute('data-title') || '').trim();
@@ -956,10 +1090,9 @@
             + '&delivery=' + encodeURIComponent(inpDelivery ? inpDelivery.value.trim() : '');
         return fetch(saveUrl, {
             method: 'POST',
-            headers: {
-                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
-                'X-Requested-With': 'XMLHttpRequest'
-            },
+            headers: mprocAuthHeaders({
+                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+            }),
             body: body,
             credentials: 'same-origin'
         }).then(function (r) { return r.json(); });
@@ -1121,6 +1254,14 @@
             });
         }
     }
+
+    var logoutLink = document.getElementById('mproc-bar-logout');
+    if (logoutLink) {
+        logoutLink.addEventListener('click', function () {
+            mprocClearLoginToken();
+        });
+    }
+    syncDeliveryFieldUi();
 })();
 </script>
 </body>

+ 8 - 0
application/index/view/index/login.html

@@ -261,6 +261,14 @@
 
     function goHome(ret) {
         if (ret && (ret.code === 1 || ret.code === '1')) {
+            var d = ret.data || {};
+            if (d.mproc_token) {
+                try {
+                    var days = parseInt(d.keep_days, 10) || 7;
+                    localStorage.setItem('mproc_token', d.mproc_token);
+                    localStorage.setItem('mproc_token_exp', String(Date.now() + days * 86400000));
+                } catch (eStore) {}
+            }
             if (ret.url) {
                 location.href = ret.url;
             } else {

+ 54 - 11
public/assets/js/backend/procuremen.js

@@ -1800,11 +1800,31 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             procuremenBindSysRqPicker('#review-sys-rq', 'reviewSysRq');
 
             function procuremenReviewWarn(msg) {
-                if (typeof Toastr !== 'undefined') {
-                    Toastr.warning(msg);
-                } else if (typeof Layer !== 'undefined') {
-                    Layer.msg(msg);
+                var shown = false;
+                try {
+                    if (typeof parent !== 'undefined' && parent.Toastr && typeof parent.Toastr.warning === 'function') {
+                        parent.Toastr.warning(msg);
+                        shown = true;
+                    }
+                } catch (ignoreParentToast) {
+                }
+                if (!shown) {
+                    if (typeof Toastr !== 'undefined') {
+                        Toastr.warning(msg);
+                    } else if (typeof Layer !== 'undefined') {
+                        Layer.msg(msg);
+                    }
+                }
+            }
+
+            function procuremenReviewFastRef() {
+                if (typeof Fast !== 'undefined' && Fast.api) {
+                    return Fast;
+                }
+                if (typeof parent !== 'undefined' && parent.Fast && parent.Fast.api) {
+                    return parent.Fast;
                 }
+                return null;
             }
 
             function procuremenReviewIsPickMode() {
@@ -1844,9 +1864,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 return selected;
             }
 
-            $(document).off('click.procuremenReviewSubmit', '#btn-review-submit').on('click.procuremenReviewSubmit', '#btn-review-submit', function (e) {
-                e.preventDefault();
-                e.stopPropagation();
+            function onReviewSubmitClick(e) {
+                if (e && typeof e.preventDefault === 'function') {
+                    e.preventDefault();
+                }
+                if (e && typeof e.stopPropagation === 'function') {
+                    e.stopPropagation();
+                }
                 var $rq = $('#review-sys-rq');
                 if ($rq.length && $rq.data('DateTimePicker')) {
                     try {
@@ -1880,10 +1904,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 if ($submitBtn.prop('disabled')) {
                     return;
                 }
+                var FastRef = procuremenReviewFastRef();
+                if (!FastRef || !FastRef.api) {
+                    procuremenReviewWarn('页面未就绪,请刷新后重试');
+                    return;
+                }
                 var submitBtnHtml = $submitBtn.html();
                 $submitBtn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 提交中…');
-                Fast.api.ajax({
-                    url: Fast.api.fixurl(isPickMode ? 'procuremen/picksubmit' : 'procuremen/review'),
+                FastRef.api.ajax({
+                    url: FastRef.api.fixurl(isPickMode ? 'procuremen/picksubmit' : 'procuremen/review'),
                     type: 'POST',
                     data: {
                         __token__: $('input[name=\'__token__\']').val(),
@@ -1911,11 +1940,25 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         parent.$('#table').bootstrapTable('refresh', {silent: true});
                     }
                     return false;
-                }, function () {
+                }, function (data, ret) {
                     $submitBtn.prop('disabled', false).html(submitBtnHtml);
+                    var errMsg = (ret && ret.msg) ? ret.msg : '提交失败,请重试';
+                    procuremenReviewWarn(errMsg);
                     return false;
                 });
-            });
+            }
+
+            function bindReviewFooterButtons() {
+                $(document)
+                    .off('click.procuremenReviewSubmit', '#btn-review-submit')
+                    .on('click.procuremenReviewSubmit', '#btn-review-submit', onReviewSubmitClick);
+                $('#btn-review-submit')
+                    .off('click.procuremenReviewSubmitDirect')
+                    .on('click.procuremenReviewSubmitDirect', onReviewSubmitClick);
+            }
+
+            bindReviewFooterButtons();
+            setTimeout(bindReviewFooterButtons, 350);
 
             function procuremenReviewCloseLayer() {
                 try {