liuhairui 3 dagar sedan
förälder
incheckning
9d400faa73
40 ändrade filer med 11107 tillägg och 1199 borttagningar
  1. 89 0
      application/admin/controller/Customer.php
  2. 2568 0
      application/admin/controller/Procuremen.php
  3. 9 0
      application/admin/lang/zh-cn/customer.php
  4. 5 0
      application/admin/lang/zh-cn/procuremen.php
  5. 49 0
      application/admin/model/Customer.php
  6. 12 0
      application/admin/model/Mcyd.php
  7. 52 0
      application/admin/model/Procuremen.php
  8. 27 0
      application/admin/validate/Customer.php
  9. 27 0
      application/admin/validate/Procuremen.php
  10. 48 0
      application/admin/view/customer/add.html
  11. 48 0
      application/admin/view/customer/edit.html
  12. 29 0
      application/admin/view/customer/index.html
  13. 15 7
      application/admin/view/index/login.html
  14. 531 0
      application/admin/view/procuremen/add.html
  15. 1 0
      application/admin/view/procuremen/details.html
  16. 10 0
      application/admin/view/procuremen/details_dialog_shell.html
  17. 357 0
      application/admin/view/procuremen/details_fragment.html
  18. 88 0
      application/admin/view/procuremen/details_pdf_shell.html
  19. 531 0
      application/admin/view/procuremen/edit.html
  20. 277 0
      application/admin/view/procuremen/index.html
  21. 168 0
      application/admin/view/procuremen/outward_detail.html
  22. 10 0
      application/admin/view/procuremen/outward_detail_lite_shell.html
  23. 490 0
      application/admin/view/procuremen/review.html
  24. 2 1
      application/api/controller/Fourth.php
  25. 533 211
      application/api/controller/Index.php
  26. 1 1
      application/api/controller/Machines.php
  27. 123 0
      application/api/controller/Procuremen.php
  28. 939 428
      application/api/controller/Second.php
  29. 89 0
      application/common/library/AliyunOss.php
  30. 28 1
      application/config.php
  31. 18 0
      application/extra/mproc.php
  32. 1223 174
      application/index/controller/Index.php
  33. 801 266
      application/index/view/index/index.html
  34. 325 107
      application/index/view/index/login.html
  35. 11 3
      composer.json
  36. 226 0
      public/assets/js/backend/customer.js
  37. 1347 0
      public/assets/js/backend/procuremen.js
  38. 0 0
      public/xinhua/.gitkeep
  39. BIN
      public/xinhua/2026/05/13/202603709S_23.pdf
  40. BIN
      public/xinhua/2026/05/14/202603709S_23.pdf

+ 89 - 0
application/admin/controller/Customer.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Db;
+
+/**
+ *
+ *
+ * @icon fa fa-circle-o
+ */
+class Customer extends Backend
+{
+
+    /**
+     * Customer模型对象
+     * @var \app\admin\model\Customer
+     */
+    protected $model = null;
+
+    /**
+     * 弹窗内 AJAX 拉取业务分类候选,免配菜单权限(与路由 action 小写一致)
+     */
+    protected $noNeedRight = ['company_type_options', 'companyTypeOptions'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Customer;
+
+        $action = strtolower((string)$this->request->action());
+        if (in_array($action, ['add', 'edit'], true)) {
+            try {
+                $list = $this->collectDistinctCompanyTypes();
+            } catch (\Throwable $e) {
+                $list = [];
+            }
+            $this->assign('companyTypeOptions', $list);
+        }
+    }
+
+    /**
+     * 从 customer.company_type 拆词去重,供新增/编辑多选
+     */
+    public function companyTypeOptions()
+    {
+        try {
+            $list = $this->collectDistinctCompanyTypes();
+        } catch (\Throwable $e) {
+            $list = [];
+        }
+        // 直接 JSON,避免 success 参数顺序与 isAjax 在弹层内不一致导致前端拿不到 list
+        return json(['code' => 1, 'msg' => '', 'data' => ['list' => $list]]);
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function collectDistinctCompanyTypes(): array
+    {
+        $rows = Db::name('customer')->column('company_type');
+        if (!is_array($rows)) {
+            return [];
+        }
+        $set = [];
+        foreach ($rows as $v) {
+            $v = trim((string)$v);
+            if ($v === '') {
+                continue;
+            }
+            foreach (preg_split('/[、,,]+/u', $v) as $p) {
+                $p = trim($p);
+                if ($p !== '') {
+                    $set[$p] = true;
+                }
+            }
+        }
+        $list = array_keys($set);
+        sort($list, SORT_STRING);
+        return $list;
+    }
+
+    /*
+     * 新增/编辑/删除:未在本类重写时,直接使用父类混入的 \app\admin\library\traits\Backend 中的 add/edit/del(弹窗提交、校验、入库等)。
+     * 本模块相关逻辑位置:业务分类下拉数据在 _initialize 里 assign;表单与多选在 view + public/assets/js/backend/customer.js;入库前字段处理在 model Customer。
+     * 若要在保存前后加自定义逻辑,再把 trait 里对应方法复制到本类并修改(勿只写 parent::add() 空壳)。
+     */
+}

+ 2568 - 0
application/admin/controller/Procuremen.php

@@ -0,0 +1,2568 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\library\AliyunOss;
+use PHPMailer\PHPMailer\PHPMailer;
+use think\Config;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use think\Db;
+use think\Log;
+
+class Procuremen extends Backend
+{
+
+    /**
+     * 顶部快速搜索(多列 OR LIKE);字段须为真实列名且带别名 a/b,列不宜过多以免 SQL 异常
+     */
+    protected $searchFields = 'a.ID,b.CCYDH,b.CYJMC,a.CDXMC,a.cGzzxMc';
+
+    /**
+     * Procuremen模型对象
+     * @var \app\admin\model\Procuremen
+     */
+    protected $model = null;
+
+    /**
+     * @var array
+     */
+    protected $noNeedRight = ['review', 'reviewCompanies', 'outward_detail', 'export_month_outward', 'snapshotToProcure'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Procuremen;
+
+    }
+
+    /**
+     * 手机端外发明细首页直达链接(登录后打开对应明细,免搜索)
+     * 配置 application/extra/mproc.php:mobile_base_url、mobile_index_path
+     *
+     * @param int $purchaseOrderDetailId purchase_order_detail 主键
+     */
+    protected function buildMprocMobileOrderUrl($purchaseOrderDetailId)
+    {
+        static $mprocCfgLoaded = false;
+        if (!$mprocCfgLoaded) {
+            $mprocCfgLoaded = true;
+            if (is_file(APP_PATH . 'extra/mproc.php')) {
+                Config::load(APP_PATH . 'extra/mproc.php', 'mproc');
+            }
+        }
+        $eid = (int)$purchaseOrderDetailId;
+        $base = $this->resolveMprocMobilePublicBaseUrl();
+        if ($base === '') {
+            throw new \Exception('请在 application/extra/mproc.php 中配置外网可访问的正式域名');
+        }
+        $path = trim((string)Config::get('mproc.mobile_index_path'));
+        if ($path === '') {
+            $path = '/index.php/index/index/index';
+        } else {
+            $path = '/' . ltrim($path, '/');
+        }
+        $query = ['main_tab' => 'orders'];
+        if ($eid > 0) {
+            $query['focus_eid'] = $eid;
+        }
+
+        return $base . $path . '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
+    }
+
+    /**
+     * 邮件/短信外链:须为带点的公网域名
+     */
+    protected function procuremenOutboundBaseUrlLooksValid($baseUrl)
+    {
+        $u = trim((string)$baseUrl);
+        if ($u === '' || !preg_match('#^https?://#i', $u)) {
+            return false;
+        }
+        $host = parse_url($u, PHP_URL_HOST);
+        if (!is_string($host) || $host === '') {
+            return false;
+        }
+        if (strcasecmp($host, 'localhost') === 0) {
+            return false;
+        }
+        if (preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) {
+            return false;
+        }
+        // 无点主机名(xh、erp 等)在小米/企业邮安全跳转中常报 Invalid url
+        if (strpos($host, '.') === false) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 解析用于外发邮件/短信的手机端站点根(https://域名 无末尾斜杠)
+     */
+    protected function resolveMprocMobilePublicBaseUrl()
+    {
+        $candidates = [];
+        $candidates[] = trim((string)Config::get('mproc.mobile_base_url'));
+        $candidates[] = trim((string)Config::get('site.indexurl'));
+        $cdn = trim((string)Config::get('site.cdnurl'));
+        if ($cdn !== '' && preg_match('#^https?://#i', $cdn)) {
+            $candidates[] = rtrim($cdn, '/');
+        }
+        $reqBase = rtrim($this->request->scheme() . '://' . $this->request->host(), '/');
+        $candidates[] = $reqBase;
+
+        foreach ($candidates as $c) {
+            if ($c === '') {
+                continue;
+            }
+            $c = rtrim($c, '/');
+            if ($this->procuremenOutboundBaseUrlLooksValid($c)) {
+                return $c;
+            }
+        }
+
+        Log::write('外发邮件手机链接:未配置有效 mproc.mobile_base_url,且当前访问主机不适合作为外链,请在 application/extra/mproc.php 设置 https 正式域名', 'warning');
+
+        return '';
+    }
+
+    /**
+     * 排除 mcyd.dputrecord 空串、零日期等非法值,避免 MySQL 1525(DATE_FORMAT/BETWEEN 遇 '' 报错)
+     */
+    protected function whereMcydDputrecordValid($query, $alias = 'a')
+    {
+        $c = $alias . '.dStamp';
+        // 先转成 CHAR 再判断,避免 DATETIME 列里存 '' / 0000-00-00 时参与日期函数报错
+        return $query->whereRaw(
+            $c . ' IS NOT NULL'
+            . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) <> \'\''
+            . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) NOT LIKE \'0000-00-00%\''
+            . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) REGEXP \'^[12][0-9]{3}-[01][0-9]-[0-3][0-9]\''
+        );
+    }
+
+    /**
+     * 左侧菜单:仅「近 12 个自然月(含当月)」
+     */
+    protected function getIndexSidebarYearMonths()
+    {
+        $ymSet = [];
+        $anchor = strtotime(date('Y-m-01'));
+        for ($i = 0; $i < 12; $i++) {
+            $ym = date('Y-m', strtotime("-{$i} month", $anchor));
+            $ymSet[$ym] = true;
+        }
+        $windowKeys = array_keys($ymSet);
+        $minYm = min($windowKeys);
+        $maxYm = max($windowKeys);
+
+        $rows = [];
+        try {
+            $query = Db::table('scydgy')
+                ->alias('a')
+                ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
+                ->where([
+                    'a.bwjg'    => 1,
+                    'a.iEndBz'  => 0,
+                    'a.iType'   => 0,
+                    'a.iStatus' => 10,
+                ]);
+            $this->whereMcydDputrecordValid($query, 'a');
+            $rows = $query->field("DATE_FORMAT(a.dputrecord, '%Y-%m') as ym")
+                ->group("DATE_FORMAT(a.dputrecord, '%Y-%m')")
+                ->orderRaw("DATE_FORMAT(a.dputrecord, '%Y-%m') DESC")
+                ->select();
+        } catch (\Exception $e) {
+            $rows = [];
+        }
+        foreach ($rows as $r) {
+            $ym = isset($r['ym']) ? trim((string)$r['ym']) : '';
+            if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+                continue;
+            }
+            if (strcmp($ym, $minYm) >= 0 && strcmp($ym, $maxYm) <= 0) {
+                $ymSet[$ym] = true;
+            }
+        }
+
+        $ymList = array_keys($ymSet);
+        rsort($ymList, SORT_STRING);
+
+        $byYear = [];
+        foreach ($ymList as $ym) {
+            if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+                continue;
+            }
+            $y = substr($ym, 0, 4);
+            $m = (int)substr($ym, 5, 2);
+            if (!isset($byYear[$y])) {
+                $byYear[$y] = [];
+            }
+            $byYear[$y][] = ['ym' => $ym, 'label' => $m . '月'];
+        }
+        krsort($byYear, SORT_NUMERIC);
+        foreach ($byYear as $y => $items) {
+            usort($byYear[$y], function ($a, $b) {
+                return strcmp($b['ym'], $a['ym']);
+            });
+        }
+
+        $sidebar = [];
+        foreach ($byYear as $y => $months) {
+            $sidebar[] = ['year' => $y, 'months' => $months];
+        }
+
+        return $sidebar;
+    }
+
+    /**
+     * 列表页
+     */
+    public function index(){
+        $this->request->filter(['strip_tags', 'trim']);
+
+        if (!$this->request->isAjax()) {
+            $this->view->assign('defaultYm', date('Y-m'));
+            $this->view->assign('sidebarYearMonths', $this->getIndexSidebarYearMonths());
+            return $this->view->fetch();
+        }
+
+        if ($this->request->request('keyField')) {
+            return $this->selectpage();
+        }
+
+        list(, $sort, $order, $offset, $limit) = $this->buildparams();
+        $limit = max(10, min(500, (int)$limit));
+
+        $ym = $this->request->request('ym', date('Y-m'));
+        $ym = is_string($ym) ? trim($ym) : '';
+        if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+            $ym = date('Y-m');
+        }
+        $monthStart = $ym . '-01 00:00:00';
+        $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart));
+
+        $search = trim((string)$this->request->get('search', ''));
+        $hasActiveSearch = ($search !== '');
+        if (!$hasActiveSearch) {
+            $filterStr = $this->request->get('filter', '');
+            if ($filterStr !== '' && $filterStr !== '[]' && $filterStr !== '{}') {
+                $arr = json_decode($filterStr, true);
+                if (is_array($arr) && count($arr) > 0) {
+                    foreach ($arr as $v) {
+                        if (is_array($v)) {
+                            if (array_filter($v, function ($x) {
+                                return $x !== '' && $x !== null;
+                            })) {
+                                $hasActiveSearch = true;
+                                break;
+                            }
+                        } elseif ($v !== '' && $v !== null) {
+                            $hasActiveSearch = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        $applyMonthRange = !$hasActiveSearch;
+
+        $filterArr = (array)json_decode($this->request->get('filter', ''), true);
+        $opArr = (array)json_decode($this->request->get('op', ''), true);
+
+        /* wff_tab:all=未发;pending=已下发;picked=已选中(主表 status=1 且存在明细 status=1);done=已完结 */
+        $wffTab = trim((string)$this->request->request('wff_tab', 'all'));
+        if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) {
+            $wffTab = 'all';
+        }
+
+        try {
+            $issuedSet = $this->loadIssuedScydgySet();
+
+            if (in_array($wffTab, ['pending', 'done', 'picked'], true)) {
+                $wantStatus = $wffTab === 'pending' ? 0 : 1;
+                $pool = [];
+                $dbRows = [];
+                try {
+                    if ($wantStatus === 0) {
+                        $dbRows = Db::table('purchase_order')
+                            ->where('status', $wantStatus)
+                            ->select();
+                    } else {
+                        $dbRows = Db::table('purchase_order')
+                            ->where('status', $wantStatus)
+                            ->select();
+                    }
+                } catch (\Throwable $e) {
+                    $dbRows = [];
+                }
+                if (!is_array($dbRows)) {
+                    $dbRows = [];
+                }
+                if ($wffTab === 'picked') {
+                    $pickedSidSet = $this->loadScydgyIdsWithPickedSupplierDetail();
+                    $dbRows = array_values(array_filter($dbRows, function ($dbRow) use ($pickedSidSet) {
+                        if (!is_array($dbRow)) {
+                            return false;
+                        }
+                        $sid = (int)($dbRow['scydgy_id'] ?? 0);
+                        if ($sid <= 0 && isset($dbRow['SCYDGY_ID'])) {
+                            $sid = (int)$dbRow['SCYDGY_ID'];
+                        }
+
+                        return $sid > 0 && isset($pickedSidSet[$sid]);
+                    }));
+                }
+                $dStampMap = [];
+                $sidList = [];
+                foreach ($dbRows as $tmpRow) {
+                    if (is_array($tmpRow) && isset($tmpRow['scydgy_id'])) {
+                        $sid = (int)$tmpRow['scydgy_id'];
+                        if ($sid > 0) {
+                            $sidList[$sid] = true;
+                        }
+                    }
+                }
+                if ($sidList !== []) {
+                    try {
+                        $dStampMap = Db::table('scydgy')->where('ID', 'in', array_keys($sidList))->column('dStamp', 'ID');
+                    } catch (\Throwable $e) {
+                        $dStampMap = [];
+                    }
+                }
+                foreach ($dbRows as $dbRow) {
+                    $rj = null;
+                    if (!empty($dbRow['row_json'])) {
+                        $decoded = json_decode($dbRow['row_json'], true);
+                        if (is_array($decoded)) {
+                            $rj = $decoded;
+                        }
+                    }
+                    if ($rj !== null && isset($rj['ID'])) {
+                        $rid = (int)$rj['ID'];
+                        $dsFix = isset($rj['dStamp']) ? trim((string)$rj['dStamp']) : '';
+                        if (($dsFix === '' || stripos($dsFix, '0000-00-00') === 0)
+                            && isset($dStampMap[$rid]) && trim((string)$dStampMap[$rid]) !== '') {
+                            $rj['dStamp'] = $dStampMap[$rid];
+                        }
+                        if (array_key_exists('id', $dbRow)) {
+                            $rj['purchase_order_id'] = (int)$dbRow['id'];
+                        }
+                        $pool[] = $rj;
+                        continue;
+                    }
+                    $r = [];
+                    foreach ($dbRow as $k => $v) {
+                        $lk = strtolower((string)$k);
+                        if ($lk === 'id' || $lk === 'row_json') {
+                            continue;
+                        }
+                        $r[$k] = $v;
+                    }
+                    if (isset($dbRow['scydgy_id'])) {
+                        $r['ID'] = (int)$dbRow['scydgy_id'];
+                        $sid = $r['ID'];
+                        $dsOut = '';
+                        if (isset($dStampMap[$sid])) {
+                            $t = trim((string)$dStampMap[$sid]);
+                            if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
+                                $dsOut = $dStampMap[$sid];
+                            }
+                        }
+                        if ($dsOut === '' && !empty($dbRow['createtime'])) {
+                            $ct = $dbRow['createtime'];
+                            if (is_numeric($ct) && (int)$ct > 946684800) {
+                                $dsOut = date('Y-m-d H:i:s', (int)$ct);
+                            } elseif (is_string($ct) && trim($ct) !== '') {
+                                $tc = trim($ct);
+                                if ($tc !== '' && stripos($tc, '0000-00-00') !== 0) {
+                                    $dsOut = $tc;
+                                }
+                            }
+                        }
+                        if ($dsOut !== '') {
+                            $r['dStamp'] = $dsOut;
+                        }
+                    }
+                    if (array_key_exists('id', $dbRow)) {
+                        $r['purchase_order_id'] = (int)$dbRow['id'];
+                    }
+                    $pool[] = $r;
+                }
+            } else {
+                $redis = redis();
+                $raw = $redis->get('procuremen_redis');
+                if ($raw === false || $raw === '') {
+                    return json([
+                        'total' => 0,
+                        'rows'  => [],
+                        'msg'   => '暂无缓存数据',
+                    ]);
+                }
+                $decoded = json_decode($raw, true);
+                $pool = (is_array($decoded) && isset($decoded['data']) && is_array($decoded['data']))
+                    ? $decoded['data'] : [];
+
+                // 未发:主表已有且 status 为 0/1 才从列表隐藏;status 为空(NULL)仍可在未发展示
+                try {
+                    $poIds = Db::table('purchase_order')
+                        ->whereRaw('(`status` = 0 OR `status` = 1)')
+                        ->column('scydgy_id');
+                } catch (\Throwable $e) {
+                    $poIds = [];
+                }
+                if ($pool !== [] && is_array($poIds) && $poIds !== []) {
+                    $poSet = [];
+                    foreach ($poIds as $pid) {
+                        $k = (int)$pid;
+                        if ($k > 0) {
+                            $poSet[$k] = true;
+                        }
+                    }
+                    if ($poSet !== []) {
+                        $pool = array_values(array_filter($pool, function ($r) use ($poSet) {
+                            if (!is_array($r)) {
+                                return false;
+                            }
+                            $id = (int)($r['ID'] ?? 0);
+                            if ($id <= 0) {
+                                return true;
+                            }
+
+                            return !isset($poSet[$id]);
+                        }));
+                    }
+                }
+            }
+
+            $filtered = $this->filterProcuremenIndexPool(
+                $pool,
+                $monthStart,
+                $monthEnd,
+                $applyMonthRange,
+                $search,
+                $filterArr,
+                $opArr
+            );
+
+            $sortField = 'ID';
+            if (is_string($sort) && $sort !== '') {
+                $parts = explode(',', $sort);
+                $sortField = preg_replace('/^[ab]\./i', '', trim($parts[0]));
+                if ($sortField === '') {
+                    $sortField = 'ID';
+                }
+            }
+            $ord = strtoupper((string)$order) === 'ASC' ? 1 : -1;
+            $nFiltered = count($filtered);
+            if ($nFiltered > 1) {
+                // 默认按主键 ID 排序时,用 array_multisort 比 usort+闭包略省开销
+                if ($sortField === 'ID') {
+                    $sortKeys = [];
+                    foreach ($filtered as $i => $row) {
+                        $sortKeys[$i] = (int)($row['ID'] ?? 0);
+                    }
+                    $dir = $ord === 1 ? SORT_ASC : SORT_DESC;
+                    array_multisort($sortKeys, $dir, SORT_NUMERIC, $filtered);
+                } else {
+                    usort($filtered, function ($a, $b) use ($sortField, $ord) {
+                        $va = $a[$sortField] ?? null;
+                        $vb = $b[$sortField] ?? null;
+                        $sa = (string)$va;
+                        $sb = (string)$vb;
+                        if ($sa === $sb) {
+                            return 0;
+                        }
+                        return strcmp($sa, $sb) * $ord;
+                    });
+                }
+            }
+
+            $offset = max(0, (int)$offset);
+            $limit = max(1, (int)$limit);
+
+            $rows = array_slice($filtered, $offset, $limit);
+            foreach ($rows as &$rw) {
+                if (!is_array($rw)) {
+                    continue;
+                }
+                $rid = (int)($rw['ID'] ?? 0);
+                if ($wffTab === 'pending' || $wffTab === 'done' || $wffTab === 'picked') {
+                    $rw['_iss_out'] = true;
+                } else {
+                    $rw['_iss_out'] = $rid > 0 && isset($issuedSet[$rid]);
+                }
+            }
+            unset($rw);
+
+            if ($wffTab === 'pending' && count($rows) > 0) {
+                $idList = [];
+                foreach ($rows as $rw) {
+                    if (!is_array($rw)) {
+                        continue;
+                    }
+                    $id = (int)($rw['ID'] ?? 0);
+                    if ($id > 0) {
+                        $idList[$id] = true;
+                    }
+                }
+                $sumMap = $this->OrderDetailSummary(array_keys($idList));
+                foreach ($rows as &$rw) {
+                    if (!is_array($rw)) {
+                        continue;
+                    }
+                    $rid = (int)($rw['ID'] ?? 0);
+                    $s = isset($sumMap[$rid]) ? $sumMap[$rid] : ['cnt' => 0, 'amt' => 0, 'deliv' => 0];
+                    $rw['po_detail_count'] = $s['cnt'];
+                    $rw['po_amount_fill_cnt'] = $s['amt'];
+                    $rw['po_delivery_fill_cnt'] = $s['deliv'];
+                }
+                unset($rw);
+            }
+
+            if ($wffTab === 'picked' && count($rows) > 0) {
+                $idList = [];
+                foreach ($rows as $rw) {
+                    if (!is_array($rw)) {
+                        continue;
+                    }
+                    $id = (int)($rw['ID'] ?? 0);
+                    if ($id > 0) {
+                        $idList[$id] = true;
+                    }
+                }
+                $nameMap = $this->loadPickedSupplierCompanyByScydgyIds(array_keys($idList));
+                foreach ($rows as &$rw) {
+                    if (!is_array($rw)) {
+                        continue;
+                    }
+                    $rid = (int)($rw['ID'] ?? 0);
+                    $rw['picked_supplier_name'] = $nameMap[$rid] ?? '';
+                }
+                unset($rw);
+            }
+
+            if ($wffTab === 'all' && count($rows) > 0) {
+                $this->mergePurchaseOrder($rows);
+            }
+
+            return json([
+                'total' => count($filtered),
+                'rows'  => $rows,
+            ]);
+        } catch (\Throwable $e) {
+            return json([
+                'total' => 0,
+                'rows'  => [],
+                'msg'   => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * 已外发判定:主表 purchase_order.scydgy_id ∪ 明细 purchase_order_detail.scydgy_id(与工序列表行 ID 对应)
+     *
+     * @return array<int, true> 列表行 ID => true
+     */
+    protected function loadIssuedScydgySet()
+    {
+        $issuedSet = [];
+        foreach (['purchase_order', 'purchase_order_detail'] as $tbl) {
+            try {
+                $issuedList = Db::table($tbl)->group('scydgy_id')->column('scydgy_id');
+                if (!is_array($issuedList)) {
+                    continue;
+                }
+                foreach ($issuedList as $ids) {
+                    $n = (int)$ids;
+                    if ($n > 0) {
+                        $issuedSet[$n] = true;
+                    }
+                }
+            } catch (\Throwable $e) {
+            }
+        }
+        return $issuedSet;
+    }
+
+    /**
+     * 采购确认已选定供应商:明细 purchase_order_detail.status = 1 的工序行 scydgy_id 集合
+     *
+     * @return array<int, true>
+     */
+    protected function loadScydgyIdsWithPickedSupplierDetail(): array
+    {
+        $set = [];
+        try {
+            $rows = Db::table('purchase_order_detail')->where('status', 1)->field('scydgy_id')->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (!is_array($rows)) {
+            return $set;
+        }
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0);
+            if ($sid > 0) {
+                $set[$sid] = true;
+            }
+        }
+
+        return $set;
+    }
+
+    /**
+     * 每个工序行对应已选中供应商名称(取 status=1 的首条明细 company_name)
+     *
+     * @param int[] $scydgyIds
+     * @return array<int, string> scydgy_id => company_name
+     */
+    protected function loadPickedSupplierCompanyByScydgyIds(array $scydgyIds): array
+    {
+        $out = [];
+        $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
+        if ($scydgyIds === []) {
+            return $out;
+        }
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', $scydgyIds)
+                ->where('status', 1)
+                ->field('scydgy_id,company_name')
+                ->order('id', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            try {
+                $rows = Db::table('purchase_order_detail')
+                    ->where('scydgy_id', 'in', $scydgyIds)
+                    ->where('status', 1)
+                    ->field('scydgy_id,company_name')
+                    ->order('ID', 'asc')
+                    ->select();
+            } catch (\Throwable $e2) {
+                $rows = [];
+            }
+        }
+        if (!is_array($rows)) {
+            return $out;
+        }
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0);
+            if ($sid <= 0 || isset($out[$sid])) {
+                continue;
+            }
+            $name = trim((string)($r['company_name'] ?? ''));
+            $out[$sid] = $name;
+        }
+
+        return $out;
+    }
+
+    /**
+     * 已下发列表汇总:按工序行 ID 统计 purchase_order_detail 条数、已填金额条数、已填货期条数
+     *
+     * @param int[] $scydgyIds
+     * @return array<int, array{cnt:int, amt:int, deliv:int}>
+     */
+    protected function OrderDetailSummary(array $scydgyIds)
+    {
+        $out = [];
+        $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
+        if ($scydgyIds === []) {
+            return $out;
+        }
+        $list = [];
+        try {
+            $list = Db::table('purchase_order_detail')
+                ->where('scydgy_id', 'in', $scydgyIds)
+                ->field('id,scydgy_id,amount,delivery')
+                ->select();
+        } catch (\Throwable $e) {
+            $list = [];
+        }
+        if (!is_array($list)) {
+            return $out;
+        }
+        foreach ($list as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $ids = (int)($r['scydgy_id'] ?? 0);
+            if ($ids <= 0) {
+                continue;
+            }
+            if (!isset($out[$ids])) {
+                $out[$ids] = ['cnt' => 0, 'amt' => 0, 'deliv' => 0];
+            }
+            $out[$ids]['cnt']++;
+            $am = $r['amount'] ?? null;
+            if ($am !== null && $am !== '') {
+                if (!(is_string($am) && trim($am) === '')) {
+                    $out[$ids]['amt']++;
+                }
+            }
+            $dv = $r['delivery'] ?? null;
+            if ($dv !== null && trim((string)$dv) !== '') {
+                $out[$ids]['deliv']++;
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * 列表筛选:月份、dStamp 合法性、快速搜索、Bootstrap Table filter(与原先 Redis 分支一致)
+     *
+     * @return array
+     */
+    protected function filterProcuremenIndexPool(array $pool, $monthStart, $monthEnd, $applyMonthRange, $search, array $filterArr, array $opArr)
+    {
+        $strContains = function ($haystack, $needle) {
+            if ($needle === '') {
+                return false;
+            }
+            $haystack = (string)$haystack;
+            if (function_exists('mb_stripos')) {
+                return mb_stripos($haystack, $needle, 0, 'UTF-8') !== false;
+            }
+
+            return stripos($haystack, $needle) !== false;
+        };
+        $stripAlias = function ($f) {
+            return preg_replace('/^[ab]\./i', '', (string)$f);
+        };
+
+        $searchColNames = [];
+        foreach (explode(',', $this->searchFields) as $colRaw) {
+            $c = $stripAlias(trim($colRaw));
+            if ($c !== '') {
+                $searchColNames[] = $c;
+            }
+        }
+
+        $filtered = [];
+        foreach ($pool as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $ds = isset($r['dStamp']) ? trim((string)$r['dStamp']) : '';
+            if ($ds === '' || stripos($ds, '0000-00-00') === 0
+                || !preg_match('/^[12]\d{3}-\d{1,2}-\d{1,2}/', $ds)) {
+                continue;
+            }
+            if ($applyMonthRange) {
+                if (strcmp($ds, $monthStart) < 0 || strcmp($ds, $monthEnd) > 0) {
+                    continue;
+                }
+            }
+            if ($search !== '') {
+                $hitSearch = false;
+                foreach ($searchColNames as $c) {
+                    $cell = isset($r[$c]) ? (string)$r[$c] : '';
+                    if ($cell !== '' && $strContains($cell, $search)) {
+                        $hitSearch = true;
+                        break;
+                    }
+                }
+                if (!$hitSearch) {
+                    continue;
+                }
+            }
+            foreach ($filterArr as $fk => $fv) {
+                if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', (string)$fk)) {
+                    continue;
+                }
+                if (is_array($fv)) {
+                    continue;
+                }
+                if ($fv === '' && $fv !== '0' && $fv !== 0) {
+                    continue;
+                }
+                $sym = strtoupper(trim((string)($opArr[$fk] ?? '=')));
+                $sym = str_replace(['LIKE %...%', 'NOT LIKE %...%'], ['LIKE', 'NOT LIKE'], $sym);
+                $field = $stripAlias($fk);
+                $cell = array_key_exists($field, $r) ? $r[$field] : null;
+                $cellStr = trim((string)$cell);
+
+                switch ($sym) {
+                    case '=':
+                        if ((string)$cell !== (string)$fv) {
+                            continue 3;
+                        }
+                        break;
+                    case '<>':
+                        if ((string)$cell === (string)$fv) {
+                            continue 3;
+                        }
+                        break;
+                    case 'LIKE':
+                    case 'NOT LIKE':
+                        $needle = trim((string)$fv);
+                        $hit = ($needle !== '' && $strContains($cellStr, $needle));
+                        if ($sym === 'LIKE' && !$hit) {
+                            continue 3;
+                        }
+                        if ($sym === 'NOT LIKE' && $hit) {
+                            continue 3;
+                        }
+                        break;
+                    case '>':
+                    case '>=':
+                    case '<':
+                    case '<=':
+                        if (!is_numeric($cell) && $cellStr === '') {
+                            continue 3;
+                        }
+                        $cv = is_numeric($cell) ? (float)$cell : (float)$cellStr;
+                        $vv = (float)$fv;
+                        if ($sym === '>' && !($cv > $vv)) {
+                            continue 3;
+                        }
+                        if ($sym === '>=' && !($cv >= $vv)) {
+                            continue 3;
+                        }
+                        if ($sym === '<' && !($cv < $vv)) {
+                            continue 3;
+                        }
+                        if ($sym === '<=' && !($cv <= $vv)) {
+                            continue 3;
+                        }
+                        break;
+                    case 'BETWEEN':
+                    case 'NOT BETWEEN':
+                    case 'BETWEEN TIME':
+                    case 'NOT BETWEEN TIME':
+                        $rawR = is_array($fv) ? implode(',', $fv) : (string)$fv;
+                        $rawR = str_replace(' - ', ',', $rawR);
+                        $arr = array_slice(array_map('trim', explode(',', $rawR)), 0, 2);
+                        if (count($arr) < 2 || $arr[0] === '' || $arr[1] === '') {
+                            break;
+                        }
+                        $lo = $arr[0];
+                        $hi = $arr[1];
+                        $in = ($cellStr >= $lo && $cellStr <= $hi);
+                        $isNot = (strpos($sym, 'NOT') !== false);
+                        if ($isNot ? $in : !$in) {
+                            continue 3;
+                        }
+                        break;
+                    case 'NULL':
+                    case 'IS NULL':
+                        if ($cell !== null && $cell !== '') {
+                            continue 3;
+                        }
+                        break;
+                    case 'NOT NULL':
+                    case 'IS NOT NULL':
+                        if ($cell === null || $cell === '') {
+                            continue 3;
+                        }
+                        break;
+                    default:
+                        break;
+                }
+            }
+            $filtered[] = $r;
+        }
+
+        return $filtered;
+    }
+
+
+    /**
+     * 审核弹窗提交
+     * 查询订单 purchase_order 数据查有无记录,无则插、有则改(全部写在本方法内)
+     */
+    public function snapshotToProcure()
+    {
+        if (!$this->request->isPost() || !$this->request->isAjax()) {
+            $this->error(__('Invalid parameters'));
+        }
+        $row = json_decode($this->request->post('row_json', ''), true);
+        if (!is_array($row)) {
+            $this->error(__('Invalid parameters'));
+        }
+        try {
+            $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
+            if ($ids <= 0) {
+                throw new \Exception('无效的行主键 ID');
+            }
+
+            $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+
+            $data = [
+                'scydgy_id'     => $ids,
+                'CCYDH'         => $row['CCYDH'] ?? null,
+                'CYJMC'         => $row['CYJMC'] ?? null,
+                'CDXMC'         => $row['CDXMC'] ?? null,
+                'CGYBH'         => $row['CGYBH'] ?? null,
+                'CGYMC'         => $row['CGYMC'] ?? null,
+                'CDW'           => $row['CDW'] ?? null,
+                'NGZL'          => $row['NGZL'] ?? null,
+                'CDF'           => $row['CDF'] ?? null,
+                'cGzzxMc'       => $row['cGzzxMc'] ?? null,
+                'MBZ'           => $row['MBZ'] ?? null,
+                'bwjg'          => $row['bwjg'] ?? null,
+                'iStatus'       => $row['iStatus'] ?? null,
+                'dStamp'        => $row['dStamp'] ?? null,
+                'dputrecord'    => $row['dputrecord'] ?? null,
+                'cywyxm'        => $row['cywyxm'] ?? null,
+                'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
+                'ceilingPrice'  => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
+            ];
+
+            if ($exists) {
+                $upd = $data;
+                unset($upd['scydgy_id'], $upd['status']);
+                Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
+            } else {
+                // 新增:不写 status(空/走库默认);仅写创建时间
+                $data['createtime'] = date('Y-m-d H:i:s');
+                Db::table('purchase_order')->insert($data);
+            }
+        } catch (\Throwable $e) {
+            $this->error($e->getMessage());
+        }
+        // 操作记录仅在「审核提交」时写入(review POST + addOrderLog),此处同步不写日志,避免与下发记录重复
+        $this->success('操作成功');
+    }
+
+    /**
+     * 未发列表
+     * 「完结」或「仅保存本次数量/最高限价」:有则改、无则插
+     * POST finish=1(默认):并置 status=1;finish=0:更新时不改 status;新增不写 status。
+     */
+    public function completeDirectly()
+    {
+        if (!$this->request->isPost() || !$this->request->isAjax()) {
+            $this->error(__('参数错误'));
+        }
+        $row = json_decode($this->request->post('row_json', ''), true);
+        if (!is_array($row)) {
+            $this->error(__('Invalid parameters'));
+        }
+        $finishRaw = $this->request->post('finish', '1');
+        $asComplete = ($finishRaw === '1' || $finishRaw === 1 || $finishRaw === true);
+
+        try {
+            // 获取工序行 ID,对应 purchase_order.scydgy_id;有则改、无则插
+            $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
+            if ($ids <= 0) {
+                throw new \Exception('无效的行主键 ID');
+            }
+
+            $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+
+            $data = [
+                'scydgy_id'     => $ids,
+                'CCYDH'         => $row['CCYDH'] ?? null,
+                'CYJMC'         => $row['CYJMC'] ?? null,
+                'CDXMC'         => $row['CDXMC'] ?? null,
+                'CGYBH'         => $row['CGYBH'] ?? null,
+                'CGYMC'         => $row['CGYMC'] ?? null,
+                'CDW'           => $row['CDW'] ?? null,
+                'NGZL'          => $row['NGZL'] ?? null,
+                'CDF'           => $row['CDF'] ?? null,
+                'cGzzxMc'       => $row['cGzzxMc'] ?? null,
+                'MBZ'           => $row['MBZ'] ?? null,
+                'bwjg'          => $row['bwjg'] ?? null,
+                'iStatus'       => $row['iStatus'] ?? null,
+                'dStamp'        => $row['dStamp'] ?? null,
+                'dputrecord'    => $row['dputrecord'] ?? null,
+                'cywyxm'        => $row['cywyxm'] ?? null,
+                'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
+                'ceilingPrice'  => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
+            ];
+
+            if ($asComplete) {
+                $data['status'] = 1;
+            }
+
+            if ($exists) {
+                $upd = $data;
+                unset($upd['scydgy_id']);
+                if (!$asComplete) {
+                    unset($upd['status']);
+                }
+                Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
+            } else {
+                // 新增:不写 status;完结时 $data 已含 status=1
+                $data['createtime'] = date('Y-m-d H:i:s');
+                Db::table('purchase_order')->insert($data);
+            }
+        } catch (\Throwable $e) {
+            $this->error($e->getMessage());
+        }
+        $poIdLog = null;
+        try {
+            $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+            if (is_array($rpo)) {
+                $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
+                $poIdLog = $tid > 0 ? $tid : null;
+            }
+        } catch (\Throwable $e) {
+            $poIdLog = null;
+        }
+        $q = isset($data['This_quantity']) ? trim((string)$data['This_quantity']) : '';
+        $p = isset($data['ceilingPrice']) ? trim((string)$data['ceilingPrice']) : '';
+        if ($asComplete) {
+            $this->addOrderLog(
+                $ids,
+                'mark_complete',
+                '点击「完结」,已标记为已完结;本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」',
+                $poIdLog
+            );
+        } else {
+            $this->addOrderLog(
+                $ids,
+                'save_qty_price',
+                '保存本次数量、最高限价:本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」',
+                $poIdLog
+            );
+        }
+        $this->success("操作成功");
+    }
+
+    /**
+     * 未发列表:从 purchase_order 合并已填的本次数量、最高限价(若表中有对应列)
+     *
+     * @param array<int, array> $rows 引用传递当前页行
+     */
+    protected function mergePurchaseOrder(array &$rows)
+    {
+        if ($rows === []) {
+            return;
+        }
+        $ids = [];
+        foreach ($rows as $rw) {
+            if (!is_array($rw)) {
+                continue;
+            }
+            $id = (int)($rw['ID'] ?? 0);
+            if ($id > 0) {
+                $ids[$id] = true;
+            }
+        }
+        $idList = array_keys($ids);
+        if ($idList === []) {
+            return;
+        }
+        try {
+            $list = Db::table('purchase_order')
+                ->where('scydgy_id', 'in', $idList)
+                ->field('scydgy_id,This_quantity,ceilingPrice')
+                ->select();
+        } catch (\Throwable $e) {
+            return;
+        }
+        if (!is_array($list)) {
+            return;
+        }
+        $byId = [];
+        foreach ($list as $dbRow) {
+            if (!is_array($dbRow) || !isset($dbRow['scydgy_id'])) {
+                continue;
+            }
+            $byId[(int)$dbRow['scydgy_id']] = $dbRow;
+        }
+        foreach ($rows as &$rw) {
+            if (!is_array($rw)) {
+                continue;
+            }
+            $sid = (int)($rw['ID'] ?? 0);
+            if ($sid <= 0 || !isset($byId[$sid])) {
+                continue;
+            }
+            $db = $byId[$sid];
+            if (array_key_exists('This_quantity', $db) && $db['This_quantity'] !== null && $db['This_quantity'] !== '') {
+                $rw['This_quantity'] = $db['This_quantity'];
+            }
+            if (array_key_exists('ceilingPrice', $db) && $db['ceilingPrice'] !== null && $db['ceilingPrice'] !== '') {
+                $rw['ceilingPrice'] = $db['ceilingPrice'];
+            } elseif (array_key_exists('ceiling_price', $db) && $db['ceiling_price'] !== null && $db['ceiling_price'] !== '') {
+                $rw['ceilingPrice'] = $db['ceiling_price'];
+            }
+        }
+        unset($rw);
+    }
+
+    /**
+     * 采购确认:明细选中 status=1、未选 status=0;同时将 purchase_order 主表 status 置为 1
+     */
+    public function purchaseConfirmPick()
+    {
+        if (!$this->request->isPost() || !$this->request->isAjax()) {
+            $this->error(__('Invalid parameters'));
+        }
+        $scydgyId = trim((string)$this->request->post('scydgy_id', ''));
+        $sid = (int)$scydgyId;
+        if ($sid <= 0) {
+            $this->error('参数无效');
+        }
+
+        $parseIdList = function ($raw) {
+            if (is_array($raw)) {
+                $arr = $raw;
+            } else {
+                $decoded = json_decode((string)$raw, true);
+                $arr = is_array($decoded) ? $decoded : [];
+            }
+
+            return array_values(array_unique(array_filter(array_map('intval', $arr))));
+        };
+
+        $selectedRaw = $this->request->post('selected_ids', null);
+        if ($selectedRaw === null || $selectedRaw === '') {
+            $legacy = (int)$this->request->post('selected_id', 0);
+            $selectedIds = $legacy > 0 ? [$legacy] : [];
+        } else {
+            $selectedIds = $parseIdList($selectedRaw);
+        }
+
+        $unselectedIds = $parseIdList($this->request->post('unselected_ids', '[]'));
+
+        if ($selectedIds === []) {
+            $this->error('请至少勾选一条明细');
+        }
+
+        $inter = array_intersect($selectedIds, $unselectedIds);
+        if ($inter !== []) {
+            $this->error('选中与未选中 ID 不能重复');
+        }
+
+        $allIds = $this->purchaseOrderDetail($sid);
+        if ($allIds === []) {
+            $this->error('未找到该工序行的下发明细');
+        }
+
+        foreach ($selectedIds as $id) {
+            if (!in_array($id, $allIds, true)) {
+                $this->error('选中 ID 不属于当前工序行');
+            }
+        }
+        foreach ($unselectedIds as $id) {
+            if (!in_array($id, $allIds, true)) {
+                $this->error('未选中 ID 不属于当前工序行');
+            }
+        }
+
+        $union = array_values(array_unique(array_merge($selectedIds, $unselectedIds)));
+        sort($union, SORT_NUMERIC);
+        $expect = $allIds;
+        sort($expect, SORT_NUMERIC);
+        if ($union !== $expect) {
+            $this->error('请提交当前工序下全部明细 ID(选中 + 未选中)');
+        }
+
+        $purchaseOrderId = (int)$this->request->post('purchase_order_id', 0);
+
+        Db::startTrans();
+        try {
+            Db::table('purchase_order_detail')->where('scydgy_id', $sid)->update(['status' => 0]);
+            $aff = 0;
+            try {
+                $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('id', 'in', $selectedIds)->update(['status' => 1]);
+            } catch (\Throwable $e) {
+                $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('ID', 'in', $selectedIds)->update(['status' => 1]);
+            }
+            if ($aff < 1) {
+                throw new \Exception('更新选中状态失败');
+            }
+
+            if ($purchaseOrderId > 0) {
+                $poAff = (int)Db::table('purchase_order')
+                    ->where('id', $purchaseOrderId)
+                    ->where('scydgy_id', $sid)
+                    ->update(['status' => 1]);
+                if ($poAff < 1) {
+                    throw new \Exception('主表订单不存在或与工序行不匹配');
+                }
+            } else {
+                $poAff = (int)Db::table('purchase_order')->where('scydgy_id', $sid)->update(['status' => 1]);
+                if ($poAff < 1) {
+                    throw new \Exception('未找到');
+                }
+            }
+
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        $poIdFinal = $purchaseOrderId;
+        if ($poIdFinal <= 0) {
+            try {
+                $rpo = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+                if (is_array($rpo)) {
+                    $poIdFinal = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
+                }
+            } catch (\Throwable $e) {
+                $poIdFinal = 0;
+            }
+        }
+        $pickNames = [];
+        foreach ($selectedIds as $pid) {
+            $dr = $this->purchaseOrderDetai($sid, $pid);
+            $nm = trim((string)($dr['company_name'] ?? ''));
+            $pickNames[] = $nm !== '' ? $nm : ('明细#' . $pid);
+        }
+        $this->addOrderLog(
+            $sid,
+            'purchase_confirm',
+            '采购确认:已选中供应商「' . implode('、', $pickNames) . '」;',
+            $poIdFinal > 0 ? $poIdFinal : null
+        );
+
+        $ccydh = '';
+        $cyjmc = '';
+        try {
+            $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+            if (is_array($po)) {
+                $ccydh = trim((string)($po['CCYDH'] ?? ''));
+                $cyjmc = trim((string)($po['CYJMC'] ?? ''));
+            }
+        } catch (\Throwable $e) {
+        }
+        if ($ccydh === '' || $cyjmc === '') {
+            $any = $this->purchaseOrderDetai($sid, $selectedIds[0] ?? 0);
+            if (is_array($any)) {
+                if ($ccydh === '') {
+                    $ccydh = trim((string)($any['CCYDH'] ?? ''));
+                }
+                if ($cyjmc === '') {
+                    $cyjmc = trim((string)($any['CYJMC'] ?? ''));
+                }
+            }
+        }
+
+        $sendSmsSafe = function ($phone, $content) {
+            $phone = trim((string)$phone);
+            if ($phone === '') {
+                return;
+            }
+            try {
+                $this->smsbao($phone, $content);
+            } catch (\Throwable $e) {
+                Log::write('采购确认短信失败 phone=' . $phone . ' ' . $e->getMessage(), 'error');
+            }
+        };
+
+        //批量发送手机短信:采购确认通过
+        foreach ($selectedIds as $pid) {
+            $dr = $this->purchaseOrderDetai($sid, $pid);
+            if (!is_array($dr)) {
+                continue;
+            }
+            $cname = trim((string)($dr['company_name'] ?? ''));
+
+            $sms = "您好,{$cname}:\n\n"
+                . "您参与的外发加工订单采购确认结果:已通过。\n"
+                . "订单号:{$ccydh}\n"
+                . "印件名称:{$cyjmc}\n\n";
+            $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
+        }
+        //批量发送手机短信:采购确认未通过
+        foreach ($unselectedIds as $pid) {
+            $dr = $this->purchaseOrderDetai($sid, $pid);
+            if (!is_array($dr)) {
+                continue;
+            }
+            $cname = trim((string)($dr['company_name'] ?? ''));
+            $sms = "您好,{$cname}:\n\n"
+                . "您参与的外发加工订单采购确认结果:未通过。\n"
+                . "订单号:{$ccydh}\n"
+                . "印件名称:{$cyjmc}\n\n";
+            $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
+        }
+
+        $line = sprintf(
+            'purchaseConfirmPick scydgy_id=%s purchase_order_id=%d selected_ids=%s unselected_ids=%s',
+            $scydgyId,
+            $purchaseOrderId,
+            json_encode($selectedIds, JSON_UNESCAPED_UNICODE),
+            json_encode($unselectedIds, JSON_UNESCAPED_UNICODE)
+        );
+        Log::write($line, 'notice');
+
+        $pdfPublicPath = '';
+        try {
+            $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($sid, $poIdFinal);
+        } catch (\Throwable $e) {
+            Log::write('采购确认PDF异常: ' . $e->getMessage(), 'error');
+        }
+
+        $this->success('操作成功', '', [
+            'scydgy_id'           => $scydgyId,
+            'purchase_order_id'   => $purchaseOrderId,
+            'selected_ids'        => $selectedIds,
+            'unselected_ids'      => $unselectedIds,
+            'purchase_confirm_pdf' => $pdfPublicPath,
+        ]);
+    }
+
+    /**
+     * @return int[]
+     */
+    protected function purchaseOrderDetail(int $scydgyId): array
+    {
+        if ($scydgyId <= 0) {
+            return [];
+        }
+        try {
+            $list = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($list)) {
+            return [];
+        }
+        $ids = [];
+        foreach ($list as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $pk = (int)($r['id'] ?? $r['ID'] ?? 0);
+            if ($pk > 0) {
+                $ids[] = $pk;
+            }
+        }
+
+        return array_values(array_unique($ids));
+    }
+
+    /**
+     * 获取当前登录用户信息 [id, 展示名]
+     */
+    protected function GetUseName(): array
+    {
+        $id = 0;
+        $name = '';
+        try {
+            if ($this->auth && $this->auth->isLogin()) {
+                $u = $this->auth->getUserInfo();
+                if (is_array($u)) {
+                    $id = (int)($u['id'] ?? 0);
+                    $name = trim((string)($u['nickname'] ?? ''));
+                    if ($name === '') {
+                        $name = trim((string)($u['username'] ?? ''));
+                    }
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+        if ($name === '') {
+            $name = '未知用户';
+        }
+
+        return [$id, $name];
+    }
+
+    /**
+     * 外发采购操作日志(表未建时仅写 runtime 日志,不中断业务)
+     */
+    protected function addOrderLog(int $scydgyId, string $action, string $content, ?int $purchaseOrderId = null): void
+    {
+        if ($scydgyId <= 0 || $content === '') {
+            return;
+        }
+        list($adminId, $adminName) = $this->GetUseName();
+        $cut = function ($s, $max) {
+            if (function_exists('mb_substr')) {
+                return mb_substr($s, 0, $max, 'UTF-8');
+            }
+            return strlen($s) <= $max ? $s : substr($s, 0, $max);
+        };
+        try {
+            Db::table('purchase_order_oper_log')->insert([
+                'scydgy_id'         => $scydgyId,
+                'purchase_order_id' => $purchaseOrderId,
+                'admin_id'          => $adminId,
+                'admin_name'        => $cut($adminName, 64),
+                'action'            => $cut((string)$action, 64),
+                'content'           => $cut($content, 1000),
+                // 表结构 createtime 为 int Unix 时间戳;日期字符串会导致插入失败或时间为 0,详情「操作记录」为空
+                'createtime'        => time(),
+            ]);
+        } catch (\Throwable $e) {
+            Log::write('procuremen addOrderLog: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 按主键取一条
+     */
+    protected function purchaseOrderDetai(int $scydgyId, int $pk): array
+    {
+        if ($scydgyId <= 0 || $pk <= 0) {
+            return [];
+        }
+        try {
+            $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('id', $pk)->find();
+            if (is_array($one)) {
+                return $one;
+            }
+        } catch (\Throwable $e) {
+        }
+        try {
+            $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('ID', $pk)->find();
+            if (is_array($one)) {
+                return $one;
+            }
+        } catch (\Throwable $e) {
+        }
+
+        return [];
+    }
+
+    /**
+     * 主表/明细时间字段统一为可读字符串(用于详情进度)
+     */
+    protected function formatProcuremenDetailTime($v): string
+    {
+        if ($v === null || $v === '') {
+            return '';
+        }
+        if (is_numeric($v) && (int)$v > 946684800) {
+            return date('Y-m-d H:i:s', (int)$v);
+        }
+        $s = trim((string)$v);
+        if ($s !== '' && stripos($s, '0000-00-00') !== 0) {
+            return $s;
+        }
+
+        return '';
+    }
+
+    /**
+     * 交货日期仅展示 YYYY-MM-DD(详情表、列表展示用)
+     */
+    protected function formatDeliveryYmd($v): string
+    {
+        if ($v === null || $v === '') {
+            return '';
+        }
+        $s = trim((string)$v);
+        if ($s === '' || preg_match('/^0000-00-00/i', $s)) {
+            return '';
+        }
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime($s);
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : $s;
+    }
+
+    /**
+     * 供应商已接单:金额与交货日期均已填写(与列表汇总逻辑一致)
+     */
+    protected function detailRowSupplierAccepted(array $r): bool
+    {
+        $am = $r['amount'] ?? null;
+        $okAmt = $am !== null && $am !== '' && !(is_string($am) && trim($am) === '');
+        $dv = isset($r['delivery']) ? trim((string)$r['delivery']) : '';
+        $okDel = $dv !== '' && !preg_match('/^0000-00-00/i', $dv);
+
+        return $okAmt && $okDel;
+    }
+
+    /**
+     * 详情弹层:进度步骤 + 订单摘要 + 下发明细(已下发 / 已完结均可用)
+     *
+     * @param array $main purchase_order 一行
+     * @param array $details purchase_order_detail 多行(已预处理 createtime_text)
+     * @return array{steps: array, orderSummary: array, orderSummaryGrid: array, detailRows: array}
+     */
+    protected function buildProcuremenDetailsViewData(array $main, array $details): array
+    {
+        $issueCnt = count($details);
+        $acceptCnt = 0;
+        $pickedName = '';
+        $pickedTime = '';
+        foreach ($details as $dr) {
+            if (!is_array($dr)) {
+                continue;
+            }
+            if ($this->detailRowSupplierAccepted($dr)) {
+                $acceptCnt++;
+            }
+            if ((int)($dr['status'] ?? 0) === 1) {
+                if ($pickedName === '') {
+                    $pickedName = trim((string)($dr['company_name'] ?? ''));
+                }
+                if ($pickedTime === '') {
+                    $pickedTime = trim((string)($dr['createtime_text'] ?? ''));
+                    if ($pickedTime === '') {
+                        $pickedTime = $this->formatProcuremenDetailTime($dr['createtime'] ?? null);
+                    }
+                }
+            }
+        }
+
+        $hasMain = $main !== [];
+        $poTime = $this->formatProcuremenDetailTime($main['createtime'] ?? null);
+        $mainStatus = isset($main['status']) ? (int)$main['status'] : 0;
+
+        $supplierTime = '';
+        if ($acceptCnt > 0) {
+            foreach ($details as $dr) {
+                if (!is_array($dr) || !$this->detailRowSupplierAccepted($dr)) {
+                    continue;
+                }
+                $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null);
+                if ($t !== '' && ($supplierTime === '' || strcmp($t, $supplierTime) < 0)) {
+                    $supplierTime = $t;
+                }
+            }
+        }
+
+        $doneTime = '';
+        if ($mainStatus === 1) {
+            $doneTime = $this->formatProcuremenDetailTime($main['updatetime'] ?? null);
+            if ($doneTime === '') {
+                foreach ($details as $dr) {
+                    if (!is_array($dr)) {
+                        continue;
+                    }
+                    $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null);
+                    if ($t !== '' && ($doneTime === '' || strcmp($t, $doneTime) > 0)) {
+                        $doneTime = $t;
+                    }
+                }
+            }
+            if ($doneTime === '') {
+                $doneTime = $poTime;
+            }
+        }
+
+        $step1Done = $hasMain;
+        $step2Done = $hasMain;
+        $step3Done = $acceptCnt > 0;
+        $step4Done = false;
+        foreach ($details as $dr) {
+            if (is_array($dr) && (int)($dr['status'] ?? 0) === 1) {
+                $step4Done = true;
+                break;
+            }
+        }
+        $step5Done = $mainStatus === 1;
+
+        // 主表已完结:中间环节按业务视为已全部完成(不显示灰色「未到达」)
+        if ($mainStatus === 1) {
+            $step3Done = true;
+            $step4Done = true;
+            if ($supplierTime === '') {
+                $fillT = $doneTime !== '' ? $doneTime : $poTime;
+                if ($fillT !== '') {
+                    $supplierTime = $fillT;
+                }
+            }
+            if ($pickedTime === '') {
+                $fillT = $doneTime !== '' ? $doneTime : $poTime;
+                if ($fillT !== '') {
+                    $pickedTime = $fillT;
+                }
+            }
+        }
+
+        $steps = [
+            [
+                'title'    => '未发',
+                'subtitle' => '',
+                'time'     => $step1Done ? '—' : '',
+                'done'     => $step1Done,
+            ],
+            [
+                'title'    => '已发未结束',
+                'subtitle' => '',
+                'time'     => $step2Done ? $poTime : '',
+                'done'     => $step2Done,
+            ],
+            [
+                'title'    => '供应商接单',
+                'subtitle' => '下发 ' . $issueCnt . ' / 接单 ' . $acceptCnt,
+                'time'     => $step3Done ? $supplierTime : '',
+                'done'     => $step3Done,
+            ],
+            [
+                'title'    => '采购确认',
+                'subtitle' => $pickedName !== '' ? ('选中供应商:' . $pickedName) : '',
+                'time'     => $step4Done ? ($pickedTime !== '' ? $pickedTime : '') : '',
+                'done'     => $step4Done,
+            ],
+            [
+                'title'    => '已完结',
+                'subtitle' => '',
+                'time'     => $step5Done ? $doneTime : '',
+                'done'     => $step5Done,
+            ],
+        ];
+
+        $currentIdx = count($steps) - 1;
+        foreach ($steps as $i => $s) {
+            if (!$s['done']) {
+                $currentIdx = $i;
+                break;
+            }
+        }
+        $nSteps = count($steps);
+        foreach ($steps as $idx => &$s) {
+            $s['current'] = ($idx === $currentIdx);
+            if ($idx === 0) {
+                $s['pdf_left_bg'] = '';
+            } else {
+                $s['pdf_left_bg'] = !empty($steps[$idx - 1]['done']) ? '#1890ff' : '#e0e0e0';
+            }
+            if ($idx >= $nSteps - 1) {
+                $s['pdf_right_bg'] = '';
+            } else {
+                $s['pdf_right_bg'] = !empty($s['done']) ? '#1890ff' : '#e0e0e0';
+            }
+        }
+        unset($s);
+
+        $labelMap = [
+            'CCYDH'         => '订单号',
+            'CYJMC'         => '印件名称',
+            'CDXMC'         => '订单名称',
+            'CGYMC'         => '工序名称',
+            'CGYBH'         => '工序编号',
+            'CDW'           => '单位',
+            'NGZL'          => '工作量',
+            'CDF'           => '单价',
+            'cGzzxMc'      => '工作中心',
+            'MBZ'           => '备注',
+            'This_quantity' => '本次数量',
+            'ceilingPrice'  => '最高限价',
+        ];
+        $orderSummary = [];
+        foreach ($labelMap as $key => $lab) {
+            if (!array_key_exists($key, $main)) {
+                continue;
+            }
+            $val = $main[$key];
+            if ($val === null) {
+                $val = '';
+            } elseif (!is_scalar($val)) {
+                $val = json_encode($val, JSON_UNESCAPED_UNICODE);
+            } else {
+                $val = (string)$val;
+            }
+            $orderSummary[] = ['label' => $lab, 'value' => $val];
+        }
+
+        $orderSummaryGrid = [];
+        $n = count($orderSummary);
+        for ($i = 0; $i < $n; $i += 2) {
+            $left = $orderSummary[$i];
+            $hasRight = ($i + 1) < $n;
+            $orderSummaryGrid[] = [
+                'l1' => $left['label'],
+                'v1' => $left['value'],
+                'l2' => $hasRight ? $orderSummary[$i + 1]['label'] : '',
+                'v2' => $hasRight ? $orderSummary[$i + 1]['value'] : '',
+            ];
+        }
+
+        return [
+            'steps'            => $steps,
+            'orderSummary'     => $orderSummary,
+            'orderSummaryGrid' => $orderSummaryGrid,
+            'detailRows'       => $details,
+        ];
+    }
+
+    /**
+     * 加载并 assign 外发采购「详情」弹窗所需变量(与 {@see details()} 页面一致,供模板 / PDF 共用)。
+     *
+     * @return array{ok: bool, ccydh: string} ok 表示存在主表或至少一条下发明细(否则 PDF 无可写内容)
+     */
+    protected function prepareProcuremenDetailsView(string $ids): array
+    {
+        $main = [];
+        try {
+            $one = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+            $main = is_array($one) ? $one : [];
+        } catch (\Throwable $e) {
+            $main = [];
+        }
+        $details = [];
+        try {
+            $details = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'asc')->select();
+        } catch (\Throwable $e) {
+            $details = [];
+        }
+        if (!is_array($details)) {
+            $details = [];
+        }
+        foreach ($details as &$r) {
+            if (is_array($r) && isset($r['ID']) && !isset($r['id'])) {
+                $r['id'] = $r['ID'];
+            }
+            if (isset($r['createtime'])) {
+                if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) {
+                    $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']);
+                } else {
+                    $r['createtime_text'] = (string)$r['createtime'];
+                }
+            } else {
+                $r['createtime_text'] = '';
+            }
+            $r['delivery_ymd'] = $this->formatDeliveryYmd($r['delivery'] ?? null);
+        }
+        unset($r);
+
+        $operLogs = [];
+        try {
+            $operLogs = Db::table('purchase_order_oper_log')->where('scydgy_id', $ids)->order('id', 'asc')->select();
+        } catch (\Throwable $e) {
+            $operLogs = [];
+        }
+        if (!is_array($operLogs)) {
+            $operLogs = [];
+        }
+        foreach ($operLogs as &$lg) {
+            if (!is_array($lg)) {
+                continue;
+            }
+            $ct = isset($lg['createtime']) ? (int)$lg['createtime'] : 0;
+            $lg['createtime_text'] = $ct > 946684800 ? date('Y-m-d H:i:s', $ct) : '';
+        }
+        unset($lg);
+
+        $bundle = $this->buildProcuremenDetailsViewData($main, $details);
+        $ccydh = isset($main['CCYDH']) ? trim((string)$main['CCYDH']) : '';
+        if ($ccydh === '') {
+            foreach ($details as $r) {
+                if (!is_array($r)) {
+                    continue;
+                }
+                $ccydh = trim((string)($r['CCYDH'] ?? ''));
+                if ($ccydh !== '') {
+                    break;
+                }
+            }
+        }
+
+        $this->view->assign('ccydh', $ccydh);
+        $this->view->assign('steps', $bundle['steps']);
+        $this->view->assign('orderSummary', $bundle['orderSummary']);
+        $this->view->assign('orderSummaryGrid', $bundle['orderSummaryGrid']);
+        $this->view->assign('detailRows', $bundle['detailRows']);
+        $this->view->assign('operLogs', $operLogs);
+
+        return [
+            'ok'    => $main !== [] || $details !== [],
+            'ccydh' => $ccydh,
+        ];
+    }
+
+    /**
+     * 详情弹层:状态进度 + 订单信息 + 下发明细(列表「已下发」「已完结」均可打开)
+     */
+    public function details()
+    {
+        $ids = $this->request->param('ids', $this->request->param('id', ''));
+        if (is_array($ids)) {
+            $ids = isset($ids[0]) ? $ids[0] : '';
+        }
+        $ids = trim((string)$ids);
+        if ($ids === '') {
+            $this->error(__('Invalid parameters'));
+        }
+        $this->prepareProcuremenDetailsView($ids);
+
+        /* 弹层内不套 default 布局,避免出现「控制台 / Control panel」整块标题区 */
+        $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
+        $this->view->engine->layout(false);
+        try {
+            return $this->view->fetch('procuremen/details_dialog_shell');
+        } finally {
+            if ($restoreLayout) {
+                $this->view->engine->layout($restoreLayout);
+            }
+        }
+    }
+
+    /**
+     * 审核弹窗提交:GET 打开弹窗;POST 保存下发(主表 + 明细 + 操作日志)
+     */
+    public function review()
+    {
+        if ($this->request->isPost()) {
+            $rowJson = $this->request->post('row_json', '');
+            $companiesJson = $this->request->post('companies_json', '[]');
+            $row = json_decode($rowJson, true);
+            if (!is_array($row)) {
+                $this->error(__('Invalid parameters'));
+            }
+
+            $companies = json_decode($companiesJson, true);
+            if (!is_array($companies) || count($companies) === 0) {
+                $this->error('请至少选择一个下发单位');
+            }
+
+            $toDb = function ($value) {
+                if ($value === null) {
+                    return null;
+                }
+                if (is_scalar($value)) {
+                    return $value;
+                }
+
+                return json_encode($value, JSON_UNESCAPED_UNICODE);
+            };
+
+            Db::startTrans();
+            try {
+                $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
+                if ($ids <= 0) {
+                    throw new \Exception('无效的行主键 ID');
+                }
+
+                $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+                $data = [
+                    'scydgy_id'     => $ids,
+                    'CCYDH'         => $row['CCYDH'] ?? null,
+                    'CYJMC'         => $row['CYJMC'] ?? null,
+                    'CDXMC'         => $row['CDXMC'] ?? null,
+                    'CGYBH'         => $row['CGYBH'] ?? null,
+                    'CGYMC'         => $row['CGYMC'] ?? null,
+                    'CDW'           => $row['CDW'] ?? null,
+                    'NGZL'          => $row['NGZL'] ?? null,
+                    'CDF'           => $row['CDF'] ?? null,
+                    'cGzzxMc'       => $row['cGzzxMc'] ?? null,
+                    'MBZ'           => $row['MBZ'] ?? null,
+                    'bwjg'          => $row['bwjg'] ?? null,
+                    'iStatus'       => $row['iStatus'] ?? null,
+                    'dStamp'        => $row['dStamp'] ?? null,
+                    'dputrecord'    => $row['dputrecord'] ?? null,
+                    'cywyxm'        => $row['cywyxm'] ?? null,
+                    'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
+                    'ceilingPrice'  => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
+                    'status'        => 0,
+                ];
+
+                if ($exists) {
+                    $upd = $data;
+                    unset($upd['scydgy_id']);
+                    Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
+                } else {
+                    $data['createtime'] = date('Y-m-d H:i:s');
+                    Db::table('purchase_order')->insert($data);
+                }
+
+                $row['scydgy_id'] = (int)($row['ID'] ?? $row['id'] ?? 0);
+                unset($row['ID'], $row['id'], $row['_iss_out'], $row['ids'], $row['purchase_order_id']);
+
+                foreach ($companies as $c) {
+                    if (!is_array($c)) {
+                        continue;
+                    }
+                    $one = [
+                        'scydgy_id'    => $toDb($row['scydgy_id']),
+                        'CCYDH'        => $toDb($row['CCYDH'] ?? null),
+                        'CYJMC'        => $toDb($row['CYJMC'] ?? null),
+                        'company_name' => isset($c['name']) ? (string)$c['name'] : null,
+                        'email'        => isset($c['email']) ? (string)$c['email'] : null,
+                        'phone'        => isset($c['phone']) ? (string)$c['phone'] : null,
+                        'createtime'   => date('Y-m-d H:i:s'),
+                        'status'       => 0,
+                        'status_name'       => "未提交",
+                    ];
+
+                    // 先校验邮箱与手机号,再写入明细(写入后根据主键生成手机端直达链接 focus_eid)
+                    $toEmail = isset($c['email']) ? trim((string)$c['email']) : '';
+                    $companyName = isset($c['name']) ? trim((string)$c['name']) : '外协单位';
+
+                    if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
+                        throw new \Exception($companyName . ' 未填写有效邮箱,无法发送邮件,未写入数据');
+                    }
+
+                    $phone = isset($c['phone']) ? trim((string)$c['phone']) : '';
+                    if ($phone === '') {
+                        $cname = isset($c['name']) ? trim((string)$c['name']) : '';
+                        throw new \Exception(($cname !== '' ? $cname : '外协单位') . ' 未填写手机号,无法发送短信,未写入数据');
+                    }
+
+                    Db::table('purchase_order_detail')->insert($one);
+                    $detailId = (int)Db::getLastInsID();
+                    $mprocUrl = $this->buildMprocMobileOrderUrl($detailId);
+                    $mprocUrlEsc = htmlspecialchars($mprocUrl, ENT_QUOTES, 'UTF-8');
+
+                    // 发送邮箱
+                    $mailConfig = Config::get('Mailer');
+                    if (!is_array($mailConfig) || empty($mailConfig['host']) || empty($mailConfig['addr'])) {
+                        throw new \Exception('邮件未配置');
+                    }
+                    $mail = new PHPMailer(true);
+                    $mail->isSMTP();
+                    $mail->Host       = $mailConfig['host'];
+                    $mail->SMTPAuth   = true;
+                    $mail->Username   = $mailConfig['addr'];
+                    $mail->Password   = $mailConfig['pass'];
+                    $mail->SMTPSecure = $mailConfig['security'];
+                    $mail->Port       = $mailConfig['port'];
+                    $mail->CharSet    = $mailConfig['charset'];
+
+                    $mail->setFrom($mailConfig['addr'], $mailConfig['name']);
+                    $mail->addAddress($toEmail, $companyName);
+
+                    $mail->isHTML(true);
+                    $mail->Subject = '【外发加工订单通知】';
+                    $mail->Body    = "
+                        您好,{$companyName}:<br><br>
+                        您有新的外发加工订单待处理:<br>
+                        订单号:{$row['CCYDH']}<br>
+                        印件名称:{$row['CYJMC']}<br><br>
+                        请点击下面链接,登录后可直接打开本条订单(无需再搜索):<br>
+                        <a href=\"{$mprocUrlEsc}\">{$mprocUrlEsc}</a><br><br>
+                        登录后可在手机端填写金额与交货日期。<br><br>
+                        请及时查收并处理,谢谢!
+                    ";
+
+                    $mail->send();
+
+                    $ccydh = isset($row['CCYDH']) ? (string)$row['CCYDH'] : '';
+                    $cyjmc = isset($row['CYJMC']) ? (string)$row['CYJMC'] : '';
+                    $smsContent = "您好,{$companyName}:\n\n"
+                        . "您有新的外发加工订单待处理:\n"
+                        . "订单号:{$ccydh}\n"
+                        . "印件名称:{$cyjmc}\n\n"
+                        . "手机打开链接(登录后直达本条):\n"
+                        . $mprocUrl . "\n\n"
+                        . '请及时查收并处理,谢谢!';
+                    $this->smsbao($phone, $smsContent);
+                }
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $poIdLog = null;
+            try {
+                $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+                if (is_array($rpo)) {
+                    $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
+                    $poIdLog = $tid > 0 ? $tid : null;
+                }
+            } catch (\Throwable $e) {
+                $poIdLog = null;
+            }
+            $names = [];
+            foreach ($companies as $c) {
+                if (!is_array($c)) {
+                    continue;
+                }
+                $nm = trim((string)($c['name'] ?? $c['company_name'] ?? ''));
+                if ($nm !== '') {
+                    $names[] = $nm;
+                }
+            }
+            $n = count($names);
+            $nameStr = $n ? implode('、', $names) : '(未查询到单位名称)';
+            $this->addOrderLog(
+                $ids,
+                'review_issue',
+                '审核共选择 ' . $n . ' 家单位:' . $nameStr,
+                $poIdLog
+            );
+            $this->success('操作成功');
+        }
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 审核弹窗获取公司列表
+     */
+    public function reviewCompanies()
+    {
+        if (!$this->request->isAjax()) {
+            $this->error(__('Invalid parameters'));
+        }
+
+        $list = [];
+        try {
+            $rows = Db::table('customer')->order('id', 'desc')->select();
+        } catch (\Throwable $e) {
+            $this->success('', '', []);
+            return;
+        }
+        if (!is_array($rows)) {
+            $this->success('', '', []);
+            return;
+        }
+
+        $detailColCandidates = [
+            'detail', 'mingxi', 'remark', 'memo', 'notes', 'description',
+            'company_detail', 'company_desc', 'address',
+        ];
+
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $norm = [];
+            foreach ($row as $k => $v) {
+                $norm[is_string($k) ? strtolower($k) : $k] = $v;
+            }
+            $row = $norm;
+
+            $id = isset($row['id']) ? (string)$row['id'] : '';
+
+            $companyName = '';
+            foreach (['company_name', 'name'] as $nk) {
+                if (!empty($row[$nk])) {
+                    $companyName = trim((string)$row[$nk]);
+                    break;
+                }
+            }
+
+            $username = '';
+            foreach (['username', 'contact', 'linkman', 'contacts'] as $uk) {
+                if (isset($row[$uk]) && trim((string)$row[$uk]) !== '') {
+                    $username = trim((string)$row[$uk]);
+                    break;
+                }
+            }
+
+            $email = isset($row['email']) ? trim((string)$row['email']) : '';
+            $phone = isset($row['phone']) ? trim((string)$row['phone']) : '';
+
+            $category = '';
+            foreach (['company_type', 'category', 'type_name'] as $ck) {
+                if (isset($row[$ck]) && trim((string)$row[$ck]) !== '') {
+                    $category = trim((string)$row[$ck]);
+                    break;
+                }
+            }
+
+            $detail = '';
+            foreach ($detailColCandidates as $dk) {
+                if (!isset($row[$dk])) {
+                    continue;
+                }
+                $dv = trim((string)$row[$dk]);
+                if ($dv !== '') {
+                    $detail = $dv;
+                    break;
+                }
+            }
+
+            $list[] = [
+                'id'             => $id,
+                'name'           => $companyName,
+                'company_name'   => $companyName,
+                'username'       => $username,
+                'email'          => $email,
+                'phone'          => $phone,
+                'category'       => $category,
+                'company_type'   => $category,
+                'detail'         => $detail,
+            ];
+        }
+
+        $this->success('', '', $list);
+    }
+
+    /**
+     * 采购确认-下发明细弹窗:
+     * 查 purchase_order_detail;ids 为工序行 scydgy.ID,对应明细 scydgy_id
+     */
+    public function outward_detail()
+    {
+        $ids = $this->request->param('ids', $this->request->param('id', ''));
+        if (is_array($ids)) {
+            $ids = isset($ids[0]) ? $ids[0] : '';
+        }
+        $ids = trim((string)$ids);
+        if ($ids === '') {
+            $this->error(__('Invalid parameters'));
+        }
+
+        $wffTab = trim((string)$this->request->param('wff_tab', 'all'));
+        if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) {
+            $wffTab = 'all';
+        }
+        $headTitle = $wffTab === 'pending' ? '采购确认(明细)' : '下发明细';
+
+        $rows = [];
+        try {
+            $rows = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'desc')->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+
+        foreach ($rows as &$r) {
+            if (is_array($r) && isset($r['ID']) && !isset($r['id'])) {
+                $r['id'] = $r['ID'];
+            }
+            if (isset($r['createtime'])) {
+                if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) {
+                    $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']);
+                } else {
+                    $r['createtime_text'] = (string)$r['createtime'];
+                }
+            } else {
+                $r['createtime_text'] = '';
+            }
+        }
+        unset($r);
+
+        $purchaseOrderId = 0;
+        try {
+            $po = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
+            if (is_array($po)) {
+                $purchaseOrderId = (int)($po['id'] ?? $po['ID'] ?? 0);
+            }
+        } catch (\Throwable $e) {
+            $purchaseOrderId = 0;
+        }
+
+        $this->view->assign('rows', $rows ?: []);
+        $this->view->assign('rowCount', count($rows ?: []));
+        $this->view->assign('scydgyId', $ids);
+        $this->view->assign('purchaseOrderId', $purchaseOrderId);
+        $this->view->assign('headTitle', $headTitle);
+        $this->view->assign('showPurchaseConfirm', $wffTab === 'pending' ? 1 : 0);
+        $this->view->assign('detailColspan', $wffTab === 'pending' ? 10 : 9);
+
+        /* 采购确认(pending)需在 iframe 内加载 require-backend,以便勾选与提交;其它 tab 仅只读表格,用轻量壳避免整站 JS/CSS 二次初始化导致弹窗极慢 */
+        if ($wffTab === 'pending') {
+            return $this->view->fetch();
+        }
+
+        $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
+        $this->view->engine->layout(false);
+        try {
+            $bodyHtml = $this->view->fetch('procuremen/outward_detail');
+            $this->view->assign('dialogBody', $bodyHtml);
+
+            return $this->view->fetch('procuremen/outward_detail_lite_shell');
+        } finally {
+            if ($restoreLayout) {
+                $this->view->engine->layout($restoreLayout);
+            }
+        }
+    }
+
+
+    /**
+     * 月份外发明细导出
+     */
+    public function export_month_outward()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        $ym = trim((string)$this->request->get('ym', date('Y-m')));
+        if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+            $ym = date('Y-m');
+        }
+        $monthStart = $ym . '-01 00:00:00';
+        $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart));
+        $unixStart = (int)strtotime($monthStart);
+        $unixEnd = (int)strtotime($monthEnd);
+
+        $rows = [];
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->whereRaw(
+                    '(TRIM(CAST(createtime AS CHAR(64))) LIKE ?)'
+                    . ' OR ((createtime REGEXP \'^[0-9]+$\') AND CAST(createtime AS UNSIGNED) BETWEEN ? AND ?)',
+                    [$ym . '%', $unixStart, $unixEnd]
+                )
+                ->order('CCYDH', 'asc')
+                ->order('company_name', 'asc')
+                ->order('id', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+
+        $groups = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $k = (string)($r['CCYDH'] ?? '') . "\x1f" . (string)($r['company_name'] ?? '');
+            if (!isset($groups[$k])) {
+                $groups[$k] = [];
+            }
+            $groups[$k][] = $r;
+        }
+        ksort($groups, SORT_STRING);
+
+        $spreadsheet = new Spreadsheet();
+        $sheet = $spreadsheet->getActiveSheet();
+        $sheet->setTitle('外发明细');
+
+        $mon = (int)substr($ym, 5, 2);
+        $sheet->mergeCells('A1:H1');
+        $sheet->setCellValue('A1', $mon . '月外发明细');
+        $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
+        $sheet->getStyle('A1')->getAlignment()
+            ->setHorizontal(Alignment::HORIZONTAL_CENTER)
+            ->setVertical(Alignment::VERTICAL_CENTER);
+
+        $headers = ['序号', '传票号', '传票名称', '外加工单位', '订法', '客户名称', '工序', '加工金额'];
+        $col = 'A';
+        foreach ($headers as $h) {
+            $sheet->setCellValue($col . '2', $h);
+            $col++;
+        }
+        $sheet->getStyle('A2:H2')->getFont()->setBold(true);
+        $sheet->getStyle('A2:H2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+
+        $rowNum = 3;
+        $sumSubtotalCounts = 0;
+        $grandAmount = 0.0;
+
+        if (empty($groups)) {
+            $sheet->mergeCells('A3:H3');
+            $sheet->setCellValue('A3', '(当月暂无外发明细)');
+            $sheet->getStyle('A3')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+            $rowNum = 4;
+        } else {
+            foreach ($groups as $items) {
+                if (empty($items)) {
+                    continue;
+                }
+                $groupLineCount = count($items);
+                $subAmount = 0.0;
+                $i = 0;
+                foreach ($items as $r) {
+                    $i++;
+                    $amt = $this->procuremenExportAmount($r);
+                    $subAmount += $amt;
+
+                    $sheet->setCellValue('A' . $rowNum, $i);
+                    $sheet->setCellValue('B' . $rowNum, (string)($r['CCYDH'] ?? ''));
+                    $sheet->setCellValue('C' . $rowNum, (string)($r['CYJMC'] ?? ''));
+                    $sheet->setCellValue('D' . $rowNum, (string)($r['company_name'] ?? ''));
+                    $sheet->setCellValue('E' . $rowNum, (string)($r['CDF'] ?? ''));
+                    $sheet->setCellValue('F' . $rowNum, (string)($r['cGzzxMc'] ?? ''));
+                    $sheet->setCellValue('G' . $rowNum, $this->procuremenExportGxText($r));
+                    $sheet->setCellValue('H' . $rowNum, $amt);
+                    $sheet->getStyle('H' . $rowNum)->getNumberFormat()->setFormatCode('"¥"#,##0.00');
+                    $rowNum++;
+                }
+
+                $sumSubtotalCounts += $groupLineCount;
+                $grandAmount += $subAmount;
+
+                $sheet->setCellValue('A' . $rowNum, $groupLineCount);
+                $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum);
+                $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($subAmount, 2, '.', ','));
+                $sheet->getStyle('G' . $rowNum)->getAlignment()
+                    ->setHorizontal(Alignment::HORIZONTAL_RIGHT)
+                    ->setVertical(Alignment::VERTICAL_CENTER);
+                $rowNum++;
+            }
+        }
+
+        $sheet->setCellValue('A' . $rowNum, '总合');
+        $sheet->setCellValue('B' . $rowNum, $sumSubtotalCounts);
+        $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum);
+        $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($grandAmount, 2, '.', ','));
+        $sheet->getStyle('A' . $rowNum . ':H' . $rowNum)->getFont()->setBold(true);
+        $sheet->getStyle('G' . $rowNum)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
+
+        $lastRow = $rowNum;
+        $sheet->getStyle('A1:H' . $lastRow)->applyFromArray([
+            'borders' => [
+                'allBorders' => [
+                    'borderStyle' => Border::BORDER_THIN,
+                    'color'       => ['rgb' => '000000'],
+                ],
+            ],
+        ]);
+        $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
+        // 长文案列:加宽 + 自动换行,避免导出后被截断
+        if ($lastRow >= 3) {
+            $sheet->getStyle('C3:C' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
+            $sheet->getStyle('D3:D' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
+            $sheet->getStyle('F3:F' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
+            $sheet->getStyle('G3:G' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
+        }
+        $sheet->getColumnDimension('A')->setWidth(7);
+        $sheet->getColumnDimension('B')->setWidth(16);
+        $sheet->getColumnDimension('C')->setWidth(52);
+        $sheet->getColumnDimension('D')->setWidth(32);
+        $sheet->getColumnDimension('E')->setWidth(12);
+        $sheet->getColumnDimension('F')->setWidth(44);
+        $sheet->getColumnDimension('G')->setWidth(26);
+        $sheet->getColumnDimension('H')->setWidth(13);
+        $sheet->getRowDimension(1)->setRowHeight(28);
+
+        $fileBase = '外发明细_' . str_replace('-', '', $ym);
+        $filename = $fileBase . '.xlsx';
+
+        if (ob_get_length()) {
+            ob_end_clean();
+        }
+        $asciiName = 'outward_' . str_replace('-', '', $ym) . '.xlsx';
+        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+        header('Content-Disposition: attachment;filename="' . $asciiName . '"; filename*=UTF-8\'\'' . rawurlencode($filename));
+        header('Cache-Control: max-age=0');
+        $writer = new Xlsx($spreadsheet);
+        $writer->save('php://output');
+        $spreadsheet->disconnectWorksheets();
+        unset($spreadsheet);
+        exit;
+    }
+
+    /**
+     * 导出用:工序列文案
+     */
+    protected function procuremenExportGxText(array $r)
+    {
+        $a = trim((string)($r['CDXMC'] ?? ''));
+        $b = trim((string)($r['CGYMC'] ?? ''));
+        if ($a !== '' && $b !== '') {
+            return $a . ':' . $b;
+        }
+        return $a !== '' ? $a : $b;
+    }
+
+    /**
+     * 导出用:加工金额(表中有 amount/jje/jgje 等则读取,否则 0)
+     */
+    protected function procuremenExportAmount(array $r)
+    {
+        foreach (['amount', 'jje', 'jgje', 'processing_amount'] as $k) {
+            if (!array_key_exists($k, $r)) {
+                continue;
+            }
+            $v = $r[$k];
+            if ($v === null || $v === '') {
+                continue;
+            }
+            if (is_numeric($v)) {
+                return (float)$v;
+            }
+            $v = preg_replace('/[^\d\.\-]/', '', (string)$v);
+            if ($v !== '' && is_numeric($v)) {
+                return (float)$v;
+            }
+        }
+        return 0.0;
+    }
+
+    /**
+     * 订单号等用于 PDF 文件名片段(去掉路径非法字符)
+     */
+    protected function sanitizePurchaseConfirmPdfOrderKey(string $ccydh): string
+    {
+        $s = trim($ccydh);
+        if ($s === '') {
+            return 'ORDER';
+        }
+        $s = preg_replace('@[\\\\/:*?"<>|\\s]+@u', '_', $s);
+        $s = trim($s, '._-');
+        if ($s === '') {
+            return 'ORDER';
+        }
+        if (function_exists('mb_substr')) {
+            return mb_substr($s, 0, 80, 'UTF-8');
+        }
+
+        return strlen($s) <= 80 ? $s : substr($s, 0, 80);
+    }
+
+    /**
+     * 采购确认 PDF 相对路径(与 OSS 对象键一致,无前导斜杠):
+     * xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf
+     *
+     * @return array{objectKey: string, webPath: string}
+     */
+    protected function buildPurchaseConfirmPdfPaths(int $scydgyId, string $ccydhRaw): array
+    {
+        $sid = (int)$scydgyId;
+        $safeOrder = $this->sanitizePurchaseConfirmPdfOrderKey($ccydhRaw);
+        $y = date('Y');
+        $m = date('m');
+        $d = date('d');
+        $basename = $safeOrder . '_' . $sid . '.pdf';
+        $objectKey = 'xinhua/' . $y . '/' . $m . '/' . $d . '/' . $sid . '/' . $basename;
+
+        return [
+            'objectKey' => $objectKey,
+            'webPath'   => '/' . $objectKey,
+        ];
+    }
+
+    /**
+     * 采购确认成功后:用与「详情」弹窗相同的模板片段渲染 HTML,再存为 PDF(改 details_fragment 后 PDF 同步变化)。
+     * 优先上传至阿里云 OSS(application/config.php 的 oss 节点);失败或未配置时回退到 public 下与 objectKey 相同目录结构。
+     * 成功后将相对路径写入 purchase_order.pdf_url(形如 /xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf)。
+     *
+     * @return string OSS 返回 https 完整 URL;本地回退为以 / 开头的 Web 路径;失败返回空串
+     */
+    protected function savePurchaseConfirmDetailPdf(int $scydgyId, int $purchaseOrderId): string
+    {
+        if ($scydgyId <= 0) {
+            return '';
+        }
+        $ids = trim((string)$scydgyId);
+        $prep = $this->prepareProcuremenDetailsView($ids);
+        if (!$prep['ok']) {
+            return '';
+        }
+        $ccydh = $prep['ccydh'];
+
+        $paths = $this->buildPurchaseConfirmPdfPaths((int)$scydgyId, $ccydh);
+        $objectKey = $paths['objectKey'];
+        $webPath = $paths['webPath'];
+
+        $meta = sprintf('工序行ID %s | 主表订单ID %d | PDF生成时间 %s', $ids, (int)$purchaseOrderId, date('Y-m-d H:i:s'));
+        $this->view->assign([
+            'pdf_export'   => 1,
+            'pdfMetaLine' => $meta,
+        ]);
+        // 关闭后台 layout,避免 default 布局里的「控制台」面包屑等被打进 PDF
+        $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
+        $this->view->engine->layout(false);
+        try {
+            $html = $this->view->fetch('procuremen/details_pdf_shell');
+        } catch (\Throwable $e) {
+            if ($restoreLayout) {
+                $this->view->engine->layout($restoreLayout);
+            }
+            Log::write('采购确认PDF模板渲染失败: ' . $e->getMessage(), 'error');
+            $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
+
+            return '';
+        }
+        if ($restoreLayout) {
+            $this->view->engine->layout($restoreLayout);
+        }
+        $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
+
+        $tempDir = ROOT_PATH . 'runtime' . DIRECTORY_SEPARATOR . 'mpdf_tmp';
+        if (!is_dir($tempDir)) {
+            @mkdir($tempDir, 0755, true);
+        }
+        $tempPdf = $tempDir . DIRECTORY_SEPARATOR . uniqid('pc_pdf_', true) . '.pdf';
+        try {
+            $mpdf = new \Mpdf\Mpdf([
+                'mode'              => 'utf-8',
+                'format'            => 'A4',
+                'margin_left'       => 12,
+                'margin_right'      => 12,
+                'margin_top'        => 14,
+                'margin_bottom'     => 14,
+                'tempDir'           => $tempDir,
+                'autoScriptToLang'  => true,
+                'autoLangToFont'    => true,
+            ]);
+            // 与当前站点 HTTP_HOST 不同的占位 base,使 basepathIsLocal=false,避免 mPDF CssManager
+            // 在 parse_url 得到有 scheme 无 host 时对 $tr['host'] 触发「Undefined index: host」(日志已复现)。
+            $mpdf->SetBasePath('http://127.0.0.1/');
+            $mpdf->WriteHTML($html);
+            $mpdf->Output($tempPdf, \Mpdf\Output\Destination::FILE);
+        } catch (\Throwable $e) {
+            Log::write('采购确认PDF写入失败: ' . $e->getMessage(), 'error');
+            if (is_file($tempPdf)) {
+                @unlink($tempPdf);
+            }
+
+            return '';
+        }
+
+        $ossUrl = AliyunOss::uploadLocalFile($tempPdf, $objectKey);
+        if ($ossUrl !== '') {
+            @unlink($tempPdf);
+            $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath);
+
+            return $ossUrl;
+        }
+
+        $pi = pathinfo($objectKey);
+        $dirRel = isset($pi['dirname']) ? (string)$pi['dirname'] : 'xinhua';
+        $baseFile = isset($pi['basename']) ? (string)$pi['basename'] : '';
+        if ($baseFile === '') {
+            @unlink($tempPdf);
+
+            return '';
+        }
+        $dir = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $dirRel);
+        if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
+            Log::write('采购确认PDF目录创建失败: ' . $dir, 'error');
+            @unlink($tempPdf);
+
+            return '';
+        }
+        $fullPath = $dir . DIRECTORY_SEPARATOR . $baseFile;
+        if (!@copy($tempPdf, $fullPath)) {
+            Log::write('采购确认PDF复制到本地失败: ' . $fullPath, 'error');
+            @unlink($tempPdf);
+
+            return '';
+        }
+        @unlink($tempPdf);
+        $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath);
+
+        return $webPath;
+    }
+
+    /**
+     * 采购确认 PDF 生成成功后写入 purchase_order.pdf_url(列不存在时仅记日志)
+     */
+    protected function persistPurchaseOrderPdfUrl(int $scydgyId, string $webPath): void
+    {
+        if ($scydgyId <= 0 || $webPath === '') {
+            return;
+        }
+        try {
+            Db::table('purchase_order')->where('scydgy_id', $scydgyId)->update(['pdf_url' => $webPath]);
+        } catch (\Throwable $e) {
+            Log::write('purchase_order.pdf_url 更新失败 scydgy_id=' . $scydgyId . ' ' . $e->getMessage(), 'notice');
+        }
+    }
+
+    /**
+     * 短信宝发送;失败抛异常(供事务回滚,避免「已入库但未通知」与「通知失败仍入库」)
+     * @throws \Exception
+     */
+    protected function smsbao($phone, $content)
+    {
+        $statusStr = [
+            '0'  => '短信发送成功',
+            '-1' => '参数不全',
+            '-2' => '服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间!',
+            '30' => '密码错误',
+            '40' => '账号不存在',
+            '41' => '余额不足',
+            '42' => '帐户已过期',
+            '43' => 'IP地址限制',
+            '50' => '内容含有敏感词',
+        ];
+        $smsapi = 'http://api.smsbao.com/';
+        $user = 'zhuwei123';
+        $pass = md5('1d1e605c101e4c1f8a156c6d7b19f126');
+        $sendurl = $smsapi . 'sms?u=' . $user . '&p=' . $pass . '&m=' . $phone . '&c=' . urlencode($content);
+        $result = @file_get_contents($sendurl);
+        if ($result === false) {
+            \think\Log::record('smsbao 请求失败 phone=' . $phone, 'error');
+            throw new \Exception('短信接口请求失败,请检查网络或稍后再试(未写入数据)');
+        }
+        $result = trim((string)$result);
+        if ($result !== '0') {
+            $msg = isset($statusStr[$result]) ? $statusStr[$result] : ('返回码 ' . $result);
+            \think\Log::record('smsbao 发送失败 phone=' . $phone . ' code=' . $result . ' ' . $msg, 'error');
+            throw new \Exception('短信发送失败:' . $msg . '(' . $phone . '),未写入数据');
+        }
+    }
+}

+ 9 - 0
application/admin/lang/zh-cn/customer.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'Company_name' => '客户名称',
+    'Username'     => '姓名',
+    'Email'        => '邮箱',
+    'Phone'        => '手机号',
+    'Company_type' => '业务分类',
+];

+ 5 - 0
application/admin/lang/zh-cn/procuremen.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+
+];

+ 49 - 0
application/admin/model/Customer.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+
+class Customer extends Model
+{
+    // 表名
+    protected $table = 'customer';
+    
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+
+    protected static function init()
+    {
+        self::beforeWrite(function ($model) {
+            $data = $model->getData();
+            foreach (['phone', 'email'] as $field) {
+                if (!array_key_exists($field, $data)) {
+                    continue;
+                }
+                $v = $data[$field];
+                if ($v === null || $v === '') {
+                    continue;
+                }
+                $v = str_replace([',', ';', ';', '|', '|', "\n", "\r", "\t"], ',', (string)$v);
+                $parts = preg_split('/\s*,\s*/', $v, -1, PREG_SPLIT_NO_EMPTY);
+                $parts = array_map('trim', $parts);
+                $parts = array_filter($parts, function ($s) {
+                    return $s !== '';
+                });
+                $model->setAttr($field, implode(',', $parts));
+            }
+        });
+    }
+
+}

+ 12 - 0
application/admin/model/Mcyd.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Mcyd extends Model
+{
+    // 表名
+    protected $table = 'mcyd';
+    
+}

+ 52 - 0
application/admin/model/Procuremen.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+
+class Procuremen extends Model
+{
+
+    
+
+    
+
+    // 表名
+    protected $table = 'scydgy';
+    
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'nPrepareTime_text'
+    ];
+    
+
+    
+
+
+
+    public function getNpreparetimeTextAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['nPrepareTime']) ? $data['nPrepareTime'] : '');
+        return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
+    }
+
+    protected function setNpreparetimeAttr($value)
+    {
+        return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
+    }
+
+
+    public function mcyd()
+    {
+        return $this->belongsTo('Mcyd', 'ID', 'ICYDID', [], 'LEFT')->setEagerlyType(0);
+    }
+}

+ 27 - 0
application/admin/validate/Customer.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\validate;
+
+use think\Validate;
+
+class Customer extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+    
+}

+ 27 - 0
application/admin/validate/Procuremen.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\validate;
+
+use think\Validate;
+
+class Procuremen extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+    
+}

+ 48 - 0
application/admin/view/customer/add.html

@@ -0,0 +1,48 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Company_name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-company_name" class="form-control" name="row[company_name]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Username')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-username" class="form-control" name="row[username]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-email" class="form-control" name="row[email]" type="text" placeholder="多条请用英文逗号 , 分隔">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Phone')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-phone" class="form-control" name="row[phone]" type="text" placeholder="多条请用英文逗号 , 分隔">
+        </div>
+    </div>
+    <div class="form-group company-type-form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Company_type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[company_type]" id="c-company_type" value="">
+            <div class="input-group company-type-onebox">
+                <select id="c-company_type_select" class="form-control selectpicker" multiple data-live-search="true" data-width="100%" data-selected-text-format="static" data-live-search-placeholder="无匹配时按回车添加" title="请选择">
+                    {volist name="companyTypeOptions" id="vo"}
+                    <option value="{$vo|htmlentities}">{$vo|htmlentities}</option>
+                    {/volist}
+                </select>
+                <span class="input-group-addon" id="c-company_type_count" style="min-width:6.5em;text-align:center;">已选 0</span>
+            </div>
+            <p class="help-block" style="margin-top:6px;">下拉中搜索;列表显示「无匹配」时,在搜索框内按 <strong>Enter</strong> 可将当前文字添加为新分类并选中。</p>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+        </div>
+    </div>
+</form>

+ 48 - 0
application/admin/view/customer/edit.html

@@ -0,0 +1,48 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Company_name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-company_name" class="form-control" name="row[company_name]" type="text" value="{$row.company_name|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Username')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-username" class="form-control" name="row[username]" type="text" value="{$row.username|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-email" class="form-control" name="row[email]" type="text" value="{$row.email|htmlentities}" placeholder="多条请用英文逗号 , 分隔">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Phone')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-phone" class="form-control" name="row[phone]" type="text" value="{$row.phone|htmlentities}" placeholder="多条请用英文逗号 , 分隔">
+        </div>
+    </div>
+    <div class="form-group company-type-form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Company_type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[company_type]" id="c-company_type" value="{$row.company_type|htmlentities}">
+            <div class="input-group company-type-onebox">
+                <select id="c-company_type_select" class="form-control selectpicker" multiple data-live-search="true" data-width="100%" data-selected-text-format="static" data-live-search-placeholder="无匹配时按回车添加" title="请选择">
+                    {volist name="companyTypeOptions" id="vo"}
+                    <option value="{$vo|htmlentities}">{$vo|htmlentities}</option>
+                    {/volist}
+                </select>
+                <span class="input-group-addon" id="c-company_type_count" style="min-width:6.5em;text-align:center;">已选 0</span>
+            </div>
+            <p class="help-block">点击展开下拉可多选;已选前有√;右侧显示已选数量;保存时多项以「、」连接。列表显示「无匹配」时,在搜索框内按 <strong>Enter</strong> 可添加为新分类。</p>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+        </div>
+    </div>
+</form>

+ 29 - 0
application/admin/view/customer/index.html

@@ -0,0 +1,29 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('customer/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
+                        <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('customer/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('customer/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
+                        
+
+                        
+
+                        
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('customer/edit')}"
+                           data-operate-del="{:$auth->check('customer/del')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 15 - 7
application/admin/view/index/login.html

@@ -7,7 +7,7 @@
             width: 100%;
             height: 100vh;
             background-image: url("http://xh-erp.7in6.com/img/bg1.jpg");
-            background-size: cover; /* 等比例放大或缩小背景图以完全覆盖容器 */
+            background-size: cover;
             background-repeat: no-repeat;
             display: flex;
             flex-direction: column;
@@ -15,7 +15,7 @@
             align-items: center;
             margin: 0;
             position: relative;
-            background-position: center;
+            /*background-position: center;*/
             filter: brightness(1.2); /* 调整亮度 */
         }
 
@@ -26,23 +26,28 @@
         .login-wrapper {
             display: flex;
             flex-direction: column;
-            align-items: center;
+            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: 600px;
-            display: flex;
+            max-width: 475px;
+            /* display: flex; */
             flex-direction: column;
-            align-items: center;
+            align-items: end;
             border-radius: 3px;
             box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
-            padding: 20px;
+            padding: 15px;
             box-sizing: border-box;
+
+            background-color: rgba(115, 162, 229, 0.1);
         }
 
         .profile-img-card {
@@ -101,6 +106,7 @@
         .head .head-title {
             font-size: 24px;
             margin: 0;
+            margin-top: 30px;
             text-align: center;
             color: whitesmoke;
             font-family: 'Songti', '宋体', serif;
@@ -187,6 +193,8 @@
     <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>

+ 531 - 0
application/admin/view/procuremen/add.html

@@ -0,0 +1,531 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Icydid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ICYDID" class="form-control" name="row[ICYDID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IDXID" class="form-control" name="row[IDXID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDXMC" class="form-control" name="row[CDXMC]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxlxid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IDXLXID" class="form-control" name="row[IDXLXID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdxlxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDXLXMC" class="form-control" name="row[CDXLXMC]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ixh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IXH" class="form-control" name="row[IXH]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-pid" class="form-control" name="row[pid]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igyid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IGYID" class="form-control" name="row[IGYID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgybh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CGYBH" class="form-control" name="row[CGYBH]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgymc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CGYMC" class="form-control" name="row[CGYMC]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDW" class="form-control" name="row[CDW]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgzzxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cGzzxMc" class="form-control" name="row[cGzzxMc]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgzzxbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cGzzxBh" class="form-control" name="row[cGzzxBh]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbmmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CBMMC" class="form-control" name="row[CBMMC]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbmbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cBmBh" class="form-control" name="row[cBmBh]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Csbmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CSBMC" class="form-control" name="row[CSBMC]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdedw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDEDW" class="form-control" name="row[CDEDW]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ndesl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NDESL" class="form-control" name="row[NDESL]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NGZL" class="form-control" name="row[NGZL]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dqsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-DQSRQ" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[DQSRQ]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Djsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-DJSRQ" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[DJSRQ]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngyzq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NGYZQ" class="form-control" name="row[NGYZQ]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Gybz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-GyBz" class="form-control" name="row[GyBz]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Isccpid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ISCCPID" class="form-control" name="row[ISCCPID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cnextgy')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-Cnextgy" class="form-control" name="row[Cnextgy]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cpregy')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-Cpregy" class="form-control" name="row[Cpregy]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Itype')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iType" class="form-control" name="row[iType]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iendbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iEndBz" class="form-control" name="row[iEndBz]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfinishsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dFinishsl" class="form-control" name="row[dFinishsl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngygs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ngyGs" class="form-control" step="0.01" name="row[ngyGs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Bpb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-bPb" class="form-control" name="row[bPb]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipbid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iPbId" class="form-control" name="row[iPbId]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cpbbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cPbBh" class="form-control" name="row[cPbBh]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nifjs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nifjs" class="form-control" name="row[nifjs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nxhl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nxhl" class="form-control" step="0.001" name="row[nxhl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cxhdw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cxhdw" class="form-control" name="row[cxhdw]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyjfsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyjfsl" class="form-control" name="row[nyjfsl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyjssl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyjssl" class="form-control" name="row[nyjssl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cxhjsfs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cxhjsfs" class="form-control" name="row[cxhjsfs]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszj" class="form-control" name="row[biszj]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Czjnr')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-czjnr" class="form-control" name="row[czjnr]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dzjrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dzjrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dzjrq]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszjbj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszjbj" class="form-control" name="row[biszjbj]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-idxxh" class="form-control" name="row[idxxh]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igxxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-igxxh" class="form-control" name="row[igxxh]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszjwcbj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszjwcbj" class="form-control" name="row[biszjwcbj]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Izjdid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-izjdid" class="form-control" name="row[izjdid]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Izjdlx')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-izjdlx" class="form-control" name="row[izjdlx]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dyjqsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dyjqsrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dyjqsrq]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dyjwcrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dyjwcrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dyjwcrq]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dbhr')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dbhr" class="form-control" step="0.0001" name="row[dbhr]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfactgs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dfactgs" class="form-control" step="0.001" name="row[dfactgs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfactcb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dfactcb" class="form-control" step="0.001" name="row[dfactcb]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dbzcb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dbzcb" class="form-control" step="0.001" name="row[dbzcb]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyfpgzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyfpgzl" class="form-control" step="0.001" name="row[nyfpgzl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Bwjg')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-bwjg" class="form-control" name="row[bwjg]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ijhfpbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ijhfpbz" class="form-control" name="row[ijhfpbz]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iywcgzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iywcgzl" class="form-control" step="0.001" name="row[iywcgzl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ddexs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ddexs" class="form-control" step="0.001" name="row[ddexs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipjzs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ipjzs" class="form-control" step="0.001" name="row[ipjzs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyfpgzl1')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyfpgzl1" class="form-control" step="0.001" name="row[nyfpgzl1]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Mbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-Mbz" class="form-control " rows="5" name="row[Mbz]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iquality')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iQuality" class="form-control" name="row[iQuality]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ncs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ncs" class="form-control" name="row[ncs]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igroupsort')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iGroupSort" class="form-control" name="row[iGroupSort]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ibornid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iBornID" class="form-control" name="row[iBornID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipicode')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ipicode" class="form-control" name="row[ipicode]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Blasproc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-BLasproc" class="form-control" name="row[BLasproc]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igaoj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iGaoJ" class="form-control" name="row[iGaoJ]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cBZ" class="form-control" name="row[cBZ]" type="text">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iscxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iScxh" class="form-control" name="row[iScxh]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Isbid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iSbid" class="form-control" name="row[iSbid]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nxhlv')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nXHLv" class="form-control" step="0.001" name="row[nXHLv]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdescriptionxml')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-cDescriptionXML" class="form-control " rows="5" name="row[cDescriptionXML]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdescription')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-cDescription" class="form-control " rows="5" name="row[cDescription]" cols="50"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ibzhjxmid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iBzHjxmID" class="form-control" name="row[iBzHjxmID]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nsl" class="form-control" step="0.001" name="row[nsl]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Imaterialstate')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iMaterialState" class="form-control" name="row[iMaterialState]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Inumberorder')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iNumberOrder" class="form-control" name="row[iNumberOrder]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Inum_fb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iNum_FB" class="form-control" name="row[iNum_FB]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Istatus')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iStatus" class="form-control" name="row[iStatus]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dstamp')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dStamp" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dStamp]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Npreparetime')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nPrepareTime" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[nPrepareTime]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iendtech')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iEndTech" class="form-control" name="row[iEndTech]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Imcpaperstatus')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iMCPaperStatus" class="form-control" name="row[iMCPaperStatus]" type="number">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dmcpaperarrival')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dMCPaperArrival" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dMCPaperArrival]" type="text" value="{:date('Y-m-d H:i:s')}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Web_order_product_pbom_tech_id')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-web_order_product_pbom_tech_id" data-rule="required" data-source="web/order/product/pbom/tech/index" class="form-control selectpage" name="row[web_order_product_pbom_tech_id]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+        </div>
+    </div>
+</form>

+ 1 - 0
application/admin/view/procuremen/details.html

@@ -0,0 +1 @@
+{include file="procuremen/details_fragment" /}


+ 10 - 0
application/admin/view/procuremen/details_dialog_shell.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+{include file="common/meta" /}
+<title>详情</title>
+</head>
+<body class="inside-header inside-aside is-dialog">
+{include file="procuremen/details_fragment" /}
+</body>
+</html>

+ 357 - 0
application/admin/view/procuremen/details_fragment.html

@@ -0,0 +1,357 @@
+<style>
+    body.is-dialog .procuremen-details-wrap {
+        margin: 0;
+        padding: 12px 14px 16px;
+    }
+    .procuremen-details-wrap .page-head {
+        font-size: 15px;
+        margin-bottom: 14px;
+    }
+    .procuremen-details-wrap .procuremen-details-order-no {
+        color: #000;
+        font-weight: 700;
+        font-size: 15px;
+        line-height: 1.55;
+        word-wrap: break-word;
+        word-break: break-all;
+        white-space: normal;
+        max-width: 100%;
+        margin-bottom: 0;
+    }
+    .procuremen-details-wrap .section-title {
+        font-size: 14px;
+        font-weight: 600;
+        margin: 18px 0 10px;
+        color: #333;
+        padding-bottom: 6px;
+        border-bottom: 1px solid #eee;
+    }
+    .proc-steps-wrap {
+        display: flex;
+        flex-wrap: nowrap;
+        justify-content: space-between;
+        align-items: flex-start;
+        padding: 8px 4px 4px;
+        overflow-x: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+    .proc-step-item {
+        flex: 1 1 0;
+        min-width: 100px;
+        text-align: center;
+        position: relative;
+        padding: 0 4px;
+    }
+    .proc-step-item:not(:last-child)::after {
+        content: '';
+        position: absolute;
+        top: 14px;
+        left: 50%;
+        width: 100%;
+        height: 2px;
+        background: #e0e0e0;
+        z-index: 0;
+    }
+    .proc-step-item.is-done:not(:last-child)::after {
+        background: #1890ff;
+    }
+    .proc-step-circle {
+        width: 28px;
+        height: 28px;
+        line-height: 28px;
+        border-radius: 50%;
+        margin: 0 auto 6px;
+        font-size: 13px;
+        font-weight: 600;
+        position: relative;
+        z-index: 1;
+        background: #f5f5f5;
+        color: #999;
+        border: 2px solid #ddd;
+    }
+    .proc-step-item.is-done .proc-step-circle {
+        background: #1890ff;
+        border-color: #1890ff;
+        color: #fff;
+    }
+    .proc-step-item.is-current .proc-step-circle {
+        background: #1890ff;
+        border-color: #1890ff;
+        color: #fff;
+        box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.25);
+    }
+    .proc-step-item.is-done .proc-step-circle .fa {
+        line-height: 24px;
+    }
+    .proc-step-title {
+        font-size: 12px;
+        color: #333;
+        line-height: 1.35;
+        margin-bottom: 2px;
+    }
+    .proc-step-sub {
+        font-size: 11px;
+        color: #888;
+        line-height: 1.3;
+        min-height: 2.6em;
+    }
+    .proc-step-time {
+        font-size: 11px;
+        color: #aaa;
+        margin-top: 4px;
+        white-space: nowrap;
+    }
+    .procuremen-details-wrap .procuremen-order-info-block {
+        border: 1px solid #ddd;
+        margin-top: 18px;
+        margin-bottom: 0;
+    }
+    .procuremen-details-wrap .procuremen-order-info-head {
+        background: #f0f0f0;
+        border-bottom: 1px solid #ddd;
+        padding: 10px 12px;
+        font-size: 14px;
+        font-weight: 700;
+        color: #222;
+        margin: 0;
+    }
+    .procuremen-details-wrap .procuremen-order-grid {
+        width: 100%;
+        border-collapse: collapse;
+        table-layout: fixed;
+        font-size: 13px;
+        margin: 0;
+        background: #fff;
+        border: none;
+    }
+    .procuremen-details-wrap .procuremen-order-grid td {
+        border: 1px solid #ddd;
+        padding: 10px 12px;
+        vertical-align: middle;
+    }
+    .procuremen-details-wrap .procuremen-order-grid .og-label {
+        width: 14%;
+        background: #f5f5f5;
+        color: #333;
+        text-align: right;
+        font-weight: normal;
+    }
+    .procuremen-details-wrap .procuremen-order-grid .og-value {
+        width: 36%;
+        background: #fff;
+        color: #000;
+        text-align: left;
+        font-weight: 700;
+        word-break: break-word;
+    }
+    .procuremen-details-wrap .procuremen-oper-log {
+        list-style: none;
+        margin: 0 0 8px;
+        padding: 0;
+        font-size: 13px;
+        line-height: 1.65;
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        background: #fafafa;
+        max-height: 280px;
+        overflow-y: auto;
+    }
+    .procuremen-details-wrap .procuremen-oper-log li {
+        padding: 8px 12px;
+        border-bottom: 1px solid #eee;
+        word-break: break-word;
+    }
+    .procuremen-details-wrap .procuremen-oper-log li:last-child {
+        border-bottom: none;
+    }
+    .procuremen-details-wrap .procuremen-oper-log .op-user {
+        font-weight: 700;
+        color: #000;
+    }
+    .procuremen-details-wrap .procuremen-oper-log .op-time {
+        color: #666;
+        margin: 0 6px;
+    }
+    .procuremen-details-wrap .procuremen-oper-log .op-text {
+        color: #333;
+    }
+    .procuremen-details-wrap .procuremen-oper-log-empty {
+        font-size: 13px;
+        color: #999;
+        padding: 10px 12px;
+        border: 1px dashed #ddd;
+        border-radius: 4px;
+        margin-bottom: 8px;
+    }
+    .procuremen-details-wrap .detail-mini {
+        font-size: 12px;
+    }
+    .procuremen-details-wrap .detail-mini th,
+    .procuremen-details-wrap .detail-mini td {
+        padding: 6px 8px;
+        vertical-align: middle;
+    }
+</style>
+
+<div class="panel panel-default panel-intro procuremen-details-wrap{if !empty($pdf_export)} procuremen-pdf-inner{/if}" style="border:0;box-shadow:none;">
+    <div class="page-head">
+        {notempty name="ccydh"}
+        {if !empty($pdf_export)}
+        <table class="proc-pdf-order-no" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:0;padding:0;">
+            <tr>
+                <td style="font-size:15px;font-weight:700;color:#000;padding:0 8px 0 0;vertical-align:baseline;white-space:nowrap;">订单号</td>
+                <td style="font-size:15px;font-weight:700;color:#000;vertical-align:baseline;word-break:break-all;">{$ccydh|htmlentities}</td>
+            </tr>
+        </table>
+        {else /}
+        <div class="procuremen-details-order-no">订单号:{$ccydh|htmlentities}</div>
+        {/if}
+        {else /}
+        <div class="procuremen-details-order-no text-muted">订单号:—</div>
+        {/notempty}
+    </div>
+
+    <div class="section-title"{if !empty($pdf_export)} style="margin:16px 0 10px;font-size:14px;font-weight:600;color:#333;padding-bottom:6px;border-bottom:1px solid #eee;"{/if}>状态进度</div>
+    {notempty name="pdf_export"}
+    <div class="proc-pdf-steps-outer" style="overflow:hidden;margin:0;padding:0;">
+    <table class="proc-steps-table-pdf" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;table-layout:fixed;width:100%;margin:0 0 18px;">
+        <tr>
+            {volist name="steps" id="st"}
+            <td style="vertical-align:top;text-align:center;padding:4px 1px 0;border:0 !important;width:20%;">
+                <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;height:152px;margin:0 auto;">
+                    <tr>
+                        <td style="vertical-align:top;padding:0;border:0 !important;">
+                            <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:0 auto 4px;">
+                                <tr style="height:28px;">
+                                    <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:28px;">
+                                        {if $st.pdf_left_bg}
+                                        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;"><tr><td style="padding:0;border:0 !important;height:0;line-height:0;font-size:0;border-top:2px solid {$st.pdf_left_bg};">&#160;</td></tr></table>
+                                        {/if}
+                                    </td>
+                                    <td style="width:28px;padding:0;border:0 !important;vertical-align:middle;text-align:center;height:28px;">
+                                        {if $st.done}
+                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #1890ff;background-color:#1890ff;color:#ffffff;font-size:11px;font-weight:bold;">&#10003;</span>
+                                        {elseif $st.current /}
+                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #1890ff;background-color:#1890ff;color:#ffffff;font-size:11px;font-weight:bold;">{$i}</span>
+                                        {else /}
+                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #dddddd;background-color:#f5f5f5;color:#999999;font-size:11px;font-weight:bold;">{$i}</span>
+                                        {/if}
+                                    </td>
+                                    <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:28px;">
+                                        {if $st.pdf_right_bg}
+                                        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;"><tr><td style="padding:0;border:0 !important;height:0;line-height:0;font-size:0;border-top:2px solid {$st.pdf_right_bg};">&#160;</td></tr></table>
+                                        {/if}
+                                    </td>
+                                </tr>
+                            </table>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="vertical-align:top;padding:2px 2px 0;border:0 !important;font-size:12px;color:#333;line-height:1.35;font-weight:600;text-align:center;">{$st.title|htmlentities}</td>
+                    </tr>
+                    <tr>
+                        <td style="vertical-align:top;padding:2px 2px 0;height:44px;border:0 !important;font-size:11px;color:#888;line-height:1.3;text-align:center;overflow:hidden;">{$st.subtitle|default=''|htmlentities}</td>
+                    </tr>
+                    <tr>
+                        <td style="vertical-align:bottom;padding:2px 2px 0;border:0 !important;font-size:11px;color:#aaa;text-align:center;white-space:nowrap;">{$st.time|default=''|htmlentities}</td>
+                    </tr>
+                </table>
+            </td>
+            {/volist}
+        </tr>
+    </table>
+    </div>
+    {else /}
+    <div class="proc-steps-wrap">
+        {volist name="steps" id="st"}
+        <div class="proc-step-item{if $st.done} is-done{/if}{if $st.current} is-current{/if}">
+            <div class="proc-step-circle">
+                {if $st.done}
+                <i class="fa fa-check"></i>
+                {else /}
+                <span>{$i}</span>
+                {/if}
+            </div>
+            <div class="proc-step-title">{$st.title|htmlentities}</div>
+            <div class="proc-step-sub">{$st.subtitle|default=''|htmlentities}</div>
+            <div class="proc-step-time">{$st.time|default=''|htmlentities}</div>
+        </div>
+        {/volist}
+    </div>
+    {/notempty}
+
+    <div class="section-title">操作记录</div>
+    {notempty name="operLogs"}
+    <ul class="procuremen-oper-log">
+        {volist name="operLogs" id="lg"}
+        <li>
+            <span class="op-user">{$lg.admin_name|default=''|htmlentities}</span>
+            <span class="op-time">{$lg.createtime_text|default=''|htmlentities}</span>
+            <span class="op-text">{$lg.content|default=''|htmlentities}</span>
+        </li>
+        {/volist}
+    </ul>
+    {else /}
+    <p class="procuremen-oper-log-empty text-muted">暂无操作记录</p>
+    {/notempty}
+
+    <div class="procuremen-order-info-block">
+    <div class="procuremen-order-info-head">订单信息</div>
+    <table class="procuremen-order-grid table">
+        <tbody>
+            {notempty name="orderSummaryGrid"}
+            {volist name="orderSummaryGrid" id="gr"}
+            <tr>
+                <td class="og-label">{$gr.l1|htmlentities}</td>
+                <td class="og-value">{$gr.v1|htmlentities}</td>
+                <td class="og-label">{if $gr.l2 != ''}{$gr.l2|htmlentities}{/if}</td>
+                <td class="og-value">{if $gr.l2 != ''}{$gr.v2|htmlentities}{/if}</td>
+            </tr>
+            {/volist}
+            {else /}
+            <tr>
+                <td colspan="4" class="text-muted" style="text-align:center;padding:14px;">暂无数据</td>
+            </tr>
+            {/notempty}
+        </tbody>
+    </table>
+    </div>
+
+    <div class="section-title">供应商</div>
+    <div class="table-responsive">
+        <table class="table table-bordered table-condensed detail-mini">
+            <thead>
+            <tr>
+                <th>公司名称</th>
+                <th>加工金额</th>
+                <th>交货日期</th>
+                <th>采购确认</th>
+                <th>操作时间</th>
+            </tr>
+            </thead>
+            <tbody>
+            {notempty name="detailRows"}
+            {volist name="detailRows" id="dr"}
+            <tr>
+                <td>{$dr.company_name|default=''|htmlentities}</td>
+                <td>{$dr.amount|default=''|htmlentities}</td>
+                <td>{$dr.delivery_ymd|default=''|htmlentities}</td>
+                <td class="text-center">
+                    {if $dr.status == 1}
+                    <span class="label label-success">已选</span>
+                    {else /}
+                    <span class="text-muted"> </span>
+                    {/if}
+                </td>
+                <td>{$dr.createtime_text|default=''|htmlentities}</td>
+            </tr>
+            {/volist}
+            {else /}
+            <tr>
+                <td colspan="6" class="text-center text-muted">暂无下发明细</td>
+            </tr>
+            {/notempty}
+            </tbody>
+        </table>
+    </div>
+</div>

+ 88 - 0
application/admin/view/procuremen/details_pdf_shell.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8" />
+    <title>详情</title>
+    <style type="text/css">
+        body.procuremen-pdf-export { margin: 0; padding: 0; }
+        .procuremen-pdf-meta {
+            font-size: 9pt;
+            color: #666;
+            padding: 6px 14px 0 14px;
+            line-height: 1.5;
+        }
+        body.procuremen-pdf-export .procuremen-oper-log {
+            max-height: none !important;
+            overflow: visible !important;
+        }
+        body.procuremen-pdf-export table.table {
+            border-collapse: collapse;
+            width: 100%;
+        }
+        body.procuremen-pdf-export table.table-bordered th,
+        body.procuremen-pdf-export table.table-bordered td {
+            border: 1px solid #ddd;
+        }
+        body.procuremen-pdf-export table.table-condensed th,
+        body.procuremen-pdf-export table.table-condensed td {
+            padding: 6px 8px;
+        }
+        body.procuremen-pdf-export .label.label-success {
+            display: inline-block;
+            background: #5cb85c;
+            color: #fff;
+            padding: 2px 7px;
+            border-radius: 2px;
+            font-size: 10px;
+        }
+        body.procuremen-pdf-export .text-muted { color: #888; }
+        body.procuremen-pdf-export .text-center { text-align: center; }
+        body.procuremen-pdf-export .proc-steps-table-pdf {
+            border-collapse: collapse;
+            table-layout: fixed;
+            width: 100%;
+        }
+        body.procuremen-pdf-export .proc-steps-table-pdf td {
+            border: 0 !important;
+            border-left: 0 !important;
+            border-right: 0 !important;
+        }
+        body.procuremen-pdf-export .proc-steps-table-pdf table td {
+            border-left: 0 !important;
+            border-right: 0 !important;
+        }
+        body.procuremen-pdf-export .proc-pdf-steps-outer,
+        body.procuremen-pdf-export .procuremen-pdf-inner .page-head,
+        body.procuremen-pdf-export .procuremen-pdf-inner .section-title {
+            border-left: 0 !important;
+            border-right: 0 !important;
+        }
+        body.procuremen-pdf-export .procuremen-pdf-inner.panel,
+        body.procuremen-pdf-export .procuremen-pdf-inner.panel-default,
+        body.procuremen-pdf-export .procuremen-pdf-inner.panel-intro {
+            border: none !important;
+            border-left: none !important;
+            border-right: none !important;
+            box-shadow: none !important;
+            background: transparent !important;
+            margin: 0 !important;
+        }
+        body.procuremen-pdf-export .content-header,
+        body.procuremen-pdf-export #ribbon,
+        body.procuremen-pdf-export section.content-header {
+            display: none !important;
+            height: 0 !important;
+            margin: 0 !important;
+            padding: 0 !important;
+            overflow: hidden !important;
+            visibility: hidden !important;
+        }
+    </style>
+</head>
+<body class="is-dialog procuremen-pdf-export">
+{notempty name="pdfMetaLine"}
+<div class="procuremen-pdf-meta">{$pdfMetaLine|htmlentities}</div>
+{/notempty}
+{include file="procuremen/details_fragment" /}
+</body>
+</html>

+ 531 - 0
application/admin/view/procuremen/edit.html

@@ -0,0 +1,531 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Icydid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ICYDID" class="form-control" name="row[ICYDID]" type="number" value="{$row.ICYDID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IDXID" class="form-control" name="row[IDXID]" type="number" value="{$row.IDXID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDXMC" class="form-control" name="row[CDXMC]" type="text" value="{$row.CDXMC|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxlxid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IDXLXID" class="form-control" name="row[IDXLXID]" type="number" value="{$row.IDXLXID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdxlxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDXLXMC" class="form-control" name="row[CDXLXMC]" type="text" value="{$row.CDXLXMC|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ixh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IXH" class="form-control" name="row[IXH]" type="number" value="{$row.IXH|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Pid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-pid" class="form-control" name="row[pid]" type="number" value="{$row.pid|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igyid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-IGYID" class="form-control" name="row[IGYID]" type="number" value="{$row.IGYID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgybh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CGYBH" class="form-control" name="row[CGYBH]" type="text" value="{$row.CGYBH|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgymc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CGYMC" class="form-control" name="row[CGYMC]" type="text" value="{$row.CGYMC|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDW" class="form-control" name="row[CDW]" type="text" value="{$row.CDW|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgzzxmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cGzzxMc" class="form-control" name="row[cGzzxMc]" type="text" value="{$row.cGzzxMc|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cgzzxbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cGzzxBh" class="form-control" name="row[cGzzxBh]" type="text" value="{$row.cGzzxBh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbmmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CBMMC" class="form-control" name="row[CBMMC]" type="text" value="{$row.CBMMC|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbmbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cBmBh" class="form-control" name="row[cBmBh]" type="text" value="{$row.cBmBh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Csbmc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CSBMC" class="form-control" name="row[CSBMC]" type="text" value="{$row.CSBMC|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdedw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-CDEDW" class="form-control" name="row[CDEDW]" type="text" value="{$row.CDEDW|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ndesl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NDESL" class="form-control" name="row[NDESL]" type="number" value="{$row.NDESL|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NGZL" class="form-control" name="row[NGZL]" type="number" value="{$row.NGZL|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dqsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-DQSRQ" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[DQSRQ]" type="text" value="{$row.DQSRQ}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Djsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-DJSRQ" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[DJSRQ]" type="text" value="{$row.DJSRQ}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngyzq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-NGYZQ" class="form-control" name="row[NGYZQ]" type="number" value="{$row.NGYZQ|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Gybz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-GyBz" class="form-control" name="row[GyBz]" type="text" value="{$row.GyBz|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Isccpid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ISCCPID" class="form-control" name="row[ISCCPID]" type="number" value="{$row.ISCCPID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cnextgy')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-Cnextgy" class="form-control" name="row[Cnextgy]" type="text" value="{$row.Cnextgy|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cpregy')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-Cpregy" class="form-control" name="row[Cpregy]" type="text" value="{$row.Cpregy|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Itype')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iType" class="form-control" name="row[iType]" type="number" value="{$row.iType|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iendbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iEndBz" class="form-control" name="row[iEndBz]" type="number" value="{$row.iEndBz|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfinishsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dFinishsl" class="form-control" name="row[dFinishsl]" type="number" value="{$row.dFinishsl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ngygs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ngyGs" class="form-control" step="0.01" name="row[ngyGs]" type="number" value="{$row.ngyGs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Bpb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-bPb" class="form-control" name="row[bPb]" type="text" value="{$row.bPb|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipbid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iPbId" class="form-control" name="row[iPbId]" type="number" value="{$row.iPbId|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cpbbh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cPbBh" class="form-control" name="row[cPbBh]" type="text" value="{$row.cPbBh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nifjs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nifjs" class="form-control" name="row[nifjs]" type="number" value="{$row.nifjs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nxhl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nxhl" class="form-control" step="0.001" name="row[nxhl]" type="number" value="{$row.nxhl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cxhdw')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cxhdw" class="form-control" name="row[cxhdw]" type="text" value="{$row.cxhdw|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyjfsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyjfsl" class="form-control" name="row[nyjfsl]" type="number" value="{$row.nyjfsl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyjssl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyjssl" class="form-control" name="row[nyjssl]" type="number" value="{$row.nyjssl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cxhjsfs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cxhjsfs" class="form-control" name="row[cxhjsfs]" type="text" value="{$row.cxhjsfs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszj" class="form-control" name="row[biszj]" type="text" value="{$row.biszj|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Czjnr')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-czjnr" class="form-control" name="row[czjnr]" type="text" value="{$row.czjnr|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dzjrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dzjrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dzjrq]" type="text" value="{$row.dzjrq}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszjbj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszjbj" class="form-control" name="row[biszjbj]" type="text" value="{$row.biszjbj|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Idxxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-idxxh" class="form-control" name="row[idxxh]" type="number" value="{$row.idxxh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igxxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-igxxh" class="form-control" name="row[igxxh]" type="number" value="{$row.igxxh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Biszjwcbj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-biszjwcbj" class="form-control" name="row[biszjwcbj]" type="text" value="{$row.biszjwcbj|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Izjdid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-izjdid" class="form-control" name="row[izjdid]" type="number" value="{$row.izjdid|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Izjdlx')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-izjdlx" class="form-control" name="row[izjdlx]" type="number" value="{$row.izjdlx|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dyjqsrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dyjqsrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dyjqsrq]" type="text" value="{$row.dyjqsrq}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dyjwcrq')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dyjwcrq" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dyjwcrq]" type="text" value="{$row.dyjwcrq}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dbhr')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dbhr" class="form-control" step="0.0001" name="row[dbhr]" type="number" value="{$row.dbhr|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfactgs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dfactgs" class="form-control" step="0.001" name="row[dfactgs]" type="number" value="{$row.dfactgs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dfactcb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dfactcb" class="form-control" step="0.001" name="row[dfactcb]" type="number" value="{$row.dfactcb|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dbzcb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dbzcb" class="form-control" step="0.001" name="row[dbzcb]" type="number" value="{$row.dbzcb|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyfpgzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyfpgzl" class="form-control" step="0.001" name="row[nyfpgzl]" type="number" value="{$row.nyfpgzl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Bwjg')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-bwjg" class="form-control" name="row[bwjg]" type="number" value="{$row.bwjg|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ijhfpbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ijhfpbz" class="form-control" name="row[ijhfpbz]" type="number" value="{$row.ijhfpbz|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iywcgzl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iywcgzl" class="form-control" step="0.001" name="row[iywcgzl]" type="number" value="{$row.iywcgzl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ddexs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ddexs" class="form-control" step="0.001" name="row[ddexs]" type="number" value="{$row.ddexs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipjzs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ipjzs" class="form-control" step="0.001" name="row[ipjzs]" type="number" value="{$row.ipjzs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nyfpgzl1')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nyfpgzl1" class="form-control" step="0.001" name="row[nyfpgzl1]" type="number" value="{$row.nyfpgzl1|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Mbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-Mbz" class="form-control " rows="5" name="row[Mbz]" cols="50">{$row.Mbz|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iquality')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iQuality" class="form-control" name="row[iQuality]" type="number" value="{$row.iQuality|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ncs')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ncs" class="form-control" name="row[ncs]" type="number" value="{$row.ncs|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igroupsort')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iGroupSort" class="form-control" name="row[iGroupSort]" type="number" value="{$row.iGroupSort|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ibornid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iBornID" class="form-control" name="row[iBornID]" type="number" value="{$row.iBornID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ipicode')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-ipicode" class="form-control" name="row[ipicode]" type="number" value="{$row.ipicode|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Blasproc')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-BLasproc" class="form-control" name="row[BLasproc]" type="text" value="{$row.BLasproc|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Igaoj')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iGaoJ" class="form-control" name="row[iGaoJ]" type="number" value="{$row.iGaoJ|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cbz')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-cBZ" class="form-control" name="row[cBZ]" type="text" value="{$row.cBZ|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iscxh')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iScxh" class="form-control" name="row[iScxh]" type="number" value="{$row.iScxh|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Isbid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iSbid" class="form-control" name="row[iSbid]" type="number" value="{$row.iSbid|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nxhlv')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nXHLv" class="form-control" step="0.001" name="row[nXHLv]" type="number" value="{$row.nXHLv|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdescriptionxml')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-cDescriptionXML" class="form-control " rows="5" name="row[cDescriptionXML]" cols="50">{$row.cDescriptionXML|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Cdescription')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea id="c-cDescription" class="form-control " rows="5" name="row[cDescription]" cols="50">{$row.cDescription|htmlentities}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Ibzhjxmid')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iBzHjxmID" class="form-control" name="row[iBzHjxmID]" type="number" value="{$row.iBzHjxmID|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Nsl')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nsl" class="form-control" step="0.001" name="row[nsl]" type="number" value="{$row.nsl|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Imaterialstate')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iMaterialState" class="form-control" name="row[iMaterialState]" type="number" value="{$row.iMaterialState|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Inumberorder')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iNumberOrder" class="form-control" name="row[iNumberOrder]" type="number" value="{$row.iNumberOrder|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Inum_fb')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iNum_FB" class="form-control" name="row[iNum_FB]" type="number" value="{$row.iNum_FB|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Istatus')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iStatus" class="form-control" name="row[iStatus]" type="number" value="{$row.iStatus|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dstamp')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dStamp" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dStamp]" type="text" value="{$row.dStamp}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Npreparetime')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-nPrepareTime" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[nPrepareTime]" type="text" value="{:$row.nPrepareTime?datetime($row.nPrepareTime):''}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Iendtech')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iEndTech" class="form-control" name="row[iEndTech]" type="number" value="{$row.iEndTech|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Imcpaperstatus')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-iMCPaperStatus" class="form-control" name="row[iMCPaperStatus]" type="number" value="{$row.iMCPaperStatus|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Dmcpaperarrival')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-dMCPaperArrival" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-use-current="true" name="row[dMCPaperArrival]" type="text" value="{$row.dMCPaperArrival}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Web_order_product_pbom_tech_id')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-web_order_product_pbom_tech_id" data-rule="required" data-source="web/order/product/pbom/tech/index" class="form-control selectpage" name="row[web_order_product_pbom_tech_id]" type="text" value="{$row.web_order_product_pbom_tech_id|htmlentities}">
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+        </div>
+    </div>
+</form>

+ 277 - 0
application/admin/view/procuremen/index.html

@@ -0,0 +1,277 @@
+<style>
+    /* 工具栏贴右时避免被 #content / .col / .content 裁切 */
+    #content:has(#procuremen-layout),
+    #content .col-xs-12:has(#procuremen-layout),
+    div.content:has(#procuremen-layout) {
+        overflow-x: visible !important;
+    }
+    div.content:has(#procuremen-layout) {
+        padding-top: 0 !important;
+        padding-bottom: 0 !important;
+    }
+    #procuremen-layout > .panel-body {
+        padding-top: 16px !important;
+        padding-bottom: 0 !important;
+    }
+    #procuremen-layout.panel-intro > .panel-heading {
+        padding-top: 0 !important;
+        padding-bottom: 0 !important;
+    }
+    #procuremen-layout.panel-intro > .panel-heading .panel-lead {
+        margin-bottom: 0;
+    }
+    #procuremen-layout .tab-content,
+    #procuremen-layout .tab-pane {
+        padding: 0;
+        margin: 0;
+    }
+    .procuremen-layout { margin: 0 -10px; }
+    /* 左右列等高:与右侧整块(工具条+Tab+表格+分页)对齐;桌面下取消 float 用 flex */
+    #procuremen-layout .procuremen-layout.row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: stretch;
+    }
+    @media (min-width: 768px) {
+        #procuremen-layout .procuremen-layout > .procuremen-sidebar,
+        #procuremen-layout .procuremen-layout > .procuremen-main {
+            float: none !important;
+            display: flex;
+            flex-direction: column;
+        }
+    }
+    #procuremen-layout .procuremen-main {
+        overflow-x: visible !important;
+        padding-right: 18px !important;
+    }
+    .procuremen-sidebar {
+        flex: 1 1 auto;
+        min-height: 0;
+        overflow-y: auto;
+        border-right: 1px solid #e7e7e7;
+        background: #fafafa;
+    }
+    /* 左侧年月栏:桌面 md 用 1 列(略窄于 2 列);平板 sm 仍为 2 列 */
+    .procuremen-sidebar .year-title {
+        font-weight: 600;
+        padding: 6px 8px;
+        margin: 6px 0 2px;
+        background: #eee;
+        border-radius: 3px;
+        font-size: 12px;
+    }
+    .procuremen-sidebar .procuremen-ym-item {
+        display: block;
+        padding: 6px 8px;
+        margin: 1px 0;
+        color: #333;
+        border-radius: 3px;
+        text-decoration: none;
+        border: 1px solid transparent;
+    }
+    .procuremen-sidebar .procuremen-ym-item:hover {
+        background: #e8f4fc;
+        border-color: #bce8f1;
+    }
+    .procuremen-sidebar .procuremen-ym-item.active {
+        background: #3c8dbc;
+        color: #fff;
+        border-color: #367fa9;
+    }
+    #procuremen-layout .procuremen-main .widget-body {
+        overflow-x: visible;
+        overflow-y: hidden;
+        flex: 1 1 0%;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+    }
+    #procuremen-layout .procuremen-toolbar-host,
+    #procuremen-layout .procuremen-wff-tabs {
+        flex-shrink: 0;
+    }
+    #procuremen-layout .procuremen-main .fixed-table-pagination {
+        background: #fff;
+        border-top: 1px solid #e7e7e7;
+        box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.04);
+    }
+    /* 主表 loading 遮罩盖不住右侧固定列克隆体,刷新时短暂露出旧「操作」按钮,加载中先隐藏 */
+    #procuremen-layout .fixed-columns-right.procuremen-hide-fixed-right-loading {
+        visibility: hidden !important;
+        pointer-events: none !important;
+    }
+    /* JS 把 .fixed-table-toolbar 挪到此处:上一行按钮,下一行 tabs */
+    .procuremen-toolbar-host {
+        padding: 6px 12px 8px 0;
+        margin-bottom: 0;
+        border-bottom: 1px solid #e7e7e7;
+        width: 100%;
+        box-sizing: border-box;
+        overflow: visible;
+        position: relative;
+        z-index: 3;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar {
+        border: none;
+        margin: 0;
+        padding: 0;
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        flex-wrap: nowrap;
+        gap: 12px;
+        width: 100%;
+        box-sizing: border-box;
+        overflow: visible;
+        min-height: 36px;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar > .pull-left,
+    .procuremen-toolbar-host .fixed-table-toolbar > .bars.pull-left,
+    .procuremen-toolbar-host .fixed-table-toolbar > .bs-bars.pull-left {
+        float: none !important;
+        margin: 0 !important;
+        flex: 0 0 auto;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar > .pull-right,
+    .procuremen-toolbar-host .fixed-table-toolbar > .columns.pull-right,
+    .procuremen-toolbar-host .fixed-table-toolbar > .columns.columns-right {
+        float: none !important;
+        margin-left: auto !important;
+        flex: 0 0 auto;
+        display: flex;
+        align-items: center;
+        flex-wrap: nowrap;
+        gap: 8px;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .search {
+        float: none !important;
+        margin: 0 !important;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .search .form-control {
+        min-width: 140px;
+        max-width: 220px;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap {
+        margin-left: auto !important;
+        float: none !important;
+        display: flex !important;
+        align-items: center;
+        flex-wrap: nowrap;
+        gap: 8px;
+        flex: 0 0 auto !important;
+        flex-shrink: 0 !important;
+        min-width: min-content;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap .btn-group,
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap button[name="search"] {
+        flex-shrink: 0;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap > .search {
+        order: 0 !important;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap > *:not(.search) {
+        order: 2;
+    }
+    .procuremen-toolbar-host .fixed-table-toolbar .procuremen-toolbar-search-wrap > button[name="search"] {
+        order: 4 !important;
+    }
+    #procuremen-layout .procuremen-main .procuremen-table-area {
+        flex: 1 1 auto;
+        min-height: 0;
+    }
+    .procuremen-table-area .toolbar.procuremen-toolbar-empty {
+        display: none !important;
+    }
+    .procuremen-wff-tabs {
+        margin-bottom: 0;
+        margin-top: 0;
+        border-bottom: 1px solid #ddd;
+    }
+    .procuremen-wff-tabs > li > a {
+        padding: 8px 14px;
+    }
+    #procuremen-layout .bootstrap-table .table > thead > tr > th,
+    #procuremen-layout .bootstrap-table .table > tbody > tr > td {
+        padding: 3px 6px;
+        line-height: 1.28;
+        vertical-align: middle;
+    }
+    #procuremen-layout .bootstrap-table .table > tbody > tr > td .btn-xs {
+        padding: 2px 6px;
+        font-size: 12px;
+    }
+    /*
+     * 右固定列与主表 .fixed-table-body 为同级兄弟,主表全宽时最后一列会叠在固定层「下面」。
+     * 全局 backend.css 里 .fixed-columns-right 仅 z-index:2,易被主表内层盖住,表现为要点多次才打开弹窗。
+     * 抬高固定列整体叠层,并把主表滚动区压在 z-index:0,保证命中的是固定层上的按钮。
+     */
+    #procuremen-layout .bootstrap-table .fixed-table-container > .fixed-table-body {
+        position: relative;
+        z-index: 0;
+    }
+    #procuremen-layout .bootstrap-table .fixed-columns-right {
+        box-shadow: none !important;
+        border-left: 1px solid #e3e6ea;
+        background-color: #fff;
+        z-index: 18 !important;
+    }
+    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-header table thead th,
+    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body table tbody td {
+        border-left-color: transparent;
+    }
+    /* 操作列按钮在固定层内置于可点区域之上,减少与主表透明重叠导致的「多点一次」 */
+    /* 固定列操作按钮:抬高层级 + 明确可点,减轻与主表空白区域叠层的「点了没反应」 */
+    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body {
+        pointer-events: auto;
+    }
+    #procuremen-layout .bootstrap-table .fixed-columns-right a.procuremen-op-open {
+        position: relative;
+        z-index: 6;
+        pointer-events: auto;
+        display: inline-block;
+        vertical-align: middle;
+    }
+</style>
+
+<div class="panel panel-default panel-intro" id="procuremen-layout" data-default-ym="{$defaultYm|htmlentities}">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div class="row procuremen-layout">
+            <div class="col-xs-12 col-sm-2 col-md-1 procuremen-sidebar">
+                {foreach name="sidebarYearMonths" item="block"}
+                <div class="procuremen-year-block">
+                    <div class="year-title">{$block.year}年</div>
+                    {foreach name="block.months" item="item"}
+                    <a href="javascript:;" class="procuremen-ym-item" data-ym="{$item.ym|htmlentities}">{$item.label|htmlentities}</a>
+                    {/foreach}
+                </div>
+                {/foreach}
+            </div>
+            <div class="col-xs-12 col-sm-10 col-md-11 procuremen-main">
+                <div id="myTabContent" class="tab-content">
+                    <div class="tab-pane fade active in" id="one">
+                        <div class="widget-body no-padding">
+                            <div id="procuremen-toolbar-host" class="procuremen-toolbar-host clearfix"></div>
+                            <ul class="nav nav-tabs procuremen-wff-tabs" role="tablist">
+                                <li role="presentation" class="active"><a href="javascript:;" data-wff="all">未发</a></li>
+                                <li role="presentation"><a href="javascript:;" data-wff="pending">已下发</a></li>
+                                <li role="presentation"><a href="javascript:;" data-wff="picked">已选中</a></li>
+                                <li role="presentation"><a href="javascript:;" data-wff="done">已完结</a></li>
+                            </ul>
+                            <div class="procuremen-table-area">
+                                <div id="toolbar" class="toolbar">
+                                    <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                                    <a href="javascript:;" class="btn btn-success" id="btn-export-month-outward" title="按所选月份导出下发明细 Excel"><i class="fa fa-download"></i> 月份下发明细导出</a>
+                                </div>
+                                <table id="table"
+                                       class="table table-striped table-bordered table-hover">
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 168 - 0
application/admin/view/procuremen/outward_detail.html

@@ -0,0 +1,168 @@
+<style>
+    body.is-dialog .outward-detail-wrap {
+        margin: 0;
+        padding: 10px 12px 12px;
+    }
+    .outward-detail-head {
+        margin-bottom: 10px;
+        font-size: 13px;
+        color: #666;
+        line-height: 1.6;
+    }
+    .outward-detail-head strong {
+        color: #333;
+    }
+    .outward-detail-table-wrap {
+        overflow-x: auto;
+        overflow-y: auto;
+        max-height: calc(72vh - 120px);
+        -webkit-overflow-scrolling: touch;
+    }
+    .outward-detail-table {
+        table-layout: fixed;
+        width: 100%;
+        min-width: 1180px;
+        margin-bottom: 0;
+    }
+    .outward-detail-table > thead > tr > th,
+    .outward-detail-table > tbody > tr > td {
+        vertical-align: middle;
+        word-wrap: break-word;
+        word-break: break-word;
+        white-space: normal;
+        line-height: 1.45;
+        padding: 8px 10px;
+    }
+    .outward-detail-table > thead > tr > th {
+        font-weight: 600;
+        white-space: nowrap;
+    }
+    .outward-detail-table .col-title {
+        white-space: normal;
+    }
+    .outward-detail-table .col-amount,
+    .outward-detail-table .col-delivery {
+        text-align: center;
+        white-space: nowrap;
+    }
+    .outward-detail-table .col-sn,
+    .outward-detail-table .col-time,
+    .outward-detail-table .col-chk {
+        white-space: nowrap;
+    }
+    .outward-detail-foot {
+        margin-top: 12px;
+        padding-top: 10px;
+        border-top: 1px solid #eee;
+        text-align: right;
+    }
+    .procuremen-purchase-confirm-tip {
+        margin: 0 0 10px 0;
+        border: 1px solid #eea236;
+        background: #fcf8e3;
+        color: #8a6d3b;
+        font-size: 13px;
+        line-height: 1.65;
+    }
+    .procuremen-purchase-confirm-tip .fa {
+        margin-right: 6px;
+    }
+</style>
+
+<div class="panel panel-default panel-intro outward-detail-wrap" style="border:0;box-shadow:none;" data-scydgy-id="{$scydgyId|htmlentities}" data-purchase-order-id="{$purchaseOrderId|default=0}">
+    <div style="display:none;">{:token()}</div>
+    {if $showPurchaseConfirm}
+    <div class="alert alert-warning procuremen-purchase-confirm-tip">
+        <i class="fa fa-exclamation-triangle"></i>
+        <strong>重要提示:</strong>采购选择确认并提交后,系统将<strong>立即向各供应商下发短信</strong>;该操作<strong>不可撤回或更改</strong>,请仔细核对勾选结果后再点击「确认」。
+    </div>
+    {/if}
+    <div class="table-responsive outward-detail-table-wrap">
+        <table class="table table-striped table-bordered table-hover outward-detail-table">
+            {if $showPurchaseConfirm}
+            <colgroup>
+                <col style="width:56px" />
+                <col style="width:48px" />
+                <col style="width:118px" />
+                <col style="width:200px" />
+                <col style="width:92px" />
+                <col style="width:128px" />
+                <col style="width:200px" />
+                <col style="width:240px" />
+                <col style="width:108px" />
+                <col style="width:148px" />
+            </colgroup>
+            {else /}
+            <colgroup>
+                <col style="width:48px" />
+                <col style="width:118px" />
+                <col style="width:200px" />
+                <col style="width:92px" />
+                <col style="width:128px" />
+                <col style="width:200px" />
+                <col style="width:240px" />
+                <col style="width:108px" />
+                <col style="width:148px" />
+            </colgroup>
+            {/if}
+            <thead>
+            {if $showPurchaseConfirm}
+            <tr>
+                <th class="col-chk text-center">选择</th>
+                <th class="col-sn">序号</th>
+                <th>订单号</th>
+                <th class="col-title">印件名称</th>
+                <th class="col-amount">加工金额</th>
+                <th class="col-delivery">交货日期</th>
+                <th>公司名称</th>
+                <th>邮箱</th>
+                <th>手机号</th>
+                <th class="col-time">审核时间</th>
+            </tr>
+            {else /}
+            <tr>
+                <th class="col-sn">序号</th>
+                <th>订单号</th>
+                <th class="col-title">印件名称</th>
+                <th class="col-amount">加工金额</th>
+                <th class="col-delivery">交货日期</th>
+                <th>公司名称</th>
+                <th>邮箱</th>
+                <th>手机号</th>
+                <th class="col-time">审核时间</th>
+            </tr>
+            {/if}
+            </thead>
+            <tbody>
+            {volist name="rows" id="r"}
+            <tr>
+                {if $showPurchaseConfirm}
+                <td class="text-center col-chk">
+                    <input type="checkbox" class="pod-pick-cb" name="pod_pick[]" value="{$r.id|default=''}" title="单选" />
+                </td>
+                {/if}
+                <td class="text-center col-sn">{$i}</td>
+                <td>{$r.CCYDH|default=''}</td>
+                <td class="col-title">{$r.CYJMC|default=''}</td>
+                <td class="col-amount">{$r.amount|default=''}</td>
+                <td class="col-delivery">{$r.delivery|default=''}</td>
+                <td>{$r.company_name|default=''}</td>
+                <td>{$r.email|default=''}</td>
+                <td>{$r.phone|default=''}</td>
+                <td class="col-time">{$r.createtime_text|default=''}</td>
+            </tr>
+            {/volist}
+            {empty name="rows"}
+            <tr>
+                <td colspan="{$detailColspan|default=9}" class="text-center text-muted">暂无记录</td>
+            </tr>
+            {/empty}
+            </tbody>
+        </table>
+    </div>
+    {if $showPurchaseConfirm}
+    <div class="outward-detail-foot">
+        <button type="button" class="btn btn-primary" id="btn-pod-purchase-confirm"><i class="fa fa-check"></i> 确认</button>
+    </div>
+    {/if}
+</div>

+ 10 - 0
application/admin/view/procuremen/outward_detail_lite_shell.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+{include file="common/meta" /}
+<title>{$headTitle|default='下发明细'|htmlentities}</title>
+</head>
+<body class="inside-header inside-aside is-dialog">
+{:isset($dialogBody) ? $dialogBody : ''}
+</body>
+</html>

+ 490 - 0
application/admin/view/procuremen/review.html

@@ -0,0 +1,490 @@
+<style>
+    /*
+     * 上:订单信息(订单号、印件名称突出);下:左侧分类 + 右侧搜索与公司表格。
+     */
+    body.is-dialog #main,
+    body.is-dialog #main > .tab-content,
+    body.is-dialog #main #content {
+        height: 100%;
+        min-height: 0;
+    }
+    body.is-dialog #content > .row,
+    body.is-dialog #content > .row > [class*="col-"] {
+        height: 100%;
+        min-height: 0;
+    }
+    body.is-dialog .content {
+        height: 100%;
+        min-height: 0;
+        box-sizing: border-box;
+    }
+    body.is-dialog .review-dialog-form,
+    .review-dialog-form {
+        display: flex;
+        flex-direction: column;
+        height: 100%;
+        max-height: 100%;
+        min-height: min(72vh, 640px);
+        box-sizing: border-box;
+        padding: 8px 12px 0;
+    }
+    .review-split {
+        display: flex;
+        flex-direction: column;
+        align-items: stretch;
+        flex-wrap: nowrap;
+        gap: 10px;
+        flex: 1 1 0%;
+        min-height: 0;
+        overflow: hidden;
+    }
+    .review-split-left {
+        flex: 0 0 auto;
+        width: 100%;
+        min-width: 0;
+        padding-bottom: 10px;
+        border-bottom: 1px solid #e5e5e5;
+        overflow-x: hidden;
+    }
+    .review-split-right {
+        flex: 1 1 0%;
+        width: 100%;
+        min-width: 0;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    .review-split-left .review-block-title {
+        margin: 0 0 8px;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    /* 订单号、印件名称:标签与数值同字号;标题黑色,数值样式不变 */
+    .review-order-highlight {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: wrap;
+        align-items: baseline;
+        gap: 6px 22px;
+        margin-bottom: 8px;
+        font-size: 15px;
+        line-height: 1.5;
+    }
+    .review-order-highlight .review-highlight-item {
+        display: flex;
+        flex-direction: row;
+        align-items: baseline;
+        gap: 6px;
+        min-width: 0;
+    }
+    .review-order-highlight .review-field-label {
+        flex-shrink: 0;
+        margin: 0;
+        font-weight: 600;
+        font-size: 15px;
+        color: #000;
+    }
+    .review-order-highlight .review-highlight-value {
+        margin: 0;
+        color: #333;
+        font-size: 15px;
+        font-weight: 700;
+        word-break: break-word;
+    }
+    /* 工序名称等:标题黑色;标签与内容同 13px */
+    .review-order-meta {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: wrap;
+        align-items: baseline;
+        gap: 6px 22px;
+        font-size: 13px;
+        line-height: 1.5;
+    }
+    .review-order-meta .review-meta-item {
+        display: flex;
+        flex-direction: row;
+        align-items: baseline;
+        gap: 6px;
+        min-width: 0;
+    }
+    .review-order-meta .review-field-label {
+        flex-shrink: 0;
+        margin: 0;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    .review-order-meta .review-meta-inline {
+        margin: 0;
+        color: #333;
+        font-size: 13px;
+        word-break: break-word;
+    }
+    .review-split-right .review-right-title {
+        flex-shrink: 0;
+        margin: 0 0 6px;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    .review-split-right .review-notice {
+        flex-shrink: 0;
+        display: block;
+        margin: 0 0 8px;
+        padding: 6px 8px;
+        font-size: 12px;
+        line-height: 1.5;
+        color: #777;
+        background: #f4f8fb;
+        border: 1px solid #dce8f2;
+        border-radius: 3px;
+    }
+    .review-split-right .review-notice .fa {
+        margin-right: 4px;
+        color: #3c8dbc;
+    }
+    /* 左侧分类 + 右侧列表 */
+    .review-company-panel {
+        flex: 1 1 0%;
+        min-height: 0;
+        display: flex;
+        flex-direction: row;
+        align-items: stretch;
+        gap: 0;
+        border: 1px solid #ddd;
+        border-radius: 3px;
+        background: #fff;
+        overflow: hidden;
+    }
+    .review-category-sidebar {
+        flex: 0 0 210px;
+        width: 210px;
+        min-width: 210px;
+        max-width: 210px;
+        border-right: 1px solid #e0e0e0;
+        background: #f9fafb;
+        overflow-y: auto;
+        overflow-x: hidden;
+        font-size: 12px;
+    }
+    .review-category-sidebar .review-cat-head {
+        padding: 8px 10px;
+        font-weight: 600;
+        color: #000;
+        border-bottom: 1px solid #e5e5e5;
+        background: #f0f2f5;
+    }
+    .review-category-sidebar .review-cat-item {
+        display: block;
+        padding: 8px 10px;
+        color: #333;
+        text-decoration: none;
+        border-bottom: 1px solid #eee;
+        cursor: pointer;
+        white-space: normal;
+        word-break: break-word;
+        overflow: visible;
+        line-height: 1.45;
+    }
+    .review-category-sidebar .review-cat-item:hover {
+        background: #eef6fb;
+        color: #2c6f9c;
+    }
+    .review-category-sidebar .review-cat-item.active {
+        background: #3c8dbc;
+        color: #fff;
+        font-weight: 600;
+    }
+    .review-category-sidebar .review-cat-item .review-cat-count {
+        opacity: 0.85;
+        font-weight: normal;
+        font-size: 11px;
+    }
+    .review-category-sidebar .review-cat-item.active .review-cat-count {
+        opacity: 0.95;
+    }
+    .review-company-main {
+        flex: 1 1 0%;
+        min-width: 0;
+        min-height: 0;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+    .review-company-toolbar {
+        flex-shrink: 0;
+        padding: 8px;
+        border-bottom: 1px solid #eee;
+        background: #fafafa;
+    }
+    .review-company-toolbar .form-control {
+        font-size: 13px;
+        height: 34px;
+    }
+    .review-table-scroll {
+        flex: 1 1 0%;
+        min-height: 0;
+        box-sizing: border-box;
+        width: 100%;
+        overflow: auto;
+        font-size: 12px;
+        border: none;
+        border-radius: 0;
+    }
+    .review-table-scroll .table-responsive {
+        margin-bottom: 0;
+    }
+    .review-table-scroll table {
+        margin-bottom: 0;
+    }
+    .review-table-scroll .table > thead > tr > th,
+    .review-table-scroll .table > tbody > tr > td {
+        padding: 5px 6px;
+    }
+    .review-company-table {
+        table-layout: fixed;
+        width: 100%;
+    }
+    /* 姓名、邮箱列收窄(colgroup 为主,此处补 max-width 防撑开) */
+    .review-company-table > thead > tr > th:nth-child(3),
+    .review-company-table > tbody > tr > td:nth-child(3) {
+        max-width: 88px;
+    }
+    .review-company-table > thead > tr > th:nth-child(4),
+    .review-company-table > tbody > tr > td:nth-child(4) {
+        max-width: 200px;
+    }
+    .review-company-table > thead > tr > th.review-th-cb,
+    .review-company-table > tbody > tr > td.review-td-cb {
+        width: 34px;
+        min-width: 34px;
+        max-width: 34px;
+        text-align: center;
+        vertical-align: middle !important;
+        padding-left: 2px;
+        padding-right: 2px;
+    }
+    .review-company-table .review-th-cb input[type="checkbox"],
+    .review-company-table .review-td-cb input[type="checkbox"] {
+        margin: 0;
+        vertical-align: middle;
+        position: relative;
+        top: 1px;
+    }
+    /* 单元格完整展示,不换行省略号 */
+    .review-company-table > tbody > tr > td:not(.review-td-cb) {
+        white-space: normal;
+        word-wrap: break-word;
+        word-break: break-word;
+        overflow: visible;
+        vertical-align: top;
+    }
+    .review-company-table > thead > tr > th:not(.review-th-cb) {
+        white-space: normal;
+        word-break: break-word;
+        vertical-align: middle;
+    }
+    .review-company-empty {
+        padding: 24px 12px;
+        text-align: center;
+        color: #999;
+        font-size: 13px;
+    }
+    .review-dialog-form .layer-footer {
+        flex-shrink: 0;
+        width: 100%;
+        box-sizing: border-box;
+        margin-top: 12px;
+        padding: 10px 14px 10px 0;
+        border-top: 1px solid #e5e5e5;
+        background: #fafafa;
+        text-align: right !important;
+    }
+    .review-dialog-form .layer-footer .pull-right {
+        padding-right: 4px;
+    }
+    .review-dialog-form .layer-footer .btn + .btn {
+        margin-left: 8px;
+    }
+    /* 已选单位:介于订单信息与下方提示之间,筛选滚动时仍可看到 */
+    .review-selected-summary {
+        flex-shrink: 0;
+        margin: 0 0 10px;
+        padding: 8px 10px;
+        background: #fafbfc;
+        border: 1px solid #e3e8ee;
+        border-radius: 3px;
+        font-size: 12px;
+        line-height: 1.45;
+    }
+    .review-selected-summary .review-selected-head {
+        margin: 0 0 6px;
+        font-weight: 600;
+        color: #333;
+    }
+    .review-selected-summary .review-selected-count {
+        font-weight: 700;
+        color: #3c8dbc;
+    }
+    .review-selected-summary .review-selected-empty {
+        margin: 0;
+        color: #999;
+        font-size: 12px;
+    }
+    .review-selected-summary .review-selected-tags {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 6px 8px;
+        align-items: flex-start;
+        max-height: 104px;
+        overflow-y: auto;
+        margin: 0;
+    }
+    .review-selected-summary .review-selected-chip {
+        display: inline-flex;
+        align-items: flex-start;
+        max-width: 100%;
+        padding: 2px 4px 2px 8px;
+        gap: 2px;
+        font-size: 12px;
+        color: #2c6f9c;
+        background: #e8f4fb;
+        border: 1px solid #c5dce8;
+        border-radius: 2px;
+        box-sizing: border-box;
+    }
+    .review-selected-summary .review-chip-text {
+        flex: 1 1 auto;
+        min-width: 0;
+        word-break: break-all;
+        line-height: 1.35;
+        padding-top: 1px;
+    }
+    .review-selected-summary .review-chip-remove {
+        flex: 0 0 auto;
+        margin: 0;
+        padding: 0 2px;
+        border: none;
+        background: transparent;
+        color: #c9302c;
+        font-size: 15px;
+        font-weight: 700;
+        line-height: 1.1;
+        cursor: pointer;
+        vertical-align: middle;
+    }
+    .review-selected-summary .review-chip-remove:hover,
+    .review-selected-summary .review-chip-remove:focus {
+        color: #a94442;
+        outline: none;
+    }
+</style>
+<form id="review-form" class="review-dialog-form" role="form" data-toggle="validator" method="post" action="">
+    {:token()}
+    <textarea class="hide" name="row_json" id="c-row-json" rows="1" cols="1"></textarea>
+
+    <div class="review-split">
+        <div class="review-split-left">
+            <div class="review-order-highlight">
+                <div class="review-highlight-item">
+                    <span class="review-field-label">订单号:</span>
+                    <span class="review-highlight-value" id="review-ccydh"></span>
+                </div>
+                <div class="review-highlight-item">
+                    <span class="review-field-label">印件名称:</span>
+                    <span class="review-highlight-value" id="review-cyjmc"></span>
+                </div>
+            </div>
+            <div class="review-order-meta">
+                <div class="review-meta-item">
+                    <span class="review-field-label">工序名称:</span>
+                    <span class="review-meta-inline" id="review-CGYMC"></span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">单位:</span>
+                    <span class="review-meta-inline" id="review-CDW"></span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">工作量:</span>
+                    <span class="review-meta-inline" id="review-NGZL"></span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">本次数量:</span>
+                    <span class="review-meta-inline" id="review-qty-display">—</span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">最高限价:</span>
+                    <span class="review-meta-inline" id="review-price-display">—</span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">订法:</span>
+                    <span class="review-meta-inline" id="review-CDF"></span>
+                </div>
+                <div class="review-meta-item">
+                    <span class="review-field-label">外厂单位:</span>
+                    <span class="review-meta-inline" id="review-cGzzxMc"></span>
+                </div>
+            </div>
+        </div>
+        <div class="review-selected-summary" id="review-selected-summary" aria-live="polite">
+            <div class="review-selected-head">
+                已选单位(<span class="review-selected-count" id="review-selected-count">0</span>)
+            </div>
+            <p class="review-selected-empty" id="review-selected-empty">暂未选择;在下方表格中勾选后,公司名称会显示在此处。</p>
+            <div class="review-selected-tags" id="review-selected-tags"></div>
+        </div>
+        <div class="review-split-right">
+            <span class="review-notice" title="操作提示">
+                <i class="fa fa-info-circle"></i>
+                提交将向<strong>已勾选</strong>单位发送<strong>邮件</strong>与<strong>手机短信</strong>通知。
+            </span>
+            <div class="review-company-panel">
+                <aside class="review-category-sidebar" id="review-category-sidebar">
+                    <div class="review-cat-head">本次下发分组</div>
+                    <div id="review-category-list"></div>
+                </aside>
+                <div class="review-company-main">
+                    <div class="review-company-toolbar">
+                        <input type="text" class="form-control" id="review-company-search" placeholder="搜索公司名称、联系人、邮箱、手机号、业务分类…" autocomplete="off"/>
+                    </div>
+                    <div class="review-table-scroll">
+                        <div class="table-responsive">
+                            <table class="table table-bordered table-hover table-striped table-condensed review-company-table">
+                                <colgroup>
+                                    <col style="width:6%"/>
+                                    <col style="width:27%"/>
+                                    <col style="width:7%"/>
+                                    <col style="width:14%"/>
+                                    <col style="width: 19%"/>
+                                    <col style="width:27%"/>
+                                </colgroup>
+                                <thead>
+                                <tr>
+                                    <th class="review-th-cb"><input type="checkbox" id="review-check-all"/></th>
+                                    <th>公司名称</th>
+                                    <th>姓名</th>
+                                    <th>邮箱</th>
+                                    <th>手机号</th>
+                                    <th>业务分类</th>
+                                </tr>
+                                </thead>
+                                <tbody id="review-company-tbody">
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="layer-footer clearfix">
+        <div class="pull-right">
+            <button type="reset"  style="margin-right: 20px" class="btn btn-default btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
+            <button type="button"  style="margin-right: 20px" id="btn-review-submit" class="btn btn-success btn-embossed">提交</button>
+        </div>
+    </div>
+</form>

+ 2 - 1
application/api/controller/Fourth.php

@@ -95,8 +95,9 @@ class Fourth extends Controller{
          FROM (SELECT t5.rq as rq,sum(t5.sl)/10000 as 总色令,sum(t5.zl) as 总纸令 FROM ( SELECT DATE_FORMAT( dcyrq, '%Y-%m-%d' ) as rq,sum(objmatier.nyssl) as sl,sum(objmatier.nysls) as zl
          FROM `mcyd` `ord` LEFT JOIN (( SELECT icydid,sum(nyssl) as nyssl,sum(nysls) as nysls
          FROM `scyddx` `objmatier` GROUP BY `icydid` )) as objmatier ON `ord`.`icydid`=`objmatier`.`icydid`
-         WHERE  `ord`.`icydstate` > 0 and chy != '集团教材外'  GROUP BY `rq` ) t5 WHERE  (  t5.RQ >= '{$this->start_time()}'AND t5.RQ <= '{$this->end_time()}'  )
+         WHERE  `ord`.`icydstate` > 0 GROUP BY `rq` ) t5 WHERE  (  t5.RQ >= '{$this->start_time()}'AND t5.RQ <= '{$this->end_time()}'  )
          GROUP BY year(t5.rq) ) M");
+        
         //  and chy != '集团教材外'
         $list=[];
         $res=[];

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 533 - 211
application/api/controller/Index.php


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

@@ -386,7 +386,7 @@ FROM   (SELECT DATE_FORMAT(
         WHEN DATE_FORMAT(b.dOnDuty, '%H:%i') < (SELECT CONCAT(IFNULL(csetvalue, '00:00'), ':00') FROM sysetup WHERE csetname = 'frmFSetupAtt.dxtdBegin') THEN
             DATE_ADD(b.dOnDuty, INTERVAL -1 DAY)
         ELSE b.dOnDuty
-    END) BETWEEN '{$this->qstart_time()}' AND '{$this->end_time()}')gs
+    END) BETWEEN '{$this->qianstart_time()}' AND '{$this->end_time()}')gs
 WHERE  1 = 1 /*Wheres*/
 GROUP  BY gs.机台,
           gs.日期

+ 123 - 0
application/api/controller/Procuremen.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace app\api\controller;
+
+use think\Controller;
+use think\Db;
+
+/**
+ * 外发采购列表数据 API(供第三方拉取并写入 Redis,后台列表读同一份缓存)
+ */
+class Procuremen extends Controller
+{
+    /**
+     * 外发加工候选列表
+     *
+     * 请求:GET
+     * 参数:
+     *  - refresh=1  强制查库并覆盖 Redis(查询成功才写入)
+     *  - start_time   可选,开始时间,默认近一年前 00:00:00
+     *  - end_time     可选,结束时间,默认当前日 23:59:59
+     *  - limit        可选,最大返回条数,默认不限制(与后台整包缓存一致);若传则 1~500
+     *
+     * Redis:键固定为 procuremen_redis(与 admin Procuremen::index 一致)
+     * 结构:{ "code":200, "msg":"success", "data":[ {...行字段英文键...} ] }
+     */
+    public function GetProcuremen()
+    {
+        if (!$this->request->isGet()) {
+            return json(['code' => 400, 'msg' => '请使用GET请求', 'data' => []]);
+        }
+
+        $refresh = (int)$this->request->get('refresh', 0);
+
+        $defaultStart = date('Y-m-d 00:00:00', strtotime('-1 year'));
+        $defaultEnd = date('Y-m-d 23:59:59');
+        $startTime = trim((string)$this->request->get('start_time', $defaultStart));
+        $endTime = trim((string)$this->request->get('end_time', $defaultEnd));
+
+        $limitParam = $this->request->get('limit', '');
+        $useLimit = ($limitParam !== '' && $limitParam !== null);
+        $limit = $useLimit ? max(1, min(500, (int)$limitParam)) : 0;
+
+        $redisKey = 'procuremen_redis';
+
+        $redis = null;
+        try {
+            $redis = redis();
+        } catch (\Exception $e) {
+            $redis = null;
+        }
+
+        if (!$refresh && $redis) {
+            try {
+                $cached = $redis->get($redisKey);
+                if ($cached !== false && $cached !== '') {
+                    $decoded = json_decode($cached, true);
+                    if (is_array($decoded)) {
+                        return json($decoded);
+                    }
+                }
+            } catch (\Exception $e) {
+                // 读缓存失败则继续走库
+            }
+        }
+
+        $list = [];
+        $dbOk = false;
+        try {
+            $query = Db::table('scydgy')
+                ->alias('a')
+                ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
+                ->field('a.ID, b.CCYDH, b.CYJMC, a.CDXMC, a.CGYBH, a.CGYMC, a.CDW, a.NGZL, b.CDF, a.cGzzxMc, a.MBZ, a.bwjg, a.iStatus, a.dStamp, b.dputrecord, b.cywyxm')
+                ->where([
+                    'a.bwjg'    => 1,
+                    'a.iEndBz'  => 0,
+                    'a.iType'   => 0,
+                    'a.iStatus' => 10,
+                ])
+                ->where('a.dStamp', '>=', $startTime)
+                ->where('a.dStamp', '<=', $endTime)
+                ->order('a.dStamp', 'desc');
+
+            if ($limit > 0) {
+                $query->limit($limit);
+            }
+
+            $list = $query->select();
+            $dbOk = true;
+        } catch (\Exception $e) {
+            $list = [];
+            $dbOk = false;
+        }
+
+        $result = [
+            'code' => 200,
+            'msg'  => 'success',
+            'data' => $list ?: [],
+        ];
+
+        if ($redis && $dbOk) {
+            try {
+                $redis->set($redisKey, json_encode($result, JSON_UNESCAPED_UNICODE));
+            } catch (\Exception $e) {
+                // 写缓存失败仍返回本次查询结果
+            }
+        }
+
+        if (!$dbOk && $redis) {
+            try {
+                $cached = $redis->get($redisKey);
+                if ($cached !== false && $cached !== '') {
+                    $decoded = json_decode($cached, true);
+                    if (is_array($decoded)) {
+                        return json($decoded);
+                    }
+                }
+            } catch (\Exception $e) {
+            }
+        }
+
+        return json($result);
+    }
+}

+ 939 - 428
application/api/controller/Second.php

@@ -2624,740 +2624,1251 @@ ORDER BY HZ.`序号`, HZ.`金额(万元)` DESC;";
     public function gbsjjun(){
         $name = '高白双胶纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-纯雅纸均价
     public function cyzjun(){
         $name = '纯雅纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
 //        $list = [];
 //        $currentYear = date("Y"); // 当前年份
 //        $currentMonth = date("m"); // 当前月份
 //        $previousYear = $currentYear - 1; // 上一年
-//        // 初始化类别
-//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
-//        // 初始化上一年的数据数组,填充0
-//        $list['series'][0]['name'] = $previousYear . '年';
-//        $list['series'][0]['data'] = array_fill(0, 12, 0);
-//        // 当前年份的数据数组初始化到当前月份
-//        $list['series'][1]['name'] = $currentYear . '年';
-//        $list['series'][1]['data'] = array_fill(0, (int)$currentMonth, 0); // 注意这里是 currentMonth,不是 currentMonth - 1
-//        $prevdata = 0; // 初始化前一个数据点的值
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
 //
-//        //  echo "<pre>";print_r($result);echo "<pre>";
-//        foreach ($result as $k => $v) {
-//            $year = substr($k, 0, 4);
-//            $month = substr($k, 4, 2);
-//            if ($v[$name] != 0) {
-//                $prevdata = $v[$name];
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
 //            }
-//            if ($year == $currentYear && $month <= $currentMonth) {
-//                // 如果是当前年份,并且月份小于等于当前月份,则赋值
-//                $index = (int)$month - 1;
-//                $list['series'][1]['data'][$index] = $v[$name];
-//            } elseif ($year == $previousYear) {
-//                // 如果是上一年的数据,直接赋值
-//                $index = (int)$month - 1;
-//                $list['series'][0]['data'][$index] = $v[$name];
-//
-//                // 如果上一年的某个月份为0,则获取上个月的数据
-//                if ($v[$name] == 0 && $prevdata != 0) {
-//                    $list['series'][0]['data'][$index] = $prevdata;
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
 //                }
 //
-//                // 如果上一年的某个月份为0,则获取上个月的数据
-//                if ($index === 0 && $prevdata !== 0) {
-//                    $list['series'][0]['data'][$index] = $prevdata;
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
 //                }
+//
 //            }
 //        }
-//        // echo "<pre>";print_r($list);echo "<pre>";die;
-
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-纯质纸均价
     public function czzjun(){
         $name = '纯质纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-全灰板均价
     public function qhbjun(){
         $name = '全灰板';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-白卡纸均价
     public function bkzjun(){
         $name = '白卡纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-轻型纸均价
     public function qxzjun(){
         $name = '轻型纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-亚光双面铜版纸均价
     public function ygsmtbjun(){
         $name = '亚光双面铜版纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-轻涂纸均价
     public function qtzjun(){
         $name = '轻涂纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-彩画纸均价
     public function chzjun(){
         $name = '彩画纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-特种纸均价
     public function tzzjun(){
         $name = '特种纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-本白双胶纸均价
     public function bbsjjun(){
         $name = '本白双胶纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
     //每月各类纸张采购均价-有光双面铜版纸均价
     public function ygsmtjun(){
         $name = '有光双面铜版纸';
         $result = $this->month_paperjie();
-//         echo "<pre>";print_r($result);echo "<pre>";die;
         $list = [];
         $currentYear = date("Y"); // 当前年份
         $currentMonth = date("m"); // 当前月份
         $previousYear = $currentYear - 1; // 上一年
-        $currentdata=$result[$currentYear.'01'];
-        $currentdata=0;
-        $previousdata=0;
+
+        // 修正:先判断键是否存在,再赋值(避免未定义下标)
+        $currentKey = $currentYear . '01';
+        $currentdata = isset($result[$currentKey]) ? $result[$currentKey] : 0;
+        $previousKey = $previousYear . '01';
+        $previousdata = isset($result[$previousKey]) ? $result[$previousKey] : 0;
 
         foreach ($result as $k=>$v){
-            if($k<$currentYear.'01'){
-                if($v[$name]==0){
-                    $currentdata=$currentdata;
-                }else{
-                    $currentdata=$v[$name];
+            if($k < $currentKey){
+                if(!empty($v[$name])){
+                    $currentdata = $v[$name];
                 }
             }
-            if($k<$previousYear.'01'){
-                if($v[$name]==0){
-                    $previousdata= $previousdata;
-                }else{
-                    $previousdata=$v[$name];
+            if($k < $previousKey){
+                if(!empty($v[$name])){
+                    $previousdata = $v[$name];
                 }
             }
         }
+
         $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
         foreach ($result as $k=>$v){
-            if($k>=$currentYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$currentdata;
-                }else{
-                    $list['series'][1]['name'] = $currentYear . '年';
-                    $list['series'][1]['data'][] =$v[$name];
-                    $currentdata=$v[$name];
-                }
-
-            }elseif ($k>=$previousYear.'01'){
-                if($v[$name]==0){
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$previousdata;
-                }else{
-                    $list['series'][0]['name'] = $previousYear . '年';
-                    $list['series'][0]['data'][] =$v[$name];
-                    $previousdata=$v[$name];
-                }
+            // 初始化series数组(避免未定义下标)
+            if (!isset($list['series'][0])) $list['series'][0] = ['name' => $previousYear . '年', 'data' => []];
+            if (!isset($list['series'][1])) $list['series'][1] = ['name' => $currentYear . '年', 'data' => []];
 
+            if($k >= $currentKey){
+                $val = !empty($v[$name]) ? $v[$name] : $currentdata;
+                $list['series'][1]['data'][] = $val;
+                if (!empty($v[$name])) $currentdata = $v[$name];
+            }elseif ($k >= $previousKey){
+                $val = !empty($v[$name]) ? $v[$name] : $previousdata;
+                $list['series'][0]['data'][] = $val;
+                if (!empty($v[$name])) $previousdata = $v[$name];
             }
         }
+
         $res['status'] = 0;
         $res['msg'] = '';
         $res['data'] = $list;
         return json($res);
+//        $result = $this->month_paperjie();
+////         echo "<pre>";print_r($result);echo "<pre>";die;
+//        $list = [];
+//        $currentYear = date("Y"); // 当前年份
+//        $currentMonth = date("m"); // 当前月份
+//        $previousYear = $currentYear - 1; // 上一年
+//        $currentdata=$result[$currentYear.'01'];
+//        $currentdata=0;
+//        $previousdata=0;
+//
+//        foreach ($result as $k=>$v){
+//            if($k<$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $currentdata=$currentdata;
+//                }else{
+//                    $currentdata=$v[$name];
+//                }
+//            }
+//            if($k<$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $previousdata= $previousdata;
+//                }else{
+//                    $previousdata=$v[$name];
+//                }
+//            }
+//        }
+//        $list['categories'] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
+//        foreach ($result as $k=>$v){
+//            if($k>=$currentYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$currentdata;
+//                }else{
+//                    $list['series'][1]['name'] = $currentYear . '年';
+//                    $list['series'][1]['data'][] =$v[$name];
+//                    $currentdata=$v[$name];
+//                }
+//
+//            }elseif ($k>=$previousYear.'01'){
+//                if($v[$name]==0){
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$previousdata;
+//                }else{
+//                    $list['series'][0]['name'] = $previousYear . '年';
+//                    $list['series'][0]['data'][] =$v[$name];
+//                    $previousdata=$v[$name];
+//                }
+//
+//            }
+//        }
+//        $res['status'] = 0;
+//        $res['msg'] = '';
+//        $res['data'] = $list;
+//        return json($res);
     }
 
 

+ 89 - 0
application/common/library/AliyunOss.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\common\library;
+
+use OSS\Core\OssException;
+use OSS\OssClient;
+use think\Config;
+use think\Log;
+
+/**
+ * 阿里云 OSS:上传本地文件,返回可访问 URL(对象建议 public-read,或依赖 Bucket 读策略)。
+ */
+class AliyunOss
+{
+    /**
+     * @param string $localPath 本地文件绝对路径
+     * @param string $objectKey OSS 对象键,如 xinhua/2026/05/06/订单号_1.pdf(勿以 / 开头)
+     * @return string 成功返回 https 开头完整 URL;失败返回空串
+     */
+    public static function uploadLocalFile(string $localPath, string $objectKey): string
+    {
+        $objectKey = ltrim(str_replace('\\', '/', $objectKey), '/');
+        if ($objectKey === '' || !is_file($localPath) || !is_readable($localPath)) {
+            return '';
+        }
+
+        $cfg = Config::get('oss');
+        if (!is_array($cfg) || (isset($cfg['enabled']) && $cfg['enabled'] === false)) {
+            return '';
+        }
+
+        $id = trim((string)($cfg['accessKeyId'] ?? ''));
+        $secret = trim((string)($cfg['accessKeySecret'] ?? ''));
+        $endpoint = trim((string)($cfg['endpoint'] ?? ''));
+        $bucket = trim((string)($cfg['bucket'] ?? ''));
+        if ($id === '' || $secret === '' || $endpoint === '' || $bucket === '') {
+            return '';
+        }
+
+        if (!class_exists(OssClient::class)) {
+            Log::write('AliyunOss: OssClient 类不存在,请执行 composer install', 'error');
+
+            return '';
+        }
+
+        try {
+            $ossClient = new OssClient($id, $secret, $endpoint);
+            $ossClient->uploadFile($bucket, $objectKey, $localPath);
+            try {
+                $ossClient->putObjectAcl($bucket, $objectKey, OssClient::OSS_ACL_TYPE_PUBLIC_READ);
+            } catch (\Throwable $e) {
+                Log::write('AliyunOss: putObjectAcl 失败(可改 Bucket 策略放行读): ' . $e->getMessage(), 'notice');
+            }
+
+            return self::buildPublicUrl($cfg, $objectKey);
+        } catch (OssException $e) {
+            Log::write('AliyunOss 上传失败: ' . $e->getMessage(), 'error');
+        } catch (\Throwable $e) {
+            Log::write('AliyunOss 上传异常: ' . $e->getMessage(), 'error');
+        }
+
+        return '';
+    }
+
+    /**
+     * @param array<string, mixed> $cfg
+     */
+    protected static function buildPublicUrl(array $cfg, string $objectKey): string
+    {
+        $base = trim((string)($cfg['public_base'] ?? ''));
+        if ($base !== '') {
+            return rtrim($base, '/') . '/' . $objectKey;
+        }
+        $host = trim((string)($cfg['host'] ?? ''));
+        if ($host === '') {
+            $bucket = trim((string)($cfg['bucket'] ?? ''));
+            $ep = trim((string)($cfg['endpoint'] ?? ''));
+            if ($bucket !== '' && $ep !== '') {
+                $host = $bucket . '.' . $ep;
+            }
+        }
+        if ($host === '') {
+            return '';
+        }
+        $host = preg_replace('#^https?://#i', '', $host);
+
+        return 'https://' . $host . '/' . $objectKey;
+    }
+}

+ 28 - 1
application/config.php

@@ -348,5 +348,32 @@ return [
         // 是否需要进行SQL性能分析
         'sql_explain'     => false,
     ],
-
+    'Mailer' => [
+        'driver'          => 'smtp', // 邮件驱动, 支持 smtp|sendmail|mail 三种驱动
+        'host'            => 'smtp.qq.com', // SMTP服务器地址
+        'port'            => 465, // SMTP服务器端口号,一般为25
+        'addr'            => '858357896@qq.com', // 发件邮箱地址
+        'pass'            => 'qjksdurvlahvbahj', // 发件邮箱密码
+        'name'            => 'lhr', // 发件邮箱名称
+        'content_type'    => 'text/html', // 默认文本内容 text/html|text/plain
+        'charset'         => 'utf-8', // 默认字符集
+        'security'        => 'ssl', // 加密方式 null|ssl|tls, QQ邮箱必须使用ssl
+        'sendmail'        => '/usr/sbin/sendmail -bs', // 不适用 sendmail 驱动不需要配置
+        'debug'           => true, // 开启debug模式会直接抛出异常, 记录邮件发送日志
+        'left_delimiter'  => '{', // 模板变量替换左定界符, 可选, 默认为 {
+        'right_delimiter' => '}', // 模板变量替换右定界符, 可选, 默认为 }
+        'log_driver'      => '', // 日志驱动类, 可选, 如果启用必须实现静态 public static function write($content, $level = 'debug') 方法
+        'log_path'        => '', // 日志路径, 可选, 不配置日志驱动时启用默认日志驱动, 默认路径是 /path/to/tp-mailer/log, 要保证该目录有可写权限, 最好配置自己的日志路径
+        'embed'           => 'embed:', // 邮件中嵌入图片元数据标记
+    ],
+    // 正式环境 OSS(采购确认 PDF 等);public_base 可选自定义 CDN 域名
+    'oss' => [
+        'enabled'         => true,
+        'public_base'     => '',
+        'accessKeyId'     => 'LTAI5tM8BM5Dtr8YD2Y7odry',
+        'accessKeySecret' => 'vks79JWQjR3s0MPJGpa5nWsB9WXvVr',
+        'endpoint'        => 'oss-cn-hangzhou.aliyuncs.com',
+        'bucket'          => 'a-7in6-com',
+        'host'            => 'a-7in6-com.oss-cn-hangzhou.aliyuncs.com',
+    ]
 ];

+ 18 - 0
application/extra/mproc.php

@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * 手机端外发明细(purchase_order_detail)登录与展示
+ * 手机验证码:仅 customer 表登记手机号为普通外协(按 customer 公司名筛明细);否则查 admin.mobile 为管理员(看全部、不可改金额/交期);两者皆无则无权限
+ * 账号密码:与后台 admin 一致,可看全部,不可改金额/交期
+ */
+return [
+    /** 邮件/短信中的手机端根地址,须为公网可解析域名(含 https),勿用内网短名如 http://xh/。示例 https://xh-erp.7in6.com */
+    'mobile_base_url'   => 'https://xh-erp.7in6.com',
+    /** 手机端首页路径(相对上项),留空为 /index.php/index/index/index(与伪静态无关时最稳) */
+    'mobile_index_path' => '',
+    'session_days'  => 7,
+    'sms_code_ttl'  => 300,
+    'sms_resend_cd' => 55,
+    // 仅本地调试:与登录页输入的 6 位验证码一致时跳过「获取验证码」产生的缓存校验;上线请注释或删除本项
+    'mock_sms_code' => '123456',
+];

+ 1223 - 174
application/index/controller/Index.php

@@ -4,202 +4,1251 @@ namespace app\index\controller;
 
 use app\common\controller\Frontend;
 use think\Cache;
+use think\Config;
+use think\Cookie;
 use think\Db;
 use think\Session;
 
-class Index extends Frontend {
+/**
+ * 手机端:外发明细(purchase_order_detail)验证码 / 账号密码登录 + 列表
+ * 手机验证码:customer 登记手机号为普通用户(按 customer 公司名筛明细、可填金额/交期);否则 admin.mobile 命中为管理员(看全部、仅查看);账号密码为 admin 登录(看全部、仅查看)
+ */
+class Index extends Frontend
+{
+    protected $noNeedLogin = ['*'];
+
+    protected $noNeedRight = ['*'];
 
-    protected $noNeedLogin = '*';
-    protected $noNeedRight = '*';
     protected $layout = '';
 
-    // Token缓存时间(秒)
-//    protected $tokenCacheTime = 30 * 60; // 30分钟
-    protected $tokenCacheTime = 20 * 365 * 24 * 60 * 60; // 20年
-    // 是否需要验证设备ID
-    protected $checkDeviceId = true; // 可以自定义设置是否需要验证
-
-    public function index() {
-        return $this->redirect('/lqHKfByepX.php');
-        die;
-
-
-
-        // 检查用户是否已登录
-        $token_name = Session::get('token_name');
-        $device_id = Session::get('device_id');
-        if ($token_name) {
-            // 验证缓存中的token
-            $user = Cache::get($token_name);
-            if ($user) {
-                // 如果需要验证设备ID,检查设备ID是否匹配
-                if (!$this->checkDeviceId || $user['device_id'] === $device_id) {
-                    // 检查登录时间是否在tokenCacheTime内
-                    if (time() - $user['login_time'] <= $this->tokenCacheTime) {
-                        return $this->view->fetch('index');
-                    } else {
-                        // 超过缓存时间,清除Session并跳转到登录页面
-                        Session::delete('token_name');
-                        Session::delete('device_id');
-                        Cache::rm($token_name);
+    /** @var int 登录态有效天数 */
+    protected $mprocTtlSeconds = 0;
+
+    /** @var array<string, string>|null purchase_order_detail 表字段:小写 => 真实列名 */
+    protected static $mprocProcuremenColumns = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        if (is_file(APP_PATH . 'extra/mproc.php')) {
+            Config::load(APP_PATH . 'extra/mproc.php', 'mproc');
+        }
+        $days = (int)(Config::get('mproc.session_days') ?: 7);
+        $days = max(1, min(30, $days));
+        $this->mprocTtlSeconds = $days * 86400;
+    }
+
+    /**
+     * 当前手机端登录用户;未登录返回 null
+     */
+    protected function mprocGetUser()
+    {
+        $token = Session::get('mproc_token');
+        if ($token === null || $token === '') {
+            $token = Cookie::get('mproc_token');
+        }
+        if ($token === null || $token === '') {
+            return null;
+        }
+        $token = preg_replace('/[^a-f0-9]/i', '', (string)$token);
+        if (strlen($token) < 16) {
+            return null;
+        }
+        $user = Cache::get('mproc_u_' . $token);
+        if (!is_array($user)) {
+            return null;
+        }
+        // 手机登录必有 phone;账号密码登录必有 username
+        if (empty($user['phone']) && empty($user['username'])) {
+            return null;
+        }
+        if (time() - (int)($user['login_time'] ?? 0) > $this->mprocTtlSeconds) {
+            $this->mprocClearLogin($token);
+            return null;
+        }
+        $user['token'] = $token;
+        return $user;
+    }
+
+    protected function mprocClearLogin($token)
+    {
+        if ($token !== null && $token !== '') {
+            Cache::rm('mproc_u_' . $token);
+        }
+        Session::delete('mproc_token');
+        Cookie::delete('mproc_token');
+    }
+
+    /**
+     * 登录成功后的回跳地址校验(仅允许本站「外发明细订单页」路径,防止开放重定向)
+     *
+     * @param string $raw GET/POST 的 redirect 或当前 REQUEST_URI
+     */
+    protected function mprocSanitizeRedirectUrl($raw)
+    {
+        $s = str_replace(["\r", "\n", "\0"], '', trim((string)$raw));
+        if ($s === '') {
+            return '';
+        }
+        if (preg_match('#^https?://#i', $s)) {
+            $h = parse_url($s, PHP_URL_HOST);
+            if (!is_string($h) || strcasecmp($h, (string)$this->request->host()) !== 0) {
+                return '';
+            }
+            $path = parse_url($s, PHP_URL_PATH);
+            $query = parse_url($s, PHP_URL_QUERY);
+            $s = (is_string($path) && $path !== '' ? $path : '/');
+            if (is_string($query) && $query !== '') {
+                $s .= '?' . $query;
+            }
+        }
+        if (strpos($s, '://') !== false) {
+            return '';
+        }
+        if ($s === '' || ($s[0] !== '/' && stripos($s, 'index.php') !== 0)) {
+            return '';
+        }
+        if ($s[0] !== '/') {
+            $s = '/' . ltrim($s, '/');
+        }
+        if (strpos($s, '//') === 0) {
+            return '';
+        }
+        if (stripos($s, 'index/index/index') === false) {
+            return '';
+        }
+        if (stripos($s, 'index/index/login') !== false) {
+            return '';
+        }
+
+        return $s;
+    }
+
+    /**
+     * 手机端登录页 URL。注意:勿用 url('...login', ['redirect'=>]),在 url_html_suffix 下会把参数拼进 PATHINFO 导致 404。
+     *
+     * @param string $redirectPath 已通过 {@see mprocSanitizeRedirectUrl} 的回跳路径(含 query),空则不带参数
+     */
+    protected function mprocBuildLoginUrl($redirectPath = '')
+    {
+        $root = rtrim($this->request->root(), '/');
+        $path = '/index/index/login';
+        $rp = trim((string)$redirectPath);
+        if ($rp === '') {
+            return $root . $path;
+        }
+
+        return $root . $path . '?' . http_build_query(['redirect' => $rp], '', '&', PHP_QUERY_RFC3986);
+    }
+
+    /**
+     * 登录成功后跳转到订单首页:用当前入口 {@see Request::root} 拼 URL,并从原 redirect 中只保留白名单 query(避免子目录部署丢参、开放重定向)
+     *
+     * @param string $redirectPathOrUrl 已通过 {@see mprocSanitizeRedirectUrl} 的路径或完整 URL(可含 ?focus_eid=)
+     */
+    protected function mprocBuildAfterLoginIndexUrl($redirectPathOrUrl)
+    {
+        $raw = trim((string)$redirectPathOrUrl);
+        if ($raw === '') {
+            return url('index/index/index', '', '', true);
+        }
+        $queryStr = '';
+        if (preg_match('#^https?://#i', $raw)) {
+            $pq = parse_url($raw);
+            $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : '';
+        } elseif (isset($raw[0]) && $raw[0] === '/') {
+            $pq = parse_url('http://127.0.0.1' . $raw);
+            $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : '';
+        } else {
+            $pq = parse_url('http://127.0.0.1/' . ltrim($raw, '/'));
+            $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : '';
+        }
+        $q = [];
+        if ($queryStr !== '') {
+            parse_str($queryStr, $q);
+            if (!is_array($q)) {
+                $q = [];
+            }
+        }
+        $allowed = [];
+        if (isset($q['focus_eid'])) {
+            $fe = (int)$q['focus_eid'];
+            if ($fe > 0) {
+                $allowed['focus_eid'] = $fe;
+            }
+        }
+        $mt = isset($q['main_tab']) ? trim((string)$q['main_tab']) : '';
+        if ($mt === 'me' || $mt === 'orders') {
+            $allowed['main_tab'] = $mt;
+        }
+        $tb = isset($q['tab']) ? trim((string)$q['tab']) : '';
+        if (in_array($tb, ['draft', 'submitted', 'done', 'me'], true)) {
+            $allowed['tab'] = $tb;
+        }
+        if (isset($q['q']) && trim((string)$q['q']) !== '') {
+            $allowed['q'] = substr(trim((string)$q['q']), 0, 120);
+        }
+        if (isset($allowed['focus_eid']) && !isset($allowed['main_tab'])) {
+            $allowed['main_tab'] = 'orders';
+        }
+        $base = rtrim($this->request->root(true), '/');
+        $path = '/index/index/index';
+        $qs = $allowed !== [] ? ('?' . http_build_query($allowed, '', '&', PHP_QUERY_RFC3986)) : '';
+
+        return $base . $path . $qs;
+    }
+
+    /**
+     * 从 purchase_order_detail 表解析真实列名(SHOW COLUMNS 只查一次,按候选小写名匹配第一条)
+     *
+     * @param string[] $candidatesLower 如 ['status','istatus']
+     * @return string|null
+     */
+    protected function mprocResolveProcuremenColumn(array $candidatesLower)
+    {
+        if (self::$mprocProcuremenColumns === null) {
+            self::$mprocProcuremenColumns = [];
+            try {
+                $rows = Db::query('SHOW COLUMNS FROM `purchase_order_detail`');
+                if (is_array($rows)) {
+                    foreach ($rows as $c) {
+                        $name = isset($c['Field']) ? (string)$c['Field'] : '';
+                        if ($name !== '') {
+                            self::$mprocProcuremenColumns[strtolower($name)] = $name;
+                        }
                     }
                 }
+            } catch (\Throwable $e) {
+                self::$mprocProcuremenColumns = [];
+            }
+        }
+        foreach ($candidatesLower as $low) {
+            $k = strtolower((string)$low);
+            if (isset(self::$mprocProcuremenColumns[$k])) {
+                return self::$mprocProcuremenColumns[$k];
             }
         }
+        return null;
+    }
 
-        if ($this->request->isPost()) {
-            $post = $this->request->post();
-            $where = [
-                'username' => $post['name'], // 用户名是否一致
-                'password' => md5($post['password']) // 密码是否一致
-            ];
-            $list = Db::name('user_name')->where($where)->find();
-            if ($list) {
-                // 生成一个随机的token
-                $token = md5(rand(1, 999));
-                // 生成一个设备ID
-                $device_id = md5($_SERVER['HTTP_USER_AGENT'] . $_SERVER['REMOTE_ADDR']);
-                Session::set('token_name', $token);
-                Session::set('device_id', $device_id);
-                // 记录登录时间、token和设备ID到缓存
-                $userData = ['username' => $post['name'], 'token' => $token, 'login_time' => time(), 'device_id' => $device_id];
-                Cache::set($token, $userData, $this->tokenCacheTime);
-                // 更新数据库中的token和登录时间
-                Db::name('user_name')->where('username', $post['name'])->update(['token' => $token, 'login_time' => time()]);
-                $this->success('登录成功');
-                return $this->view->fetch('index');
-            } else {
-                $this->error('账号或密码错误');
-                return $this->view->fetch('login');
-            }
-        }
-
-        // 未登录或token不匹配,跳转到指定页面
-        return $this->redirect('/lqHKfByepX.php');
-    }
-
-//    public function index() {
-//        return $this->redirect('/lqHKfByepX.php/');
-//
-//        $token_name = Session::get('token_name');
-//        $device_id = Session::get('device_id');
-//        // 检查用户是否已登录
-//        if ($token_name) {
-//            // 验证缓存中的token
-//            $user = Cache::get($token_name);
-//            if ($user) {
-//                // 如果需要验证设备ID,检查设备ID是否匹配
-//                if (!$this->checkDeviceId || $user['device_id'] === $device_id) {
-//                    // 检查登录时间是否在tokenCacheTime内
-//                    if (time() - $user['login_time'] <= $this->tokenCacheTime) {
-//                        return $this->view->fetch('index');
-//                    } else {
-//                        // 超过缓存时间,清除Session并跳转到登录页面
-//                        Session::delete('token_name');
-//                        Session::delete('device_id');
-//                        Cache::rm($token_name);
-//                    }
-//                }
-//            }
-//        }
-//
-//        if ($this->request->isPost()) {
-//            $post = $this->request->post();
-//            $where = [
-//                'username' => $post['name'], // 用户名是否一致
-//                'password' => md5($post['password']) // 密码是否一致
-//            ];
-//            $list = Db::name('user_name')->where($where)->find();
-//            if ($list) {
-//                // 生成一个随机的token
-//                $token = md5(rand(1, 999));
-//                // 生成一个设备ID
-//                $device_id = md5($_SERVER['HTTP_USER_AGENT'] . $_SERVER['REMOTE_ADDR']);
-//                Session::set('token_name', $token);
-//                Session::set('device_id', $device_id);
-//                // 记录登录时间、token和设备ID到缓存
-//                $userData = ['username' => $post['name'], 'token' => $token, 'login_time' => time(), 'device_id' => $device_id];
-//                Cache::set($token, $userData, $this->tokenCacheTime);
-//                // 更新数据库中的token和登录时间
-//                Db::name('user_name')->where('username', $post['name'])->update(['token' => $token, 'login_time' => time()]);
-//                $this->success('登录成功');
-//                return $this->view->fetch('index');
-//            } else {
-//                $this->error('账号或密码错误');
-//                return $this->view->fetch('login');
-//            }
-//        }
-//
-//        // 未登录或token不匹配,跳转到登录页面
-//        return $this->view->fetch('login');
-//    }
-
-    public function login() {
-        $token_name = Session::get('token_name');
-        $device_id = Session::get('device_id');
-        // 如果token存在,检查是否过期
-        if ($token_name) {
-            $user = Cache::get($token_name);
-            if ($user) {
-                // 如果需要验证设备ID,检查设备ID是否匹配
-                if (!$this->checkDeviceId || $user['device_id'] === $device_id) {
-                    // 检查登录时间是否在tokenCacheTime内
-                    if (time() - $user['login_time'] <= $this->tokenCacheTime) {
-                        return $this->view->fetch('index');
-                    } else {
-                        // 超过缓存时间,清除Session
-                        Session::delete('token_name');
-                        Session::delete('device_id');
-                        Cache::rm($token_name);
+    /**
+     * 列表:非管理员按 company_name 与登录时解析的单位名一致;管理员不加条件
+     *
+     * @return array 可直接 $query->where($arr),空数组表示不加条件
+     */
+    protected function mprocListWhereForLoginUser(array $user)
+    {
+        if (!empty($user['is_admin'])) {
+            return [];
+        }
+        $cCol = $this->mprocResolveProcuremenColumn(['company_name']);
+        if ($cCol === null || $cCol === '') {
+            return ['id' => 0];
+        }
+        $cn = trim((string)($user['company_name'] ?? ''));
+        if ($cn === '') {
+            $phone = trim((string)($user['phone'] ?? ''));
+            if ($phone !== '') {
+                $cn = $this->mprocResolveCompanyForLoginPhone($phone);
+            }
+        }
+        if ($cn === '') {
+            return ['id' => 0];
+        }
+        return [$cCol => $cn];
+    }
+
+    /**
+     * 登录手机号对应的外协单位名称:优先 customer 表公司名/名称;否则取 purchase_order_detail 该手机最新一条
+     */
+    protected function mprocResolveCompanyForLoginPhone(string $phone): string
+    {
+        $phone = trim($phone);
+        if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) {
+            return '';
+        }
+        $cust = $this->mprocFindCustomerRowByPhone($phone);
+        if (is_array($cust) && $cust !== []) {
+            $co = $this->mprocCustomerPickField($cust, ['company_name', 'name']);
+            if ($co !== '') {
+                return $co;
+            }
+        }
+        try {
+            $one = Db::table('purchase_order_detail')
+                ->where('phone', $phone)
+                ->order('id', 'desc')
+                ->find();
+            if (is_array($one)) {
+                $n = trim((string)($one['company_name'] ?? ''));
+                if ($n !== '') {
+                    return $n;
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+        return '';
+    }
+
+    /**
+     * 仅按手机号在 customer.phone(多号逗号分隔)中匹配一条客户记录
+     *
+     * @return array<string, mixed>|null
+     */
+    protected function mprocFindCustomerRowByPhone(string $phone): ?array
+    {
+        $phone = trim($phone);
+        if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) {
+            return null;
+        }
+        try {
+            $rows = Db::table('customer')->where('phone', '<>', '')->select();
+        } catch (\Throwable $e) {
+            return null;
+        }
+        if (!is_array($rows)) {
+            return null;
+        }
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $raw = (string)($r['phone'] ?? '');
+            $raw = str_replace(["\r", "\n", "\t", ' ', ' ', ','], ['', '', '', '', '', ','], $raw);
+            foreach (explode(',', $raw) as $seg) {
+                if (trim($seg) === $phone) {
+                    return $r;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 管理员表 fa_admin.mobile 与当前手机号一致且未禁用(用于手机验证码管理员通道)
+     *
+     * @return array<string, mixed>|null
+     */
+    protected function mprocAdminRowByMobile(string $phone): ?array
+    {
+        $phone = trim($phone);
+        if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) {
+            return null;
+        }
+        try {
+            $row = Db::name('admin')
+                ->where('mobile', $phone)
+                ->where('status', '<>', 'hidden')
+                ->order('id', 'asc')
+                ->find();
+        } catch (\Throwable $e) {
+            return null;
+        }
+        return is_array($row) && $row !== [] ? $row : null;
+    }
+
+    /**
+     * 在 customer 表中匹配当前用户:优先手机号(支持多号逗号分隔),否则按公司名与 company_name / name
+     *
+     * @return array<string, mixed>|null
+     */
+    protected function mprocFindCustomerRowForUser(array $user): ?array
+    {
+        $phone = trim((string)($user['phone'] ?? ''));
+        $cn = trim((string)($user['company_name'] ?? ''));
+        if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) {
+            $byPhone = $this->mprocFindCustomerRowByPhone($phone);
+            if ($byPhone !== null) {
+                return $byPhone;
+            }
+        }
+        if ($cn !== '') {
+            try {
+                $hit = Db::table('customer')
+                    ->where(function ($q) use ($cn) {
+                        $q->where('company_name', $cn)->whereOr('name', $cn);
+                    })
+                    ->order('id', 'desc')
+                    ->find();
+            } catch (\Throwable $e) {
+                $hit = null;
+            }
+            if (is_array($hit) && $hit !== []) {
+                return $hit;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 从 customer 行取字段(兼容列名大小写)
+     *
+     * @param string[] $candidates
+     */
+    protected function mprocCustomerPickField(array $row, array $candidates): string
+    {
+        foreach ($candidates as $want) {
+            $lw = strtolower($want);
+            foreach ($row as $k => $v) {
+                if (!is_string($k)) {
+                    continue;
+                }
+                if (strtolower($k) !== $lw) {
+                    continue;
+                }
+                $s = trim((string)$v);
+                if ($s !== '') {
+                    return $s;
+                }
+            }
+        }
+        return '';
+    }
+
+    /**
+     * 明细表关键字搜索:仅对 purchase_order_detail 真实存在的列 LIKE;
+     * 主表 purchase_order 上的订单号、印件、工序等另查 scydgy_id 再 OR 进列表(避免引用不存在的列导致整页查失败)。
+     *
+     * @param \think\db\Query $query
+     */
+    protected function mprocApplySearchKeywordToDetailQuery($query, $search)
+    {
+        $kw = trim((string)$search);
+        if ($kw === '') {
+            return;
+        }
+        $map = self::$mprocProcuremenColumns;
+        if (!is_array($map) || $map === []) {
+            return;
+        }
+        $like = '%' . addcslashes($kw, '%_\\') . '%';
+        $wantLower = ['ccydh', 'cyjmc', 'cdxmc', 'company_name', 'cgzzxmc', 'cgymc', 'cdf', 'phone', 'email'];
+        $detailCols = [];
+        foreach ($wantLower as $low) {
+            if (isset($map[$low])) {
+                $detailCols[] = $map[$low];
+            }
+        }
+        $scydgyCol = isset($map['scydgy_id']) ? $map['scydgy_id'] : 'scydgy_id';
+        $idCol = isset($map['id']) ? $map['id'] : 'id';
+
+        $poSidList = [];
+        $poWant = ['CCYDH', 'CYJMC', 'CDXMC', 'CGYMC', 'cGzzxMc', 'CDF'];
+        try {
+            $col = Db::table('purchase_order')
+                ->where(function ($sub) use ($like, $poWant) {
+                    $firstPo = true;
+                    foreach ($poWant as $pf) {
+                        if ($firstPo) {
+                            $sub->where($pf, 'like', $like);
+                            $firstPo = false;
+                        } else {
+                            $sub->whereOr($pf, 'like', $like);
+                        }
+                    }
+                })
+                ->column('scydgy_id');
+            if (is_array($col)) {
+                foreach ($col as $v) {
+                    $id = (int)$v;
+                    if ($id > 0) {
+                        $poSidList[$id] = true;
+                    }
+                }
+            }
+            $poSidList = array_keys($poSidList);
+        } catch (\Throwable $e) {
+            $poSidList = [];
+        }
+
+        $query->where(function ($q2) use ($like, $detailCols, $poSidList, $scydgyCol, $idCol) {
+            $first = true;
+            foreach ($detailCols as $col) {
+                if ($first) {
+                    $q2->where($col, 'like', $like);
+                    $first = false;
+                } else {
+                    $q2->whereOr($col, 'like', $like);
+                }
+            }
+            if ($poSidList !== []) {
+                if ($first) {
+                    $q2->where($scydgyCol, 'in', $poSidList);
+                    $first = false;
+                } else {
+                    $q2->whereOr($scydgyCol, 'in', $poSidList);
+                }
+            }
+            if ($first) {
+                $q2->where($idCol, '=', 0);
+            }
+        });
+    }
+
+    /**
+     * 列表:按左侧 Tab 追加 status_name 条件(与数值 status 0/1/2 无关;值由后端维护)
+     * - draft:status_name = 未提交
+     * - submitted:status_name = 已提交
+     * - done:status_name = 已完成
+     * 表无 status_name 列时不加条件(三个 Tab 数据相同,待库表补列后再筛)
+     *
+     * @param mixed       $query
+     * @param string      $tab       draft|submitted|done
+     * @param string|null $statusNameCol 真实列名,如 status_name
+     */
+    protected function mprocApplyListTabConditions($query, $tab, $statusNameCol)
+    {
+        if ($statusNameCol === null) {
+            return;
+        }
+        $map = [
+            'draft'     => '未提交',
+            'submitted' => '已提交',
+            'done'      => '已完成',
+        ];
+        $label = $map[$tab] ?? '未提交';
+        $query->where($statusNameCol, '=', $label);
+    }
+
+    /**
+     * 「我的」:公司名称、姓名、手机、邮箱以 customer 表为准;无匹配时回退 purchase_order_detail
+     */
+    protected function mprocProfileForUser(array $user)
+    {
+        $phone = trim((string)($user['phone'] ?? ''));
+        $out = [
+            'company_name' => trim((string)($user['company_name'] ?? '')),
+            'contact_name' => '',
+            'phone'        => $phone,
+            'email'        => '',
+        ];
+        if ($phone === '' && !empty($user['username'])) {
+            if ($out['company_name'] === '') {
+                $out['company_name'] = '账号:' . (string)$user['username'];
+            }
+            return $out;
+        }
+
+        $cust = $this->mprocFindCustomerRowForUser($user);
+        if (is_array($cust) && $cust !== []) {
+            $co = $this->mprocCustomerPickField($cust, ['company_name', 'name']);
+            if ($co !== '') {
+                $out['company_name'] = $co;
+            }
+            $out['contact_name'] = $this->mprocCustomerPickField($cust, ['username', 'contact', 'linkman', 'contacts']);
+            $em = $this->mprocCustomerPickField($cust, ['email']);
+            if ($em !== '') {
+                $out['email'] = $em;
+            }
+            $rawP = $this->mprocCustomerPickField($cust, ['phone']);
+            if ($rawP !== '') {
+                $norm = str_replace(["\r", "\n", "\t", ' ', ' ', ','], ['', '', '', '', '', ','], $rawP);
+                $segs = [];
+                foreach (explode(',', $norm) as $seg) {
+                    $seg = trim($seg);
+                    if ($seg !== '') {
+                        $segs[] = $seg;
+                    }
+                }
+                if ($phone !== '' && $segs !== [] && in_array($phone, $segs, true)) {
+                    $out['phone'] = $phone;
+                } elseif ($segs !== []) {
+                    $out['phone'] = implode('、', $segs);
+                } else {
+                    $out['phone'] = $rawP;
+                }
+            }
+            return $out;
+        }
+
+        try {
+            $one = null;
+            $cCol = $this->mprocResolveProcuremenColumn(['company_name']);
+            $co = $out['company_name'];
+            if ($cCol && $co !== '') {
+                $one = Db::table('purchase_order_detail')->where($cCol, $co)->order('id', 'desc')->find();
+            }
+            if (!is_array($one) && $phone !== '') {
+                $one = Db::table('purchase_order_detail')
+                    ->where('phone', $phone)
+                    ->order('id', 'desc')
+                    ->find();
+            }
+            if (is_array($one)) {
+                if ($out['company_name'] === '') {
+                    $out['company_name'] = trim((string)($one['company_name'] ?? ''));
+                }
+                $out['email'] = trim((string)($one['email'] ?? ''));
+                $rp = trim((string)($one['phone'] ?? ''));
+                if ($rp !== '') {
+                    $out['phone'] = $rp;
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+        return $out;
+    }
+
+    /**
+     * 将 purchase_order(工序行主表)快照合并进 purchase_order_detail 行:订单级信息以主表为准;
+     * 金额、交期、外厂 company、明细 status 等仍保留明细表。
+     *
+     * @param array $row  引用:明细行
+     * @param array $poRow purchase_order 一行
+     */
+    protected function mprocMergePurchaseOrderIntoDetail(array &$row, array $poRow)
+    {
+        $pl = array_change_key_case($poRow, CASE_LOWER);
+        $hdr = [
+            'ccydh'     => 'CCYDH',
+            'cyjmc'     => 'CYJMC',
+            'cdf'       => 'CDF',
+            'cgzzxmc'   => 'cGzzxMc',
+            'cgymc'     => 'CGYMC',
+            'cdxmc'     => 'CDXMC',
+            'ngzl'      => 'NGZL',
+            'cdw'       => 'CDW',
+            'cgybh'     => 'CGYBH',
+        ];
+        foreach ($hdr as $lk => $out) {
+            if (!array_key_exists($lk, $pl)) {
+                continue;
+            }
+            $v = $pl[$lk];
+            if ($v !== null && $v !== '') {
+                $row[$out] = $v;
+            }
+        }
+        // 本次数量、最高限价:仅存在于主表;PDO 列名可能为小写 ceilingprice
+        if (array_key_exists('this_quantity', $pl)) {
+            $row['This_quantity'] = $pl['this_quantity'];
+        }
+        if (array_key_exists('ceilingprice', $pl)) {
+            $row['ceilingPrice'] = $pl['ceilingprice'];
+        } elseif (array_key_exists('ceiling_price', $pl)) {
+            $row['ceilingPrice'] = $pl['ceiling_price'];
+        }
+    }
+
+    /**
+     * 查询 purchase_order_detail 列表(订单页)
+     * 无搜索词时:左侧 Tab 按 status_name 筛选(未提交/已提交/已完成)
+     * 有搜索词时:不按 Tab 筛选,在本单位可见数据内全局关键字匹配
+     *
+     * @param string      $tab             draft|submitted|done
+     * @param string|null $statusNameCol   status_name 真实列名;为 null 时不按 Tab 过滤
+     * @return array{rows: array, done_no_status: int}
+     */
+    protected function mprocFetchProcuremenList(array $user, $tab, $q, $statusNameCol)
+    {
+        $query = Db::table('purchase_order_detail')->order('id', 'desc');
+        $userWhere = $this->mprocListWhereForLoginUser($user);
+        if ($userWhere !== []) {
+            $query->where($userWhere);
+        }
+        $this->mprocApplySearchKeywordToDetailQuery($query, $q);
+        // 有搜索词时不在此按 status_name 分栏筛选,全局匹配;无搜索词时仍按左侧 Tab(未提交/已提交/已完成)筛选
+        if (trim((string)$q) === '') {
+            $this->mprocApplyListTabConditions($query, $tab, $statusNameCol);
+        }
+
+        try {
+            $rows = $query->limit(500)->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (!is_array($rows)) {
+            $rows = [];
+        }
+
+        $poBySid = [];
+        $sidList = [];
+        foreach ($rows as $r0) {
+            if (!is_array($r0)) {
+                continue;
+            }
+            $sid0 = (int)($r0['scydgy_id'] ?? $r0['SCYDGY_ID'] ?? 0);
+            if ($sid0 > 0) {
+                $sidList[$sid0] = true;
+            }
+        }
+        if ($sidList !== []) {
+            try {
+                $poRows = Db::table('purchase_order')
+                    ->where('scydgy_id', 'in', array_values(array_keys($sidList)))
+                    ->select();
+                if (is_array($poRows)) {
+                    foreach ($poRows as $pr) {
+                        $sidk = (int)($pr['scydgy_id'] ?? $pr['SCYDGY_ID'] ?? 0);
+                        if ($sidk > 0) {
+                            $poBySid[$sidk] = $pr;
+                        }
                     }
                 }
+            } catch (\Throwable $e) {
             }
         }
 
-        if ($this->request->isPost()) {
-            $post = $this->request->post();
-            $where = [
-                'username' => $post['name'], // 用户名是否一致
-                'password' => md5($post['password']) // 密码是否一致
-            ];
-            $list = Db::name('user_name')->where($where)->find();
-            if ($list) {
-                // 生成一个随机的token
-                $token = md5(rand(1, 999));
-                // 生成一个设备ID
-                $device_id = md5($_SERVER['HTTP_USER_AGENT'] . $_SERVER['REMOTE_ADDR']);
-                Session::set('token_name', $token);
-                Session::set('device_id', $device_id);
-                // 记录登录时间、token和设备ID到缓存
-                $userData = ['username' => $post['name'], 'token' => $token, 'login_time' => time(), 'device_id' => $device_id];
-                Cache::set($token, $userData, $this->tokenCacheTime);
-                // 更新数据库中的token和登录时间
-                Db::name('user_name')->where('username', $post['name'])->update(['token' => $token, 'login_time' => time()]);
-                $this->success('登录成功');
-                return $this->view->fetch('index');
+        foreach ($rows as &$row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $row['eid'] = (int)($row['id'] ?? $row['ID'] ?? 0);
+            $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0);
+            if ($sid > 0 && isset($poBySid[$sid])) {
+                $this->mprocMergePurchaseOrderIntoDetail($row, $poBySid[$sid]);
+            }
+            // status_name 由库表/后端维护,不在此根据 amount 覆盖
+            if (!isset($row['status_name']) || $row['status_name'] === null) {
+                $row['status_name'] = '';
+            } else {
+                $row['status_name'] = trim((string)$row['status_name']);
+            }
+            $row['mproc_can_edit'] = $this->mprocCanEditRow($user, $row) ? 1 : 0;
+
+            $am = $row['amount'] ?? null;
+            if ($am === null || $am === '' || (is_string($am) && trim($am) === '')) {
+                $row['amount_display'] = '';
             } else {
-                $this->error('账号或密码错误');
-                return $this->view->fetch('login');
+                $row['amount_display'] = is_scalar($am) ? (string)$am : '';
             }
+            $dv = isset($row['delivery']) ? trim((string)$row['delivery']) : '';
+            if ($dv !== '' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $dv, $m)) {
+                $row['delivery_display'] = $m[1];
+            } elseif ($dv !== '') {
+                $row['delivery_display'] = $dv;
+            } else {
+                $row['delivery_display'] = '';
+            }
+            $row['amount_missing'] = ($am === null || $am === '' || (is_string($am) && trim($am) === '')) ? 1 : 0;
+            $row['delivery_missing'] = ($dv === '' || preg_match('/^0000-00-00/i', $dv)) ? 1 : 0;
+            $row['mproc_fill_hint'] = '';
+        }
+        unset($row);
+
+        return [
+            'rows'             => $rows ?: [],
+            'done_no_status'   => (int)($statusNameCol === null),
+        ];
+    }
+
+    /**
+     * 外发明细首页(需登录)
+     * GET:main_tab=orders|me,orders 时 tab=draft|submitted|done 对应 status_name:未提交|已提交|已完成;q 搜索词
+     */
+    public function index()
+    {
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
+            $safe = $this->mprocSanitizeRedirectUrl($uri);
+            if ($safe !== '') {
+                Session::set('mproc_intended_url', $safe);
+            }
+            $this->redirect($this->mprocBuildLoginUrl(''));
+
+            return;
+        }
+
+        $tabParam = trim((string)$this->request->get('tab', 'draft'));
+        $mainTab = trim((string)$this->request->get('main_tab', 'orders'));
+        // 旧地址 ?tab=me 表示「我的」
+        if ($tabParam === 'me') {
+            $mainTab = 'me';
+        }
+        if (!in_array($mainTab, ['orders', 'me'], true)) {
+            $mainTab = 'orders';
+        }
+        $tab = $tabParam === 'me' ? 'draft' : $tabParam;
+        if (!in_array($tab, ['draft', 'submitted', 'done'], true)) {
+            $tab = 'draft';
+        }
+        $q = trim((string)$this->request->get('q', ''));
+
+        $mprocFocusEid = 0;
+        $focusEid = (int)$this->request->get('focus_eid', 0);
+        if ($focusEid > 0 && $mainTab === 'orders') {
+            $idCol = $this->mprocResolveProcuremenColumn(['id']);
+            if ($idCol) {
+                $qw = $this->mprocListWhereForLoginUser($user);
+                $dr = null;
+                try {
+                    $qrow = Db::table('purchase_order_detail')->where($idCol, $focusEid);
+                    if ($qw !== []) {
+                        $qrow->where($qw);
+                    }
+                    $dr = $qrow->find();
+                } catch (\Throwable $e) {
+                    $dr = null;
+                }
+                if (is_array($dr) && $dr !== []) {
+                    $mprocFocusEid = $focusEid;
+                    $q = '';
+                    $sn = trim((string)($dr['status_name'] ?? $dr['STATUS_NAME'] ?? ''));
+                    $mapTab = ['未提交' => 'draft', '已提交' => 'submitted', '已完成' => 'done'];
+                    if ($sn !== '' && isset($mapTab[$sn])) {
+                        $tab = $mapTab[$sn];
+                    }
+                }
+            }
+        }
+
+        // 左侧 Tab 按 purchase_order_detail.status_name(未提交/已提交/已完成),与数值 status 无关
+        $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']);
+        $profile = $this->mprocProfileForUser($user);
+
+        $this->view->assign('mprocMainTab', $mainTab);
+        $this->view->assign('mprocTab', $tab);
+        $this->view->assign('mprocSearchQ', $q);
+        $this->view->assign('mprocProfile', $profile);
+        $this->view->assign('mprocIsAdmin', !empty($user['is_admin']) ? 1 : 0);
+        $this->view->assign('mprocFocusEid', $mprocFocusEid);
+
+        if ($mainTab === 'me') {
+            $this->view->assign('rows', []);
+
+            return $this->view->fetch();
         }
+
+        $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol);
+        $this->view->assign('rows', $bundle['rows']);
+
         return $this->view->fetch();
     }
 
-    public function logout() {
-        return $this->view->fetch('logout');
-//        $token_name = Session::get('token_name');
-//        if ($token_name) {
-//            // 清除缓存中的token
-//            Cache::rm($token_name);
-//            // 清除Session
-//            Session::delete('token_name');
-//            Session::delete('device_id');
-//        }
-//        $this->success('退出登录');
-//
-//        // 跳转到指定的URL
-////        return $this->redirect('http://xh-erp.7in6.com/');
-//        return $this->redirect('http://xh/');
+    /**
+     * 外发明细列表 JSON(需登录)
+     * main_tab=orders|me;orders 时 tab=draft|submitted|done、q=搜索词
+     */
+    public function mprocList()
+    {
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $this->error('请先登录', url('index/index/login'));
+        }
+        $tabParam = trim((string)$this->request->request('tab', 'draft'));
+        $mainTab = trim((string)$this->request->request('main_tab', 'orders'));
+        if ($tabParam === 'me') {
+            $mainTab = 'me';
+        }
+        if (!in_array($mainTab, ['orders', 'me'], true)) {
+            $mainTab = 'orders';
+        }
+        $tab = $tabParam === 'me' ? 'draft' : $tabParam;
+        if (!in_array($tab, ['draft', 'submitted', 'done'], true)) {
+            $tab = 'draft';
+        }
+        $q = trim((string)$this->request->request('q', ''));
+
+        if ($mainTab === 'me') {
+            // Jump::success($msg, $url, $data, …) 第二参是 URL,数据必须放第三参
+            $this->success('ok', '', [
+                'main_tab'       => 'me',
+                'tab'            => $tab,
+                'rows'           => [],
+                'profile'        => $this->mprocProfileForUser($user),
+                'done_no_status' => 0,
+            ]);
+        }
+
+        $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']);
+        $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol);
+        $this->success('ok', '', array_merge([
+            'main_tab' => 'orders',
+            'tab'      => $tab,
+            'is_admin' => !empty($user['is_admin']) ? 1 : 0,
+        ], $bundle));
+    }
+
+    /**
+     * 登录页(手机号验证码 / 账号密码)
+     */
+    public function login()
+    {
+        if ($this->mprocGetUser()) {
+            $this->redirect(url('index/index/index'));
+        }
+        $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', ''));
+        if ($redirect !== '') {
+            Session::set('mproc_intended_url', $redirect);
+        }
+        $this->view->assign('mprocLoginRedirect', $redirect);
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 发送登录验证码(POST:phone)
+     */
+    public function sendSms()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $phone = trim((string)$this->request->post('phone', ''));
+        if (!preg_match('/^1\d{10}$/', $phone)) {
+            $this->error('请输入正确的11位手机号');
+        }
+        if (!$this->mprocFindCustomerRowByPhone($phone) && !$this->mprocAdminRowByMobile($phone)) {
+            $this->error('账号未开通权限,请联系管理员开通');
+        }
+        $cd = (int)(Config::get('mproc.sms_resend_cd') ?: 55);
+        if (Cache::get('mproc_sms_wait_' . $phone)) {
+            $this->error('发送过于频繁,请稍后再试');
+        }
+        $code = (string)random_int(100000, 999999);
+        $ttl = (int)(Config::get('mproc.sms_code_ttl') ?: 300);
+        $ttl = max(60, min(600, $ttl));
+        Cache::set('mproc_code_' . $phone, $code, $ttl);
+        Cache::set('mproc_sms_wait_' . $phone, 1, $cd);
+
+        try {
+            $this->mprocSmsSend($phone, '【登录验证】您的验证码为' . $code . ',' . (int)($ttl / 60) . '分钟内有效,请勿泄露。');
+        } catch (\Exception $e) {
+            Cache::rm('mproc_code_' . $phone);
+            Cache::rm('mproc_sms_wait_' . $phone);
+            $this->error($e->getMessage());
+        }
+        $this->success('验证码已发送');
+    }
+
+    /**
+     * 验证码登录(POST:phone、code)
+     */
+    public function doLogin()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $phone = trim((string)$this->request->post('phone', ''));
+        $code = trim((string)$this->request->post('code', ''));
+        if (!preg_match('/^1\d{10}$/', $phone)) {
+            $this->error('手机号格式不正确');
+        }
+        if (!preg_match('/^\d{6}$/', $code)) {
+            $this->error('请输入6位验证码');
+        }
+        // 本地调试:application/extra/mproc.php 中配置 mock_sms_code 与输入一致时,不校验短信缓存(生产务必留空)
+        $mock = Config::get('mproc.mock_sms_code');
+        if ($mock !== null && $mock !== '' && (string)$mock === $code) {
+            Cache::rm('mproc_code_' . $phone);
+        } else {
+            $cached = Cache::get('mproc_code_' . $phone);
+            if ($cached === false || $cached === null || (string)$cached !== $code) {
+                $this->error('验证码错误或已过期');
+            }
+            Cache::rm('mproc_code_' . $phone);
+        }
+
+        $cust = $this->mprocFindCustomerRowByPhone($phone);
+        if (is_array($cust) && $cust !== []) {
+            $isAdmin = 0;
+            $companyName = $this->mprocCustomerPickField($cust, ['company_name', 'name']);
+            if ($companyName === '') {
+                try {
+                    $one = Db::table('purchase_order_detail')->where('phone', $phone)->order('id', 'desc')->find();
+                    if (is_array($one)) {
+                        $companyName = trim((string)($one['company_name'] ?? ''));
+                    }
+                } catch (\Throwable $e) {
+                }
+            }
+        } elseif ($this->mprocAdminRowByMobile($phone)) {
+            $isAdmin = 1;
+            $companyName = '';
+        } else {
+            $this->error('账号未开通权限,请联系管理员开通');
+        }
+
+        $old = Session::get('mproc_token');
+        if ($old) {
+            Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old));
+        }
+
+        $token = bin2hex(random_bytes(16));
+        $userData = [
+            'phone'         => $phone,
+            'company_name'  => $companyName,
+            'username'      => '',
+            'login_type'    => 'sms',
+            'is_admin'      => $isAdmin ? 1 : 0,
+            'login_time'    => time(),
+        ];
+        // 缓存略长于逻辑有效期,过期以 login_time 为准
+        Cache::set('mproc_u_' . $token, $userData, $this->mprocTtlSeconds + 86400);
+        Session::set('mproc_token', $token);
+        Cookie::set('mproc_token', $token, $this->mprocTtlSeconds);
+
+        $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);
+    }
+
+    /**
+     * 账号密码登录(POST:username、password)
+     * 与后台 FastAdmin 一致:表 admin、密码 md5(md5(明文)+salt)、状态禁用与失败锁定规则同 admin/library/Auth::login
+     * 成功后仅建立手机端外发明细会话(不写后台 Session,避免与 PC 后台互踢登录态)
+     */
+    public function doLoginPwd()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $username = trim((string)$this->request->post('username', ''));
+        $password = (string)$this->request->post('password', '');
+        if ($username === '' || $password === '') {
+            $this->error('请输入账号和密码');
+        }
+
+        // 直接用 Db 查 admin,避免加载 Admin 模型与 AdminAuth(连带 fast\Auth),缩短首包时间
+        $row = null;
+        try {
+            $row = Db::name('admin')
+                ->field('id,username,password,salt,status,loginfailure,updatetime')
+                ->where('username', $username)
+                ->find();
+        } catch (\Throwable $e) {
+            $row = null;
+        }
+        if (!$row || !is_array($row)) {
+            $this->error('账号或密码错误');
+        }
+        $id = (int)($row['id'] ?? 0);
+        if (($row['status'] ?? '') == 'hidden') {
+            $this->error('该账号已禁用');
+        }
+        if (Config::get('fastadmin.login_failure_retry') && (int)($row['loginfailure'] ?? 0) >= 10 && time() - (int)($row['updatetime'] ?? 0) < 86400) {
+            $this->error('登录失败次数过多,请24小时后再试');
+        }
+
+        $salt = (string)($row['salt'] ?? '');
+        $hashStored = (string)($row['password'] ?? '');
+        $hashInput = md5(md5($password) . $salt);
+        if ($hashStored === '' || $hashInput !== $hashStored) {
+            if ($id > 0) {
+                try {
+                    Db::name('admin')->where('id', $id)->update([
+                        'loginfailure' => (int)($row['loginfailure'] ?? 0) + 1,
+                        'updatetime'   => time(),
+                    ]);
+                } catch (\Throwable $e) {
+                }
+            }
+            $this->error('账号或密码错误');
+        }
+
+        if ($id > 0) {
+            try {
+                Db::name('admin')->where('id', $id)->update([
+                    'loginfailure' => 0,
+                    'updatetime'   => time(),
+                ]);
+            } catch (\Throwable $e) {
+            }
+        }
+
+        $old = Session::get('mproc_token');
+        if ($old) {
+            Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old));
+        }
+
+        $token = bin2hex(random_bytes(16));
+        $userData = [
+            'phone'         => '',
+            'company_name'  => '',
+            'username'      => $username,
+            'login_type'    => 'pwd',
+            'is_admin'      => 1,
+            'login_time'    => time(),
+        ];
+        Cache::set('mproc_u_' . $token, $userData, $this->mprocTtlSeconds + 86400);
+        Session::set('mproc_token', $token);
+        Cookie::set('mproc_token', $token, $this->mprocTtlSeconds);
+
+        $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);
+    }
+
+    /**
+     * 是否允许当前登录用户修改该条 purchase_order_detail 的金额、交期
+     * 仅普通外协用户(短信登录且非管理员)可改;管理员手机号、账号密码登录仅可查看
+     */
+    protected function mprocCanEditRow(array $user, array $row)
+    {
+        if (!empty($user['is_admin'])) {
+            return false;
+        }
+        if (($user['login_type'] ?? '') === 'pwd') {
+            return false;
+        }
+        $uCo = trim((string)($user['company_name'] ?? ''));
+        if ($uCo === '') {
+            $uPhone = trim((string)($user['phone'] ?? ''));
+            if ($uPhone !== '') {
+                $uCo = $this->mprocResolveCompanyForLoginPhone($uPhone);
+            }
+        }
+        $rCo = trim((string)($row['company_name'] ?? ''));
+        if ($uCo !== '' && $rCo !== '' && strcmp($rCo, $uCo) === 0) {
+            return true;
+        }
+        $uPhone = trim((string)($user['phone'] ?? ''));
+        $rPhone = trim((string)($row['phone'] ?? ''));
+        return $uPhone !== '' && $rPhone !== '' && strcasecmp($rPhone, $uPhone) === 0;
+    }
+
+    /**
+     * 保存单条外发明细的金额、交期(POST:id、amount、delivery)
+     */
+    public function mprocSave()
+    {
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $this->error('请先登录', url('index/index/login'));
+        }
+        $id = (int)$this->request->post('id', 0);
+        if ($id <= 0) {
+            $this->error('参数错误');
+        }
+        $row = null;
+        try {
+            $row = Db::table('purchase_order_detail')->where('id', $id)->find();
+            if (!$row) {
+                $row = Db::table('purchase_order_detail')->where('ID', $id)->find();
+            }
+        } catch (\Throwable $e) {
+            $row = null;
+        }
+        if (!$row || !is_array($row)) {
+            $this->error('记录不存在');
+        }
+        if (!$this->mprocCanEditRow($user, $row)) {
+            if (!empty($user['is_admin']) || (($user['login_type'] ?? '') === 'pwd')) {
+                $this->error('当前账号仅可查看,不能修改金额与交货日期');
+            }
+            $this->error('无权修改该记录');
+        }
+
+        $amountRaw = trim((string)$this->request->post('amount', ''));
+        $deliveryRaw = trim((string)$this->request->post('delivery', ''));
+
+        $data = [];
+        if ($amountRaw === '') {
+            $data['amount'] = null;
+        } else {
+            if (!preg_match('/^-?\d+(\.\d{1,5})?$/', $amountRaw)) {
+                $this->error('金额格式不正确,最多五位小数');
+            }
+            $data['amount'] = $amountRaw;
+        }
+        if ($deliveryRaw === '') {
+            $data['delivery'] = null;
+        } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryRaw)) {
+            // 仅选年月日:存 DATETIME,禁止写成 00:00:00——原记录有非零点时间则沿用,否则用当前服务器时分秒
+            $existingDel = isset($row['delivery']) ? trim((string)$row['delivery']) : '';
+            $timePart = date('H:i:s');
+            if ($existingDel !== '') {
+                $tsEx = strtotime(str_replace('T', ' ', $existingDel));
+                if ($tsEx !== false) {
+                    $hms = date('H:i:s', $tsEx);
+                    if ($hms !== '00:00:00') {
+                        $timePart = $hms;
+                    }
+                }
+            }
+            $data['delivery'] = $deliveryRaw . ' ' . $timePart;
+        } else {
+            $deliveryRaw = str_replace('T', ' ', $deliveryRaw);
+            $ts = strtotime($deliveryRaw);
+            if ($ts === false) {
+                $this->error('交期时间格式不正确');
+            }
+            $data['delivery'] = date('Y-m-d H:i:s', $ts);
+        }
+        $dcCol = $this->mprocResolveProcuremenColumn(['delivery_createtime', 'deliverycreatetime']);
+        if ($dcCol !== null && array_key_exists('delivery', $data) && $data['delivery'] !== null && $data['delivery'] !== '') {
+            $data[$dcCol] = date('Y-m-d H:i:s');
+        }
+
+        // 同步 status_name(与列表 Tab 一致):金额或交期任一有有效数据 → 已提交,否则未提交;已是「已完成」不覆盖
+        $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']);
+        if ($statusNameCol !== null) {
+            $curSn = '';
+            foreach ($row as $k => $v) {
+                if (strcasecmp((string)$k, $statusNameCol) === 0) {
+                    $curSn = trim((string)$v);
+                    break;
+                }
+            }
+            if ($curSn !== '已完成') {
+                $effAm = array_key_exists('amount', $data) ? $data['amount'] : ($row['amount'] ?? null);
+                $effDv = array_key_exists('delivery', $data) ? trim((string)$data['delivery']) : trim((string)($row['delivery'] ?? ''));
+                $amountFilled = !($effAm === null || $effAm === '' || (is_string($effAm) && trim($effAm) === ''));
+                $deliveryFilled = ($effDv !== '' && !preg_match('/^0000-00-00/i', $effDv));
+                $data[$statusNameCol] = ($amountFilled || $deliveryFilled) ? '已提交' : '未提交';
+            }
+        }
+
+        $pkField = isset($row['id']) ? 'id' : (isset($row['ID']) ? 'ID' : 'id');
+        $pkVal = (int)($row[$pkField] ?? $id);
+        try {
+            $aff = Db::table('purchase_order_detail')->where($pkField, $pkVal)->update($data);
+        } catch (\Throwable $e) {
+            $msg = $e->getMessage();
+            if (stripos($msg, 'Unknown column') !== false) {
+                $msg = '请确认数据表 purchase_order_detail 已包含 amount、delivery 字段';
+            }
+            $this->error('保存失败:' . $msg);
+        }
+        if ($aff === false) {
+            $this->error('保存失败');
+        }
+        $this->success('已保存');
+    }
+
+    /**
+     * 退出登录
+     */
+    public function logout()
+    {
+        $token = Session::get('mproc_token');
+        if ($token === null || $token === '') {
+            $token = Cookie::get('mproc_token');
+        }
+        if ($token) {
+            $this->mprocClearLogin(preg_replace('/[^a-f0-9]/i', '', (string)$token));
+        }
+        $this->redirect(url('index/index/login'));
     }
 
+    /**
+     * 短信宝(与后台外发审核一致,便于复用账号)
+     *
+     * @throws \Exception
+     */
+    protected function mprocSmsSend($phone, $content)
+    {
+        $smsapi = 'http://api.smsbao.com/';
+        $user = 'zhuwei123';
+        $pass = md5('1d1e605c101e4c1f8a156c6d7b19f126');
+        $sendurl = $smsapi . 'sms?u=' . $user . '&p=' . $pass . '&m=' . $phone . '&c=' . urlencode($content);
+        $result = @file_get_contents($sendurl);
+        if ($result === false) {
+            throw new \Exception('短信发送失败:网络异常');
+        }
+        $result = trim((string)$result);
+        if ($result !== '0') {
+            throw new \Exception('短信发送失败,错误码:' . $result);
+        }
+    }
 }

+ 801 - 266
application/index/view/index/index.html

@@ -1,310 +1,845 @@
 <!DOCTYPE html>
-<html>
+<html lang="zh-CN">
 <head>
-    <meta charset="utf-8">
-    <title>浙江印刷集团有限公司</title>
-    <!--浏览器标签 logo -->
-    <link rel="icon" href="./img/logo1.png" type="image/png" style="">
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>外发明细</title>
     <style>
-        /* 重置默认样式 */
-        html, body {
-            margin: 0;
-            padding: 0;
-            height: 100%; /* 确保根元素高度填满整个视口 */
+        * { box-sizing: border-box; }
+        html {
+            height: 100%;
+            height: -webkit-fill-available;
         }
         body {
-            overflow: hidden;
-            display: flex;
-            align-items: center; /* 水平居中 */
-            justify-content: center; /* 垂直居中 */
-            font-family: 'Kaiti', '楷体', serif; /* 设置字体 */
-            color: white; /* 设置文字颜色 */
+            margin: 0;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+            background: #f5f5f5;
+            color: #333;
         }
-        /* 背景样式 */
-        .bg {
-            width: 100%;
-            height: 100vh;
-            background-image: url("./img/bg.jpg");/*背景图片*/
-            background-size: cover;
-            background-repeat: no-repeat;
-            background-position: center;
-            /*position: relative; !* 调整为 relative,便于子元素定位 *!*/
-        }
-        /* 大标题样式 */
-        .head {
+        /* 订单 / 我的:一屏高、中间 flex 吃掉剩余高度;底栏占位。flex-basis:0 才能让子级出现独立滚动条 */
+        /* 顶栏、订单/我的主区、底栏:主区用 fixed + top/bottom 留出明确高度,避免移动端 flex 算不出滚动高度 */
+        :root {
+            /* 顶栏固定高度:订单页隐藏「退出」后也不能变矮,否则与「我的」页不一致、fixed 主区 top 会错位 */
+            --mproc-bar-h: 52px;
+            --mproc-tabbar-h: 52px;
+        }
+        body.mproc-layout-orders,
+        body.mproc-layout-me {
+            overflow-x: hidden;
+        }
+        .bar {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            z-index: 50;
+            height: var(--mproc-bar-h, 52px);
+            box-sizing: border-box;
+            padding: 0 16px;
+            background: #3c8dbc;
+            color: #fff;
             display: flex;
-            justify-content: center;
             align-items: center;
-            width: 400px;
-            height: 50px;
+            justify-content: space-between;
+        }
+        .bar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; line-height: 1.25; }
+        .bar a { color: #fff; text-decoration: none; font-size: 14px; line-height: 1.2; padding: 5px 10px; border: 1px solid rgba(255,255,255,.6); border-radius: 6px; }
+        /* 订单页不显示退出;「我的」里再退出 */
+        body.mproc-layout-orders .mproc-bar-logout { display: none !important; }
+        .search-wrap { flex-shrink: 0; padding: 10px 12px; background: #e8f4fc; border-bottom: 1px solid #d2e7f4; width: 100%; }
+        .search-form { display: flex; gap: 8px; align-items: center; max-width: 640px; margin: 0 auto; }
+        .search-form input[type="search"],
+        .search-form input[type="text"] { flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid #bcd8ea; border-radius: 8px; font-size: 15px; background: #fff; }
+        .search-form input:focus { outline: none; border-color: #3c8dbc; }
+        .search-form button[type="submit"] { flex-shrink: 0; padding: 10px 16px; border: none; border-radius: 8px; background: #3c8dbc; color: #fff; font-size: 14px; font-weight: 600; }
+        .sub { font-size: 12px; opacity: .95; padding: 8px 16px; background: #f0f7fb; color: #367fa9; border-bottom: 1px solid #e2ecf3; }
+        .list { padding: 10px 12px; }
+        /* 左侧固定宽;右侧 absolute 铺满剩余区域并单独滚动(不依赖 height% 在 flex 里的解析) */
+        .mproc-order-body {
+            position: relative;
+            display: flex;
+            align-items: stretch;
+            flex: 1 1 0%;
+            min-height: 0;
+            overflow: hidden;
+        }
+        .mproc-side {
+            width: 86px;
+            flex-shrink: 0;
+            align-self: stretch;
+            background: #fff;
+            border-right: 1px solid #e5e5e5;
+            padding: 8px 0;
+            position: relative;
+            z-index: 1;
+        }
+        .mproc-cat { display: block; width: 100%; padding: 12px 6px; border: none; background: transparent; font-size: 13px; color: #555; cursor: pointer; text-align: center; font-family: inherit; line-height: 1.35; -webkit-tap-highlight-color: transparent; }
+        .mproc-cat.active { color: #3c8dbc; font-weight: 600; background: #f0f7fb; box-shadow: inset 3px 0 0 #3c8dbc; }
+        .mproc-order-main {
             position: absolute;
-            top: 5%;
-            left: 50%;
-            transform: translate(-50%, -50%);
+            top: 0;
+            right: 0;
+            bottom: 0;
+            left: 86px;
+            overflow-y: scroll;
+            overflow-x: hidden;
+            -webkit-overflow-scrolling: touch;
+            overscroll-behavior-y: contain;
+            background: #f5f5f5;
         }
-        /* 大标题样式 */
-        .head .head-title {
-            font-size: 24px;
-            margin: 0;
-            text-align: center;
-            font-family: 'Songti', '宋体', serif;
+        .mproc-order-main .list { padding: 10px 10px 12px; }
+        .card { background: #fff; border-radius: 10px; padding: 14px 14px 12px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }
+        .js-card.mproc-card-highlight {
+            box-shadow: 0 0 0 2px #1890ff, 0 1px 6px rgba(24, 144, 255, .22);
         }
-        /* 容器样式 */
-        .container {
+        .card .title { font-size: 15px; font-weight: 600; line-height: 1.45; margin: 0 0 10px; color: #222; }
+        .kv { font-size: 13px; line-height: 1.65; color: #555; }
+        .kv span { color: #888; margin-right: 4px; }
+        .kv .kv-miss { color: #c05621; font-size: 13px; font-weight: 500; }
+        .card-foot {
+            margin-top: 12px;
+            padding-top: 10px;
+            border-top: 1px solid #eee;
+            display: flex;
+            flex-wrap: wrap;
+            align-items: flex-start;
+            justify-content: flex-end;
+            gap: 8px 10px;
+        }
+        .btn-edit { font-size: 13px; padding: 8px 14px; border: 1px solid #3c8dbc; background: #fff; color: #3c8dbc; border-radius: 8px; cursor: pointer; }
+        .btn-edit:active { opacity: .85; }
+        .empty { text-align: center; padding: 48px 20px; color: #999; font-size: 14px; }
+        .list.mproc-loading { opacity: .55; pointer-events: none; transition: opacity .15s; }
+        .me-panel { padding: 16px 14px 24px; max-width: 480px; margin: 0 auto; }
+        .me-card { background: #fff; border-radius: 12px; padding: 18px 16px; box-shadow: 0 1px 6px rgba(0,0,0,.06); }
+        .me-card h2 { font-size: 1rem; margin: 0 0 14px; color: #222; font-weight: 600; }
+        .me-row { font-size: 14px; line-height: 1.7; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
+        .me-row:last-child { border-bottom: none; }
+        .me-row span { display: inline-block; min-width: 4.5em; color: #888; }
+        .me-hint { font-size: 12px; color: #999; margin-top: 14px; line-height: 1.5; }
+        .tabbar {
+            position: fixed;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            z-index: 40;
             display: flex;
+            background: #fff;
+            border-top: 1px solid #e5e5e5;
+            box-shadow: 0 -2px 10px rgba(0,0,0,.05);
+            padding-bottom: env(safe-area-inset-bottom, 0);
+        }
+        .tabbar .tabbar-btn { flex: 1; text-align: center; padding: 10px 4px 12px; color: #666; font-size: 14px; line-height: 1.25; border: none; border-top: 3px solid transparent; margin-top: -1px; background: #fff; cursor: pointer; font-family: inherit; -webkit-tap-highlight-color: transparent; }
+        .tabbar .tabbar-btn.active { color: #3c8dbc; font-weight: 600; border-top-color: #3c8dbc; background: #f8fbfd; }
+        /* 主内容区:夹在固定顶栏与固定底栏之间(高度确定 → 内层才能滚动) */
+        #mproc-pane-orders,
+        #mproc-pane-me {
+            position: fixed;
+            left: 0;
+            right: 0;
+            top: var(--mproc-bar-h, 52px);
+            bottom: var(--mproc-tabbar-h, 52px);
+            z-index: 10;
+            background: #f5f5f5;
+        }
+        #mproc-pane-orders {
             flex-direction: column;
-            justify-content: center;
-            align-items: center;
-            height: 100%;
-            /*height: 85%;*/
+            overflow: hidden;
         }
-        /* 按钮容器样式 */
-        .button-row {
-            text-align: center;
-            display: flex;/*浮动*/
-            justify-content: center;
-            align-items: center;
-            flex-wrap: wrap;
+        #mproc-pane-me {
+            overflow-y: auto;
+            overflow-x: hidden;
+            -webkit-overflow-scrolling: touch;
         }
-        /* 按钮样式 */
-        .my-button {
-            width: 150px; /* 按钮宽度 */
-            height: 100px; /* 按钮高度 */
-            border: 2px solid #0D4888; /* 边框样式和颜色 */
-            background: linear-gradient(to bottom, #0D4888, #074477); /* 背景渐变色 */
-            border-radius: 20%; /* 圆角边框 */
-            /*box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); !* 移除悬停时的模糊效果 *!*/
-            transition: background 0.3s, transform 0.3s; /* 过渡效果,用于背景颜色和变换 */
-            margin: 15px; /* 外边距 */
-            font-size: 16px; /* 按钮字体大小 */
-        }
-        /*退出*/
-        .my-button-color {
-            width: 100px; /* 按钮宽度 */
-            height: 50px; /* 按钮高度 */
-            /*border: 2px solid #0D4888; !* 边框样式和颜色 *!*/
-            background: linear-gradient(to bottom, red, red); /* 背景渐变色 */
-            border-radius: 20%; /* 圆角边框 */
-            /*box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); !* 移除悬停时的模糊效果 *!*/
-            transition: background 0.3s, transform 0.3s; /* 过渡效果,用于背景颜色和变换 */
-            margin: 15px; /* 外边距 */
-            font-size: 16px; /* 按钮字体大小 */
-        }
-
-        /* 按钮悬停样式 */
-        .my-button:hover {
-            background: linear-gradient(to bottom, #0d7be0, #085da6);
-            transform: scale(1.05);
+        .modal-mask { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 16px; }
+        .modal-mask.show { display: flex; }
+        .modal-sheet { width: 100%; max-width: 400px; background: #fff; border-radius: 14px; padding: 18px 18px 20px; max-height: calc(100vh - 32px); overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,.12); animation: modalIn .2s ease-out; }
+        @keyframes modalIn { from { transform: scale(.96); opacity: .85; } to { transform: scale(1); opacity: 1; } }
+        .modal-head { font-size: 16px; font-weight: 600; margin: 0 0 14px; color: #222; line-height: 1.4; word-break: break-all; }
+        .modal-field { margin-bottom: 14px; }
+        .modal-field label,
+        .modal-field-label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
+        .modal-field input { width: 100%; padding: 11px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; }
+        .modal-field input:focus { outline: none; border-color: #3c8dbc; }
+        /* 交货日期:仅一层边框,与上方「金额」一致 */
+        .date-field-shell {
+            position: relative;
+            display: block;
+            margin: 0;
+            padding: 0;
+            border: 1px solid #ddd;
+            border-radius: 8px;
+            background: #fff;
             cursor: pointer;
+            -webkit-tap-highlight-color: transparent;
+            overflow: hidden;
         }
-        /* 标题样式 */
-        .head-title {
-            font-size: 36px;
-            font-weight: bold;
-            text-align: center;
-        }
-        /* 底部信息样式 */
-        .footer {
-            position: fixed;
-            bottom: 0;
+        .date-field-shell:focus-within { border-color: #3c8dbc; }
+        .date-field-shell:active { background: #fafcfd; }
+        .inp-date {
+            display: block;
             width: 100%;
-            /*background-color: #333;*/
-            color: white;
-            text-align: center;
-            /*padding: 10px;*/
-            font-family: 'Songti', '宋体', serif;
-        }
-        /* 图片容器样式 */
-        .image-container {
-            display: none;
-            position: absolute;
-            top: 100%;
-            left: 50%;
-            transform: translateX(-50%);
-            z-index: 1;
+            margin: 0;
+            padding: 11px 36px 11px 12px;
+            border: none;
+            border-radius: 8px;
+            font-size: 16px;
+            line-height: 1.4;
+            min-height: 0;
+            background: transparent;
+            cursor: pointer;
+            box-sizing: border-box;
         }
-        /* 触碰按钮显示图片样式 */
-        .image-container img {
-            width: 200px;
-            height: auto;
+        .inp-date:focus { outline: none; }
+        .inp-date::-webkit-calendar-picker-indicator {
+            cursor: pointer;
+            width: 22px;
+            height: 22px;
+            padding: 0 2px;
+            margin: 0;
+            opacity: 0.72;
         }
-        /* 按钮悬停时显示图片容器 */
-        .my-button:hover .image-container {
-            display: block;
+        .modal-actions { display: flex; gap: 10px; margin-top: 18px; }
+        .modal-actions button { flex: 1; padding: 12px; border-radius: 8px; font-size: 15px; border: none; cursor: pointer; }
+        .modal-actions .btn-cancel { background: #f0f0f0; color: #555; }
+        .modal-actions .btn-ok { background: #3c8dbc; color: #fff; font-weight: 600; }
+        .modal-actions .btn-ok:disabled { opacity: .6; }
+    </style>
+</head>
+<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>
+</div>
+
+<div id="mproc-pane-orders" style="{eq name='mprocMainTab' value='me'}display:none{else/}display:flex{/eq}">
+<form class="search-wrap" id="mproc-search-form" method="get" action="{:url('index/index/index')}">
+    <input type="hidden" name="main_tab" value="orders" id="mproc-search-main-tab">
+    <input type="hidden" name="tab" id="mproc-search-tab" value="{$mprocTab|default='draft'|htmlentities}">
+    <div class="search-form">
+        <input type="search" name="q" id="mproc-input-q" value="{$mprocSearchQ|htmlentities}" placeholder="请输入搜索内容" maxlength="120" enterkeyhint="search">
+        <button type="submit">搜索</button>
+    </div>
+</form>
+<div class="mproc-order-body">
+    <nav class="mproc-side" id="mproc-list-cats" aria-label="订单状态">
+        <button type="button" class="mproc-cat {eq name='mprocTab' value='draft'}active{/eq}" data-list-tab="draft">未提交</button>
+        <button type="button" class="mproc-cat {eq name='mprocTab' value='submitted'}active{/eq}" data-list-tab="submitted">已提交</button>
+        <button type="button" class="mproc-cat {eq name='mprocTab' value='done'}active{/eq}" data-list-tab="done">已完成</button>
+    </nav>
+    <div class="mproc-order-main">
+    <div class="list" id="mproc-list-inner">
+    {empty name="rows"}
+    <div class="empty">暂无数据</div>
+    {else /}
+    {volist name="rows" id="r"}
+    <div class="card js-card"
+         data-id="{$r.eid|default=0}"
+         data-ccydh="{$r.CCYDH|default=''|htmlentities}"
+         data-amount="{$r.amount|default=''|htmlentities}"
+         data-delivery="{$r.delivery|default=''|htmlentities}"
+         data-title="{$r.CYJMC|default=''|htmlentities}">
+        <p class="title">{$r.CYJMC|default=''}</p>
+        <div class="kv">{$r.company_name|default=''|htmlentities}</div>
+        <div class="kv"><span>订单号</span>{$r.CCYDH|default=''}</div>
+        <div class="kv"><span>本次数量</span>{$r.This_quantity|default=''|htmlentities}</div>
+        <div class="kv"><span>最高限价</span>{$r.ceilingPrice|default=''|htmlentities}</div>
+        <div class="kv"><span>金额</span>{if $r.amount_missing}<span class="kv-miss">未填写</span>{else /}{$r.amount_display|htmlentities}{/if}</div>
+        <div class="kv"><span>交货日期</span>{if $r.delivery_missing}<span class="kv-miss">未填写</span>{else /}{$r.delivery_display|htmlentities}{/if}</div>
+        <div class="card-foot">
+            {if $r.mproc_can_edit}
+            <button type="button" class="btn-edit js-open-edit" data-stop="1">填写金额 / 交货日期</button>
+            {/if}
+        </div>
+    </div>
+    {/volist}
+    {/empty}
+    </div>
+    </div>
+</div>
+</div>
+
+<div id="mproc-pane-me" class="me-panel" style="{eq name='mprocMainTab' value='me'}display:block{else/}display:none{/eq}">
+    <div class="me-card">
+        <div class="me-row"><span>公司名称</span>{$mprocProfile.company_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>手机号</span>{$mprocProfile.phone|default=''|htmlentities}</div>
+        <div class="me-row"><span>邮箱</span>{$mprocProfile.email|default=''|htmlentities}</div>
+    </div>
+</div>
+
+<nav class="tabbar" id="mproc-tabbar" aria-label="主导航">
+    <button type="button" class="tabbar-btn {eq name='mprocMainTab' value='orders'}active{/eq}" data-main-tab="orders">订单</button>
+    <button type="button" class="tabbar-btn {eq name='mprocMainTab' value='me'}active{/eq}" data-main-tab="me">我的</button>
+</nav>
+
+<div class="modal-mask" id="edit-mask" aria-hidden="true">
+    <div class="modal-sheet" id="edit-sheet">
+        <p class="modal-head" id="edit-sheet-title">编辑</p>
+        <div class="modal-field">
+            <label for="inp-amount">金额</label>
+            <input type="text" id="inp-amount" inputmode="decimal" autocomplete="off" maxlength="24">
+        </div>
+        <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="点击选择年月日">
+                <input type="date" id="inp-delivery" class="inp-date" lang="zh-CN" autocomplete="off" enterkeyhint="done" aria-labelledby="lbl-delivery">
+            </div>
+        </div>
+        <div class="modal-actions">
+            <button type="button" class="btn-cancel" id="edit-cancel">取消</button>
+            <button type="button" class="btn-ok" id="edit-save">保存</button>
+        </div>
+    </div>
+</div>
+
+<script>
+// 弹窗编辑金额/交期;订单列表走 mprocList(main_tab + tab + q)
+(function () {
+    function mprocEndpointUrl(fileName) {
+        var p = window.location.pathname || '';
+        var path = p.replace(/(\/index\/index\/)index(?:\.html)?$/i, '$1' + fileName);
+        var okRe = new RegExp('/index/index/' + fileName.replace(/\./g, '\\.') + '$', 'i');
+        if (!okRe.test(path)) {
+            path = p.replace(/\/index(?:\.html)?$/i, '/' + fileName);
+        }
+        if (path.indexOf('index.php') === -1 && path.charAt(0) === '/') {
+            path = '/index.php' + path;
+        }
+        return window.location.origin + path;
+    }
+    var listUrl = mprocEndpointUrl('mprocList.html');
+    var saveUrl = mprocEndpointUrl('mprocSave.html');
+    var boot = {:json_encode(['main_tab' => $mprocMainTab, 'tab' => $mprocTab, 'q' => $mprocSearchQ, 'is_admin' => $mprocIsAdmin, 'focus_eid' => isset($mprocFocusEid) ? (int)$mprocFocusEid : 0], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
+    var currentMain = boot.main_tab === 'me' ? 'me' : 'orders';
+    var currentListTab = boot.tab && ['draft', 'submitted', 'done'].indexOf(boot.tab) !== -1 ? boot.tab : 'draft';
+
+    var tabbar = document.getElementById('mproc-tabbar');
+    var paneOrders = document.getElementById('mproc-pane-orders');
+    var paneMe = document.getElementById('mproc-pane-me');
+    var listCats = document.getElementById('mproc-list-cats');
+    var listInner = document.getElementById('mproc-list-inner');
+    var formSearch = document.getElementById('mproc-search-form');
+    /** 切换 Tab 时取消未完成的列表请求,避免堆叠慢查询 */
+    var mprocListAbort = null;
+
+    /** 测量固定顶栏、底栏高度,写入 CSS 变量,保证中间 fixed 区域高度正确(右侧列表才能滚) */
+    function mprocSyncFixedInsets() {
+        /* 顶栏高度由 CSS 固定为 var(--mproc-bar-h),避免订单页隐藏「退出」后测矮导致主区 top 与蓝条错位 */
+        var tab = document.getElementById('mproc-tabbar');
+        var th = tab ? Math.ceil(tab.getBoundingClientRect().height) : 52;
+        document.documentElement.style.setProperty('--mproc-tabbar-h', th + 'px');
+    }
+    mprocSyncFixedInsets();
+    window.addEventListener('resize', mprocSyncFixedInsets);
+    window.addEventListener('orientationchange', function () {
+        setTimeout(mprocSyncFixedInsets, 250);
+    });
+    if (window.visualViewport) {
+        window.visualViewport.addEventListener('resize', mprocSyncFixedInsets);
+    }
+    window.addEventListener('load', function () {
+        mprocSyncFixedInsets();
+        setTimeout(mprocSyncFixedInsets, 100);
+    });
+
+    function setBottomActive(main) {
+        if (!tabbar) return;
+        tabbar.querySelectorAll('.tabbar-btn').forEach(function (b) {
+            b.classList.toggle('active', b.getAttribute('data-main-tab') === main);
+        });
+    }
+
+    function setSideActive(listTab) {
+        if (!listCats) return;
+        listCats.querySelectorAll('.mproc-cat').forEach(function (b) {
+            b.classList.toggle('active', b.getAttribute('data-list-tab') === listTab);
+        });
+    }
+
+    function showMain(main) {
+        currentMain = main;
+        if (paneOrders) {
+            paneOrders.style.display = main === 'orders' ? 'flex' : 'none';
+        }
+        if (paneMe) {
+            paneMe.style.display = main === 'me' ? 'block' : 'none';
+        }
+        document.body.classList.remove('mproc-layout-orders', 'mproc-layout-me');
+        document.body.classList.add(main === 'me' ? 'mproc-layout-me' : 'mproc-layout-orders');
+        setBottomActive(main);
+        mprocSyncFixedInsets();
+        setTimeout(mprocSyncFixedInsets, 50);
+        setTimeout(mprocSyncFixedInsets, 300);
+    }
+
+    function mprocEsc(s) {
+        return String(s == null ? '' : s)
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/"/g, '&quot;')
+            .replace(/'/g, '&#39;');
+    }
+
+    function mprocEscAttr(s) {
+        return mprocEsc(s).replace(/\r|\n/g, ' ');
+    }
+
+    function pick(r, keys) {
+        var i, k;
+        for (i = 0; i < keys.length; i++) {
+            k = keys[i];
+            if (Object.prototype.hasOwnProperty.call(r, k) && r[k] != null && r[k] !== '') {
+                return String(r[k]);
+            }
         }
+        return '';
+    }
 
-        /*按钮文字设置*/
-        .title{
-            color: white;/*按钮文字颜色*/
+    function mprocAmountLineHtml(r) {
+        var miss = parseInt(r.amount_missing, 10);
+        if (miss === 1) {
+            return '<span class="kv-miss">未填写</span>';
+        }
+        if (miss === 0) {
+            return mprocEsc(pick(r, ['amount_display', 'amount']));
         }
+        var am = Object.prototype.hasOwnProperty.call(r, 'amount') ? r.amount : null;
+        if (am == null || am === '' || (typeof am === 'string' && am.trim() === '')) {
+            return '<span class="kv-miss">未填写</span>';
+        }
+        return mprocEsc(pick(r, ['amount_display', 'amount']));
+    }
 
-        /* 响应式布局 */
-        @media (max-width: 768px) {
-            .head .head-title {
-                font-size: 16px;
+    function mprocDeliveryLineHtml(r) {
+        var miss = parseInt(r.delivery_missing, 10);
+        if (miss === 1) {
+            return '<span class="kv-miss">未填写</span>';
+        }
+        if (miss === 0) {
+            return mprocEsc(pick(r, ['delivery_display', 'delivery']));
+        }
+        var del = r.delivery != null ? String(r.delivery).trim() : '';
+        if (del === '' || /^0000-00-00/i.test(del)) {
+            return '<span class="kv-miss">未填写</span>';
+        }
+        return mprocEsc(pick(r, ['delivery_display', 'delivery']));
+    }
+
+    function renderListHtml(rows, doneNoStatus) {
+        if (!rows || !rows.length) {
+            if (doneNoStatus) {
+                return '<div class="empty">表中未找到 status_name 列,无法按「未提交/已提交/已完成」筛选;请在 purchase_order_detail 增加该字段后由后端写入状态文案</div>';
             }
+            return '<div class="empty">暂无数据</div>';
         }
+        return rows.map(function (r) {
+            var eid = parseInt(r.eid, 10) || 0;
+            var tit = pick(r, ['CYJMC', 'cyjmc']);
+            var amtRaw = r.amount != null ? String(r.amount) : '';
+            var delRaw = r.delivery != null ? String(r.delivery) : '';
+            var thisQty = pick(r, ['This_quantity', 'this_quantity']);
+            var ceilP = pick(r, ['ceilingPrice', 'ceiling_price']);
+            var canEdit = parseInt(r.mproc_can_edit, 10) === 1;
+            var ccydh = pick(r, ['CCYDH', 'ccydh']);
+            var cname = pick(r, ['company_name', 'Company_name']);
+            return '<div class="card js-card" data-id="' + eid + '" data-ccydh="' + mprocEscAttr(ccydh) + '" data-amount="' + mprocEscAttr(amtRaw) + '" data-delivery="' + mprocEscAttr(delRaw) + '" data-title="' + mprocEscAttr(tit) + '">'
+                + '<p class="title">' + mprocEsc(tit) + '</p>'
+                + '<div class="kv">' + mprocEsc(cname) + '</div>'
+                + '<div class="kv"><span>订单号</span>' + mprocEsc(pick(r, ['CCYDH', 'ccydh'])) + '</div>'
+                + '<div class="kv"><span>本次数量</span>' + mprocEsc(thisQty) + '</div>'
+                + '<div class="kv"><span>最高限价</span>' + mprocEsc(ceilP) + '</div>'
+                + '<div class="kv"><span>金额</span>' + mprocAmountLineHtml(r) + '</div>'
+                + '<div class="kv"><span>交货日期</span>' + mprocDeliveryLineHtml(r) + '</div>'
+                + (function () {
+                    var btn = canEdit ? '<button type="button" class="btn-edit js-open-edit" data-stop="1">填写金额 / 交货日期</button>' : '';
+                    return '<div class="card-foot">' + btn + '</div>';
+                })()
+                + '</div>';
+        }).join('');
+    }
+
+    function getSearchQ() {
+        var inp = document.getElementById('mproc-input-q');
+        return inp ? String(inp.value || '').trim() : '';
+    }
 
-        @media (max-width: 480px) {
-            .head .head-title {
-                font-size: 12px;
+    /** 拉取订单列表:左侧 status 分类 draft=0 / submitted=1 / done=2 */
+    function fetchOrderList(listTab, q) {
+        currentListTab = listTab && ['draft', 'submitted', 'done'].indexOf(listTab) !== -1 ? listTab : 'draft';
+        showMain('orders');
+        setSideActive(currentListTab);
+        var hTab = document.getElementById('mproc-search-tab');
+        if (hTab) {
+            hTab.value = currentListTab;
+        }
+        if (mprocListAbort) {
+            try {
+                mprocListAbort.abort();
+            } catch (e) {}
+        }
+        mprocListAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
+        var myAbort = mprocListAbort;
+        if (listInner) {
+            listInner.classList.add('mproc-loading');
+        }
+        var u = new URL(listUrl, window.location.href);
+        u.searchParams.set('main_tab', 'orders');
+        u.searchParams.set('tab', currentListTab);
+        if (q) {
+            u.searchParams.set('q', q);
+        } else {
+            u.searchParams.delete('q');
+        }
+        var fetchOpts = {
+            method: 'GET',
+            credentials: 'same-origin',
+            headers: {
+                'X-Requested-With': 'XMLHttpRequest',
+                'Accept': 'application/json'
             }
+        };
+        if (myAbort) {
+            fetchOpts.signal = myAbort.signal;
         }
+        return fetch(u.toString(), fetchOpts).then(function (res) { return res.json(); }).then(function (ret) {
+            if (myAbort && mprocListAbort !== myAbort) {
+                return;
+            }
+            if (listInner) {
+                listInner.classList.remove('mproc-loading');
+            }
+            if (!(ret && (ret.code === 1 || ret.code === '1'))) {
+                alert(ret && ret.msg ? ret.msg : '加载失败');
+                return;
+            }
+            var d = ret.data || ret;
+            var rows = d.rows || [];
+            var dns = parseInt(d.done_no_status, 10) || 0;
+            if (listInner) {
+                listInner.innerHTML = renderListHtml(rows, dns);
+            }
+        }).catch(function (e) {
+            if (myAbort && mprocListAbort !== myAbort) {
+                return;
+            }
+            if (listInner) {
+                listInner.classList.remove('mproc-loading');
+            }
+            if (e && e.name === 'AbortError') {
+                return;
+            }
+            alert('网络错误');
+        });
+    }
+
+    if (tabbar) {
+        tabbar.addEventListener('click', function (e) {
+            var btn = e.target.closest('button[data-main-tab]');
+            if (!btn) return;
+            var m = btn.getAttribute('data-main-tab') || 'orders';
+            if (m === 'me') {
+                showMain('me');
+                return;
+            }
+            fetchOrderList(currentListTab, getSearchQ());
+        });
+    }
+
+    if (listCats) {
+        listCats.addEventListener('click', function (e) {
+            var btn = e.target.closest('button[data-list-tab]');
+            if (!btn) return;
+            var t = btn.getAttribute('data-list-tab') || 'draft';
+            fetchOrderList(t, getSearchQ());
+        });
+    }
 
-        @media (max-width: 1920px) {
-            .head .head-title {
-                font-size: 30px;
+    if (formSearch) {
+        formSearch.addEventListener('submit', function (e) {
+            e.preventDefault();
+            var qVal = getSearchQ();
+            if (currentMain === 'me') {
+                showMain('orders');
             }
+            fetchOrderList(currentListTab, qVal);
+        });
+    }
+
+    var mask = document.getElementById('edit-mask');
+    var sheet = document.getElementById('edit-sheet');
+    var inpAmount = document.getElementById('inp-amount');
+    var inpDelivery = document.getElementById('inp-delivery');
+    var dateFieldShell = document.getElementById('date-field-shell');
+    var btnSave = document.getElementById('edit-save');
+    var titleEl = document.getElementById('edit-sheet-title');
+    var currentId = 0; // 当前编辑的 purchase_order_detail 主键
+    if (inpAmount) {
+        mprocBindAmountInput(inpAmount);
+    }
+
+    // 把后端 datetime 转成 <input type="date"> 要的 yyyy-MM-dd
+    function deliveryToDateVal(s) {
+        s = (s || '').trim();
+        if (!s) return '';
+        var m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
+        if (m) return m[1] + '-' + m[2] + '-' + m[3];
+        m = s.match(/^(\d{4})\/(\d{2})\/(\d{2})/);
+        if (m) return m[1] + '-' + m[2] + '-' + m[3];
+        var t = Date.parse(s.replace(/-/g, '/'));
+        if (!isNaN(t)) {
+            var d = new Date(t);
+            var y = d.getFullYear();
+            var mo = ('0' + (d.getMonth() + 1)).slice(-2);
+            var da = ('0' + d.getDate()).slice(-2);
+            return y + '-' + mo + '-' + da;
         }
-        .button-tui{
-            padding: 10px 20px;
-            background-color: #f44336;
-            color: white;
-            border: none;
-            border-radius: 5px;
-            cursor: pointer;
-            font-size: 16px;
-            margin: 50px 0px 0px 0px;
+        return '';
+    }
+
+    /** 金额位数限制:最多X位小数 */
+    function mprocSanitizeAmountValue(raw) {
+        raw = String(raw || '');
+        var neg = raw.charAt(0) === '-';
+        var s = raw.replace(/^-/, '').replace(/[^\d.]/g, '');
+        var dot = s.indexOf('.');
+        var head;
+        var tail;
+        if (dot === -1) {
+            head = s;
+            tail = '';
+        } else {
+            head = s.slice(0, dot);
+            tail = s.slice(dot + 1).replace(/\./g, '').slice(0, 5);
         }
-    </style>
-</head>
-<body>
-<!-- 背景容器 -->
-<div class="bg">
-    <!-- 大标题 -->
-    <div class="head">
-        <p class="head-title">浙江印刷集团有限公司</p>
-    </div>
-<!--    <form action="{:url('/index.php/index/index/logout')}" method="post">-->
-<!--        <div style="text-align: right; margin-right: 30px;">-->
-<!--            <input type="hidden" id="tuichu" name="tuichu" value="退出">-->
-<!--            <button type="submit" class="button-tui">-->
-<!--                <i class="fas fa-sign-out-alt"></i> 退出登录-->
-<!--            </button>-->
-<!--        </div>-->
-<!--    </form>-->
-    <!-- 主内容容器 -->
-    <div class="container">
-        <!-- 按钮行 -->
-        <div class="button-row">
-            <button class="my-button" title="业务总览(第一页)" onclick="window.open('https://sugar.aipage.com/slider/1e8b117c278d0274e915da75f13fb6f4')">
-                <p class="title">业务总览</p>
-                <div class="image-container">
-                    <img src="./img/yiy.jpg" alt="业务总览图片">
-                </div>
-            </button>
-            <button class="my-button" title="业务总览(第二页)" onclick="window.open('https://sugar.aipage.com/slider/e3db9646983f86b87c1e0abfce4e0e1f')">
-                <p class="title">每月生产走势</p>
-                <div class="image-container">
-                    <img src="./img/yier.jpg" alt="每月生产走势图片">
-                </div>
-            </button>
-            <button class="my-button" title="主要机台每月生产走势" onclick="window.open('https://sugar.aipage.com/slider/536729d13e733546dfdfa32817b5c970')">
-                <p class="title">主要机台每月生产走势</p>
-                <div class="image-container">
-                    <img src="./img/yuejt.jpg" alt="主要机台每月生产走势">
-                </div>
-            </button>
-            <button class="my-button" title="采购管理(第一页)" onclick="window.open('https://sugar.aipage.com/slider/1988989bc6eec768d634dc58107ad92e')">
-                <p class="title">原辅材料总览</p>
-                <div class="image-container">
-                    <img src="./img/ery.jpg" alt="原辅材料总览图片">
-                </div>
-            </button>
+        if (head === '' && tail !== '') {
+            head = '0';
+        }
+        var out = (neg ? '-' : '') + head;
+        if (dot !== -1) {
+            out += '.' + tail;
+        }
+        return out;
+    }
 
+    function mprocBindAmountInput(el) {
+        if (!el || el.dataset.mprocAmountBound) {
+            return;
+        }
+        el.dataset.mprocAmountBound = '1';
+        var composing = false;
+        el.addEventListener('compositionstart', function () {
+            composing = true;
+        });
+        el.addEventListener('compositionend', function () {
+            composing = false;
+            var next = mprocSanitizeAmountValue(el.value);
+            if (el.value !== next) {
+                el.value = next;
+            }
+        });
+        /** 阻止非法键入(字母等不会进框),避免先插入再整段替换导致数字被清掉 */
+        el.addEventListener('keydown', function (e) {
+            if (e.isComposing || composing) {
+                return;
+            }
+            if (e.ctrlKey || e.metaKey || e.altKey) {
+                return;
+            }
+            var k = e.key;
+            if (k === 'Backspace' || k === 'Delete' || k === 'Tab' || k === 'Escape' || k === 'Enter') {
+                return;
+            }
+            if (k === 'ArrowLeft' || k === 'ArrowRight' || k === 'ArrowUp' || k === 'ArrowDown' || k === 'Home' || k === 'End') {
+                return;
+            }
+            if (k.length !== 1) {
+                return;
+            }
+            if (k >= '0' && k <= '9') {
+                return;
+            }
+            if (k === '-') {
+                var pos = el.selectionStart;
+                if (pos !== 0 || String(el.value).indexOf('-') >= 0) {
+                    e.preventDefault();
+                }
+                return;
+            }
+            if (k === '.') {
+                if (String(el.value).indexOf('.') >= 0) {
+                    e.preventDefault();
+                }
+                return;
+            }
+            e.preventDefault();
+        });
+        el.addEventListener('input', function (e) {
+            if (e.isComposing || composing) {
+                return;
+            }
+            var next = mprocSanitizeAmountValue(el.value);
+            if (el.value !== next) {
+                el.value = next;
+            }
+        });
+        el.addEventListener('paste', function (e) {
+            e.preventDefault();
+            var t = (e.clipboardData || window.clipboardData).getData('text') || '';
+            el.value = mprocSanitizeAmountValue(t);
+        });
+        el.addEventListener('blur', function () {
+            var v = mprocSanitizeAmountValue(el.value);
+            if (v === '-' || v === '.' || v === '-.') {
+                el.value = '';
+                return;
+            }
+            if (v.length > 1 && v.charAt(v.length - 1) === '.') {
+                el.value = v.slice(0, -1);
+            }
+        });
+    }
 
-        </div>
+    // 点交期区域弹出系统日期选择
+    function openDeliveryPicker() {
+        if (!inpDelivery) return;
+        if (typeof inpDelivery.showPicker === 'function') {
+            try {
+                inpDelivery.showPicker();
+                return;
+            } catch (e) {}
+        }
+        inpDelivery.focus();
+    }
 
-        <!--        第二行-->
-        <div class="button-row">
-            <button class="my-button" title="采购管理(第二页)" onclick="window.open('https://sugar.aipage.com/slider/04db16287f3e6f1a7f6edfbd4d634a10')">
-                <p class="title">纸张每月采购均价</p>
-                <div class="image-container">
-                    <img src="./img/erer.jpg" alt="纸张每月采购均价图片">
-                </div>
-            </button>
-            <button class="my-button" title="生产管理(第一页)" onclick="window.open('https://sugar.aipage.com/slider/cfbf4e7df6d3ca6864c7baa09b796a46')">
-                <p class="title">实时生产进度</p>
-                <div class="image-container">
-                    <img src="./img/sany.jpg" alt="实时生产进度图片">
-                </div>
-            </button>
-            <button class="my-button" title="生产管理(第二页)" onclick="window.open('https://sugar.aipage.com/slider/7cffa4091eed25ebd2b1e089e261fe0c')">
-                <p class="title">生产效率分析</p>
-                <div class="image-container">
-                    <img src="./img/saner.jpg" alt="生产效率分析图片">
-                </div>
-            </button>
-
-            <button class="my-button" title="生产管理(第三页)" onclick="window.open('https://sugar.aipage.com/slider/ca485092b13c48ffaf911529fbf00b7f')">
-                <p class="title">质量管控</p>
-                <div class="image-container">
-                    <img src="./img/zlgg.png" alt="质量管控质量管控图片">
-                </div>
-            </button>
+    // 交期外壳点击/键盘触发选日期
+    if (dateFieldShell && inpDelivery) {
+        dateFieldShell.addEventListener('click', function (e) {
+            if (e.target === inpDelivery) return;
+            openDeliveryPicker();
+        });
+        dateFieldShell.addEventListener('keydown', function (e) {
+            if (e.key === 'Enter' || e.key === ' ') {
+                e.preventDefault();
+                openDeliveryPicker();
+            }
+        });
+    }
 
+    // 打开弹窗,从卡片 data-* 带入金额、交期
+    function openEdit(card) {
+        if (!card) return;
+        var id = parseInt(card.getAttribute('data-id'), 10);
+        if (!id) return;
+        currentId = id;
+        inpAmount.value = mprocSanitizeAmountValue((card.getAttribute('data-amount') || '').replace(/&quot;/g, '"'));
+        inpDelivery.value = deliveryToDateVal(card.getAttribute('data-delivery') || '');
+        var no = String(card.getAttribute('data-ccydh') || '').trim();
+        var name = String(card.getAttribute('data-title') || '').trim();
+        var line = [no, name].filter(function (s) { return s.length > 0; }).join(' ');
+        titleEl.textContent = line || '编辑';
+        mask.classList.add('show');
+        mask.setAttribute('aria-hidden', 'false');
+    }
 
-        </div>
+    // 关弹窗
+    function closeEdit() {
+        mask.classList.remove('show');
+        mask.setAttribute('aria-hidden', 'true');
+        currentId = 0;
+    }
 
-        <!--        第三行-->
-        <div class="button-row">
-            <button class="my-button" title="营销管理(第一页)" onclick="window.open('https://sugar.aipage.com/slider/cd8084adf3f785304085db229dfc2377')">
-                <p class="title">经营总览</p>
-                <div class="image-container">
-                    <img src="./img/sy.jpg" alt="经营总览图片">
-                </div>
-            </button>
-            <button class="my-button" title="营销管理(第二页)" onclick="window.open('https://sugar.aipage.com/slider/edb2f972411cb1856c63cf4fb7b8605b')">
-                <p class="title">客户分析</p>
-                <div class="image-container">
-                    <img src="./img/sier.jpg" alt="客户分析图片">
-                </div>
-            </button>
-            <button class="my-button" title="营销管理(第二页)" onclick="window.open('https://sugar.aipage.com/dashboard/0950125942b5daec4324afc1a6d5c04d')">
-                <p class="title">成品库存</p>
-                <div class="image-container">
-                    <img src="./img/cpkc.png" alt="客户分析图片">
-                </div>
-            </button>
-
-            <button disabled class="my-button">
-                <p class="title">能源管理</p>
-                <div class="image-container">
-                </div>
-            </button>
-            <!--                <button class="my-button" title="" onclick="window.open('http://xh-erp.7in6.com/lqHKfByepX.php')">-->
-            <!--                <p class="title">驾驶舱后台系统</p>-->
-            <!--                </button>-->
-        </div>
-        <!-- 底部信息 -->
-        <div class="footer">
-            <div class="container">
-                <p>Copyright @ 浙江易盒包装科技有限公司 2023-{:date('Y',time())} 版权所有 <a href="https://beian.miit.gov.cn" target="_blank">{$site.beian|htmlentities}</a></p>
-            </div>
-        </div>
-    </div>
-</div>
-</body>
-</html>
-<script>
-    // 点击按钮跳转新窗口
-    document.querySelectorAll('.my-button').forEach(button => {
-        button.addEventListener('click', function(event) {
-            event.preventDefault();
-            window.open(this.getAttribute('data-href'));
+    // POST mprocSave
+    function postSave() {
+        var amt = '';
+        if (inpAmount) {
+            amt = mprocSanitizeAmountValue(inpAmount.value).trim();
+            if (amt.length > 1 && amt.charAt(amt.length - 1) === '.') {
+                amt = amt.slice(0, -1);
+            }
+            if (amt === '-' || amt === '.' || amt === '-.') {
+                amt = '';
+            }
+            inpAmount.value = amt;
+        }
+        var body = 'id=' + encodeURIComponent(String(currentId))
+            + '&amount=' + encodeURIComponent(amt)
+            + '&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'
+            },
+            body: body,
+            credentials: 'same-origin'
+        }).then(function (r) { return r.json(); });
+    }
+
+    // 「填写金额/交期」:列表区事件委托(接口刷新列表后仍有效)
+    if (listInner) {
+        listInner.addEventListener('click', function (e) {
+            var btn = e.target.closest('.js-open-edit');
+            if (!btn) return;
+            e.stopPropagation();
+            openEdit(btn.closest('.js-card'));
         });
+    }
+
+    // 取消、点遮罩关弹窗;点白底不冒泡到遮罩
+    document.getElementById('edit-cancel').addEventListener('click', closeEdit);
+    mask.addEventListener('click', function (e) {
+        if (e.target === mask) closeEdit();
     });
+    sheet.addEventListener('click', function (e) { e.stopPropagation(); });
 
-    // 禁止在页面上进行放大缩小
-    document.addEventListener('wheel', function(event){
-        if (event.ctrlKey === true || event.metaKey) {
-            event.preventDefault();
-        }
-    }, { passive: false });
+    // 保存
+    btnSave.addEventListener('click', function () {
+        if (!currentId) return;
+        btnSave.disabled = true;
+        postSave().then(function (ret) {
+            btnSave.disabled = false;
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                closeEdit();
+                if (currentMain === 'orders') {
+                    fetchOrderList(currentListTab, getSearchQ());
+                }
+            } else {
+                alert(ret && ret.msg ? ret.msg : '保存失败');
+            }
+        }).catch(function () {
+            btnSave.disabled = false;
+            alert('网络错误');
+        });
+    });
 
-    //禁止右键
-    // document.addEventListener('contextmenu', function(e) {
-    //     e.preventDefault();
-    // });
+    /** 邮件/短信直达链接:高亮并滚动到对应明细卡片 */
+    function mprocTryScrollToFocusCard() {
+        var eid = parseInt(boot.focus_eid, 10) || 0;
+        if (!eid || !listInner) {
+            return;
+        }
+        var card = listInner.querySelector('.js-card[data-id="' + eid + '"]');
+        if (!card) {
+            return;
+        }
+        card.classList.add('mproc-card-highlight');
+        var pane = document.querySelector('.mproc-order-main');
+        if (!pane) {
+            return;
+        }
+        try {
+            var top = card.getBoundingClientRect().top - pane.getBoundingClientRect().top + pane.scrollTop - 20;
+            pane.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
+        } catch (e1) {
+            try {
+                card.scrollIntoView({ behavior: 'smooth', block: 'center' });
+            } catch (e2) {}
+        }
+    }
+    setTimeout(mprocTryScrollToFocusCard, 200);
+    setTimeout(mprocTryScrollToFocusCard, 650);
+})();
 </script>
+</body>
+</html>

+ 325 - 107
application/index/view/index/login.html

@@ -1,138 +1,356 @@
 <!DOCTYPE html>
-<html>
+<html lang="zh-CN">
 <head>
-    <title>浙江印刷集团有限公司登录</title>
-    <link rel="icon" href="./img/logo1.png" type="image/png" style="">
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>外发明细登录</title>
     <style>
-        /* 重置默认样式 */
-        html, body {
-            margin: 0;
-            padding: 0;
-            height: 100%; /* 确保根元素高度填满整个视口 */
-        }
-        body {
+        * { box-sizing: border-box; }
+        html, body { margin: 0; height: 100%; width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f0f2f5; }
+        body { display: flex; flex-direction: column; min-height: 100vh; }
+        .login-shell {
+            flex: 1 1 auto;
             width: 100%;
-            height: 100vh;
-            background-image: url("./img/bg.jpg"); /*背景图片*/
-            background-size: cover;
-            background-repeat: no-repeat;
-            background-position: center;
+            min-height: 0;
             display: flex;
-            justify-content: center;
+            flex-direction: column;
             align-items: center;
-            margin: 0;
-        }
-        .login-container {
-            background: rgba(255, 255, 255, 0.8);
-            padding: 20px;
-            border-radius: 10px;
-            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
-            text-align: center;
-            width: 300px;
+            justify-content: center;
+            padding: 24px 16px 52px;
+            box-sizing: border-box;
         }
-        .login-container form {
+        .page { width: 100%; max-width: 420px; margin: 0; padding: 0; flex: 0 0 auto; }
+        .logo-area { text-align: center; margin-bottom: 18px; }
+        .logo-area h1 { font-size: 1.2rem; color: #333; margin: 0; font-weight: 600; }
+        .login-tabs {
             display: flex;
-            flex-direction: column;
-            align-items: center;
+            margin-bottom: 14px;
+            border-radius: 10px;
+            overflow: hidden;
+            border: 1px solid #ddd;
+            background: #fff;
         }
-        .login-container input {
-            width: 90%;
-            padding: 10px;
-            margin: 10px 0;
-            border: 1px solid #ccc;
-            border-radius: 5px;
-        }
-        .login-container button {
-            padding: 10px 20px;
-            background-color: #007BFF; /* 蓝色背景 */
-            color: white;
+        .login-tabs button {
+            flex: 1;
+            padding: 12px 8px;
             border: none;
-            border-radius: 5px;
+            background: #f5f5f5;
+            color: #666;
+            font-size: 15px;
             cursor: pointer;
-            width: 95%;
         }
-        .login-container button:hover {
-            background-color: #0056b3; /* 蓝色加深 */
+        .login-tabs button.active {
+            background: #3c8dbc;
+            color: #fff;
+            font-weight: 600;
         }
-
-        /* 大标题样式 */
-        .head {
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            width: 400px;
-            height: 50px;
-            position: absolute;
-            top: 5%;
+        .panel { display: none; }
+        .panel.active { display: block; }
+        .card { background: #fff; border-radius: 12px; padding: 22px 18px; box-shadow: 0 2px 12px rgba(0,0,0,.06); }
+        .field { margin-bottom: 16px; }
+        .field label { display: block; font-size: 0.85rem; color: #555; margin-bottom: 6px; }
+        .field input { width: 100%; padding: 12px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; }
+        .field input:focus { outline: none; border-color: #3c8dbc; }
+        .row-code { display: flex; gap: 10px; }
+        .row-code input { flex: 1; }
+        .btn-code { flex-shrink: 0; padding: 0 14px; border: 1px solid #3c8dbc; background: #fff; color: #3c8dbc; border-radius: 8px; font-size: 14px; white-space: nowrap; min-width: 108px; }
+        .btn-code:disabled { opacity: .55; border-color: #ccc; color: #999; }
+        .btn-submit { width: 100%; padding: 14px; border: none; border-radius: 8px; background: #3c8dbc; color: #fff; font-size: 16px; font-weight: 600; margin-top: 8px; }
+        .btn-submit:active { opacity: .9; }
+        .btn-submit.busy { opacity: .75; cursor: wait; }
+        .tip { font-size: 12px; color: #999; margin-top: 12px; line-height: 1.5; }
+        .footer { position: fixed; bottom: 0; left: 0; right: 0; text-align: center; padding: 10px; font-size: 12px; color: #999; background: #f0f2f5; }
+        /* 页内提示(替代 alert) */
+        .login-toast {
+            position: fixed;
             left: 50%;
-            transform: translate(-50%, -50%);
-        }
-        /* 大标题样式 */
-        .head .head-title {
-            font-size: 24px;
-            margin: 0;
+            /* 抬高:避开页脚版权区,大致落在登录按钮下方附近 */
+            bottom: calc(max(120px, min(32vh, 240px)) + env(safe-area-inset-bottom, 0px));
+            transform: translate3d(-50%, 16px, 0);
+            max-width: min(92vw, 360px);
+            padding: 12px 18px;
+            border-radius: 10px;
+            font-size: 14px;
+            line-height: 1.5;
             text-align: center;
-            font-family: 'Songti', '宋体', serif;
+            box-shadow: 0 4px 20px rgba(0, 0, 0, .2);
+            z-index: 10000;
+            opacity: 0;
+            visibility: hidden;
+            pointer-events: none;
+            transition: opacity .25s ease, transform .25s ease, visibility .25s;
         }
-        /* 标题样式 */
-        .head-title {
-            font-size: 36px;
-            font-weight: bold;
-            text-align: center;
+        .login-toast.show {
+            opacity: 1;
+            visibility: visible;
+            transform: translate3d(-50%, 0, 0);
         }
-        /* 底部信息样式 */
-        .footer {
-            position: fixed;
-            bottom: 0;
-            width: 100%;
-            /*background-color: #333;*/
-            color: white;
-            text-align: center;
-            /*padding: 10px;*/
-            font-family: 'Songti', '宋体', serif;
+        .login-toast.err {
+            background: #c0392b;
+            color: #fff;
+        }
+        .login-toast.info {
+            background: rgba(60, 141, 188, .96);
+            color: #fff;
+        }
+    </style>
+</head>
+<body>
+<div class="login-shell">
+<div class="page">
+    <div class="logo-area">
+        <h1>登录</h1>
+    </div>
+    <div class="login-tabs" role="tablist">
+        <button type="button" class="active" data-tab="phone">手机号登录</button>
+        <button type="button" data-tab="account">账号密码</button>
+    </div>
+    <div class="card">
+        <div id="panel-phone" class="panel active">
+            <div class="field">
+                <label for="phone">手机号</label>
+                <input type="tel" id="phone" name="phone" maxlength="11" placeholder="请输入11位手机号" autocomplete="tel">
+            </div>
+            <div class="field">
+                <label for="code">验证码</label>
+                <div class="row-code">
+                    <input type="text" id="code" name="code" maxlength="6" inputmode="numeric" placeholder="6位验证码" autocomplete="one-time-code">
+                    <button type="button" class="btn-code" id="btn-send">获取验证码</button>
+                </div>
+            </div>
+            <button type="button" class="btn-submit" id="btn-login">登 录</button>
+        </div>
+        <div id="panel-account" class="panel">
+            <div class="field">
+                <label for="username">账号</label>
+                <input type="text" id="username" name="username" maxlength="64" placeholder="用户名" autocomplete="username">
+            </div>
+            <div class="field">
+                <label for="password">密码</label>
+                <input type="password" id="password" name="password" placeholder="密码" autocomplete="current-password">
+            </div>
+            <button type="button" class="btn-submit" id="btn-login-pwd">登 录</button>
+        </div>
+    </div>
+</div>
+</div>
+<div class="footer">
+    <p>Copyright © {:date('Y')} {$site.name|default=''|htmlentities}</p>
+</div>
+<div id="login-toast" class="login-toast err" role="status" aria-live="polite" aria-atomic="true" hidden></div>
+<script>
+(function () {
+    var sendUrl = "{:url('index/index/sendSms')}";
+    var loginUrl = "{:url('index/index/doLogin')}";
+    var loginPwdUrl = "{:url('index/index/doLoginPwd')}";
+    var indexUrl = "{:url('index/index/index')}";
+    var loginRedirect = {:json_encode(isset($mprocLoginRedirect) ? $mprocLoginRedirect : '', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
+
+    var $tabs = document.querySelectorAll('.login-tabs button');
+    var $panelPhone = document.getElementById('panel-phone');
+    var $panelAccount = document.getElementById('panel-account');
+    var $phone = document.getElementById('phone');
+    var $code = document.getElementById('code');
+    var $username = document.getElementById('username');
+    var $password = document.getElementById('password');
+    var $btnSend = document.getElementById('btn-send');
+    var $btnLogin = document.getElementById('btn-login');
+    var $btnLoginPwd = document.getElementById('btn-login-pwd');
+    var cd = 0;
+    var timer = null;
+    var $toast = document.getElementById('login-toast');
+    var toastHideTimer = null;
+
+    /** 页内轻提示,替代 alert */
+    function showToast(msg, kind) {
+        kind = kind === 'info' ? 'info' : 'err';
+        if (!$toast) {
+            return;
+        }
+        if (toastHideTimer) {
+            clearTimeout(toastHideTimer);
+            toastHideTimer = null;
         }
+        $toast.textContent = msg || '';
+        $toast.className = 'login-toast ' + kind;
+        $toast.removeAttribute('hidden');
+        requestAnimationFrame(function () {
+            $toast.classList.add('show');
+        });
+        toastHideTimer = setTimeout(function () {
+            $toast.classList.remove('show');
+            toastHideTimer = setTimeout(function () {
+                $toast.setAttribute('hidden', 'hidden');
+                $toast.textContent = '';
+            }, 280);
+        }, 3200);
+    }
 
-        /* 响应式布局 */
-        @media (max-width: 768px) {
-            .head .head-title {
-                font-size: 16px;
+    function postForm(url, data, timeoutMs) {
+        timeoutMs = timeoutMs || 0;
+        var body = [];
+        for (var k in data) {
+            if (Object.prototype.hasOwnProperty.call(data, k)) {
+                body.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
             }
         }
-
-        @media (max-width: 480px) {
-            .head .head-title {
-                font-size: 12px;
+        var opts = {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+                'X-Requested-With': 'XMLHttpRequest'
+            },
+            body: body.join('&'),
+            credentials: 'same-origin'
+        };
+        var tid = null;
+        if (timeoutMs > 0 && typeof AbortController !== 'undefined') {
+            var ctrl = new AbortController();
+            opts.signal = ctrl.signal;
+            tid = setTimeout(function () {
+                try { ctrl.abort(); } catch (e1) {}
+            }, timeoutMs);
+        }
+        return fetch(url, opts).then(function (r) {
+            if (tid) {
+                clearTimeout(tid);
+                tid = null;
             }
+            return r.json();
+        }).catch(function (e) {
+            if (tid) {
+                clearTimeout(tid);
+                tid = null;
+            }
+            throw e;
+        });
+    }
+
+    /** 提交中:立刻改文案,避免用户以为没点到 */
+    function setSubmitBusy(btn, busy, busyText) {
+        if (!btn) return;
+        if (!Object.prototype.hasOwnProperty.call(btn.dataset, 'idleText')) {
+            btn.dataset.idleText = (btn.textContent || '').trim();
         }
+        btn.disabled = !!busy;
+        btn.classList.toggle('busy', !!busy);
+        btn.textContent = busy ? (busyText || '请稍候…') : btn.dataset.idleText;
+    }
 
-        @media (max-width: 1920px) {
-            .head .head-title {
-                font-size: 30px;
+    function goHome(ret) {
+        if (ret && (ret.code === 1 || ret.code === '1')) {
+            if (ret.url) {
+                location.href = ret.url;
+            } else {
+                location.href = indexUrl;
             }
+        } else {
+            showToast(ret && ret.msg ? ret.msg : '登录失败', 'err');
         }
-    </style>
-</head>
-<body>
-<div class="head">
-    <p class="head-title" style="color: whitesmoke">浙江印刷集团有限公司</p>
-</div>
+    }
 
-<div class="login-container">
-    <form name="form" id="login-form" class="form-vertical" method="POST" action="">
-        <label for="name">用户名:</label>
-        <input type="text" id="name" name="name" required>
-        <label for="password">密码:</label>
-        <input type="password" id="password" name="password" required>
-        <button type="submit">登录</button>
-    </form>
-</div>
+    $tabs.forEach(function (btn) {
+        btn.addEventListener('click', function () {
+            var tab = btn.getAttribute('data-tab');
+            $tabs.forEach(function (b) { b.classList.toggle('active', b === btn); });
+            $panelPhone.classList.toggle('active', tab === 'phone');
+            $panelAccount.classList.toggle('active', tab === 'account');
+        });
+    });
 
-<!-- 底部信息 -->
-<div class="footer">
-    <div class="container">
-        <p>Copyright @ 浙江易盒包装科技有限公司 2023-{:date('Y',time())} 版权所有 <a href="https://beian.miit.gov.cn" target="_blank">{$site.beian|htmlentities}</a></p>
-    </div>
-</div>
+    function tick() {
+        if (cd <= 0) {
+            clearInterval(timer);
+            timer = null;
+            $btnSend.disabled = false;
+            $btnSend.textContent = '获取验证码';
+            return;
+        }
+        $btnSend.textContent = cd + '秒后重发';
+        cd--;
+    }
+
+    $btnSend.addEventListener('click', function () {
+        var phone = ($phone.value || '').trim();
+        if (!/^1\d{10}$/.test(phone)) {
+            showToast('请输入正确的手机号', 'err');
+            return;
+        }
+        if (cd > 0) return;
+        $btnSend.disabled = true;
+        $btnSend.textContent = '发送中…';
+        postForm(sendUrl, { phone: phone }, 25000).then(function (ret) {
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                cd = 60;
+                tick();
+                timer = setInterval(tick, 1000);
+                showToast('验证码已发送,请查收短信', 'info');
+            } else {
+                $btnSend.disabled = false;
+                $btnSend.textContent = '获取验证码';
+                showToast(ret && ret.msg ? ret.msg : '发送失败', 'err');
+            }
+        }).catch(function (e) {
+            $btnSend.disabled = false;
+            $btnSend.textContent = '获取验证码';
+            if (e && e.name === 'AbortError') {
+                showToast('请求超时,请稍后再试', 'err');
+            } else {
+                showToast('网络错误', 'err');
+            }
+        });
+    });
 
+    $btnLogin.addEventListener('click', function () {
+        var phone = ($phone.value || '').trim();
+        var code = ($code.value || '').trim();
+        if (!/^1\d{10}$/.test(phone)) {
+            showToast('请输入正确的手机号', 'err');
+            return;
+        }
+        if (!/^\d{6}$/.test(code)) {
+            showToast('请输入6位验证码', 'err');
+            return;
+        }
+        setSubmitBusy($btnLogin, true, '登录中…');
+        postForm(loginUrl, { phone: phone, code: code, redirect: loginRedirect }, 25000).then(function (ret) {
+            setSubmitBusy($btnLogin, false);
+            goHome(ret);
+        }).catch(function (e) {
+            setSubmitBusy($btnLogin, false);
+            if (e && e.name === 'AbortError') {
+                showToast('请求超时,请稍后再试', 'err');
+            } else {
+                showToast('网络错误', 'err');
+            }
+        });
+    });
+
+    $btnLoginPwd.addEventListener('click', function () {
+        var u = ($username.value || '').trim();
+        var p = $password.value || '';
+        if (!u) {
+            showToast('请输入账号', 'err');
+            return;
+        }
+        if (!p) {
+            showToast('请输入密码', 'err');
+            return;
+        }
+        setSubmitBusy($btnLoginPwd, true, '登录中…');
+        postForm(loginPwdUrl, { username: u, password: p, redirect: loginRedirect }, 25000).then(function (ret) {
+            setSubmitBusy($btnLoginPwd, false);
+            goHome(ret);
+        }).catch(function (e) {
+            setSubmitBusy($btnLoginPwd, false);
+            if (e && e.name === 'AbortError') {
+                showToast('请求超时,请稍后再试', 'err');
+            } else {
+                showToast('网络错误', 'err');
+            }
+        });
+    });
+})();
+</script>
 </body>
 </html>

+ 11 - 3
composer.json

@@ -24,16 +24,24 @@
         "karsonzhang/fastadmin-addons": "~1.3.2",
         "overtrue/pinyin": "^3.0",
         "phpoffice/phpspreadsheet": "1.19",
-        "overtrue/wechat": "^4.6",
         "nelexa/zip": "^3.3",
         "ext-json": "*",
         "ext-curl": "*",
         "ext-pdo": "*",
         "ext-bcmath": "*",
-        "txthinking/mailer": "^2.0"
+        "txthinking/mailer": "^2.0",
+        "yzh52521/think-mail": "^3.0",
+        "w7corp/easywechat": "^5.36",
+        "phpmailer/phpmailer": "^6.11",
+        "mpdf/mpdf": "8.1",
+        "aliyuncs/oss-sdk-php": "2.6"
     },
     "config": {
-        "preferred-install": "dist"
+        "preferred-install": "dist",
+        "allow-plugins": {
+            "topthink/think-installer": true,
+            "easywechat-composer/easywechat-composer": true
+        }
     },
     "repositories": [
         {

+ 226 - 0
public/assets/js/backend/customer.js

@@ -0,0 +1,226 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    function parseCompanyTypeVal(s) {
+        s = (s || '').trim();
+        if (!s) {
+            return [];
+        }
+        return s.split(/[、,,]+/).map(function (x) {
+            return $.trim(x);
+        }).filter(Boolean);
+    }
+
+    function ensureCompanyTypeOneboxCss() {
+        if ($('#company-type-onebox-style').length) {
+            return;
+        }
+        $('head').append(
+            '<style id="company-type-onebox-style">' +
+            '.company-type-form-group{overflow:visible !important;position:relative;z-index:10;}' +
+            '.input-group.company-type-onebox{display:flex !important;flex-direction:row;flex-wrap:nowrap;align-items:stretch;width:100%;}' +
+            '.company-type-onebox .bootstrap-select.btn-group{flex:1 1 auto;min-width:0;}' +
+            '.company-type-onebox .bootstrap-select > .dropdown-toggle{width:100%;text-align:left;}' +
+            '.company-type-onebox > .input-group-addon{flex:0 0 auto;display:flex;align-items:center;}' +
+            '.company-type-onebox .bootstrap-select.open{z-index:1065;}' +
+            '.company-type-onebox .bootstrap-select .dropdown-menu{margin-top:0 !important;}' +
+            '</style>'
+        );
+    }
+
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'customer/index' + location.search,
+                    add_url: 'customer/add',
+                    edit_url: 'customer/edit',
+                    del_url: 'customer/del',
+                    multi_url: 'customer/multi',
+                    import_url: 'customer/import',
+                    table: 'customer',
+                }
+            });
+
+            var table = $("#table");
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', title: __('序号')},
+                        {field: 'company_name', title: __('客户名称'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'username', title: __('姓名'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'email', title: __('邮箱'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'phone', title: __('手机号'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'company_type', title: __('业务分类'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+            setTimeout(function () {
+                Controller.api.initCompanyType();
+            }, 50);
+        },
+        edit: function () {
+            Controller.api.bindevent();
+            setTimeout(function () {
+                Controller.api.initCompanyType();
+            }, 50);
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            },
+            initCompanyType: function () {
+                var $hidden = $('#c-company_type');
+                var $sel = $('#c-company_type_select');
+                var $count = $('#c-company_type_count');
+                if (!$hidden.length || !$sel.length || !$count.length) {
+                    return;
+                }
+
+                ensureCompanyTypeOneboxCss();
+
+                var initialParts = parseCompanyTypeVal($hidden.val());
+
+                function syncHidden() {
+                    var v = $sel.selectpicker('val');
+                    if (!$.isArray(v)) {
+                        v = v ? [v] : [];
+                    }
+                    v = v.filter(function (x) {
+                        return x != null && String(x).trim() !== '';
+                    });
+                    $hidden.val(v.join('、'));
+                }
+
+                function updateCount() {
+                    var v = $sel.selectpicker('val');
+                    var n = 0;
+                    if ($.isArray(v)) {
+                        n = v.filter(function (x) {
+                            return x != null && String(x).trim() !== '';
+                        }).length;
+                    } else if (v) {
+                        n = 1;
+                    }
+                    $count.text('已选 ' + n);
+                }
+
+                require(['bootstrap-select', 'bootstrap-select-lang'], function () {
+                    try {
+                        if ($sel.data('selectpicker')) {
+                            $sel.selectpicker('destroy');
+                        }
+                    } catch (e) {
+                        // ignore
+                    }
+
+                    $.each(initialParts, function (i, p) {
+                        if (!p) {
+                            return;
+                        }
+                        if (!$sel.find('option').filter(function () {
+                            return $(this).val() === p;
+                        }).length) {
+                            $sel.append($('<option>').attr('value', p).text(p));
+                        }
+                    });
+
+                    $sel.selectpicker({
+                        liveSearch: true,
+                        selectedTextFormat: 'static',
+                        noneSelectedText: '请选择',
+                        width: '100%'
+                    });
+
+                    var toVal = [];
+                    $.each(initialParts, function (i, p) {
+                        if (!p) {
+                            return;
+                        }
+                        if ($sel.find('option').filter(function () {
+                            return $(this).val() === p;
+                        }).length) {
+                            toVal.push(p);
+                        }
+                    });
+                    $sel.selectpicker('val', toVal);
+                    syncHidden();
+                    updateCount();
+
+                    $sel.off('changed.bs.select.companyct').on('changed.bs.select.companyct', function () {
+                        syncHidden();
+                        updateCount();
+                    });
+
+                    $sel.off('shown.bs.select.companyct').on('shown.bs.select.companyct', function () {
+                        var sp = $sel.data('selectpicker');
+                        if (!sp || !sp.$searchbox || !sp.$searchbox.length) {
+                            return;
+                        }
+                        sp.$searchbox.off('keydown.companyct').on('keydown.companyct', function (e) {
+                            if (e.which !== 13) {
+                                return;
+                            }
+                            var $box = $(this);
+                            var q = $.trim($box.val());
+                            if (!q) {
+                                return;
+                            }
+                            var hasOption = $sel.find('option').filter(function () {
+                                return $(this).val() === q;
+                            }).length > 0;
+                            var noResultsVisible = sp.$menu && sp.$menu.find('li.no-results').is(':visible');
+
+                            if (hasOption) {
+                                e.preventDefault();
+                                var cur = $sel.selectpicker('val');
+                                if (!$.isArray(cur)) {
+                                    cur = cur ? [cur] : [];
+                                }
+                                if ($.inArray(q, cur) === -1) {
+                                    cur.push(q);
+                                    $sel.selectpicker('val', cur);
+                                    syncHidden();
+                                    updateCount();
+                                }
+                                $box.val('').trigger('input');
+                                return;
+                            }
+                            if (noResultsVisible) {
+                                e.preventDefault();
+                                $sel.append($('<option>').val(q).text(q));
+                                $sel.selectpicker('refresh');
+                                var cur2 = $sel.selectpicker('val') || [];
+                                if (!$.isArray(cur2)) {
+                                    cur2 = cur2 ? [cur2] : [];
+                                }
+                                if ($.inArray(q, cur2) === -1) {
+                                    cur2.push(q);
+                                }
+                                $sel.selectpicker('val', cur2);
+                                syncHidden();
+                                updateCount();
+                                $box.val('').trigger('input');
+                            }
+                        });
+                    });
+
+                    $('form[role=form]').off('submit.companyct').on('submit.companyct', function () {
+                        syncHidden();
+                    });
+                });
+            }
+        }
+    };
+    return Controller;
+});

+ 1347 - 0
public/assets/js/backend/procuremen.js

@@ -0,0 +1,1347 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    /* 列表:Controller.index 内顺序执行。审核:Controller.review 内顺序执行。无其它本文件自定义函数。 */
+
+    var Controller = {
+        currYm: '',
+        index: function () {
+            var $layout = $('#procuremen-layout');
+            var table = $('#table');
+            var issuedOnlyCols = ['purchase_order_id'];
+            var pendingOnlyCols = ['po_detail_count', 'po_amount_fill_cnt', 'po_delivery_fill_cnt'];
+            var pickedOnlyCols = ['picked_supplier_name'];
+            var allOnlyCols = ['ID'];
+
+            Controller.currYm = ($layout.data('defaultYm') || '').toString();
+            Controller.wffTab = 'all';
+
+            Table.api.init({
+                extend: {
+                    index_url: 'procuremen/index' + location.search,
+                    multi_url: 'procuremen/multi',
+                    import_url: 'procuremen/import',
+                    table: 'scydgy',
+                }
+            });
+
+            $('.procuremen-ym-item').removeClass('active');
+            $('.procuremen-ym-item[data-ym="' + Controller.currYm + '"]').addClass('active');
+            $('.procuremen-wff-tabs').find('li').removeClass('active');
+            $('.procuremen-wff-tabs').find('a[data-wff="' + Controller.wffTab + '"]').parent().addClass('active');
+
+            $(document).off('click.procuremenWff', '.procuremen-wff-tabs a').on('click.procuremenWff', '.procuremen-wff-tabs a', function (e) {
+                e.preventDefault();
+                var t = $(this).data('wff');
+                if (!t || t === Controller.wffTab) {
+                    return;
+                }
+                Controller.wffTab = t;
+                $('.procuremen-wff-tabs').find('li').removeClass('active');
+                $('.procuremen-wff-tabs').find('a[data-wff="' + Controller.wffTab + '"]').parent().addClass('active');
+                /* 先清空:否则 show/hideColumn 会立刻用上一状态的 data 重绘一行/多行,直到 refresh 返回才消失 */
+                try {
+                    table.bootstrapTable('removeAll');
+                } catch (ignore) {
+                }
+                var issuedNow = Controller.wffTab === 'pending' || Controller.wffTab === 'done' || Controller.wffTab === 'picked';
+                issuedOnlyCols.forEach(function (field) {
+                    try {
+                        table.bootstrapTable(issuedNow ? 'showColumn' : 'hideColumn', field);
+                    } catch (ignore) {
+                    }
+                });
+                pendingOnlyCols.forEach(function (field) {
+                    try {
+                        table.bootstrapTable(Controller.wffTab === 'pending' ? 'showColumn' : 'hideColumn', field);
+                    } catch (ignore) {
+                    }
+                });
+                pickedOnlyCols.forEach(function (field) {
+                    try {
+                        table.bootstrapTable(Controller.wffTab === 'picked' ? 'showColumn' : 'hideColumn', field);
+                    } catch (ignore) {
+                    }
+                });
+                allOnlyCols.forEach(function (field) {
+                    try {
+                        table.bootstrapTable(issuedNow ? 'hideColumn' : 'showColumn', field);
+                    } catch (ignore) {
+                    }
+                });
+                table.bootstrapTable('refresh', {pageNumber: 1});
+            });
+
+            $(document).off('click.procuremenYm', '.procuremen-ym-item').on('click.procuremenYm', '.procuremen-ym-item', function () {
+                var ym = $(this).data('ym');
+                if (!ym || ym === Controller.currYm) {
+                    return;
+                }
+                Controller.currYm = ym;
+                $('.procuremen-ym-item').removeClass('active');
+                $(this).addClass('active');
+                try {
+                    table.bootstrapTable('removeAll');
+                } catch (ignore) {
+                }
+                table.bootstrapTable('refresh', {pageNumber: 1});
+            });
+
+            $(document).off('click.procuremenExportMonth', '#btn-export-month-outward').on('click.procuremenExportMonth', '#btn-export-month-outward', function () {
+                var ymDefault = (Controller.currYm || '').toString();
+                if (!/^\d{4}-\d{2}$/.test(ymDefault)) {
+                    ymDefault = ($layout.data('defaultYm') || '').toString();
+                }
+                if (!/^\d{4}-\d{2}$/.test(ymDefault)) {
+                    ymDefault = '';
+                }
+                if (!ymDefault) {
+                    var d = new Date();
+                    ymDefault = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
+                }
+                var escYm = String(ymDefault == null ? '' : ymDefault).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                var html = ''
+                    + '<div style="padding:14px 18px 6px;">'
+                    + '<p class="text-muted" style="margin:0 0 12px;font-size:13px;line-height:1.55;">'
+                    + '默认与左侧当前月份一致,可选择导出其它月份的下发明细。'
+                    + '</p>'
+                    + '<div class="form-group" style="margin-bottom:0;">'
+                    + '<input type="month" id="export-outward-ym-input" class="form-control" value="' + escYm + '" style="max-width:220px;" />'
+                    + '</div>'
+                    + '</div>';
+                Layer.open({
+                    type: 1,
+                    title: '月份下发明细导出',
+                    area: ['440px', 'auto'],
+                    shadeClose: true,
+                    content: html,
+                    btn: ['导出明细'],
+                    yes: function (index, layero) {
+                        var v = (layero.find('#export-outward-ym-input').val() || '').trim();
+                        if (!/^\d{4}-\d{2}$/.test(v)) {
+                            Toastr.warning('请选择有效月份');
+                            return;
+                        }
+                        Layer.close(index);
+                        var url = Fast.api.fixurl('procuremen/export_month_outward?ym=' + encodeURIComponent(v));
+                        setTimeout(function () {
+                            window.open(url, '_blank');
+                        }, 100);
+                    }
+                });
+            });
+
+            table.on('post-header.bs.table', function () {
+                var $host = $('#procuremen-toolbar-host');
+                var $bt = table.closest('.bootstrap-table');
+                if (!$host.length || !$bt.length) {
+                    return;
+                }
+                var $ft = $bt.children('.fixed-table-toolbar').first();
+                if (!$ft.length) {
+                    $ft = $host.find('.fixed-table-toolbar').first();
+                }
+                if (!$ft.length) {
+                    return;
+                }
+                if ($ft.parent()[0] !== $host[0]) {
+                    $host.children('.fixed-table-toolbar').remove();
+                    $host.append($ft);
+                    var $src = $bt.closest('.procuremen-table-area').find('#toolbar').first();
+                    if ($src.length && !$src.children().length) {
+                        $src.addClass('procuremen-toolbar-empty');
+                    }
+                }
+                $ft.find('.procuremen-toolbar-search-wrap').removeClass('procuremen-toolbar-search-wrap');
+                var $search = $ft.find('.search').first();
+                if (!$search.length) {
+                    return;
+                }
+                var $right = $search.closest('.columns, .pull-right').first();
+                if (!$right.length || ($ft[0] && !$.contains($ft[0], $right[0]))) {
+                    return;
+                }
+                $ft.children().each(function () {
+                    var $c = $(this);
+                    if ($c.is('.bars, .bs-bars') || $c[0] === $right[0]) {
+                        return;
+                    }
+                    var mergeToolbarExtras = $c.hasClass('columns')
+                        || $c.hasClass('pull-right')
+                        || ($c.hasClass('btn-group') && $c.find('.search').length === 0);
+                    if (!mergeToolbarExtras || $c.find('.search').length) {
+                        return;
+                    }
+                    if ($c.hasClass('btn-group') && !$c.hasClass('columns')) {
+                        $c.appendTo($right);
+                    } else {
+                        $c.children().appendTo($right);
+                        if (!$c.children().length) {
+                            $c.remove();
+                        }
+                    }
+                });
+                $ft.find('.search').first().prependTo($right);
+                $right.addClass('procuremen-toolbar-search-wrap');
+            });
+
+            var indexInitWffTab = Controller.wffTab;
+            var indexShowall = indexInitWffTab === 'all';
+            var indexShowIssued = indexInitWffTab === 'pending' || indexInitWffTab === 'done' || indexInitWffTab === 'picked';
+            var indexShowPendingOnly = indexInitWffTab === 'pending';
+            var indexShowPickedOnly = indexInitWffTab === 'picked';
+            function procuremenEscAttr(s) {
+                return String(s == null ? '' : s)
+                    .replace(/&/g, '&amp;')
+                    .replace(/"/g, '&quot;')
+                    .replace(/'/g, '&#39;');
+            }
+            var indexTableColumns = [
+                // {field: 'ID', title: __('ID'), operate: 'LIKE', table: 'a', width: 100, align: 'center',
+                //     visible: indexShowall,
+                //     formatter: function (v) {
+                //         return v != null && v !== '' && String(v) !== '0' ? String(v) : '';
+                //     }
+                // },
+                // {field: 'purchase_order_id',title: 'ID',operate: false,table: 'a',width: 88,align: 'center',
+                //     visible: indexShowIssued,
+                //     formatter: function (v) {
+                //         return v != null && v !== '' && String(v) !== '0' ? String(v) : '';
+                //     }
+                // },
+                {field: 'CCYDH', title: __('订单号'), operate: 'LIKE', table: 'b', width: 100, align: 'center'},
+                {field: 'CYJMC', title: __('印件名称'), operate: 'LIKE', table: 'b', width: 270, align: 'left'},
+                {field: 'CGYMC', title: __('工序名称'), operate: 'LIKE', table: 'a', width: 100, align: 'center'},
+                {field: 'CDW', title: __('单位'), operate: 'LIKE', table: 'a', width: 50, align: 'center'},
+                {field: 'NGZL', title: __('工作量'), operate: false, table: 'a', width: 80, align: 'center'},
+                {field: 'This_quantity', title: '本次数量', operate: false, table: 'a', width: 96, align: 'center',
+                    visible: true,
+                    formatter: function (v, row, index) {
+                        var tab = Controller.wffTab || 'all';
+                        if (tab === 'all') {
+                            var val = (v != null && v !== '') ? String(v) : '';
+                            return '<input type="text" class="form-control input-sm procuremen-po-field procuremen-po-qty" '
+                                + 'style="min-width:72px;max-width:96px;height:28px;padding:2px 6px;" '
+                                + 'data-field="This_quantity" data-row-index="' + index + '" value="'
+                                + procuremenEscAttr(val) + '" placeholder="填写" autocomplete="off"/>';
+                        }
+                        return (v == null || v === '') ? '' : String(v);
+                    }
+                },
+                {field: 'ceilingPrice', title: '最高限价', operate: false, table: 'a', width: 96, align: 'center',
+                    visible: true,
+                    formatter: function (value, row, index) {
+                        var tab = Controller.wffTab || 'all';
+                        var v = value;
+                        if (v == null || v === '') {
+                            v = (row && row.ceilingPrice != null && row.ceilingPrice !== '') ? row.ceilingPrice
+                                : (row && row.ceiling_price != null ? row.ceiling_price : '');
+                        }
+                        if (tab === 'all') {
+                            var val = (v != null && v !== '') ? String(v) : '';
+                            return '<input type="text" class="form-control input-sm procuremen-po-field procuremen-po-price" '
+                                + 'style="min-width:72px;max-width:96px;height:28px;padding:2px 6px;" '
+                                + 'data-field="ceilingPrice" data-row-index="' + index + '" value="'
+                                + procuremenEscAttr(val) + '" placeholder="填写" autocomplete="off"/>';
+                        }
+                        return (v == null || v === '') ? '' : String(v);
+                    }
+                },
+                {field: 'po_detail_count',title: '已下发数量',operate: false,table: 'a',width: 96,align: 'center',
+                    visible: indexShowPendingOnly,
+                    formatter: function (v) {
+                        return v != null && v !== '' ? String(v) : '0';
+                    }
+                },
+                {field: 'po_amount_fill_cnt',title: '已上报金额',operate: false,table: 'a',width: 96,align: 'center',
+                    visible: indexShowPendingOnly,
+                    formatter: function (v) {
+                        return v != null && v !== '' ? String(v) : '0';
+                    }
+                },
+                {field: 'po_delivery_fill_cnt',title: '已上报货期',operate: false,table: 'a',width: 96,align: 'center',
+                    visible: indexShowPendingOnly,
+                    formatter: function (v) {
+                        return v != null && v !== '' ? String(v) : '0';
+                    }
+                },
+                {field: 'picked_supplier_name', title: '已选供应商', operate: false, table: 'a', width: 200, align: 'left',
+                    visible: indexShowPickedOnly,
+                    formatter: function (v) {
+                        return v != null && v !== '' ? String(v) : '';
+                    }
+                },
+                {field: 'CDF', title: __('订法'), operate: false, table: 'a', width: 100, align: 'center'},
+                {field: 'cGzzxMc', title: __('外厂单位'), operate: 'LIKE', table: 'a', width: 220, align: 'center'},
+                {field: 'MBZ', title: __('备注'), operate: 'LIKE', table: 'a', width: 150, align: 'center'},
+                {field: 'dStamp', title: __('操作日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'a', width: 165, align: 'center'},
+                {field: 'dputrecord', title: __('提交日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'b', width: 170, align: 'center'},
+                {field: 'cywyxm', title: __('业务员'), operate: 'LIKE', table: 'b', width: 80, align: 'center'},
+                {field: 'operate',title: '操作',width: 260,align: 'center',fixed: 'right',
+                    table: table,
+                    formatter: function (value, row, index) {
+                        var tab = Controller.wffTab || 'all';
+                        var pk = row && (row.ID != null ? row.ID : row.id);
+                        if (!row || pk == null || pk === '' || String(pk) === '0') {
+                            return '';
+                        }
+                        var area = ' data-area=\'["76%","100%"]\'';
+                        var parts = [];
+                        if (tab === 'all') {
+                            //未发
+                            parts.push('<a class="btn btn-xs btn-info procuremen-op-open procuremen-btn-review"' + area + ' href="procuremen/review" data-row-index="' + index + '" title="审核"><i class="fa fa-check"></i> 审核</a>');
+                            parts.push('<a class="btn btn-xs btn-warning procuremen-btn-finish" href="javascript:;" data-row-index="' + index + '" title="完结"><i class="fa fa-flag-checkered"></i> 完结</a>');
+                        } else if (tab === 'pending') {
+                            //已下发
+                            parts.push('<a class="btn btn-xs btn-success procuremen-op-open procuremen-btn-outward"' + area + ' href="procuremen/outward_detail" data-row-index="' + index + '" title="采购确认"><i class="fa fa-shopping-cart"></i> 采购确认</a>');
+                            parts.push('<a class="btn btn-xs btn-default procuremen-op-open procuremen-btn-details"' + area + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>');
+                        } else if (tab === 'picked') {
+                            parts.push('<a class="btn btn-xs btn-default procuremen-op-open procuremen-btn-details"' + area + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>');
+                        } else {
+                            //已完结
+                            parts.push('<a class="btn btn-xs btn-default procuremen-op-open procuremen-btn-details"' + area + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>');
+                        }
+                        return '<div class="btn-group">' + parts.join(' ') + '</div>';
+                    },
+                    events: {}
+                }
+            ];
+
+            var $wbh = $layout.find('.procuremen-main .widget-body');
+            var tableHeight = !$wbh.length ? 400 : Math.max(168, $(window).height() - $wbh.offset().top - 56);
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'ID',
+                sortName: 'a.ID',
+                width: 'auto',
+                height: tableHeight,
+                fixedColumns: true,
+                fixedRightNumber: 1,
+                clickToSelect: false,
+                dblClickToEdit: false,
+                commonSearch: false,
+                showToggle: false,
+                showExport: false,
+                smartDisplay: false,
+                queryParams: function (params) {
+                    params.ym = Controller.currYm;
+                    params.wff_tab = Controller.wffTab;
+                    return params;
+                },
+                columns: [indexTableColumns]
+
+            });
+
+            Table.api.bindevent(table);
+
+            (function () {
+                var $wrap = table.closest('.bootstrap-table');
+                function procuremenSetFixedRightStaleLoading(hide) {
+                    $wrap.find('.fixed-columns-right').toggleClass('procuremen-hide-fixed-right-loading', !!hide);
+                }
+                table.on('refresh.bs.table', function () {
+                    if ($wrap.find('.fixed-table-loading').is(':visible')) {
+                        procuremenSetFixedRightStaleLoading(true);
+                    }
+                });
+                table.on('load-success.bs.table load-error.bs.table', function () {
+                    procuremenSetFixedRightStaleLoading(false);
+                });
+            })();
+
+            var $bt = table.closest('.bootstrap-table');
+            var procuremenPoBlurTimer = null;
+            function procuremenNormPoCell(v) {
+                if (v == null || v === '') {
+                    return '';
+                }
+                return String(v).trim();
+            }
+            if ($bt.length) {
+                $bt.off('blur.procuremenPoSave', '.procuremen-po-field').on('blur.procuremenPoSave', '.procuremen-po-field', function () {
+                    if (Controller.wffTab !== 'all') {
+                        return;
+                    }
+                    var $inp = $(this);
+                    clearTimeout(procuremenPoBlurTimer);
+                    procuremenPoBlurTimer = setTimeout(function () {
+                        procuremenPoBlurTimer = null;
+                        var rowIdx = parseInt($inp.data('rowIndex'), 10);
+                        if (isNaN(rowIdx)) {
+                            return;
+                        }
+                        var dataRows = table.bootstrapTable('getData');
+                        if (!dataRows[rowIdx]) {
+                            return;
+                        }
+                        var $qty = $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"]');
+                        var $price = $bt.find('.procuremen-po-price[data-row-index="' + rowIdx + '"]');
+                        var q = ($qty.length ? String($qty.val()) : '').trim();
+                        var p = ($price.length ? String($price.val()) : '').trim();
+                        var baseRow = dataRows[rowIdx];
+                        var origQ = procuremenNormPoCell(baseRow.This_quantity);
+                        var origP = procuremenNormPoCell(baseRow.ceilingPrice);
+                        if (origP === '' && baseRow.ceiling_price != null && baseRow.ceiling_price !== '') {
+                            origP = procuremenNormPoCell(baseRow.ceiling_price);
+                        }
+                        /* 与当前行数据一致则不调接口、不弹*/
+                        if (q === origQ && p === origP) {
+                            return;
+                        }
+                        var row = $.extend({}, baseRow);
+                        row.This_quantity = q;
+                        row.ceilingPrice = p;
+                        Fast.api.ajax({
+                            url: 'procuremen/completeDirectly',
+                            type: 'POST',
+                            data: {
+                                row_json: JSON.stringify(row),
+                                finish: '0',
+                                __token__: $('input[name=\'__token__\']').val() || ''
+                            }
+                        }, function () {
+                            try {
+                                table.bootstrapTable('updateRow', {
+                                    index: rowIdx,
+                                    row: $.extend({}, baseRow, {This_quantity: q, ceilingPrice: p})
+                                });
+                            } catch (ignore) {
+                            }
+                            Toastr.success('操作成功');
+                            return false;
+                        });
+                    }, 220);
+                });
+            }
+            if ($bt.length && $bt[0]) {
+                var tableRoot = $bt[0];
+                Controller._procuremenOpTableClick = function (e) {
+                    var t = e.target;
+                    if (!t || typeof t.closest !== 'function') {
+                        return;
+                    }
+                    var finish = t.closest('a.procuremen-btn-finish');
+                    if (finish && tableRoot.contains(finish) && !finish.classList.contains('disabled')) {
+                        e.preventDefault();
+                        if (typeof e.stopImmediatePropagation === 'function') {
+                            e.stopImmediatePropagation();
+                        }
+                        e.stopPropagation();
+                        var $f = $(finish);
+                        var rowIdxF = parseInt($f.data('rowIndex'), 10);
+                        var dataRowsF = table.bootstrapTable('getData');
+                        var rowF = !isNaN(rowIdxF) && dataRowsF[rowIdxF] !== undefined ? dataRowsF[rowIdxF] : null;
+                        if (!rowF) {
+                            Toastr.error('无法取得行数据');
+                            return;
+                        }
+                        var $qtyF = $bt.find('.procuremen-po-qty[data-row-index="' + rowIdxF + '"]');
+                        var $priceF = $bt.find('.procuremen-po-price[data-row-index="' + rowIdxF + '"]');
+                        if ($qtyF.length) {
+                            rowF = $.extend({}, rowF, {This_quantity: String($qtyF.val()).trim()});
+                        }
+                        if ($priceF.length) {
+                            rowF = $.extend({}, rowF, {ceilingPrice: String($priceF.val()).trim()});
+                        }
+                        Layer.confirm('确认后仅将本单标记为已完结。', function (idx) {
+                            Layer.close(idx);
+                            Fast.api.ajax({
+                                url: 'procuremen/completeDirectly',
+                                type: 'POST',
+                                data: {
+                                    row_json: JSON.stringify(rowF),
+                                    finish: '1',
+                                    __token__: $('input[name=\'__token__\']').val() || ''
+                                }
+                            }, function () {
+                                table.bootstrapTable('refresh');
+                                Toastr.success('操作成功');
+                                return false;
+                            });
+                        });
+                        return;
+                    }
+                    var details = t.closest('a.procuremen-btn-details');
+                    if (details && tableRoot.contains(details) && !details.classList.contains('disabled')) {
+                        e.preventDefault();
+                        if (typeof e.stopImmediatePropagation === 'function') {
+                            e.stopImmediatePropagation();
+                        }
+                        e.stopPropagation();
+                        var $d = $(details);
+                        var rowIdxD = parseInt($d.data('rowIndex'), 10);
+                        var dataRowsD = table.bootstrapTable('getData');
+                        var rowD = !isNaN(rowIdxD) && dataRowsD[rowIdxD] !== undefined ? dataRowsD[rowIdxD] : null;
+                        var detUrl;
+                        if (rowD) {
+                            detUrl = Fast.api.fixurl(Table.api.replaceurl('procuremen/details', rowD, table));
+                        } else {
+                            detUrl = Backend.api.replaceids(details, $d.attr('href'));
+                        }
+                        var tabForDet = Controller.wffTab ? Controller.wffTab : 'done';
+                        if (String(detUrl).indexOf('wff_tab=') === -1) {
+                            detUrl += (detUrl.indexOf('?') > -1 ? '&' : '?') + 'wff_tab=' + encodeURIComponent(tabForDet);
+                        }
+                        var $ad = $(details);
+                        var optsd = $.extend({}, $ad.data() || {});
+                        var titled = $ad.attr('title') || $ad.data('title') || '详情';
+                        var buttond = Backend.api.gettablecolumnbutton(optsd);
+                        var layerOptsd = $.extend({}, optsd);
+                        if (buttond && typeof buttond.callback === 'function') {
+                            layerOptsd.callback = buttond.callback;
+                        }
+                        if (buttond && buttond.layerArea && buttond.layerArea.length) {
+                            layerOptsd.area = buttond.layerArea;
+                        }
+                        var winNd = $ad.data('window') || 'self';
+                        var winD = window[winNd] || window;
+                        if (!winD.Backend || !winD.Backend.api) {
+                            Toastr.error('Backend 未就绪,请刷新页面');
+                            return;
+                        }
+                        winD.Backend.api.open(detUrl, titled, layerOptsd);
+                        return;
+                    }
+                    var outward = t.closest('a.procuremen-btn-outward');
+                    if (outward && !outward.classList.contains('disabled')) {
+                        e.preventDefault();
+                        if (typeof e.stopImmediatePropagation === 'function') {
+                            e.stopImmediatePropagation();
+                        }
+                        e.stopPropagation();
+                        var $o = $(outward);
+                        var oOpts = $.extend({}, $o.data() || {});
+                        var rowIdx = parseInt(oOpts.rowIndex, 10);
+                        var dataRows = table.bootstrapTable('getData');
+                        var row = !isNaN(rowIdx) && dataRows[rowIdx] !== undefined ? dataRows[rowIdx] : null;
+                        var tab = Controller.wffTab ? Controller.wffTab : 'all';
+                        var base = 'procuremen/outward_detail?wff_tab=' + encodeURIComponent(tab);
+                        var outUrl;
+                        if (row) {
+                            outUrl = Fast.api.fixurl(Table.api.replaceurl(base, row, table));
+                        } else {
+                            outUrl = Backend.api.replaceids(outward, $o.attr('href'));
+                            if (String(outUrl).indexOf('wff_tab=') === -1) {
+                                outUrl += (outUrl.indexOf('?') > -1 ? '&' : '?') + 'wff_tab=' + encodeURIComponent(tab);
+                            }
+                        }
+                        var $a = $(outward);
+                        var opts = $.extend({}, $a.data() || {});
+                        var title = $a.attr('title') || $a.data('title') || $a.data('original-title');
+                        var button = Backend.api.gettablecolumnbutton(opts);
+                        var layerOpts = $.extend({}, opts);
+                        if (button && typeof button.callback === 'function') {
+                            layerOpts.callback = button.callback;
+                        }
+                        if (button && button.layerArea && button.layerArea.length) {
+                            layerOpts.area = button.layerArea;
+                        }
+                        var winName = $a.data('window') || 'self';
+                        var win = window[winName] || window;
+                        if (!win.Backend || !win.Backend.api) {
+                            Toastr.error('Backend 未就绪,请刷新页面');
+                            return;
+                        }
+                        if (typeof layerOpts.confirm !== 'undefined') {
+                            Layer.confirm(layerOpts.confirm, function (index) {
+                                win.Backend.api.open(outUrl, title, layerOpts);
+                                Layer.close(index);
+                            });
+                        } else {
+                            win.Backend.api.open(outUrl, title, layerOpts);
+                        }
+                        return;
+                    }
+                    var review = t.closest('a.procuremen-btn-review');
+                    if (!review || !tableRoot.contains(review) || review.classList.contains('disabled')) {
+                        return;
+                    }
+                    e.preventDefault();
+                    if (typeof e.stopImmediatePropagation === 'function') {
+                        e.stopImmediatePropagation();
+                    }
+                    e.stopPropagation();
+                    var $a2 = $(review);
+                    var rowIdx2 = parseInt($a2.data('rowIndex'), 10);
+                    var dataRows2 = table.bootstrapTable('getData');
+                    var row2 = !isNaN(rowIdx2) && dataRows2[rowIdx2] !== undefined ? dataRows2[rowIdx2] : null;
+                    var revUrl;
+                    if (row2) {
+                        revUrl = Fast.api.fixurl(Table.api.replaceurl('procuremen/review', row2, table));
+                    } else {
+                        revUrl = Backend.api.replaceids(review, $a2.attr('href'));
+                    }
+                    var opts2 = $.extend({}, $a2.data() || {});
+                    var title2 = $a2.attr('title') || $a2.data('title') || $a2.data('original-title');
+                    var button2 = Backend.api.gettablecolumnbutton(opts2);
+                    var layerOpts2 = $.extend({}, opts2);
+                    if (button2 && typeof button2.callback === 'function') {
+                        layerOpts2.callback = button2.callback;
+                    }
+                    if (button2 && button2.layerArea && button2.layerArea.length) {
+                        layerOpts2.area = button2.layerArea;
+                    }
+                    var winName2 = $a2.data('window') || 'self';
+                    var win2 = window[winName2] || window;
+                    if (!win2.Backend || !win2.Backend.api) {
+                        Toastr.error('Backend 未就绪,请刷新页面');
+                        return;
+                    }
+                    if (typeof layerOpts2.confirm !== 'undefined') {
+                        Layer.confirm(layerOpts2.confirm, function (index) {
+                            win2.Backend.api.open(revUrl, title2, layerOpts2);
+                            Layer.close(index);
+                        });
+                    } else {
+                        win2.Backend.api.open(revUrl, title2, layerOpts2);
+                    }
+                };
+                tableRoot.addEventListener('click', Controller._procuremenOpTableClick, true);
+            }
+
+            $layout.find('.procuremen-main').off('click.procuremenTbRefresh').on('click.procuremenTbRefresh', '#procuremen-toolbar-host .btn-refresh', function (e) {
+                e.preventDefault();
+                table.bootstrapTable('refresh');
+            });
+            table.on('refresh.bs.table', function () {
+                $('#procuremen-toolbar-host .btn-refresh .fa').addClass('fa-spin');
+                setTimeout(function () {
+                    var $host2 = $('#procuremen-toolbar-host');
+                    var $bt2 = table.closest('.bootstrap-table');
+                    if (!$host2.length || !$bt2.length) {
+                        return;
+                    }
+                    var $ft2 = $bt2.children('.fixed-table-toolbar').first();
+                    if (!$ft2.length) {
+                        $ft2 = $host2.find('.fixed-table-toolbar').first();
+                    }
+                    if (!$ft2.length) {
+                        return;
+                    }
+                    if ($ft2.parent()[0] !== $host2[0]) {
+                        $host2.children('.fixed-table-toolbar').remove();
+                        $host2.append($ft2);
+                        var $src2 = $bt2.closest('.procuremen-table-area').find('#toolbar').first();
+                        if ($src2.length && !$src2.children().length) {
+                            $src2.addClass('procuremen-toolbar-empty');
+                        }
+                    }
+                    $ft2.find('.procuremen-toolbar-search-wrap').removeClass('procuremen-toolbar-search-wrap');
+                    var $search2 = $ft2.find('.search').first();
+                    if (!$search2.length) {
+                        return;
+                    }
+                    var $right2 = $search2.closest('.columns, .pull-right').first();
+                    if (!$right2.length || ($ft2[0] && !$.contains($ft2[0], $right2[0]))) {
+                        return;
+                    }
+                    $ft2.children().each(function () {
+                        var $c2 = $(this);
+                        if ($c2.is('.bars, .bs-bars') || $c2[0] === $right2[0]) {
+                            return;
+                        }
+                        var merge2 = $c2.hasClass('columns')
+                            || $c2.hasClass('pull-right')
+                            || ($c2.hasClass('btn-group') && $c2.find('.search').length === 0);
+                        if (!merge2 || $c2.find('.search').length) {
+                            return;
+                        }
+                        if ($c2.hasClass('btn-group') && !$c2.hasClass('columns')) {
+                            $c2.appendTo($right2);
+                        } else {
+                            $c2.children().appendTo($right2);
+                            if (!$c2.children().length) {
+                                $c2.remove();
+                            }
+                        }
+                    });
+                    $ft2.find('.search').first().prependTo($right2);
+                    $right2.addClass('procuremen-toolbar-search-wrap');
+                }, 0);
+            });
+            table.on('post-body.bs.table', function () {
+                $('#procuremen-toolbar-host .btn-refresh .fa').removeClass('fa-spin');
+            });
+            $(window).off('resize.procuremenIndex').on('resize.procuremenIndex', function () {
+                try {
+                    var $wbr = $layout.find('.procuremen-main .widget-body');
+                    var h = !$wbr.length ? 400 : Math.max(168, $(window).height() - $wbr.offset().top - 56);
+                    table.bootstrapTable('resetView', {height: h});
+                } catch (ignore) {
+                }
+            });
+        },
+
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        outward_detail: function () {
+            Controller.api.bindevent();
+            var $wrap = $('.outward-detail-wrap');
+            if (!$wrap.length || !$('#btn-pod-purchase-confirm').length) {
+                return;
+            }
+            var scydgyId = String($wrap.attr('data-scydgy-id') || '').trim();
+            if (!scydgyId) {
+                return;
+            }
+            var purchaseOrderId = String($wrap.attr('data-purchase-order-id') || '').trim();
+            $wrap.off('.procuremenPurchaseConfirm');
+            $wrap.on('change.procuremenPurchaseConfirm', '.pod-pick-cb', function () {
+                if (this.checked) {
+                    $wrap.find('.pod-pick-cb').not(this).prop('checked', false);
+                }
+            });
+            $wrap.on('click.procuremenPurchaseConfirm', '#btn-pod-purchase-confirm', function () {
+                var FastRef = (typeof Fast !== 'undefined' && Fast.api) ? Fast
+                    : (typeof parent !== 'undefined' && parent.Fast && parent.Fast.api ? parent.Fast : null);
+                if (!FastRef || !FastRef.api) {
+                    if (typeof parent !== 'undefined' && parent.Toastr) {
+                        parent.Toastr.error('页面未就绪,请刷新后重试');
+                    }
+                    return;
+                }
+                var $cbs = $wrap.find('.pod-pick-cb');
+                var all = $cbs.map(function () { return String($(this).val()); }).get();
+                var sel = $cbs.filter(':checked').val();
+                if (!sel) {
+                    var L = (typeof parent !== 'undefined' && parent.Layer) ? parent.Layer : (typeof Layer !== 'undefined' ? Layer : null);
+                    if (L && typeof L.msg === 'function') {
+                        L.msg('请勾选一条明细');
+                    } else if (typeof Toastr !== 'undefined') {
+                        Toastr.warning('请勾选一条明细');
+                    }
+                    return;
+                }
+                var unsel = $.grep(all, function (id) { return String(id) !== String(sel); });
+                var selArr = [String(sel)];
+                var tk = $('input[name=\'__token__\']').val() || '';
+
+                function podRowCompany($tr) {
+                    if (!$tr || !$tr.length) {
+                        return '(无名称)';
+                    }
+                    var $tds = $tr.children('td');
+                    if ($tds.length < 8) {
+                        return '(无名称)';
+                    }
+                    var t = $.trim($tds.eq(6).text());
+                    return t !== '' ? t : '(无名称)';
+                }
+
+                function escHtml(s) {
+                    return String(s)
+                        .replace(/&/g, '&amp;')
+                        .replace(/</g, '&lt;')
+                        .replace(/>/g, '&gt;')
+                        .replace(/"/g, '&quot;');
+                }
+
+                var $trOk = $cbs.filter(':checked').closest('tr');
+                var okName = podRowCompany($trOk);
+                var unNames = [];
+                $cbs.not(':checked').each(function () {
+                    unNames.push(podRowCompany($(this).closest('tr')));
+                });
+                var nUn = unNames.length;
+                var unListText = nUn ? (nUn <= 4 ? unNames.join('、') : (unNames.slice(0, 3).join('、') + ' 等共' + nUn + '家')) : '—';
+
+                var confirmHtml = ''
+                    + '<div style="text-align:left;line-height:1.75;font-size:13px;">'
+                    + '<p style="margin:0 0 10px 0;">提交后将<strong>立即发送短信</strong>,且<strong>不可撤回或更改</strong>。请确认以下通知:</p>'
+                    + '<ul style="margin:0;padding-left:1.2em;">'
+                    + '<li style="margin-bottom:6px;"><strong>已选中 1 条</strong>:采购确认结果视为「<strong>通过</strong>」,将向 <strong>' + escHtml(okName) + '</strong> 发送「已通过」短信;</li>'
+                    + '<li><strong>未选中 ' + nUn + ' 条</strong>:视为「<strong>未通过</strong>」,将向对应供应商发送「未通过」短信。</li>'
+                    + '</ul>'
+                    + (nUn ? ('<p style="margin:8px 0 0 0;color:#888;font-size:12px;">未选中涉及:' + escHtml(unListText) + '</p>') : '')
+                    + '<p style="margin:12px 0 0 0;"><strong>是否确认提交?</strong></p>'
+                    + '</div>';
+
+                var Lr = (typeof parent !== 'undefined' && parent.Layer) ? parent.Layer : (typeof Layer !== 'undefined' ? Layer : null);
+                if (!Lr || typeof Lr.confirm !== 'function') {
+                    if (typeof Toastr !== 'undefined') {
+                        Toastr.error('弹层组件未就绪,请刷新后重试');
+                    }
+                    return;
+                }
+
+                Lr.confirm(confirmHtml, {
+                    icon: 3,
+                    title: '采购确认 — 短信通知',
+                    area: ['480px', 'auto'],
+                    btn: ['确定提交', '取消']
+                }, function (idx) {
+                    Lr.close(idx);
+                    FastRef.api.ajax({
+                        url: FastRef.api.fixurl('procuremen/purchaseConfirmPick'),
+                        type: 'POST',
+                        data: {
+                            scydgy_id: scydgyId,
+                            purchase_order_id: purchaseOrderId,
+                            selected_id: sel,
+                            selected_ids: JSON.stringify(selArr),
+                            unselected_ids: JSON.stringify(unsel),
+                            all_detail_ids: JSON.stringify(all),
+                            __token__: tk
+                        }
+                    }, function () {
+                        if (typeof parent !== 'undefined' && parent.Layer) {
+                            var ix = parent.Layer.getFrameIndex(window.name);
+                            if (typeof ix !== 'undefined') {
+                                parent.Layer.close(ix);
+                            }
+                        }
+                        var p$ = (typeof parent !== 'undefined' && parent.jQuery) ? parent.jQuery : (typeof parent !== 'undefined' ? parent.$ : null);
+                        if (p$) {
+                            var $doneTab = p$('.procuremen-wff-tabs a[data-wff="done"]');
+                            if ($doneTab.length) {
+                                $doneTab.trigger('click');
+                            } else if (p$('#table').length) {
+                                p$('#table').bootstrapTable('refresh');
+                            }
+                        }
+                    });
+                });
+            });
+        },
+
+        details: function () {
+            Controller.api.bindevent();
+        },
+
+        review: function () {
+            var ids = window.Fast && Fast.api ? Fast.api.query('ids') : null;
+            if (ids === null || ids === '') {
+                Toastr.error('请刷新页面后重试');
+                return;
+            }
+            var $ptable = parent.$('#table');
+            if (!$ptable.length) {
+                Toastr.error('请刷新页面后重试');
+                return;
+            }
+            var pk = $ptable.bootstrapTable('getOptions').pk || 'ID';
+            var row = null;
+            $.each($ptable.bootstrapTable('getData'), function (i, r) {
+                if (String(r[pk]) === String(ids)) {
+                    row = r;
+                    return false;
+                }
+            });
+            if (!row) {
+                Toastr.error('请刷新页面后重试');
+                return;
+            }
+
+            $('#review-ccydh').text(row.CCYDH || '');
+            $('#review-cyjmc').text(row.CYJMC || '');
+            $('#review-CGYMC').text(row.CGYMC || '');
+            $('#review-CDW').text(row.CDW != null && row.CDW !== '' ? row.CDW : '');
+            $('#review-NGZL').text(row.NGZL != null && row.NGZL !== '' ? row.NGZL : '');
+            $('#review-CDF').text(row.CDF || '');
+            $('#review-cGzzxMc').text(row.cGzzxMc || '');
+            var qDisp = row.This_quantity != null && String(row.This_quantity).trim() !== '' ? String(row.This_quantity).trim()
+                : (row.this_quantity != null && String(row.this_quantity).trim() !== '' ? String(row.this_quantity).trim() : '');
+            var pDisp = row.ceilingPrice != null && String(row.ceilingPrice).trim() !== '' ? String(row.ceilingPrice).trim()
+                : (row.ceiling_price != null && String(row.ceiling_price).trim() !== '' ? String(row.ceiling_price).trim() : '');
+            $('#review-qty-display').text(qDisp !== '' ? qDisp : '—');
+            $('#review-price-display').text(pDisp !== '' ? pDisp : '—');
+            $('#c-row-json').val(JSON.stringify(row));
+
+            var reviewCompaniesAll = [];
+            var activeReviewCategory = null;
+            var reviewCompanySearchQ = '';
+            var reviewSelectedMap = {};
+            var reviewSearchTimer = null;
+
+            function syncReviewSelectedSummary() {
+                var entries = [];
+                $.each(reviewSelectedMap, function (k, c) {
+                    if (!c) {
+                        return;
+                    }
+                    var t = String(c.name || c.company_name || '').trim() || '(无名称)';
+                    entries.push({ k: k, label: t });
+                });
+                entries.sort(function (a, b) {
+                    return a.label.localeCompare(b.label, 'zh-CN');
+                });
+                var n = entries.length;
+                $('#review-selected-count').text(String(n));
+                $('#review-selected-tags').empty();
+                if (!n) {
+                    $('#review-selected-empty').show();
+                    return;
+                }
+                $('#review-selected-empty').hide();
+                $.each(entries, function (i, e) {
+                    var $chip = $('<span class="review-selected-chip"/>').attr('title', e.label);
+                    $chip.data('reviewSelKey', e.k);
+                    $chip.append(
+                        $('<span class="review-chip-text"/>').text(e.label),
+                        $('<button type="button" class="review-chip-remove" title="取消选择" aria-label="取消选择"/>').text('\u00d7')
+                    );
+                    $('#review-selected-tags').append($chip);
+                });
+            }
+            syncReviewSelectedSummary();
+
+            $('#review-selected-tags').off('click.reviewChipRemove').on('click.reviewChipRemove', '.review-chip-remove', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+                var k = $(this).closest('.review-selected-chip').data('reviewSelKey');
+                if (k == null || k === '') {
+                    return;
+                }
+                delete reviewSelectedMap[k];
+                $('#review-company-tbody .review-company-cb').each(function () {
+                    var c = $(this).data('company');
+                    var kk = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                    if (kk === k) {
+                        $(this).prop('checked', false);
+                    }
+                });
+                var $cbsR = $('#review-company-tbody .review-company-cb');
+                var $masterR = $('#review-check-all');
+                if (!$cbsR.length) {
+                    $masterR.prop('checked', false).prop('indeterminate', false);
+                } else {
+                    var nR = $cbsR.filter(':checked').length;
+                    $masterR.prop('checked', nR === $cbsR.length);
+                    $masterR.prop('indeterminate', nR > 0 && nR < $cbsR.length);
+                }
+                syncReviewSelectedSummary();
+            });
+
+            $('#review-category-sidebar').off('click.reviewCat').on('click.reviewCat', '.review-cat-item', function () {
+                var $t = $(this);
+                if ($t.hasClass('review-cat-all')) {
+                    activeReviewCategory = null;
+                } else {
+                    activeReviewCategory = $t.data('reviewCat');
+                }
+                $('#review-category-list .review-cat-item').removeClass('active');
+                $t.addClass('active');
+
+                var q = reviewCompanySearchQ;
+                var searchActive = q.length > 0;
+                var list = [];
+                $.each(reviewCompaniesAll, function (i, c) {
+                    if (!c || typeof c !== 'object') {
+                        return;
+                    }
+                    if (!searchActive && activeReviewCategory !== null) {
+                        var segOk = activeReviewCategory == null || activeReviewCategory === '';
+                        if (!segOk) {
+                            var rawCat = (c.company_type != null && String(c.company_type).trim() !== '')
+                                ? String(c.company_type).trim()
+                                : String(c.category || '').trim();
+                            var segs = [];
+                            if (!rawCat) {
+                                segs = ['未分类'];
+                            } else {
+                                var parts = rawCat.split('、');
+                                var seen = {};
+                                $.each(parts, function (idx, p) {
+                                    var tt = String(p).trim();
+                                    if (!tt || seen[tt]) {
+                                        return;
+                                    }
+                                    seen[tt] = true;
+                                    segs.push(tt);
+                                });
+                                if (!segs.length) {
+                                    segs = ['未分类'];
+                                }
+                            }
+                            if ($.inArray(activeReviewCategory, segs) === -1) {
+                                return;
+                            }
+                        }
+                    }
+                    if (q) {
+                        var blob = [
+                            c.name, c.company_name, c.username, c.email, c.phone,
+                            c.company_type, c.category
+                        ].map(function (x) {
+                            return x == null ? '' : String(x);
+                        }).join(' ').toLowerCase();
+                        if (blob.indexOf(q) === -1) {
+                            return;
+                        }
+                    }
+                    list.push(c);
+                });
+
+                var $tbody = $('#review-company-tbody').empty();
+                if (!list.length) {
+                    $tbody.append($('<tr/>').append($('<td colspan="6" class="review-company-empty"/>').text('暂无符合条件的单位')));
+                    var $cbs0 = $('#review-company-tbody .review-company-cb');
+                    var $master0 = $('#review-check-all');
+                    if (!$cbs0.length) {
+                        $master0.prop('checked', false).prop('indeterminate', false);
+                    } else {
+                        var n0 = $cbs0.filter(':checked').length;
+                        $master0.prop('checked', n0 === $cbs0.length);
+                        $master0.prop('indeterminate', n0 > 0 && n0 < $cbs0.length);
+                    }
+                    syncReviewSelectedSummary();
+                    return;
+                }
+                $.each(list, function (i, c) {
+                    var $cb = $('<input type="checkbox" class="review-company-cb"/>');
+                    $cb.data('company', c);
+                    var k = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                    if (k && reviewSelectedMap[k]) {
+                        $cb.prop('checked', true);
+                    }
+                    var $tr = $('<tr/>');
+                    $tr.append($('<td class="review-td-cb"/>').append($cb));
+                    $tr.append($('<td/>').text(c.name || c.company_name || ''));
+                    $tr.append($('<td/>').text(c.username || ''));
+                    $tr.append($('<td/>').text(c.email || ''));
+                    $tr.append($('<td/>').text(c.phone || ''));
+                    $tr.append($('<td/>').text((c.company_type != null && String(c.company_type).trim() !== '') ? String(c.company_type).trim() : (c.category || '')));
+                    $tbody.append($tr);
+                });
+                var $cbs = $('#review-company-tbody .review-company-cb');
+                var $master = $('#review-check-all');
+                var n = $cbs.filter(':checked').length;
+                $master.prop('checked', n === $cbs.length);
+                $master.prop('indeterminate', n > 0 && n < $cbs.length);
+                syncReviewSelectedSummary();
+            });
+
+            $('#review-company-search').off('input.reviewSearch').on('input.reviewSearch', function () {
+                var v = ($(this).val() || '').trim().toLowerCase();
+                if (reviewSearchTimer) {
+                    clearTimeout(reviewSearchTimer);
+                }
+                reviewSearchTimer = setTimeout(function () {
+                    reviewSearchTimer = null;
+                    reviewCompanySearchQ = v;
+                    var q = reviewCompanySearchQ;
+                    var searchActive = q.length > 0;
+                    var list = [];
+                    $.each(reviewCompaniesAll, function (i, c) {
+                        if (!c || typeof c !== 'object') {
+                            return;
+                        }
+                        if (!searchActive && activeReviewCategory !== null) {
+                            var segOk2 = activeReviewCategory == null || activeReviewCategory === '';
+                            if (!segOk2) {
+                                var rawCat2 = (c.company_type != null && String(c.company_type).trim() !== '')
+                                    ? String(c.company_type).trim()
+                                    : String(c.category || '').trim();
+                                var segs2 = [];
+                                if (!rawCat2) {
+                                    segs2 = ['未分类'];
+                                } else {
+                                    var parts2 = rawCat2.split('、');
+                                    var seen2 = {};
+                                    $.each(parts2, function (idx, p) {
+                                        var tt2 = String(p).trim();
+                                        if (!tt2 || seen2[tt2]) {
+                                            return;
+                                        }
+                                        seen2[tt2] = true;
+                                        segs2.push(tt2);
+                                    });
+                                    if (!segs2.length) {
+                                        segs2 = ['未分类'];
+                                    }
+                                }
+                                if ($.inArray(activeReviewCategory, segs2) === -1) {
+                                    return;
+                                }
+                            }
+                        }
+                        if (q) {
+                            var blob2 = [
+                                c.name, c.company_name, c.username, c.email, c.phone,
+                                c.company_type, c.category
+                            ].map(function (x) {
+                                return x == null ? '' : String(x);
+                            }).join(' ').toLowerCase();
+                            if (blob2.indexOf(q) === -1) {
+                                return;
+                            }
+                        }
+                        list.push(c);
+                    });
+                    var $tbody2 = $('#review-company-tbody').empty();
+                    if (!list.length) {
+                        $tbody2.append($('<tr/>').append($('<td colspan="6" class="review-company-empty"/>').text('暂无符合条件的单位')));
+                    } else {
+                        $.each(list, function (i, c) {
+                            var $cb = $('<input type="checkbox" class="review-company-cb"/>');
+                            $cb.data('company', c);
+                            var k2 = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                            if (k2 && reviewSelectedMap[k2]) {
+                                $cb.prop('checked', true);
+                            }
+                            var $tr = $('<tr/>');
+                            $tr.append($('<td class="review-td-cb"/>').append($cb));
+                            $tr.append($('<td/>').text(c.name || c.company_name || ''));
+                            $tr.append($('<td/>').text(c.username || ''));
+                            $tr.append($('<td/>').text(c.email || ''));
+                            $tr.append($('<td/>').text(c.phone || ''));
+                            $tr.append($('<td/>').text((c.company_type != null && String(c.company_type).trim() !== '') ? String(c.company_type).trim() : (c.category || '')));
+                            $tbody2.append($tr);
+                        });
+                    }
+                    var $cbsS = $('#review-company-tbody .review-company-cb');
+                    var $masterS = $('#review-check-all');
+                    if (!$cbsS.length) {
+                        $masterS.prop('checked', false).prop('indeterminate', false);
+                    } else {
+                        var nS = $cbsS.filter(':checked').length;
+                        $masterS.prop('checked', nS === $cbsS.length);
+                        $masterS.prop('indeterminate', nS > 0 && nS < $cbsS.length);
+                    }
+                    syncReviewSelectedSummary();
+                }, 160);
+            });
+
+            $('#review-company-tbody').off('change.reviewCb').on('change.reviewCb', '.review-company-cb', function () {
+                var c = $(this).data('company');
+                var k = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                var $cbsC = $('#review-company-tbody .review-company-cb');
+                var $masterC = $('#review-check-all');
+                if (!k) {
+                    if (!$cbsC.length) {
+                        $masterC.prop('checked', false).prop('indeterminate', false);
+                    } else {
+                        var nC = $cbsC.filter(':checked').length;
+                        $masterC.prop('checked', nC === $cbsC.length);
+                        $masterC.prop('indeterminate', nC > 0 && nC < $cbsC.length);
+                    }
+                    syncReviewSelectedSummary();
+                    return;
+                }
+                if ($(this).prop('checked')) {
+                    reviewSelectedMap[k] = c;
+                } else {
+                    delete reviewSelectedMap[k];
+                }
+                var n2 = $cbsC.filter(':checked').length;
+                $masterC.prop('checked', n2 === $cbsC.length);
+                $masterC.prop('indeterminate', n2 > 0 && n2 < $cbsC.length);
+                syncReviewSelectedSummary();
+            });
+
+            $('#review-check-all').off('change.reviewAll').on('change.reviewAll', function () {
+                var on = $(this).prop('checked');
+                $('#review-company-tbody .review-company-cb').each(function () {
+                    $(this).prop('checked', on).trigger('change');
+                });
+            });
+
+            $('#btn-review-submit').off('click.reviewSubmit').on('click.reviewSubmit', function () {
+                var selected = [];
+                $.each(reviewSelectedMap, function (k, c) {
+                    if (c) {
+                        selected.push(c);
+                    }
+                });
+                if (!selected.length) {
+                    Toastr.warning('请至少选择一个下发单位');
+                    return;
+                }
+                var cons;
+                try {
+                    cons = JSON.parse($('#c-row-json').val() || '{}');
+                } catch (e) {
+                    cons = {};
+                }
+                Fast.api.ajax({
+                    url: 'procuremen/review',
+                    type: 'POST',
+                    data: {
+                        __token__: $('input[name=\'__token__\']').val(),
+                        row_json: JSON.stringify(cons),
+                        companies_json: JSON.stringify(selected),
+                    }
+                }, function (data, ret) {
+                    var msg = (ret && ret.msg) ? ret.msg : '操作成功';
+                    if (typeof parent !== 'undefined' && parent.Toastr) {
+                        parent.Toastr.success(msg);
+                    }
+                    if (parent && parent.$ && parent.$('#table').length) {
+                        parent.$('#table').bootstrapTable('refresh');
+                    }
+                    var index = parent.Layer.getFrameIndex(window.name);
+                    parent.Layer.close(index);
+                    return false;
+                });
+            });
+
+            Fast.api.ajax({
+                url: 'procuremen/snapshotToProcure',
+                type: 'POST',
+                data: {
+                    __token__: $('input[name=\'__token__\']').val(),
+                    row_json: JSON.stringify(row)
+                }
+            }, function () {
+                Fast.api.ajax({
+                    url: 'procuremen/reviewCompanies',
+                    type: 'GET',
+                    loading: false
+                }, function (data) {
+                    reviewCompaniesAll = Array.isArray(data) ? data : [];
+                    reviewSelectedMap = {};
+                    activeReviewCategory = null;
+                    reviewCompanySearchQ = '';
+                    $('#review-company-search').val('');
+
+                    var counts = {};
+                    var total = 0;
+                    $.each(reviewCompaniesAll, function (i, c) {
+                        if (!c || typeof c !== 'object') {
+                            return;
+                        }
+                        total++;
+                        var raw = (c.company_type != null && String(c.company_type).trim() !== '')
+                            ? String(c.company_type).trim()
+                            : String(c.category || '').trim();
+                        var segs = [];
+                        if (!raw) {
+                            segs = ['未分类'];
+                        } else {
+                            var parts = raw.split('、');
+                            var seen = {};
+                            $.each(parts, function (idx, p) {
+                                var tt = String(p).trim();
+                                if (!tt || seen[tt]) {
+                                    return;
+                                }
+                                seen[tt] = true;
+                                segs.push(tt);
+                            });
+                            if (!segs.length) {
+                                segs = ['未分类'];
+                            }
+                        }
+                        $.each(segs, function (j, seg) {
+                            counts[seg] = (counts[seg] || 0) + 1;
+                        });
+                    });
+                    var cats = Object.keys(counts);
+                    cats.sort(function (a, b) {
+                        return a.localeCompare(b, 'zh-CN');
+                    });
+                    var $list = $('#review-category-list').empty();
+                    var $all = $('<div class="review-cat-item review-cat-all"/>').append(
+                        $('<span/>').text('全部 '),
+                        $('<span class="review-cat-count"/>').text('(' + total + ')')
+                    );
+                    $all.toggleClass('active', activeReviewCategory === null);
+                    $list.append($all);
+                    $.each(cats, function (i, cat) {
+                        var $it = $('<div class="review-cat-item"/>').append(
+                            $('<span/>').text(cat + ' '),
+                            $('<span class="review-cat-count"/>').text('(' + counts[cat] + ')')
+                        );
+                        $it.data('reviewCat', cat);
+                        $it.toggleClass('active', activeReviewCategory === cat);
+                        $list.append($it);
+                    });
+
+                    var q = reviewCompanySearchQ;
+                    var searchActive = q.length > 0;
+                    var list = [];
+                    $.each(reviewCompaniesAll, function (i, c) {
+                        if (!c || typeof c !== 'object') {
+                            return;
+                        }
+                        if (!searchActive && activeReviewCategory !== null) {
+                            var segOk = activeReviewCategory == null || activeReviewCategory === '';
+                            if (!segOk) {
+                                var rawCat = (c.company_type != null && String(c.company_type).trim() !== '')
+                                    ? String(c.company_type).trim()
+                                    : String(c.category || '').trim();
+                                var segsB = [];
+                                if (!rawCat) {
+                                    segsB = ['未分类'];
+                                } else {
+                                    var partsB = rawCat.split('、');
+                                    var seenB = {};
+                                    $.each(partsB, function (idx, p) {
+                                        var ttB = String(p).trim();
+                                        if (!ttB || seenB[ttB]) {
+                                            return;
+                                        }
+                                        seenB[ttB] = true;
+                                        segsB.push(ttB);
+                                    });
+                                    if (!segsB.length) {
+                                        segsB = ['未分类'];
+                                    }
+                                }
+                                if ($.inArray(activeReviewCategory, segsB) === -1) {
+                                    return;
+                                }
+                            }
+                        }
+                        if (q) {
+                            var blob = [
+                                c.name, c.company_name, c.username, c.email, c.phone,
+                                c.company_type, c.category
+                            ].map(function (x) {
+                                return x == null ? '' : String(x);
+                            }).join(' ').toLowerCase();
+                            if (blob.indexOf(q) === -1) {
+                                return;
+                            }
+                        }
+                        list.push(c);
+                    });
+                    var $tbody = $('#review-company-tbody').empty();
+                    if (!list.length) {
+                        $tbody.append($('<tr/>').append($('<td colspan="6" class="review-company-empty"/>').text('暂无符合条件的单位')));
+                    } else {
+                        $.each(list, function (i, c) {
+                            var $cb = $('<input type="checkbox" class="review-company-cb"/>');
+                            $cb.data('company', c);
+                            var k3 = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                            if (k3 && reviewSelectedMap[k3]) {
+                                $cb.prop('checked', true);
+                            }
+                            var $tr = $('<tr/>');
+                            $tr.append($('<td class="review-td-cb"/>').append($cb));
+                            $tr.append($('<td/>').text(c.name || c.company_name || ''));
+                            $tr.append($('<td/>').text(c.username || ''));
+                            $tr.append($('<td/>').text(c.email || ''));
+                            $tr.append($('<td/>').text(c.phone || ''));
+                            $tr.append($('<td/>').text((c.company_type != null && String(c.company_type).trim() !== '') ? String(c.company_type).trim() : (c.category || '')));
+                            $tbody.append($tr);
+                        });
+                    }
+                    var $cbsF = $('#review-company-tbody .review-company-cb');
+                    var $masterF = $('#review-check-all');
+                    if (!$cbsF.length) {
+                        $masterF.prop('checked', false).prop('indeterminate', false);
+                    } else {
+                        var nF = $cbsF.filter(':checked').length;
+                        $masterF.prop('checked', nF === $cbsF.length);
+                        $masterF.prop('indeterminate', nF > 0 && nF < $cbsF.length);
+                    }
+                    syncReviewSelectedSummary();
+                    return false;
+                });
+                return false;
+            });
+        },
+
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($('form[role=form]'));
+            }
+        }
+    };
+    return Controller;
+});

+ 0 - 0
public/xinhua/.gitkeep


BIN
public/xinhua/2026/05/13/202603709S_23.pdf


BIN
public/xinhua/2026/05/14/202603709S_23.pdf


Vissa filer visades inte eftersom för många filer har ändrats