liuhairui před 1 dnem
rodič
revize
59b388e9c0
39 změnil soubory, kde provedl 3659 přidání a 558 odebrání
  1. 692 203
      application/admin/controller/Procuremen.php
  2. 84 0
      application/admin/controller/Procuremenarchive.php
  3. 46 0
      application/admin/controller/Procuremenexport.php
  4. 130 0
      application/admin/controller/Procuremenmenu.php
  5. 92 0
      application/admin/controller/Procuremensms.php
  6. 100 0
      application/admin/controller/Purchaseemail.php
  7. 5 0
      application/admin/lang/zh-cn/purchaseemail.php
  8. 49 0
      application/admin/model/Purchaseemail.php
  9. 12 0
      application/admin/model/Purchasemonthexport.php
  10. 78 0
      application/admin/model/Purchasesmstemplate.php
  11. 27 0
      application/admin/validate/Purchaseemail.php
  12. 3 3
      application/admin/view/index/login.html
  13. 247 0
      application/admin/view/index/生产经营驾驶舱系统登录页面.html
  14. 193 0
      application/admin/view/procuremen/audit_issue.html
  15. 2 21
      application/admin/view/procuremen/details_fragment.html
  16. 49 8
      application/admin/view/procuremen/index.html
  17. 120 0
      application/admin/view/procuremen/pick_add.html
  18. 89 64
      application/admin/view/procuremen/review.html
  19. 16 0
      application/admin/view/procuremenarchive/index.html
  20. 14 0
      application/admin/view/procuremenexport/index.html
  21. 126 0
      application/admin/view/procuremensms/edit.html
  22. 62 0
      application/admin/view/procuremensms/index.html
  23. 35 0
      application/admin/view/purchaseemail/edit.html
  24. 17 0
      application/admin/view/purchaseemail/index.html
  25. 3 2
      application/config.php
  26. 24 0
      application/extra/procuremen_drop_optional_columns.sql
  27. 56 0
      application/extra/procuremen_tables.sql
  28. 17 0
      application/extra/procuremen_workflow.sql
  29. 28 0
      application/extra/procuremen_workflow_patch.sql
  30. 10 0
      application/extra/purchase_email.sql
  31. 16 0
      application/extra/purchase_sms_template_split.sql
  32. 57 0
      application/index/controller/Index.php
  33. 138 15
      application/index/view/index/index.html
  34. 66 0
      public/assets/css/backend.css
  35. 548 242
      public/assets/js/backend/procuremen.js
  36. 79 0
      public/assets/js/backend/procuremenarchive.js
  37. 75 0
      public/assets/js/backend/procuremenexport.js
  38. 163 0
      public/assets/js/backend/procuremensms.js
  39. 91 0
      public/assets/js/backend/purchaseemail.js

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 692 - 203
application/admin/controller/Procuremen.php


+ 84 - 0
application/admin/controller/Procuremenarchive.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Db;
+
+/**
+ * 外发采购 — 历史存证档案查询(已完结工序 + 操作记录详情)
+ *
+ * @icon fa fa-archive
+ */
+class Procuremenarchive extends Backend
+{
+    protected $searchFields = 'CCYDH,CYJMC,CGYMC';
+
+    /** purchase_order.status 为已完结(兼容 varchar / 数字 / 首尾空格) */
+    protected function applyPurchaseOrderCompletedWhere($query): void
+    {
+        $query->whereRaw(
+            "(TRIM(CAST(`status` AS CHAR)) = '1' OR CAST(`status` AS UNSIGNED) = 1)"
+        );
+    }
+
+    public function index()
+    {
+        $this->relationSearch = false;
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $ccydhKw = trim((string)$this->request->get('ccydh', ''));
+
+            $query = Db::table('purchase_order');
+            $this->applyPurchaseOrderCompletedWhere($query);
+            if ($ccydhKw !== '') {
+                $query->where('CCYDH', 'like', '%' . $ccydhKw . '%');
+            }
+            if (is_callable($where)) {
+                $query->where($where);
+            }
+
+            $total = (clone $query)->count();
+            $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? $sort : 'id';
+            $orderDir = strtoupper((string)$order) === 'ASC' ? 'ASC' : 'DESC';
+            $rows = $query
+                ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,createtime,dStamp')
+                ->order($sortField, $orderDir)
+                ->limit($offset, $limit)
+                ->select();
+            if (!is_array($rows)) {
+                $rows = [];
+            }
+
+            $out = [];
+            foreach ($rows as $r) {
+                if (!is_array($r)) {
+                    continue;
+                }
+                $sid = (int)($r['scydgy_id'] ?? 0);
+                $doneText = '';
+                $ct = $r['createtime'] ?? null;
+                if (is_numeric($ct) && (int)$ct > 946684800) {
+                    $doneText = date('Y-m-d H:i:s', (int)$ct);
+                } elseif (is_string($ct) && trim($ct) !== '' && stripos(trim($ct), '0000-00-00') !== 0) {
+                    $doneText = trim($ct);
+                } elseif (!empty($r['dStamp']) && stripos((string)$r['dStamp'], '0000-00-00') !== 0) {
+                    $doneText = trim((string)$r['dStamp']);
+                }
+                $out[] = [
+                    'id'              => (int)($r['id'] ?? 0),
+                    'scydgy_id'       => $sid,
+                    'CCYDH'           => trim((string)($r['CCYDH'] ?? '')),
+                    'CYJMC'           => trim((string)($r['CYJMC'] ?? '')),
+                    'CGYMC'           => trim((string)($r['CGYMC'] ?? '')),
+                    'createtime_text' => $doneText,
+                ];
+            }
+
+            return json(['total' => $total, 'rows' => $out]);
+        }
+
+        return $this->view->fetch();
+    }
+}

+ 46 - 0
application/admin/controller/Procuremenexport.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+
+/**
+ * 外发采购 — 月度报表导出列表
+ *
+ * @icon fa fa-file-excel-o
+ */
+class Procuremenexport extends Backend
+{
+    /**
+     * @var \app\admin\model\Purchasemonthexport
+     */
+    protected $model = null;
+
+    protected $searchFields = 'ym,admin_name';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Purchasemonthexport;
+    }
+
+    public function index()
+    {
+        $this->relationSearch = false;
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $list = $this->model
+                ->where($where)
+                ->order($sort ?: 'id', $order ?: 'desc')
+                ->paginate($limit);
+            foreach ($list as $row) {
+                $ct = (int)($row['createtime'] ?? 0);
+                $row->createtime_text = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
+                $row->total_amount_text = number_format((float)($row['total_amount'] ?? 0), 2, '.', ',');
+            }
+            return json(['total' => $list->total(), 'rows' => $list->items()]);
+        }
+        return $this->view->fetch();
+    }
+}

+ 130 - 0
application/admin/controller/Procuremenmenu.php

@@ -0,0 +1,130 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Db;
+
+/**
+ * 外发采购子菜单安装(仅超级管理员访问一次)
+ */
+class Procuremenmenu extends Backend
+{
+    protected $noNeedRight = ['install'];
+
+    public function install()
+    {
+        if (!$this->auth->isSuperAdmin()) {
+            $this->error('仅超级管理员可执行');
+        }
+        $t = time();
+        $listRule = Db::name('auth_rule')->where('name', 'procuremen/pick')->find();
+        if (!$listRule) {
+            $listRule = Db::name('auth_rule')->where('name', 'procuremen/index')->find();
+        }
+        if (!$listRule) {
+            $this->error('未找到外发下发菜单,请先在权限规则中配置 procuremen/pick 或 procuremen/index');
+        }
+        $pid = (int)($listRule['pid'] ?? 0);
+        if ($pid <= 0) {
+            $existsRoot = Db::name('auth_rule')->where('name', 'procuremenroot')->find();
+            if ($existsRoot) {
+                $pid = (int)$existsRoot['id'];
+            } else {
+                $pid = Db::name('auth_rule')->insertGetId([
+                    'type'       => 'file',
+                    'pid'        => 0,
+                    'name'       => 'procuremenroot',
+                    'title'      => '外发采购',
+                    'icon'       => 'fa fa-share-alt',
+                    'url'        => '',
+                    'ismenu'     => 1,
+                    'menutype'   => 'addtabs',
+                    'weigh'      => 88,
+                    'status'     => 'normal',
+                    'createtime' => $t,
+                    'updatetime' => $t,
+                ]);
+            }
+            Db::name('auth_rule')->where('id', (int)$listRule['id'])->update([
+                'pid'        => $pid,
+                'title'      => '外发下发',
+                'name'       => 'procuremen/pick',
+                'updatetime' => $t,
+            ]);
+        }
+        $added = 0;
+        $workflowMenus = [
+            ['name' => 'procuremen/pick', 'title' => '外发下发', 'icon' => 'fa fa-paper-plane', 'weigh' => 90],
+            ['name' => 'procuremen/audit', 'title' => '确认供应商', 'icon' => 'fa fa-check-square-o', 'weigh' => 89],
+            ['name' => 'procuremen/confirm', 'title' => '采购确认', 'icon' => 'fa fa-shopping-cart', 'weigh' => 88],
+        ];
+        foreach ($workflowMenus as $wm) {
+            if (Db::name('auth_rule')->where('name', $wm['name'])->find()) {
+                Db::name('auth_rule')->where('name', $wm['name'])->update([
+                    'title'      => $wm['title'],
+                    'icon'       => $wm['icon'],
+                    'weigh'      => $wm['weigh'],
+                    'updatetime' => $t,
+                ]);
+                continue;
+            }
+            Db::name('auth_rule')->insertGetId([
+                'type'       => 'file',
+                'pid'        => $pid,
+                'name'       => $wm['name'],
+                'title'      => $wm['title'],
+                'icon'       => $wm['icon'],
+                'url'        => '',
+                'ismenu'     => 1,
+                'menutype'   => 'addtabs',
+                'weigh'      => $wm['weigh'],
+                'status'     => 'normal',
+                'createtime' => $t,
+                'updatetime' => $t,
+            ]);
+            $added++;
+        }
+        $menus = [
+            ['name' => 'procuremensms/index', 'title' => '短信模版维护', 'icon' => 'fa fa-commenting-o', 'weigh' => 87],
+            ['name' => 'procuremenarchive/index', 'title' => '历史存证档案查询', 'icon' => 'fa fa-archive', 'weigh' => 86],
+            ['name' => 'procuremenexport/index', 'title' => '月度报表导出列表', 'icon' => 'fa fa-file-excel-o', 'weigh' => 85],
+        ];
+        foreach ($menus as $m) {
+            if (Db::name('auth_rule')->where('name', $m['name'])->find()) {
+                continue;
+            }
+            $menuId = Db::name('auth_rule')->insertGetId([
+                'type'       => 'file',
+                'pid'        => $pid,
+                'name'       => $m['name'],
+                'title'      => $m['title'],
+                'icon'       => $m['icon'],
+                'url'        => '',
+                'ismenu'     => 1,
+                'menutype'   => 'addtabs',
+                'weigh'      => $m['weigh'],
+                'status'     => 'normal',
+                'createtime' => $t,
+                'updatetime' => $t,
+            ]);
+            $added++;
+            if ($m['name'] === 'procuremensms/index') {
+                Db::name('auth_rule')->insert([
+                    'type'       => 'file',
+                    'pid'        => $menuId,
+                    'name'       => 'procuremensms/edit',
+                    'title'      => '编辑',
+                    'icon'       => 'fa fa-circle-o',
+                    'ismenu'     => 0,
+                    'status'     => 'normal',
+                    'createtime' => $t,
+                    'updatetime' => $t,
+                ]);
+                $added++;
+            }
+        }
+        \think\Cache::rm('__menu__');
+        $this->success('菜单安装完成,新增节点 ' . $added . ' 个。请刷新后台并到「权限管理」为角色勾选新菜单。');
+    }
+}

+ 92 - 0
application/admin/controller/Procuremensms.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Db;
+use think\exception\PDOException;
+use think\exception\ValidateException;
+use Exception;
+
+/**
+ * 外发采购 — 短信模版维护
+ *
+ * @icon fa fa-commenting-o
+ */
+class Procuremensms extends Backend
+{
+    /**
+     * @var \app\admin\model\Purchasesmstemplate
+     */
+    protected $model = null;
+
+    protected $searchFields = 'title,content,remark';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Purchasesmstemplate;
+        $this->view->assign('sceneList', $this->model->getSceneList());
+        $this->view->assign('statusList', $this->model->getStatusList());
+    }
+
+    public function index()
+    {
+        $this->relationSearch = false;
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $list = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->paginate($limit);
+            return json(['total' => $list->total(), 'rows' => $list->items()]);
+        }
+        return $this->view->fetch();
+    }
+
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if (false === $this->request->isPost()) {
+            $sceneList = $this->model->getSceneList();
+            $sc = (string)($row['scene'] ?? '');
+            $this->view->assign('row', $row);
+            $this->view->assign('sceneText', $sceneList[$sc] ?? $sc);
+            $this->view->assign('isEmailScene', \app\admin\model\Purchasesmstemplate::isEmailScene($sc));
+            $this->view->assign('smsVarGuide', \app\admin\model\Purchasesmstemplate::getVariableGuideForScene($sc));
+            return $this->view->fetch();
+        }
+        $params = $this->request->post('row/a');
+        if (empty($params)) {
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $params = $this->preExcludeFields($params);
+        $content = trim((string)($params['content'] ?? ''));
+        if ($content === '') {
+            $this->error('请填写模版正文');
+        }
+        $params['content'] = $content;
+        $params['title'] = trim((string)($params['title'] ?? ''));
+        $params['remark'] = trim((string)($params['remark'] ?? ''));
+        if (!isset($params['status']) || $params['status'] === '') {
+            $params['status'] = '1';
+        }
+        if ($params['status'] === 'normal') {
+            $params['status'] = '1';
+        }
+        if ($params['status'] === 'hidden') {
+            $params['status'] = '0';
+        }
+        $params['updatetime'] = date('Y-m-d H:i:s');
+        try {
+            $row->allowField(true)->save($params);
+        } catch (ValidateException|PDOException|Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success();
+    }
+}

+ 100 - 0
application/admin/controller/Purchaseemail.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Db;
+use think\exception\PDOException;
+use think\exception\ValidateException;
+use Exception;
+
+/**
+ * 外发发件邮箱配置(仅维护编辑,不增删)
+ *
+ * @icon fa fa-envelope
+ */
+class Purchaseemail extends Backend
+{
+    /** @var \app\admin\model\Purchaseemail */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Purchaseemail;
+    }
+
+    public function index()
+    {
+        $this->relationSearch = false;
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+
+            $list = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->paginate($limit);
+
+            foreach ($list as $row) {
+                $row->visible(['id', 'email_addr', 'email_pass', 'createtime', 'updatetime']);
+            }
+
+            return json(['total' => $list->total(), 'rows' => $list->items()]);
+        }
+
+        return $this->view->fetch();
+    }
+
+    /** 发件配置固定一条,不提供新增 */
+    public function add()
+    {
+        $this->error('请使用「编辑」修改发件邮箱配置');
+    }
+
+    /** 不提供删除 */
+    public function del($ids = null)
+    {
+        $this->error('发件邮箱配置不可删除,请使用编辑修改');
+    }
+
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if (false === $this->request->isPost()) {
+            $this->view->assign('row', $row);
+
+            return $this->view->fetch();
+        }
+        $params = $this->request->post('row/a');
+        if (empty($params)) {
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $params = $this->preExcludeFields($params);
+        $addr = trim((string)($params['email_addr'] ?? ''));
+        if ($addr === '' || !filter_var($addr, FILTER_VALIDATE_EMAIL)) {
+            $this->error('请填写有效的发件邮箱');
+        }
+        $pass = trim((string)($params['email_pass'] ?? ''));
+        if ($pass === '') {
+            $this->error('请填写邮箱授权码');
+        }
+        $save = [
+            'email_addr' => $addr,
+            'email_pass' => $pass,
+        ];
+
+        try {
+            $row->save($save);
+        } catch (ValidateException|PDOException|Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success();
+    }
+}

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

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

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

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Config;
+use think\Model;
+
+class Purchaseemail extends Model
+{
+    protected $table = 'purchase_email';
+
+    protected $autoWriteTimestamp = 'integer';
+
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    /**
+     * 发件配置:SMTP 等来自 config.php Mailer;addr、pass 仅来自 purchase_email 表
+     *
+     * @return array<string, mixed>
+     */
+    public static function getActiveMailerConfig(): array
+    {
+        $fileCfg = Config::get('Mailer');
+        if (!is_array($fileCfg)) {
+            $fileCfg = [];
+        }
+        unset($fileCfg['addr'], $fileCfg['pass']);
+
+        try {
+            $row = (new self())->order('id', 'asc')->find();
+        } catch (\Throwable $e) {
+            $row = null;
+        }
+        if (!$row) {
+            return $fileCfg;
+        }
+        $addr = trim((string)($row['email_addr'] ?? ''));
+        $pass = trim((string)($row['email_pass'] ?? ''));
+        if ($addr === '' || $pass === '') {
+            return $fileCfg;
+        }
+        $fileCfg['addr'] = $addr;
+        $fileCfg['pass'] = $pass;
+
+        return $fileCfg;
+    }
+}

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

@@ -0,0 +1,12 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Purchasemonthexport extends Model
+{
+    protected $table = 'purchase_month_export_log';
+
+    protected $autoWriteTimestamp = false;
+}

+ 78 - 0
application/admin/model/Purchasesmstemplate.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Purchasesmstemplate extends Model
+{
+    protected $table = 'purchase_sms_template';
+
+    /** 时间由控制器写入 datetime 字符串(库表为 datetime,勿用 int 时间戳) */
+    protected $autoWriteTimestamp = false;
+
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+
+    public function getSceneList()
+    {
+        return [
+            'review_email' => '外发下发-邮箱',
+            'review_sms'   => '外发下发-短信',
+            'confirm_ok'   => '采购确认-通过',
+            'confirm_fail' => '采购确认-未通过',
+        ];
+    }
+
+    /** status:1=正常 0=禁用 */
+    public function getStatusList()
+    {
+        return ['1' => '正常', '0' => '禁用'];
+    }
+
+    public static function isEmailScene(string $scene): bool
+    {
+        return $scene === 'review_email' || $scene === 'review';
+    }
+
+    public static function isSmsScene(string $scene): bool
+    {
+        return in_array($scene, ['review_sms', 'confirm_ok', 'confirm_fail'], true);
+    }
+
+    /**
+     * 各场景编辑弹窗右侧变量说明(统一列表,改 getVariableGuide() 一处即可)
+     *
+     * @return array<int, array{tag:string,label:string,example:string,scenes:string}>
+     */
+    public static function getVariableGuideForScene(string $scene): array
+    {
+        unset($scene);
+
+        return static::getVariableGuide();
+    }
+
+    /**
+     * 模版变量说明(唯一维护处:右侧「说明」列改这里 label 即可,四个模版弹窗显示相同)
+     *
+     * @return array<int, array{tag:string,label:string,example:string,scenes:string}>
+     */
+    public static function getVariableGuide(): array
+    {
+        return [
+            ['tag' => '{company_name}', 'label' => '供应商名称', 'example' => '浙江某某印刷有限公司', 'scenes' => '全部'],
+            ['tag' => '{contact_name}', 'label' => '姓名', 'example' => '张三', 'scenes' => '全部'],
+            ['tag' => '{phone}', 'label' => '手机号', 'example' => '13800138000', 'scenes' => '全部'],
+            ['tag' => '{email}', 'label' => '邮箱', 'example' => 'user@example.com', 'scenes' => '全部'],
+            ['tag' => '{ccydh}', 'label' => '订单号', 'example' => '202603668L', 'scenes' => '全部'],
+            ['tag' => '{cyjmc}', 'label' => '印件名称', 'example' => '藏书票2', 'scenes' => '全部'],
+            ['tag' => '{cgymc}', 'label' => '工序名称(单道工序)', 'example' => '骑马订', 'scenes' => '外发下发'],
+            ['tag' => '{category}', 'label' => '业务分类', 'example' => '出版物印刷', 'scenes' => '外发下发'],
+            ['tag' => '{deadline}', 'label' => '截止时间', 'example' => '2026-05-18 14:30', 'scenes' => '外发下发'],
+            ['tag' => '{process_lines}', 'label' => '订单工序明细(文本)', 'example' => "1.压折线 单位:张\n2.模切", 'scenes' => '外发下发'],
+            ['tag' => '{process_lines_html}', 'label' => '订单工序明细(表格)', 'example' => '<table>…</table>', 'scenes' => '外发下发邮箱'],
+            ['tag' => '{platform_url}', 'label' => '平台链接', 'example' => 'https://…', 'scenes' => '外发下发邮箱'],
+        ];
+    }
+}

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

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

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

@@ -6,7 +6,8 @@
         body {
             width: 100%;
             height: 100vh;
-            background-image: url("http://xh-erp.7in6.com/img/bg1.jpg");
+            /* background-image: url("http://xh-erp.7in6.com/img/bg1.jpg"); */
+            background-color: #071d31;
             background-size: cover;
             background-repeat: no-repeat;
             display: flex;
@@ -16,7 +17,6 @@
             margin: 0;
             position: relative;
             /*background-position: center;*/
-            filter: brightness(1.2); /* 调整亮度 */
         }
 
         a {
@@ -196,7 +196,7 @@
     <div class="tmbg">
     </div>
     <div class="login-screen">
-        <p class="head-title-logo">生产经营驾驶舱系统</p>
+        <p class="head-title-logo">供应商存证系统</p>
         <div>
             <div class="login-form">
                 <p id="profile-name" class="profile-name-card"></p>

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

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

+ 193 - 0
application/admin/view/procuremen/audit_issue.html

@@ -0,0 +1,193 @@
+<style>
+    body.is-dialog .audit-issue-wrap {
+        padding: 12px 14px 0;
+        max-width: 100%;
+        box-sizing: border-box;
+    }
+    .audit-issue-wrap .audit-block-title {
+        margin: 0 0 6px;
+        font-weight: 600;
+        font-size: 13px;
+        color: #333;
+    }
+    .audit-issue-wrap .audit-table-wrap {
+        max-height: 240px;
+        overflow: auto;
+        margin-bottom: 12px;
+        border: 1px solid #e5e5e5;
+    }
+    .audit-issue-wrap .audit-process-table {
+        margin: 0;
+        font-size: 12px;
+        table-layout: fixed;
+        width: 100%;
+        min-width: 860px;
+    }
+    .audit-issue-wrap .audit-process-table th {
+        background: #f5f5f5;
+        white-space: nowrap;
+        font-weight: 600;
+        padding: 6px 8px;
+        vertical-align: middle;
+    }
+    .audit-issue-wrap .audit-process-table th.text-center,
+    .audit-issue-wrap .audit-process-table td.text-center {
+        text-align: center;
+    }
+    .audit-issue-wrap .audit-process-table td {
+        padding: 6px 8px;
+        vertical-align: middle;
+        word-wrap: break-word;
+    }
+    .audit-issue-wrap .audit-table {
+        margin: 0;
+        font-size: 12px;
+    }
+    .audit-issue-wrap .audit-table th {
+        background: #f5f5f5;
+        white-space: nowrap;
+    }
+    .audit-issue-wrap .audit-quote-lines {
+        margin: 0;
+        padding: 0;
+        font-size: 11px;
+        color: #555;
+        line-height: 1.65;
+    }
+    .audit-issue-wrap .audit-quote-line {
+        margin: 0;
+        padding: 0;
+        list-style: none;
+    }
+    .audit-issue-wrap .audit-quote-line + .audit-quote-line {
+        margin-top: 4px;
+    }
+    .audit-issue-wrap .audit-quote-line .quote-gymc {
+        color: #333;
+    }
+    .audit-issue-wrap .audit-quote-line .quote-ok {
+        color: #3c763d;
+    }
+    .audit-issue-wrap .audit-quote-line .quote-wait {
+        color: #999;
+    }
+    .audit-issue-wrap .audit-quote-line .quote-meta {
+        color: #666;
+    }
+    .audit-issue-wrap .audit-quote-line .quote-empty {
+        color: #d9534f;
+    }
+    .audit-issue-wrap .audit-pick-table td label {
+        margin: 0;
+        font-weight: normal;
+        cursor: pointer;
+    }
+    .audit-issue-wrap tr.audit-row-no-quote td {
+        color: #666;
+    }
+    .audit-issue-wrap tr.audit-row-no-quote label {
+        cursor: pointer;
+    }
+    .audit-issue-wrap .text-quote-ok {
+        color: #3c763d;
+    }
+    .audit-issue-wrap .text-quote-wait {
+        color: #999;
+    }
+</style>
+<div class="audit-issue-wrap">
+    <div class="audit-block-title">本单工序({$processCount|default='1'} 项)</div>
+    <div class="audit-table-wrap">
+        <table class="table table-bordered table-condensed audit-process-table">
+            <thead>
+            <tr>
+                <th class="text-center" style="width:100px;">订单号</th>
+                <th style="min-width:160px;">印件名称</th>
+                <th>工序名称</th>
+                <th class="text-center" style="width:56px;">单位</th>
+                <th class="text-center" style="width:72px;">工作量</th>
+                <th class="text-center" style="width:72px;">本次数量</th>
+                <th class="text-center" style="width:72px;">最高限价</th>
+                <th class="text-center" style="width:80px;">订法</th>
+            </tr>
+            </thead>
+            <tbody>
+            {volist name="processDisplayRows" id="pr"}
+            <tr>
+                {if $pr.show_order_cells}
+                <td class="text-center"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''}</td>
+                <td{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''}</td>
+                {/if}
+                <td>{$pr.CGYMC|default=''}</td>
+                <td class="text-center">{$pr.CDW|default=''}</td>
+                <td class="text-center">{$pr.NGZL|default=''}</td>
+                <td class="text-center">{$pr.This_quantity|default=''}</td>
+                <td class="text-center">{$pr.ceilingPrice|default=''}</td>
+                <td class="text-center">{$pr.CDF|default=''}</td>
+            </tr>
+            {/volist}
+            </tbody>
+        </table>
+    </div>
+    <div class="audit-block-title">供应商报价(请勾选一家)</div>
+    <div class="audit-table-wrap">
+        <table class="table table-bordered table-condensed audit-table audit-pick-table">
+            <thead>
+            <tr>
+                <th style="width:42px;"></th>
+                <th>公司名称</th>
+                <th>联系人</th>
+                <th>邮箱</th>
+                <th>手机号</th>
+                <th>报价明细</th>
+            </tr>
+            </thead>
+            <tbody id="audit-pick-tbody">
+            {volist name="supplierGroups" id="co" key="k"}
+            <tr class="{if !$co.has_quote}audit-row-no-quote{/if}" data-has-quote="{if $co.has_quote}1{else /}0{/if}">
+                <td class="text-center">
+                    <label>
+                        <input type="radio" name="audit_pick_company" class="audit-pick-radio" value="{$k-1}"/>
+                    </label>
+                </td>
+                <td>{$co.name|default=''}</td>
+                <td>{$co.username|default=''}</td>
+                <td>{$co.email|default=''}</td>
+                <td>{$co.phone|default=''}</td>
+                <td>
+                    {if isset($co.lines) && $co.lines}
+                    <div class="audit-quote-lines">
+                        {volist name="co.lines" id="ln"}
+                        <div class="audit-quote-line">
+                            <span class="quote-meta">工序名称:</span><span class="quote-gymc">{$ln.cgymc|default='工序'}</span>
+                            <span class="quote-meta"> · 金额 </span>{if condition="$ln.amount_filled"}<span>{$ln.amount_show}</span>{else /}<span class="quote-empty">未填</span>{/if}<span class="quote-meta">,货期 </span>{if condition="$ln.delivery_filled"}<span>{$ln.delivery_show}</span>{else /}<span class="quote-empty">未填</span>{/if}
+                        </div>
+                        {/volist}
+                    </div>
+                    {else /}
+                    <span class="text-quote-wait">待报价</span>
+                    {/if}
+                </td>
+            </tr>
+            {/volist}
+            </tbody>
+        </table>
+    </div>
+    <form id="audit-issue-form" class="form-horizontal" role="form">
+        {:token()}
+        <input type="hidden" name="scydgy_id" value="{$scydgyId|htmlentities}"/>
+        <input type="hidden" id="audit-supplier-groups-json" value="{$supplierGroupsJson|htmlentities}"/>
+    </form>
+    <div class="form-group layer-footer">
+        <div class="row procuremen-audit-issue-footer-row">
+            <div class="col-xs-12 procuremen-audit-issue-footer-btns">
+                <button type="button" style="margin-right: 20px" class="btn btn-primary btn-embossed" id="btn-audit-issue-submit">
+                    <i class="fa fa-check"></i> 确认
+                </button>
+                <button type="button" style="margin-right: 20px" class="btn btn-default btn-embossed" id="btn-audit-issue-close">
+                    <i class="fa fa-times"></i> 关闭
+                </button>
+            </div>
+        </div>
+    </div>
+</div>

+ 2 - 21
application/admin/view/procuremen/details_fragment.html

@@ -3,21 +3,6 @@
         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;
@@ -194,22 +179,18 @@
 </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;">
+    {notempty name="pdf_export"}
     <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>
+    {/notempty}
 
     <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"}

+ 49 - 8
application/admin/view/procuremen/index.html

@@ -196,6 +196,48 @@
         line-height: 1.28;
         vertical-align: middle;
     }
+    /* 复选框列:表头 .th-inner 与表体 td 内边距一致,避免全选与行勾选框错位 */
+    #procuremen-layout .bootstrap-table th.bs-checkbox,
+    #procuremen-layout .bootstrap-table td.bs-checkbox {
+        width: 42px !important;
+        min-width: 42px !important;
+        max-width: 42px !important;
+        padding: 3px 4px !important;
+        text-align: center !important;
+        vertical-align: middle !important;
+        box-sizing: border-box;
+    }
+    #procuremen-layout .bootstrap-table th.bs-checkbox .th-inner {
+        padding: 0 !important;
+        margin: 0 !important;
+        text-align: center !important;
+        line-height: 1;
+        min-height: 0;
+    }
+    #procuremen-layout .bootstrap-table th.bs-checkbox input[type="checkbox"],
+    #procuremen-layout .bootstrap-table td.bs-checkbox input[type="checkbox"] {
+        margin: 0 auto !important;
+        vertical-align: middle !important;
+        float: none;
+        position: static;
+        top: auto;
+        width: 16px;
+        height: 16px;
+        cursor: pointer;
+        display: block;
+    }
+    #procuremen-layout .bootstrap-table tbody td.bs-checkbox {
+        min-height: 28px;
+        cursor: pointer;
+    }
+    /* 避免出现纵向滚动条时表头与表体列宽错位 */
+    #procuremen-layout .bootstrap-table .fixed-table-body {
+        overflow-y: scroll;
+    }
+    #procuremen-layout .nice-validator .bootstrap-table td.bs-checkbox input[type="checkbox"],
+    #procuremen-layout .nice-validator .bootstrap-table th.bs-checkbox input[type="checkbox"] {
+        vertical-align: middle !important;
+    }
     #procuremen-layout .bootstrap-table .table > tbody > tr > td .btn-xs {
         padding: 2px 6px;
         font-size: 12px;
@@ -244,7 +286,10 @@
     }
 </style>
 
-<div class="panel panel-default panel-intro" id="procuremen-layout" data-default-ym="{$defaultYm|htmlentities}" data-procuremen-redis-api="{$procuremenRedisApi|htmlentities}">
+<div class="panel panel-default panel-intro" id="procuremen-layout"
+     data-default-ym="{$defaultYm|htmlentities}"
+     data-procuremen-redis-api="{$procuremenRedisApi|htmlentities}"
+     data-procuremen-stage="{$procuremenStage|default='pick'|htmlentities}">
     {:build_heading()}
 
     <div class="panel-body">
@@ -264,16 +309,12 @@
                     <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="按月份导出外发明细(8 列固定表头,已完结且已选外协)"><i class="fa fa-download"></i> 月份下发明细导出</a>
+                                    <a href="javascript:;" class="btn btn-success" id="btn-procuremen-pick-add" title="手工新增一条外发工序" style="display:none;"><i class="fa fa-plus"></i> 新增</a>
+                                    <a href="javascript:;" class="btn btn-info" id="btn-procuremen-pick-review" title="勾选一条或多条工序进行下发(多条须同一订单号)" style="display:none;"><i class="fa fa-paper-plane"></i> 下发</a>
+                                    <a href="javascript:;" class="btn btn-warning" id="btn-procuremen-batch-finish" title="勾选一条或多条工序标记为已完结" style="display:none;"><i class="fa fa-flag-checkered"></i> 完结</a>
                                 </div>
                                 <table id="table"
                                        class="table table-striped table-bordered table-hover">

+ 120 - 0
application/admin/view/procuremen/pick_add.html

@@ -0,0 +1,120 @@
+<style>
+    body.is-dialog .content {
+        padding: 10px 14px 0;
+        overflow: hidden;
+    }
+    .procuremen-pick-add-form {
+        padding: 4px 6px 0;
+        max-width: 100%;
+    }
+    .procuremen-pick-add-form .form-group {
+        display: flex;
+        align-items: center;
+        margin: 0 0 8px;
+    }
+    .procuremen-pick-add-form .control-label {
+        flex: 0 0 78px;
+        width: 78px;
+        margin: 0;
+        padding: 0 8px 0 0;
+        text-align: right;
+        font-weight: normal;
+        font-size: 13px;
+        line-height: 1.3;
+        white-space: nowrap;
+    }
+    .procuremen-pick-add-form .field-wrap {
+        flex: 1 1 auto;
+        min-width: 0;
+    }
+    .procuremen-pick-add-form .form-control {
+        height: 32px;
+        padding: 4px 10px;
+        font-size: 13px;
+    }
+    .procuremen-pick-add-form .form-tip {
+        margin: 2px 0 10px 78px;
+        font-size: 12px;
+        color: #999;
+        line-height: 1.45;
+    }
+    .procuremen-pick-add-form .form-group.layer-footer {
+        display: none;
+    }
+</style>
+<form id="add-form" class="form-horizontal procuremen-pick-add-form" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label class="control-label"><span class="text-danger">*</span> 订单号:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[CCYDH]" type="text" data-rule="required" placeholder="" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label"><span class="text-danger">*</span> 印件名称:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[CYJMC]" type="text" data-rule="required" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label"><span class="text-danger">*</span> 工序名称:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[CGYMC]" type="text" data-rule="required" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">单位:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[CDW]" type="text" placeholder="" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">工作量:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[NGZL]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">本次数量:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[This_quantity]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">最高限价:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[ceilingPrice]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">订法:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[CDF]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">外厂单位:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[cGzzxMc]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">业务员:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[cywyxm]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label">备注:</label>
+        <div class="field-wrap">
+            <input class="form-control" name="row[MBZ]" type="text" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <div class="row procuremen-pick-add-footer-row">
+            <div class="col-xs-12 procuremen-pick-add-footer-btns">
+                <button type="submit" class="btn btn-primary btn-embossed disabled">保存</button>
+                <button type="button" class="btn btn-default btn-embossed btn-procuremen-pick-add-close">关闭</button>
+            </div>
+        </div>
+    </div>
+</form>

+ 89 - 64
application/admin/view/procuremen/review.html

@@ -167,19 +167,23 @@
         font-size: 13px;
         color: #000;
     }
-    .review-split-right .procuremen-review-submit-tip {
-        flex-shrink: 0;
-        margin: 0 0 8px;
-        padding: 8px 10px;
-        border: 1px solid #eea236;
-        background: #fcf8e3;
+    .review-selected-summary .procuremen-review-submit-tip {
+        margin: 0;
+        padding: 0;
+        border: none;
+        background: transparent;
         color: #8a6d3b;
-        font-size: 13px;
-        line-height: 1.65;
-        border-radius: 3px;
+        font-size: 12px;
+        line-height: 1.5;
+        flex: 1 1 auto;
+        min-width: 0;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
     }
-    .review-split-right .procuremen-review-submit-tip .fa {
-        margin-right: 6px;
+    .review-selected-summary .procuremen-review-submit-tip .fa {
+        margin-right: 4px;
+        color: #f0ad4e;
     }
     /* 提交:与列表审核 / 采购确认、弹层「确定」一致 */
     .btn.procuremen-btn-slate {
@@ -382,16 +386,27 @@
         font-size: 12px;
         line-height: 1.45;
     }
-    .review-selected-summary .review-selected-head {
+    .review-selected-summary .review-selected-top {
+        display: flex;
+        flex-wrap: nowrap;
+        align-items: center;
+        gap: 10px 14px;
         margin: 0 0 6px;
+        min-width: 0;
+    }
+    .review-selected-summary .review-selected-head {
+        margin: 0;
         font-weight: 600;
         color: #333;
+        flex-shrink: 0;
+        white-space: nowrap;
     }
     .review-selected-summary .review-selected-count {
         font-weight: 700;
         color: #3c8dbc;
     }
     .review-selected-summary .review-selected-empty {
+        display: none;
         margin: 0;
         color: #999;
         font-size: 12px;
@@ -443,51 +458,59 @@
         color: #a94442;
         outline: none;
     }
+    .review-merge-panel {
+        margin: 0 0 10px;
+        display: block;
+    }
+    .review-merge-panel .review-block-title {
+        margin: 0 0 8px;
+        font-weight: 600;
+        font-size: 13px;
+        color: #000;
+    }
+    .review-merge-table-wrap {
+        max-height: 200px;
+        overflow: auto;
+        border: 1px solid #e5e5e5;
+    }
+    .review-merge-table {
+        margin: 0;
+        font-size: 12px;
+    }
+    .review-merge-table th {
+        white-space: nowrap;
+        background: #f5f5f5;
+    }
+    .review-merge-table th.text-center,
+    .review-merge-table td.text-center {
+        text-align: center;
+        vertical-align: middle;
+    }
 </style>
-<form id="review-form" class="review-dialog-form" role="form" data-toggle="validator" method="post" action="">
+<form id="review-form" class="review-dialog-form" role="form" data-toggle="validator" method="post" action="" data-pick-mode="{$pickMode|default=0}">
     {:token()}
     <textarea class="hide" name="row_json" id="c-row-json" rows="1" cols="1"></textarea>
+    <textarea class="hide" name="merge_rows_json" id="c-merge-rows-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 class="review-merge-panel" id="review-merge-panel">
+                <div class="review-merge-table-wrap">
+                    <table class="table table-bordered table-condensed review-merge-table">
+                        <thead>
+                        <tr>
+                            <th class="text-center" style="width:100px;">订单号</th>
+                            <th style="min-width:160px;">印件名称</th>
+                            <th>工序名称</th>
+                            <th class="text-center" style="width:56px;">单位</th>
+                            <th class="text-center" style="width:72px;">工作量</th>
+                            <th class="text-center" style="width:72px;">本次数量</th>
+                            <th class="text-center" style="width:72px;">最高限价</th>
+                            <th class="text-center" style="width:80px;">订法</th>
+                        </tr>
+                        </thead>
+                        <tbody id="review-merge-tbody"></tbody>
+                    </table>
                 </div>
             </div>
             <div class="review-deadline-row">
@@ -500,20 +523,22 @@
             </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 class="review-selected-top">
+                <div class="review-selected-head">
+                    已选单位(<span class="review-selected-count" id="review-selected-count">0</span>)
+                </div>
+                <p class="review-selected-tip procuremen-review-submit-tip" role="note">
+                    <i class="fa fa-exclamation-triangle"></i>
+                    <strong>重要提示:</strong>提交将向<strong>已勾选</strong>供应商发送<strong>邮件</strong>与<strong>手机短信</strong>;该操作<strong>不可撤回</strong>,请核对后再确认。
+                </p>
             </div>
-            <p class="review-selected-empty" id="review-selected-empty">暂未选择;在下方表格中勾选后,公司名称会显示在此处。</p>
+            <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">
-            <div class="alert alert-warning procuremen-review-submit-tip" role="alert">
-                <i class="fa fa-exclamation-triangle"></i>
-                <strong>重要提示:</strong>提交将向<strong>已勾选</strong>单位发送<strong>邮件</strong>与<strong>手机短信</strong>通知;该操作<strong>不可撤回或更改</strong>,请仔细核对勾选结果后再点击「确认」。
-            </div>
             <div class="review-company-panel">
                 <aside class="review-category-sidebar" id="review-category-sidebar">
-                    <div class="review-cat-head">本次下发分组</div>
+                    <div class="review-cat-head">业务分组</div>
                     <div id="review-category-list"></div>
                 </aside>
                 <div class="review-company-main">
@@ -526,15 +551,15 @@
                                 <colgroup>
                                     <col style="width:6%"/>
                                     <col style="width:27%"/>
-                                    <col style="width:7%"/>
-                                    <col style="width:14%"/>
-                                    <col style="width: 19%"/>
+                                    <col style="width:9%"/>
+                                    <col style="width:16%"/>
+                                    <col style="width: 15%"/>
                                     <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>
@@ -553,7 +578,7 @@
 
     <div class="layer-footer clearfix">
         <div class="pull-right">
-            <button type="button" style="margin-right: 20px" id="btn-review-submit" class="btn procuremen-btn-slate btn-embossed"> 提交</button>
+            <button type="button" style="margin-right: 20px" id="btn-review-submit" class="btn procuremen-btn-slate btn-embossed">确认</button>
             <button type="reset"  style="margin-right: 20px" class="btn btn-default btn-embossed btn-close" onclick="Layer.closeAll();">{:__('Close')}</button>
         </div>
     </div>

+ 16 - 0
application/admin/view/procuremenarchive/index.html

@@ -0,0 +1,16 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <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>
+                <form class="form-inline" style="display:inline-block;margin-left:8px;" onsubmit="return false;">
+                    <input type="text" id="filter-ccydh" class="form-control input-sm" placeholder="订单号筛选" style="width:160px;">
+                    <button type="button" class="btn btn-default btn-sm" id="btn-filter-ccydh"><i class="fa fa-search"></i> 查询</button>
+                </form>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
+        </div>
+    </div>
+</div>

+ 14 - 0
application/admin/view/procuremenexport/index.html

@@ -0,0 +1,14 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <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" id="btn-export-month-outward" title="按月份导出外发明细(8 列固定表头,已完结且已选外协)"><i class="fa fa-download"></i> 月份下发明细导出</a>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
+            <p class="help-block" style="padding:8px 12px;margin:0;">在此选择月份导出外发明细 Excel(已完结且采购确认已选定供应商);下方列表记录每次导出的时间与统计。</p>
+        </div>
+    </div>
+</div>

+ 126 - 0
application/admin/view/procuremensms/edit.html

@@ -0,0 +1,126 @@
+<style>
+    body.is-dialog .content {
+        padding: 12px 14px 0;
+        height: 100%;
+        box-sizing: border-box;
+        overflow: hidden;
+    }
+    .procuremen-sms-edit-wrap {
+        display: flex;
+        flex-direction: column;
+        height: 100%;
+        min-height: 460px;
+        overflow: hidden;
+    }
+    .procuremen-sms-edit-layout {
+        display: flex;
+        align-items: stretch;
+        flex: 1 1 auto;
+        min-height: 0;
+        overflow: hidden;
+    }
+    .procuremen-sms-edit-left {
+        flex: 1 1 58%;
+        min-width: 0;
+        overflow-x: hidden;
+        overflow-y: auto;
+        padding-right: 14px;
+    }
+    .procuremen-sms-edit-right {
+        flex: 0 0 38%;
+        max-width: 360px;
+        min-width: 220px;
+        border-left: 1px solid #e8e8e8;
+        padding-left: 14px;
+        overflow-x: hidden;
+        overflow-y: auto;
+        background: #fafbfc;
+    }
+    .procuremen-sms-edit-right .var-guide-title {
+        margin: 0 0 10px;
+        font-weight: 600;
+        font-size: 14px;
+        color: #333;
+        line-height: 1.5;
+    }
+    .procuremen-sms-edit-right .var-guide-table {
+        margin: 0;
+        background: #fff;
+        font-size: 13px;
+        table-layout: fixed;
+        width: 100%;
+    }
+    .procuremen-sms-edit-right .var-guide-table code {
+        color: #c7254e;
+        cursor: pointer;
+        user-select: all;
+        word-break: break-all;
+    }
+    #edit-form {
+        width: 100%;
+        max-width: 100%;
+    }
+    #edit-form .sms-edit-field {
+        margin-bottom: 14px;
+    }
+    #edit-form .sms-edit-field > label {
+        display: block;
+        margin-bottom: 6px;
+        font-weight: normal;
+        color: #333;
+    }
+    #edit-form .sms-edit-field textarea.form-control {
+        min-height: 160px;
+        resize: vertical;
+    }
+    #edit-form .form-group.layer-footer {
+        display: none;
+    }
+</style>
+
+<form id="edit-form" class="procuremen-sms-edit-wrap" role="form" data-toggle="validator" method="POST" action="">
+    <input type="hidden" name="row[scene]" value="{$row.scene|htmlentities}">
+    <div class="procuremen-sms-edit-layout">
+        <div class="procuremen-sms-edit-left">
+            <div class="sms-edit-field">
+                <label>模版名称:</label>
+                <input class="form-control" name="row[title]" type="text" value="{$row.title|htmlentities}" data-rule="required">
+            </div>
+            <div class="sms-edit-field">
+                <label>{if $isEmailScene}邮箱正文{else /}短信正文{/if}:</label>
+                <textarea id="sms-template-content" class="form-control" name="row[content]" rows="8" data-rule="required">{$row.content|htmlentities}</textarea>
+            </div>
+            <div class="sms-edit-field">
+                <label>备注:</label>
+                <input class="form-control" name="row[remark]" type="text" value="{$row.remark|htmlentities}">
+            </div>
+        </div>
+        <aside class="procuremen-sms-edit-right">
+            <p class="var-guide-title">模版变量<br><span style="font-weight:400;font-size:12px;color:#666;">复制或点击左侧变量名插入到正文(四个场景说明相同,改 model 一处即可)</span></p>
+            <table class="table table-bordered table-condensed var-guide-table">
+                <thead>
+                <tr style="background:#f5f5f5;">
+                    <th style="width:42%;">变量</th>
+                    <th>说明</th>
+                </tr>
+                </thead>
+                <tbody>
+                {volist name="smsVarGuide" id="vg"}
+                <tr>
+                    <td><code class="sms-var-tag" data-tag="{$vg.tag|htmlentities}" title="点击插入">{$vg.tag|htmlentities}</code></td>
+                    <td>{$vg.label|htmlentities}</td>
+                </tr>
+                {/volist}
+                </tbody>
+            </table>
+        </aside>
+    </div>
+    <div class="form-group layer-footer">
+        <div class="row procuremen-sms-footer-row">
+            <div class="col-xs-12 procuremen-sms-footer-btns">
+                <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                <button type="button" class="btn btn-default btn-embossed btn-procuremen-sms-close">关闭</button>
+            </div>
+        </div>
+    </div>
+</form>

+ 62 - 0
application/admin/view/procuremensms/index.html

@@ -0,0 +1,62 @@
+<style>
+    /* bootstrap-table 表头在 .fixed-table-header 克隆表里,需单独居中 .th-inner */
+    .procuremen-sms-template-page .bootstrap-table thead th,
+    .procuremen-sms-template-page .fixed-table-header thead th,
+    .procuremen-sms-template-page .fixed-table-container thead th {
+        text-align: center !important;
+        vertical-align: middle !important;
+    }
+    .procuremen-sms-template-page .bootstrap-table thead th .th-inner,
+    .procuremen-sms-template-page .fixed-table-header thead th .th-inner,
+    .procuremen-sms-template-page .fixed-table-container thead th .th-inner {
+        text-align: center !important;
+        justify-content: center !important;
+        padding-left: 8px !important;
+        padding-right: 8px !important;
+    }
+    .procuremen-sms-template-table {
+        table-layout: fixed;
+        width: 100% !important;
+    }
+    .procuremen-sms-template-page .bootstrap-table tbody td,
+    .procuremen-sms-template-table tbody td {
+        text-align: center !important;
+        vertical-align: middle !important;
+    }
+    .procuremen-sms-template-table .sms-content-cell {
+        text-align: center;
+    }
+    .procuremen-sms-template-table td.col-sms-content,
+    .procuremen-sms-template-table th.col-sms-content {
+        width: 360px;
+        max-width: 360px;
+        white-space: normal !important;
+        word-break: break-word;
+    }
+    .procuremen-sms-template-table .sms-content-cell {
+        white-space: pre-wrap;
+        word-break: break-word;
+        line-height: 1.55;
+        max-width: 100%;
+    }
+    .procuremen-sms-template-table td[data-field="status"],
+    .procuremen-sms-template-table td[data-field="updatetime"] {
+        white-space: nowrap !important;
+    }
+</style>
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body procuremen-sms-template-page">
+        <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-edit btn-disabled disabled {:$auth->check('procuremensms/edit')?'':'hide'}" title="{:__('Edit')}" data-area='["960px","580px"]'><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover procuremen-sms-template-table"
+                   data-operate-edit="{:$auth->check('procuremensms/edit')}"
+                   width="100%">
+            </table>
+        </div>
+    </div>
+</div>

+ 35 - 0
application/admin/view/purchaseemail/edit.html

@@ -0,0 +1,35 @@
+<style>
+    body.is-dialog .purchase-email-edit-form {
+        padding: 16px 20px 8px;
+        min-width: 560px;
+    }
+    .purchase-email-edit-form .form-control {
+        font-size: 14px;
+        height: 38px;
+    }
+    .purchase-email-edit-form .form-group {
+        margin-bottom: 18px;
+    }
+</style>
+<form id="edit-form" class="form-horizontal purchase-email-edit-form" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-3">发件邮箱:</label>
+        <div class="col-xs-12 col-sm-9">
+            <input class="form-control" name="row[email_addr]" type="text" value="{$row.email_addr|htmlentities}" data-rule="required;email" placeholder="858357896@qq.com" autocomplete="off">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-3">邮箱授权码:</label>
+        <div class="col-xs-12 col-sm-9">
+            <input class="form-control" name="row[email_pass]" type="text" value="{$row.email_pass|htmlentities}" placeholder="QQ 邮箱 SMTP 授权码" autocomplete="off">
+            <p class="help-block" style="color: red;">授权码获取:QQ邮箱设置->账号与安全->安全设置->生成授权码</p>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-3"></label>
+        <div class="col-xs-12 col-sm-9">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">保存</button>
+            <button type="button" class="btn btn-default btn-embossed btn-purchase-email-close" style="margin-left:12px;">关闭</button>
+        </div>
+    </div>
+</form>

+ 17 - 0
application/admin/view/purchaseemail/index.html

@@ -0,0 +1,17 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body purchase-email-config-page">
+        <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-edit btn-disabled disabled {:$auth->check('purchaseemail/edit')?'':'hide'}" title="编辑" data-area='["720px","420px"]'><i class="fa fa-pencil"></i> 编辑</a>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                   data-operate-edit="{:$auth->check('purchaseemail/edit')}"
+                   data-operate-del="false"
+                   width="100%">
+            </table>
+        </div>
+    </div>
+</div>

+ 3 - 2
application/config.php

@@ -352,8 +352,7 @@ return [
         'driver'          => 'smtp', // 邮件驱动, 支持 smtp|sendmail|mail 三种驱动
         'host'            => 'smtp.qq.com', // SMTP服务器地址
         'port'            => 465, // SMTP服务器端口号,一般为25
-        'addr'            => '858357896@qq.com', // 发件邮箱地址
-        'pass'            => 'qjksdurvlahvbahj', // 发件邮箱密码
+        // 发件邮箱 addr、授权码 pass 仅从数据库 purchase_email 读取,请在后台「邮箱配置」维护
         'name'            => 'lhr', // 发件邮箱名称
         'content_type'    => 'text/html', // 默认文本内容 text/html|text/plain
         'charset'         => 'utf-8', // 默认字符集
@@ -366,6 +365,8 @@ return [
         'log_path'        => '', // 日志路径, 可选, 不配置日志驱动时启用默认日志驱动, 默认路径是 /path/to/tp-mailer/log, 要保证该目录有可写权限, 最好配置自己的日志路径
         'embed'           => 'embed:', // 邮件中嵌入图片元数据标记
     ],
+    // 外发通知演练:true=不发真实短信/邮件,仅写日志并在 pickSubmit 返回 notify_preview
+    'procuremen_notify_dry_run' => Env::get('procuremen.notify_dry_run', false),
     // 正式环境 OSS(采购确认 PDF 等);public_base 可选自定义 CDN 域名
     'oss' => [
         'enabled'         => true,

+ 24 - 0
application/extra/procuremen_drop_optional_columns.sql

@@ -0,0 +1,24 @@
+-- purchase_order 精简:删除代码已不再使用的列(请先备份,逐条执行)
+-- 保留建议见 application/extra/procuremen_workflow.sql 说明
+
+-- 必删(代码已改为用表字段 + purchase_order_detail)
+ALTER TABLE `purchase_order` DROP COLUMN `pick_companies_json`;
+ALTER TABLE `purchase_order` DROP COLUMN `row_json`;
+
+-- 可选删(联系人/电话从 purchase_order_detail 或 customer 表查;操作人从 purchase_order_oper_log 查)
+ALTER TABLE `purchase_order` DROP COLUMN `pick_contact_name`;
+ALTER TABLE `purchase_order` DROP COLUMN `pick_email`;
+ALTER TABLE `purchase_order` DROP COLUMN `pick_phone`;
+ALTER TABLE `purchase_order` DROP COLUMN `pick_admin_id`;
+ALTER TABLE `purchase_order` DROP COLUMN `pick_admin_name`;
+ALTER TABLE `purchase_order` DROP COLUMN `audit_admin_id`;
+ALTER TABLE `purchase_order` DROP COLUMN `audit_admin_name`;
+ALTER TABLE `purchase_order` DROP COLUMN `audit_time`;
+
+-- 建议保留的 workflow 字段:
+-- wflow_status      流程:0待下发 1待确认供应商 2待采购确认(与 status 完结 不同,不能互相替代)
+-- pick_company_name 审核选定的合作供应商
+-- pick_time         下发时间(用于判断是否已从「外发下发」列表隐藏,也可用 detail 表判断)
+-- status            您原有字段:完结等
+-- sys_rq            您原有字段:报价截止时间
+-- CCYDH/CYJMC/CGYMC/本次数量/最高限价 等 列表与弹窗展示

+ 56 - 0
application/extra/procuremen_tables.sql

@@ -0,0 +1,56 @@
+-- 外发采购审核扩展表(执行一次)
+
+CREATE TABLE IF NOT EXISTS `purchase_sms_template` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `scene` varchar(32) NOT NULL COMMENT 'review_email|review_sms|confirm_ok|confirm_fail',
+  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '模版名称',
+  `content` text NOT NULL COMMENT '模版正文(邮箱可含链接变量,短信勿含链接)',
+  `remark` varchar(255) DEFAULT '' COMMENT '备注',
+  `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1正常0禁用',
+  `createtime` datetime DEFAULT NULL,
+  `updatetime` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_scene` (`scene`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外发通知模版';
+
+CREATE TABLE IF NOT EXISTS `purchase_month_export_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `ym` char(7) NOT NULL COMMENT 'YYYY-MM',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT 0,
+  `admin_name` varchar(64) NOT NULL DEFAULT '',
+  `row_count` int(10) unsigned NOT NULL DEFAULT 0,
+  `total_amount` decimal(14,2) NOT NULL DEFAULT 0.00,
+  `createtime` int(10) unsigned DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_ym` (`ym`),
+  KEY `idx_ct` (`createtime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月度外发明细导出记录';
+
+INSERT IGNORE INTO `purchase_sms_template` (`scene`, `title`, `content`, `remark`, `status`, `createtime`, `updatetime`) VALUES
+('review_email', '外发下发-邮箱', '您好,{company_name}:
+
+您有新的外发加工订单待处理:
+订单号:{ccydh}
+印件名称:{cyjmc}
+{process_lines}
+请在 {deadline} 前登陆我司平台处理
+
+平台链接:
+{platform_links_html}
+
+请及时查收并处理,谢谢!', '邮箱可用链接 HTML', 1, NOW(), NOW()),
+('review_sms', '外发下发-短信', '您好,{company_name}:您有新的外发加工订单待处理。订单号:{ccydh},印件名称:{cyjmc}。{process_lines}请在 {deadline} 前登录我司平台处理。谢谢!', '短信勿含链接', 1, NOW(), NOW()),
+('confirm_ok', '采购确认-通过', '您好,{company_name}:
+
+您参与的外发加工订单采购确认结果:已通过。
+订单号:{ccydh}
+印件名称:{cyjmc}
+
+', '', 1, NOW(), NOW()),
+('confirm_fail', '采购确认-未通过', '您好,{company_name}:
+
+您参与的外发加工订单采购确认结果:未通过。
+订单号:{ccydh}
+印件名称:{cyjmc}
+
+', '', 1, NOW(), NOW());

+ 17 - 0
application/extra/procuremen_workflow.sql

@@ -0,0 +1,17 @@
+-- 外发三步流程(仅保留必要列;若列已存在会报错,可跳过)
+-- ① 外发下发 pick:选工序+多家供应商,发短信邮件通知报价 → wflow_status=1
+-- ② 确认供应商 audit:从已通知的报价中选一家 → wflow_status=2,不发短信
+-- ③ 采购确认 confirm:定标,发送通过/未通过短信 → status 完结等
+-- purchase_order.status(与 wflow_status 不同):
+--   空/null = 未完结:仅保存本次数量/最高限价,或尚未写入状态
+--   0       = 外发流程进行中(已下发/待确认供应商/待采购确认,未点「完结」)
+--   1       = 已完结(外发下发点「完结」,或采购确认提交成功)
+-- wflow_status:0待下发 1待确认供应商 2待采购确认(三步菜单用)
+-- wflow_status:流程阶段,与 status 分工不同,不能只用 status 代替
+
+ALTER TABLE `purchase_order`
+  ADD COLUMN `wflow_status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0待下发1待确认供应商2待采购确认' AFTER `status`,
+  ADD COLUMN `pick_company_name` varchar(200) DEFAULT '' COMMENT '第二步确认供应商选定' AFTER `wflow_status`,
+  ADD COLUMN `pick_time` datetime DEFAULT NULL COMMENT '下发时间' AFTER `pick_company_name`;
+
+-- 删除无用列见 procuremen_drop_optional_columns.sql

+ 28 - 0
application/extra/procuremen_workflow_patch.sql

@@ -0,0 +1,28 @@
+-- 外发流程字段补丁(可重复执行思路:逐条执行,若报 Duplicate column 说明该列已有,跳过即可)
+-- 先查看已有列:SHOW COLUMNS FROM `purchase_order` LIKE 'wflow%';
+--              SHOW COLUMNS FROM `purchase_order` LIKE 'pick%';
+--              SHOW COLUMNS FROM `purchase_order` LIKE 'audit%';
+
+-- 以下每条单独选中执行;1060 Duplicate column = 已存在,换下一行
+
+-- ALTER TABLE `purchase_order` ADD COLUMN `wflow_status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0待初选1待审核2已下发' AFTER `status`;
+-- ALTER TABLE `purchase_order` ADD COLUMN `pick_company_name` varchar(200) DEFAULT '' COMMENT '初选供应商' AFTER `wflow_status`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_contact_name` varchar(64) DEFAULT '' COMMENT '初选联系人' AFTER `pick_company_name`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_email` varchar(128) DEFAULT '' AFTER `pick_contact_name`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_phone` varchar(32) DEFAULT '' AFTER `pick_email`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_admin_id` int(10) unsigned NOT NULL DEFAULT 0 AFTER `pick_phone`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_admin_name` varchar(64) DEFAULT '' AFTER `pick_admin_id`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_time` datetime DEFAULT NULL AFTER `pick_admin_name`;
+ALTER TABLE `purchase_order` ADD COLUMN `audit_admin_id` int(10) unsigned NOT NULL DEFAULT 0 AFTER `pick_time`;
+ALTER TABLE `purchase_order` ADD COLUMN `audit_admin_name` varchar(64) DEFAULT '' AFTER `audit_admin_id`;
+ALTER TABLE `purchase_order` ADD COLUMN `audit_time` datetime DEFAULT NULL AFTER `audit_admin_name`;
+ALTER TABLE `purchase_order` ADD COLUMN `pick_companies_json` mediumtext COMMENT '初选候选供应商JSON' AFTER `pick_time`;
+
+-- 已存在该列但类型过小(报 Data too long for column pick_companies_json)时执行:
+ALTER TABLE `purchase_order` MODIFY COLUMN `pick_companies_json` MEDIUMTEXT COMMENT '初选候选供应商JSON';
+
+-- 历史数据迁移(只需执行一次;若已跑过可跳过)
+-- UPDATE `purchase_order` po SET po.`wflow_status` = 2
+-- WHERE po.`status` = 0 AND po.`wflow_status` = 0
+--   AND EXISTS (SELECT 1 FROM `purchase_order_detail` d WHERE d.`scydgy_id` = po.`scydgy_id` LIMIT 1);
+-- UPDATE `purchase_order` SET `wflow_status` = 2 WHERE `status` = 1 AND `wflow_status` = 0;

+ 10 - 0
application/extra/purchase_email.sql

@@ -0,0 +1,10 @@
+-- 外发发件邮箱:仅存储发件邮箱与授权码,其余 SMTP 参数见 config.php Mailer
+
+CREATE TABLE IF NOT EXISTS `purchase_email` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `email_addr` varchar(128) NOT NULL DEFAULT '' COMMENT '发件邮箱',
+  `email_pass` varchar(255) NOT NULL DEFAULT '' COMMENT 'SMTP授权码',
+  `createtime` int(10) unsigned DEFAULT NULL,
+  `updatetime` int(10) unsigned DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外发发件邮箱(仅账号)';

+ 16 - 0
application/extra/purchase_sms_template_split.sql

@@ -0,0 +1,16 @@
+-- 通知模版:邮箱/短信分开(执行一次)
+-- status:1=正常 0=禁用
+
+UPDATE `purchase_sms_template` SET `status` = '1' WHERE `status` IN ('normal', '1') OR `status` = 1;
+UPDATE `purchase_sms_template` SET `status` = '0' WHERE `status` IN ('hidden', '0') OR `status` = 0;
+
+-- 按您现有数据:id=1 邮箱、id=2 短信(scene 为空);旧 review 改为邮箱
+UPDATE `purchase_sms_template` SET `scene` = 'review_email' WHERE `scene` = 'review' OR `id` = 1;
+UPDATE `purchase_sms_template` SET `scene` = 'review_sms' WHERE `scene` = '' OR `scene` IS NULL OR `id` = 2;
+
+-- 确保四条场景都存在(无则插入默认)
+INSERT IGNORE INTO `purchase_sms_template` (`scene`, `title`, `content`, `remark`, `status`, `createtime`, `updatetime`) VALUES
+('review_email', '外发下发-邮箱', '您好,{company_name}:您有新的外发加工订单待处理。订单号:{ccydh},印件名称:{cyjmc}。{process_lines}请在 {deadline} 前登陆我司平台处理。{platform_links_html}', '邮箱可含链接', '1', NOW(), NOW()),
+('review_sms', '外发下发-短信', '您好,{company_name}:您有新的外发加工订单待处理。订单号:{ccydh},印件名称:{cyjmc}。{process_lines}请在 {deadline} 前登录我司平台处理。谢谢!', '短信勿含链接', '1', NOW(), NOW()),
+('confirm_ok', '采购确认-通过', '您好,{company_name}:您参与的外发加工订单采购确认结果:已通过。订单号:{ccydh},印件名称:{cyjmc}。', '', '1', NOW(), NOW()),
+('confirm_fail', '采购确认-未通过', '您好,{company_name}:您参与的外发加工订单采购确认结果:未通过。订单号:{ccydh},印件名称:{cyjmc}。', '', '1', NOW(), NOW());

+ 57 - 0
application/index/controller/Index.php

@@ -936,6 +936,7 @@ class Index extends Frontend
             $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'] = '';
+            $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row);
         }
         unset($row);
 
@@ -945,6 +946,25 @@ class Index extends Frontend
         ];
     }
 
+    /**
+     * 列表展示用「本次数量」:主表本次数量为空时回退显示 NGZL(工作量)
+     *
+     * @param array<string, mixed> $row
+     */
+    protected function mprocResolveDisplayThisQuantity(array $row): string
+    {
+        $qty = trim((string)($row['This_quantity'] ?? $row['this_quantity'] ?? ''));
+        if ($qty !== '') {
+            return $qty;
+        }
+        $gzl = $row['NGZL'] ?? $row['ngzl'] ?? '';
+        if ($gzl === null || $gzl === '') {
+            return '';
+        }
+
+        return is_scalar($gzl) ? trim((string)$gzl) : '';
+    }
+
     /**
      * 外发明细首页(需登录)
      * GET:main_tab=orders|me,orders 时 tab=draft|submitted|done 对应 status_name:未提交|已提交|已完成;q 搜索词
@@ -1289,6 +1309,39 @@ class Index extends Frontend
         return $uPhone !== '' && $rPhone !== '' && strcasecmp($rPhone, $uPhone) === 0;
     }
 
+    /**
+     * 明细行对应最高限价(来自 purchase_order;无或无效则返回 null,不校验)
+     *
+     * @param array<string, mixed> $detailRow
+     */
+    protected function mprocResolveCeilingPriceForDetailRow(array $detailRow): ?float
+    {
+        $sid = (int)($detailRow['scydgy_id'] ?? $detailRow['SCYDGY_ID'] ?? 0);
+        $raw = '';
+        if ($sid !== 0) {
+            try {
+                $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+            } catch (\Throwable $e) {
+                $po = null;
+            }
+            if (is_array($po)) {
+                $raw = trim((string)($po['ceilingPrice'] ?? $po['ceiling_price'] ?? ''));
+            }
+        }
+        if ($raw === '' || !preg_match('/^-?\d+(\.\d{1,5})?$/', $raw)) {
+            return null;
+        }
+
+        return (float)$raw;
+    }
+
+    protected function mprocFormatCeilingPriceDisplay(float $n): string
+    {
+        $s = rtrim(rtrim(sprintf('%.5F', $n), '0'), '.');
+
+        return $s === '' ? '0' : $s;
+    }
+
     /**
      * 保存单条外发明细的金额、交期(POST:id、amount、delivery)
      */
@@ -1334,6 +1387,10 @@ class Index extends Frontend
             if (!preg_match('/^-?\d+(\.\d{1,5})?$/', $amountRaw)) {
                 $this->error('金额格式不正确,最多五位小数');
             }
+            $ceilingLimit = $this->mprocResolveCeilingPriceForDetailRow($row);
+            if ($ceilingLimit !== null && (float)$amountRaw > $ceilingLimit) {
+                $this->error('金额不能超过最高限价 ' . $this->mprocFormatCeilingPriceDisplay($ceilingLimit));
+            }
             $data['amount'] = $amountRaw;
         }
         if ($deliveryRaw === '') {

+ 138 - 15
application/index/view/index/index.html

@@ -175,6 +175,9 @@
         .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; }
+        .modal-ceil-hint { margin: 6px 0 0; font-size: 12px; line-height: 1.45; color: #888; }
+        .modal-ceil-hint.is-error { color: #c05621; font-weight: 500; }
+        .modal-field input.amount-over-ceil { border-color: #c05621; }
         /* 交货日期:仅一层边框,与上方「金额」一致 */
         .date-field-shell {
             position: relative;
@@ -252,11 +255,13 @@
          data-ccydh="{$r.CCYDH|default=''|htmlentities}"
          data-amount="{$r.amount|default=''|htmlentities}"
          data-delivery="{$r.delivery|default=''|htmlentities}"
-         data-title="{$r.CYJMC|default=''|htmlentities}">
+         data-title="{$r.CYJMC|default=''|htmlentities}"
+         data-ceiling-price="{$r.ceilingPrice|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.CGYMC|default=''|htmlentities}</div>
+        <div class="kv"><span>本次数量</span>{$r.mproc_this_quantity_display|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>
@@ -318,6 +323,7 @@
         <div class="modal-field">
             <label for="inp-amount">金额</label>
             <input type="text" id="inp-amount" inputmode="decimal" autocomplete="off" maxlength="24">
+            <p class="modal-ceil-hint" id="edit-ceil-hint" style="display:none;"></p>
         </div>
         <div class="modal-field">
             <label class="modal-field-label" for="inp-delivery" id="lbl-delivery">交货日期</label>
@@ -466,6 +472,50 @@
         return mprocEsc(pick(r, ['delivery_display', 'delivery']));
     }
 
+    /** 解析最高限价;有有效限价时校验金额不得超过 */
+    function mprocParseCeilingPrice(v) {
+        var s = String(v == null ? '' : v).trim();
+        if (s === '' || !/^-?\d+(\.\d+)?$/.test(s)) {
+            return null;
+        }
+        var n = parseFloat(s);
+        return isNaN(n) ? null : n;
+    }
+
+    function mprocValidateAmountAgainstCeiling(amtStr, ceilingStr) {
+        var ceil = mprocParseCeilingPrice(ceilingStr);
+        if (ceil === null || amtStr === '') {
+            return '';
+        }
+        if (!/^-?\d+(\.\d{1,5})?$/.test(amtStr)) {
+            return '';
+        }
+        var amt = parseFloat(amtStr);
+        if (isNaN(amt)) {
+            return '';
+        }
+        if (amt > ceil) {
+            return '金额不能超过最高限价 ' + String(ceilingStr).trim();
+        }
+        return '';
+    }
+
+    function mprocDisplayThisQty(r) {
+        if (!r || typeof r !== 'object') {
+            return '';
+        }
+        if (Object.prototype.hasOwnProperty.call(r, 'mproc_this_quantity_display')
+            && r.mproc_this_quantity_display != null
+            && String(r.mproc_this_quantity_display).trim() !== '') {
+            return String(r.mproc_this_quantity_display).trim();
+        }
+        var q = pick(r, ['This_quantity', 'this_quantity']);
+        if (q !== '') {
+            return q;
+        }
+        return pick(r, ['NGZL', 'ngzl']);
+    }
+
     function renderListHtml(rows, doneNoStatus) {
         if (!rows || !rows.length) {
             if (doneNoStatus) {
@@ -478,15 +528,16 @@
             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 thisQty = mprocDisplayThisQty(r);
             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) + '">'
+            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) + '" data-ceiling-price="' + mprocEscAttr(ceilP) + '">'
                 + '<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(pick(r, ['CGYMC', 'cgymc'])) + '</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>'
@@ -613,9 +664,63 @@
     var dateFieldShell = document.getElementById('date-field-shell');
     var btnSave = document.getElementById('edit-save');
     var titleEl = document.getElementById('edit-sheet-title');
+    var editCeilHint = document.getElementById('edit-ceil-hint');
     var currentId = 0; // 当前编辑的 purchase_order_detail 主键
+    var currentCeilingPrice = ''; // 最高限价(有值时金额不可超过)
+
+    function mprocShowCeilHint(msg, isError) {
+        if (!editCeilHint) {
+            return;
+        }
+        if (!msg) {
+            editCeilHint.style.display = 'none';
+            editCeilHint.textContent = '';
+            editCeilHint.classList.remove('is-error');
+            if (inpAmount) {
+                inpAmount.classList.remove('amount-over-ceil');
+            }
+            return;
+        }
+        editCeilHint.style.display = 'block';
+        editCeilHint.textContent = msg;
+        editCeilHint.classList.toggle('is-error', !!isError);
+        if (inpAmount) {
+            inpAmount.classList.toggle('amount-over-ceil', !!isError);
+        }
+    }
+
+    function mprocSanitizedAmountFromInput() {
+        if (!inpAmount) {
+            return '';
+        }
+        var amt = mprocSanitizeAmountValue(inpAmount.value).trim();
+        if (amt.length > 1 && amt.charAt(amt.length - 1) === '.') {
+            amt = amt.slice(0, -1);
+        }
+        if (amt === '-' || amt === '.' || amt === '-.') {
+            amt = '';
+        }
+        return amt;
+    }
+
+    function mprocRefreshAmountCeilHint() {
+        var amt = mprocSanitizedAmountFromInput();
+        var err = mprocValidateAmountAgainstCeiling(amt, currentCeilingPrice);
+        if (err) {
+            mprocShowCeilHint(err, true);
+            return;
+        }
+        var ceilN = mprocParseCeilingPrice(currentCeilingPrice);
+        if (ceilN !== null) {
+            mprocShowCeilHint('最高限价 ' + currentCeilingPrice + ',金额不可超过该限价', false);
+        } else {
+            mprocShowCeilHint('', false);
+        }
+    }
     if (inpAmount) {
         mprocBindAmountInput(inpAmount);
+        inpAmount.addEventListener('input', mprocRefreshAmountCeilHint);
+        inpAmount.addEventListener('blur', mprocRefreshAmountCeilHint);
     }
 
     // 把后端 datetime 转成 <input type="date"> 要的 yyyy-MM-dd
@@ -772,8 +877,10 @@
         var id = parseInt(card.getAttribute('data-id'), 10);
         if (!id) return;
         currentId = id;
+        currentCeilingPrice = String(card.getAttribute('data-ceiling-price') || '').trim();
         inpAmount.value = mprocSanitizeAmountValue((card.getAttribute('data-amount') || '').replace(/&quot;/g, '"'));
         inpDelivery.value = deliveryToDateVal(card.getAttribute('data-delivery') || '');
+        mprocRefreshAmountCeilHint();
         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(' ');
@@ -787,21 +894,24 @@
         mask.classList.remove('show');
         mask.setAttribute('aria-hidden', 'true');
         currentId = 0;
+        currentCeilingPrice = '';
+        mprocShowCeilHint('', false);
     }
 
-    // POST mprocSave
+    // POST mprocSave;校验失败返回 null,不弹 alert
     function postSave() {
-        var amt = '';
+        var amt = mprocSanitizedAmountFromInput();
         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 ceilErr = mprocValidateAmountAgainstCeiling(amt, currentCeilingPrice);
+        if (ceilErr) {
+            mprocShowCeilHint(ceilErr, true);
+            if (inpAmount) {
+                inpAmount.focus();
+            }
+            return null;
+        }
         var body = 'id=' + encodeURIComponent(String(currentId))
             + '&amount=' + encodeURIComponent(amt)
             + '&delivery=' + encodeURIComponent(inpDelivery ? inpDelivery.value.trim() : '');
@@ -837,7 +947,12 @@
     btnSave.addEventListener('click', function () {
         if (!currentId) return;
         btnSave.disabled = true;
-        postSave().then(function (ret) {
+        var savePromise = postSave();
+        if (!savePromise) {
+            btnSave.disabled = false;
+            return;
+        }
+        savePromise.then(function (ret) {
             btnSave.disabled = false;
             if (ret && (ret.code === 1 || ret.code === '1')) {
                 closeEdit();
@@ -845,7 +960,15 @@
                     fetchOrderList(currentListTab, getSearchQ());
                 }
             } else {
-                alert(ret && ret.msg ? ret.msg : '保存失败');
+                var msg = ret && ret.msg ? ret.msg : '保存失败';
+                if (msg.indexOf('最高限价') !== -1) {
+                    mprocShowCeilHint(msg, true);
+                    if (inpAmount) {
+                        inpAmount.focus();
+                    }
+                } else {
+                    alert(msg);
+                }
             }
         }).catch(function () {
             btnSave.disabled = false;

+ 66 - 0
public/assets/css/backend.css

@@ -1677,4 +1677,70 @@ table.table-nowrap thead > tr > th {
   background-color: #3f485f !important;
   border-color: #353c4c !important;
 }
+/* 短信模版编辑:footer 克隆到主窗口,须在此右对齐 */
+.layui-layer-fast .layui-layer-footer .procuremen-sms-footer-btns,
+.layui-layer-footer .procuremen-sms-footer-btns {
+  width: 100%;
+  text-align: right;
+  padding-right: 4px;
+  box-sizing: border-box;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-sms-footer-btns .btn + .btn,
+.layui-layer-footer .procuremen-sms-footer-btns .btn + .btn {
+  margin-left: 12px;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-sms-footer-row,
+.layui-layer-footer .procuremen-sms-footer-row {
+  margin-left: 0;
+  margin-right: 0;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-sms-footer-row .procuremen-sms-footer-btns,
+.layui-layer-footer .procuremen-sms-footer-row .procuremen-sms-footer-btns {
+  padding-left: 0;
+  padding-right: 0;
+}
+/* 外发下发-新增:footer 克隆到主窗口,须在此右对齐 */
+.layui-layer-fast .layui-layer-footer .procuremen-pick-add-footer-btns,
+.layui-layer-footer .procuremen-pick-add-footer-btns {
+  width: 100%;
+  text-align: right;
+  padding-right: 4px;
+  box-sizing: border-box;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-pick-add-footer-btns .btn + .btn,
+.layui-layer-footer .procuremen-pick-add-footer-btns .btn + .btn {
+  margin-left: 12px;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-pick-add-footer-row,
+.layui-layer-footer .procuremen-pick-add-footer-row {
+  margin-left: 0;
+  margin-right: 0;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-pick-add-footer-row .procuremen-pick-add-footer-btns,
+.layui-layer-footer .procuremen-pick-add-footer-row .procuremen-pick-add-footer-btns {
+  padding-left: 0;
+  padding-right: 0;
+}
+/* 确认供应商:footer 克隆到主窗口,须在此右对齐(子页 style 不作用于克隆节点) */
+.layui-layer-fast .layui-layer-footer .procuremen-audit-issue-footer-btns,
+.layui-layer-footer .procuremen-audit-issue-footer-btns {
+  width: 100%;
+  text-align: right !important;
+  padding-right: 20px;
+  box-sizing: border-box;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-audit-issue-footer-btns .btn + .btn,
+.layui-layer-footer .procuremen-audit-issue-footer-btns .btn + .btn {
+  margin-left: 0;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-audit-issue-footer-row,
+.layui-layer-footer .procuremen-audit-issue-footer-row {
+  margin-left: 0;
+  margin-right: 0;
+}
+.layui-layer-fast .layui-layer-footer .procuremen-audit-issue-footer-row .procuremen-audit-issue-footer-btns,
+.layui-layer-footer .procuremen-audit-issue-footer-row .procuremen-audit-issue-footer-btns {
+  padding-left: 0;
+  padding-right: 0;
+}
 /*# sourceMappingURL=backend.css.map */

+ 548 - 242
public/assets/js/backend/procuremen.js

@@ -96,11 +96,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             var allOnlyCols = ['ID'];
 
             Controller.currYm = ($layout.data('defaultYm') || '').toString();
-            Controller.wffTab = 'all';
+            Controller.wffTab = ($layout.data('procuremenStage') || 'pick').toString();
+            if (['pick', 'audit', 'confirm'].indexOf(Controller.wffTab) < 0) {
+                Controller.wffTab = 'pick';
+            }
+            var listAction = Controller.wffTab;
 
             Table.api.init({
                 extend: {
-                    index_url: 'procuremen/index' + location.search,
+                    index_url: 'procuremen/' + listAction + location.search,
                     multi_url: 'procuremen/multi',
                     import_url: 'procuremen/import',
                     table: 'scydgy',
@@ -109,50 +113,35 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             $('.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;
+            var stagePick = Controller.wffTab === 'pick';
+            var stageConfirm = Controller.wffTab === 'confirm';
+            var stageAudit = Controller.wffTab === 'audit';
+            issuedOnlyCols.forEach(function (field) {
+                try {
+                    table.bootstrapTable((stageAudit || stageConfirm) ? 'showColumn' : 'hideColumn', field);
+                } catch (ignore) {
                 }
-                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 返回才消失 */
+            });
+            pendingOnlyCols.forEach(function (field) {
                 try {
-                    table.bootstrapTable('removeAll');
+                    table.bootstrapTable(stageConfirm ? 'showColumn' : 'hideColumn', field);
+                } catch (ignore) {
+                }
+            });
+            pickedOnlyCols.forEach(function (field) {
+                try {
+                    table.bootstrapTable(stageAudit ? 'showColumn' : 'hideColumn', field);
                 } 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});
             });
+            try {
+                table.bootstrapTable(stagePick ? 'showColumn' : 'hideColumn', 'state');
+            } catch (ignore) {
+            }
+            $('#btn-procuremen-pick-review').toggle(stagePick);
+            $('#btn-procuremen-batch-finish').toggle(stagePick);
+            $('#btn-procuremen-pick-add').toggle(stagePick);
 
             $(document).off('click.procuremenYm', '.procuremen-ym-item').on('click.procuremenYm', '.procuremen-ym-item', function () {
                 var ym = $(this).data('ym');
@@ -169,47 +158,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 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;">'
-                    + '<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: ['370px', 'auto'],
-                    shadeClose: true,
-                    content: html,
-                    btn: ['导出 Excel'],
-                    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');
@@ -265,10 +213,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
 
             var indexInitWffTab = Controller.wffTab;
-            var indexShowall = indexInitWffTab === 'all';
-            var indexShowIssued = indexInitWffTab === 'pending' || indexInitWffTab === 'done' || indexInitWffTab === 'picked';
-            var indexShowPendingOnly = indexInitWffTab === 'pending';
-            var indexShowPickedOnly = indexInitWffTab === 'picked';
+            var indexShowall = indexInitWffTab === 'pick';
+            var indexShowIssued = indexInitWffTab === 'audit' || indexInitWffTab === 'confirm';
+            var indexShowPendingOnly = indexInitWffTab === 'confirm';
+            var indexShowPickedOnly = indexInitWffTab === 'audit';
             function procuremenEscAttr(s) {
                 return String(s == null ? '' : s)
                     .replace(/&/g, '&amp;')
@@ -276,6 +224,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     .replace(/'/g, '&#39;');
             }
             var indexTableColumns = [
+                {checkbox: true, field: 'state', visible: indexShowall, align: 'center', width: 42, class: 'procuremen-col-checkbox'},
                 // {field: 'ID', title: __('ID'), operate: 'LIKE', table: 'a', width: 100, align: 'center',
                 //     visible: indexShowall,
                 //     formatter: function (v) {
@@ -290,14 +239,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 // },
                 {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: 'CGYMC', title: __('工序名称'), operate: 'LIKE', table: 'a', width: 140, align: 'left',
+                    formatter: function (v) {
+                        return v != null && v !== '' ? String(v) : '';
+                    }
+                },
                 {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 tab = Controller.wffTab || 'pick';
+                        if (tab === 'pick') {
                             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;" '
@@ -310,13 +263,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'ceilingPrice', title: '最高限价', operate: false, table: 'a', width: 96, align: 'center',
                     visible: true,
                     formatter: function (value, row, index) {
-                        var tab = Controller.wffTab || 'all';
+                        var tab = Controller.wffTab || 'pick';
                         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') {
+                        if (tab === 'pick') {
                             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;" '
@@ -344,41 +297,43 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         return v != null && v !== '' ? String(v) : '0';
                     }
                 },
-                {field: 'picked_supplier_name', title: '已选供应商', operate: false, table: 'a', width: 200, align: 'left',
+                {field: 'picked_supplier_name', title: '报价供应商', operate: false, table: 'a', width: 260, align: 'left',
                     visible: indexShowPickedOnly,
                     formatter: function (v) {
-                        return v != null && v !== '' ? String(v) : '';
+                        if (v == null || v === '') {
+                            return '';
+                        }
+                        return String(v).split('\n').map(function (line) {
+                            return procuremenEscAttr(line);
+                        }).join('<br>');
                     }
                 },
                 {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: 'cywyxm', title: __('业务员'), operate: 'LIKE', table: 'b', width: 80, 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: 170,align: 'center',fixed: 'right',
+                {field: 'operate',title: '操作',width: 200,align: 'center',fixed: 'right',
+                    visible: indexInitWffTab !== 'pick',
                     table: table,
                     formatter: function (value, row, index) {
-                        var tab = Controller.wffTab || 'all';
+                        var tab = Controller.wffTab || 'pick';
                         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 areaAudit = ' 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>');
+                        if (tab === 'pick') {
+                            return '';
+                        } else if (tab === 'audit') {
+                            parts.push('<a class="btn btn-xs btn-primary procuremen-op-open procuremen-btn-audit-issue"' + areaAudit + ' href="procuremen/auditissue" data-row-index="' + index + '" title="确认供应商"><i class="fa fa-check"></i> 确认供应商</a>');
+                            parts.push('<a class="btn btn-xs btn-info 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 === 'confirm') {
+                            parts.push('<a class="btn btn-xs btn-success procuremen-op-open procuremen-btn-outward"' + area + ' href="procuremen/outward_detail?wff_tab=confirm" data-row-index="' + index + '" title="采购确认"><i class="fa fa-shopping-cart"></i> 采购确认</a>');
+                            parts.push('<a class="btn btn-xs btn-info 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>';
                     },
@@ -392,13 +347,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 pk: 'ID',
-                sortName: 'a.ID',
+                sortName: indexInitWffTab === 'pick' ? 'b.dputrecord' : 'a.pick_time',
+                sortOrder: 'desc',
                 width: 'auto',
                 height: tableHeight,
                 fixedColumns: true,
-                fixedRightNumber: 1,
+                fixedRightNumber: indexInitWffTab === 'pick' ? 0 : 1,
                 clickToSelect: false,
                 dblClickToEdit: false,
+                dragCheckboxMultiselect: false,
                 commonSearch: false,
                 showToggle: false,
                 showExport: false,
@@ -414,6 +371,198 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             Table.api.bindevent(table);
 
+            function procuremenEscHtml(s) {
+                return String(s == null ? '' : s)
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/"/g, '&quot;');
+            }
+            function procuremenExpandOrderRows(row) {
+                if (!row) {
+                    return [];
+                }
+                if ($.isArray(row._order_merge_rows) && row._order_merge_rows.length) {
+                    return row._order_merge_rows;
+                }
+                return [row];
+            }
+            function procuremenRowsWithPoInputs(selRows) {
+                var $btPo = table.closest('.bootstrap-table');
+                var dataRows = table.bootstrapTable('getData');
+                var out = [];
+                $.each(selRows, function (i, r) {
+                    if (!r) {
+                        return;
+                    }
+                    var rowIdx = -1;
+                    $.each(dataRows, function (di, dr) {
+                        var pk = table.bootstrapTable('getOptions').pk || 'ID';
+                        var a = dr && (dr[pk] != null ? dr[pk] : dr.id);
+                        var b = r && (r[pk] != null ? r[pk] : r.id);
+                        if (a != null && b != null && String(a) === String(b)) {
+                            rowIdx = di;
+                            return false;
+                        }
+                    });
+                    var rowCopy = $.extend({}, r);
+                    if (rowIdx >= 0 && $btPo.length) {
+                        var $qty = $btPo.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"]');
+                        var $price = $btPo.find('.procuremen-po-price[data-row-index="' + rowIdx + '"]');
+                        if ($qty.length) {
+                            rowCopy.This_quantity = String($qty.val()).trim();
+                        }
+                        if ($price.length) {
+                            rowCopy.ceilingPrice = String($price.val()).trim();
+                        }
+                    }
+                    out.push(rowCopy);
+                });
+                return out;
+            }
+            function procuremenOpenPickReview(selRows) {
+                if (!selRows || !selRows.length) {
+                    Toastr.warning('请先勾选至少 1 条工序');
+                    return;
+                }
+                var ccydh0 = String(selRows[0].CCYDH || '').trim();
+                if (!ccydh0) {
+                    Toastr.warning('所选行缺少订单号,无法下发');
+                    return;
+                }
+                if (selRows.length > 1) {
+                    var j;
+                    for (j = 1; j < selRows.length; j++) {
+                        if (String(selRows[j].CCYDH || '').trim() !== ccydh0) {
+                            Toastr.warning('合并下发须为同一订单号(当前混有:' + ccydh0 + ' 与其它订单)');
+                            return;
+                        }
+                    }
+                }
+                var pk0 = table.bootstrapTable('getOptions').pk || 'ID';
+                var ids = [];
+                $.each(selRows, function (idx, r) {
+                    var pkVal = r && (r[pk0] != null ? r[pk0] : r.id);
+                    if (pkVal != null && String(pkVal) !== '' && String(pkVal) !== '0') {
+                        ids.push(String(pkVal));
+                    }
+                });
+                if (!ids.length) {
+                    Toastr.warning('所选行无效,无法下发');
+                    return;
+                }
+                try {
+                    sessionStorage.setItem('procuremen_merge_rows', JSON.stringify(selRows));
+                } catch (ignoreStore) {
+                }
+                var titlePick = '外发下发';
+                var revUrl = Fast.api.fixurl('procuremen/pickreview?ids=' + encodeURIComponent(ids[0]) + '&merge=1');
+                var winPick = window;
+                if (!winPick.Backend || !winPick.Backend.api) {
+                    Toastr.error('Backend 未就绪,请刷新页面');
+                    return;
+                }
+                winPick.Backend.api.open(revUrl, titlePick, {area: ['76%', '100%']});
+            }
+            $(document).off('click.procuremenPickReview', '#btn-procuremen-pick-review').on('click.procuremenPickReview', '#btn-procuremen-pick-review', function (e) {
+                e.preventDefault();
+                if (Controller.wffTab !== 'pick') {
+                    Toastr.warning('下发仅在外发下发列表可用');
+                    return;
+                }
+                var selPick = procuremenRowsWithPoInputs(table.bootstrapTable('getSelections') || []);
+                procuremenOpenPickReview(selPick);
+            });
+            $(document).off('click.procuremenBatchFinish', '#btn-procuremen-batch-finish').on('click.procuremenBatchFinish', '#btn-procuremen-batch-finish', function (e) {
+                e.preventDefault();
+                if (Controller.wffTab !== 'pick') {
+                    Toastr.warning('完结仅在外发下发列表可用');
+                    return;
+                }
+                var selFin = procuremenRowsWithPoInputs(table.bootstrapTable('getSelections') || []);
+                if (!selFin.length) {
+                    Toastr.warning('请先勾选至少 1 条工序');
+                    return;
+                }
+                var rowsHtml = '<table class="table table-bordered table-condensed" style="margin:8px 0 0;font-size:12px;">'
+                    + '<thead><tr><th>订单号</th><th>工序名称</th><th>印件名称</th></tr></thead><tbody>';
+                $.each(selFin, function (fi, rf) {
+                    rowsHtml += '<tr><td>' + procuremenEscHtml(String(rf.CCYDH || '').trim() || '—') + '</td>'
+                        + '<td>' + procuremenEscHtml(String(rf.CGYMC || '').trim() || '—') + '</td>'
+                        + '<td>' + procuremenEscHtml(String(rf.CYJMC || '').trim() || '—') + '</td></tr>';
+                });
+                rowsHtml += '</tbody></table>';
+                var finishBatchMsg = '<div style="text-align:left;line-height:1.65;font-size:13px;">'
+                    + '<p style="margin:0 0 8px 0;">即将对以下 <strong>' + selFin.length + '</strong> 条工序标记为<strong>已完结</strong>:</p>'
+                    + rowsHtml
+                    + '<p style="margin:10px 0 0;color:#a94442;"><strong>提示:</strong>点击「确定」后不可撤回或更改。</p></div>';
+                Layer.confirm(finishBatchMsg, {
+                    title: '完结确认',
+                    area: ['560px', 'auto'],
+                    icon: 3,
+                    btn: ['确定', '取消'],
+                    skin: 'layui-layer-procuremen-finish'
+                }, function (idxFin) {
+                    Layer.close(idxFin);
+                    var tokenFin = $('input[name=\'__token__\']').val() || '';
+                    var pending = selFin.length;
+                    var failed = 0;
+                    var nextFin = function (pos) {
+                        if (pos >= selFin.length) {
+                            table.bootstrapTable('refresh');
+                            if (failed) {
+                                Toastr.warning('部分工序完结失败(' + failed + '/' + selFin.length + ')');
+                            } else {
+                                Toastr.success('已完结 ' + pending + ' 条工序');
+                            }
+                            return;
+                        }
+                        Fast.api.ajax({
+                            url: 'procuremen/completeDirectly',
+                            type: 'POST',
+                            data: {
+                                row_json: JSON.stringify(selFin[pos]),
+                                finish: '1',
+                                __token__: tokenFin
+                            }
+                        }, function () {
+                            nextFin(pos + 1);
+                            return false;
+                        }, function () {
+                            failed++;
+                            nextFin(pos + 1);
+                            return false;
+                        });
+                    };
+                    nextFin(0);
+                });
+            });
+
+            $(document).off('click.procuremenPickAdd', '#btn-procuremen-pick-add').on('click.procuremenPickAdd', '#btn-procuremen-pick-add', function (e) {
+                e.preventDefault();
+                if (Controller.wffTab !== 'pick') {
+                    Toastr.warning('新增仅在外发下发列表可用');
+                    return;
+                }
+                var winAdd = window;
+                if (!winAdd.Backend || !winAdd.Backend.api) {
+                    Toastr.error('Backend 未就绪,请刷新页面');
+                    return;
+                }
+                winAdd.Backend.api.open(
+                    Fast.api.fixurl('procuremen/pickadd'),
+                    '新增',
+                    {
+                        area: ['640px', '480px'],
+                        callback: function () {
+                            if (table && table.length) {
+                                table.bootstrapTable('refresh');
+                            }
+                        }
+                    }
+                );
+            });
+
             (function () {
                 var $wrap = table.closest('.bootstrap-table');
                 function procuremenSetFixedRightStaleLoading(hide) {
@@ -439,7 +588,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             }
             if ($bt.length) {
                 $bt.off('blur.procuremenPoSave', '.procuremen-po-field').on('blur.procuremenPoSave', '.procuremen-po-field', function () {
-                    if (Controller.wffTab !== 'all') {
+                    if (Controller.wffTab !== 'pick') {
                         return;
                     }
                     var $inp = $(this);
@@ -464,6 +613,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         if (origP === '' && baseRow.ceiling_price != null && baseRow.ceiling_price !== '') {
                             origP = procuremenNormPoCell(baseRow.ceiling_price);
                         }
+                        /* 未填写数量与限价时不写 purchase_order */
+                        if (q === '' && p === '') {
+                            return;
+                        }
                         /* 与当前行数据一致则不调接口、不弹*/
                         if (q === origQ && p === origP) {
                             return;
@@ -500,70 +653,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     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()});
-                        }
-                        var escHtmlFinish = function (s) {
-                            return String(s == null ? '' : s)
-                                .replace(/&/g, '&amp;')
-                                .replace(/</g, '&lt;')
-                                .replace(/>/g, '&gt;')
-                                .replace(/"/g, '&quot;');
-                        };
-                        var ccydhF = String(rowF.CCYDH != null ? rowF.CCYDH : '').trim();
-                        var cyjmcF = String(rowF.CYJMC != null ? rowF.CYJMC : '').trim();
-                        var finishMsg = '<div style="text-align:left;line-height:1.65;font-size:13px;padding:2px 0;">'
-                            + '<p style="margin:0 0 10px 0;">即将对以下<strong>订单</strong>标记为<strong>已完结</strong>:</p>'
-                            + '<div style="margin-bottom:12px;padding:8px 10px;background:#f9f9f9;border:1px solid #e5e5e5;border-radius:3px;">'
-                            + '<div><span style="color:#888;">订单号:</span><strong>' + (ccydhF !== '' ? escHtmlFinish(ccydhF) : '—') + '</strong></div>'
-                            + '<div style="margin-top:6px;"><span style="color:#888;">印件名称:</span>' + (cyjmcF !== '' ? escHtmlFinish(cyjmcF) : '—') + '</div>'
-                            + '</div>'
-                            + '<p style="margin:0;color:#a94442;"><strong>提示:</strong>请确认核对后、。<strong>点击「确定」后不可撤回或更改。</strong></p>'
-                            + '</div>';
-                        Layer.confirm(finishMsg, {
-                            title: '完结确认',
-                            area: ['520px', 'auto'],
-                            icon: 3,
-                            btn: ['确定', '取消'],
-                            skin: 'layui-layer-procuremen-finish'
-                        }, 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();
@@ -655,49 +744,29 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         }
                         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 未就绪,请刷新页面');
+                    var auditIssueBtn = t.closest('a.procuremen-btn-audit-issue');
+                    if (auditIssueBtn && tableRoot.contains(auditIssueBtn) && !auditIssueBtn.classList.contains('disabled')) {
+                        e.preventDefault();
+                        if (typeof e.stopImmediatePropagation === 'function') {
+                            e.stopImmediatePropagation();
+                        }
+                        e.stopPropagation();
+                        var $aa = $(auditIssueBtn);
+                        var rowIdxA = parseInt($aa.data('rowIndex'), 10);
+                        var dataRowsA = table.bootstrapTable('getData');
+                        var rowA = !isNaN(rowIdxA) && dataRowsA[rowIdxA] !== undefined ? dataRowsA[rowIdxA] : null;
+                        var auditUrl;
+                        if (rowA) {
+                            auditUrl = Fast.api.fixurl(Table.api.replaceurl('procuremen/auditissue', rowA, table));
+                        } else {
+                            auditUrl = Backend.api.replaceids(auditIssueBtn, $aa.attr('href'));
+                        }
+                        var winA = window;
+                        if (winA.Backend && winA.Backend.api) {
+                            winA.Backend.api.open(auditUrl, '确认供应商', {area: ['76%', '100%']});
+                        }
                         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);
             }
@@ -716,7 +785,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         dataType: 'json',
                         timeout: 120000
                     }).done(function (ret) {
-                        
+
                         if (ret && (ret.code === 200 || ret.code === '200')) {
                             if (typeof Toastr !== 'undefined') {
                                 console.log('已刷新');
@@ -794,7 +863,34 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
             table.on('post-body.bs.table', function () {
                 $('#procuremen-toolbar-host .btn-refresh .fa').removeClass('fa-spin');
+                if (Controller.wffTab !== 'pick') {
+                    return;
+                }
+                /* FastAdmin 每次 post-body 会重复绑定复选框列拖拽,多次勾选后易错位/缺框 */
+                var $cbTd = table.closest('.bootstrap-table').find('tbody td.bs-checkbox');
+                $cbTd.off('mousedown mousemove').removeClass('overlaped');
+                $cbTd.each(function () {
+                    var $td = $(this);
+                    if (!$td.find('input[name="btSelectItem"]').length) {
+                        var idx = $td.closest('tr').data('index');
+                        if (idx !== undefined && idx !== '') {
+                            $td.html('<input name="btSelectItem" type="checkbox" data-index="' + idx + '" />');
+                        }
+                    }
+                });
             });
+            if ($bt.length) {
+                $bt.off('click.procuremenCbTd', 'tbody td.bs-checkbox').on('click.procuremenCbTd', 'tbody td.bs-checkbox', function (e) {
+                    if (Controller.wffTab !== 'pick' || $(e.target).is('input[type="checkbox"]')) {
+                        return;
+                    }
+                    var $inp = $(this).find('input[name="btSelectItem"]:not(:disabled)').first();
+                    if ($inp.length) {
+                        e.preventDefault();
+                        $inp.trigger('click');
+                    }
+                });
+            }
             $(window).off('resize.procuremenIndex').on('resize.procuremenIndex', function () {
                 try {
                     var $wbr = $layout.find('.procuremen-main .widget-body');
@@ -805,6 +901,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
         },
 
+        pick: function () {
+            Controller.index();
+        },
+        audit: function () {
+            Controller.index();
+        },
+        confirm: function () {
+            Controller.index();
+        },
+
         add: function () {
             Controller.api.bindevent();
         },
@@ -1045,24 +1151,119 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return false;
                 }
             });
-            if (!row) {
+            var mergeRows = [];
+            var mergeFlag = window.Fast && Fast.api ? String(Fast.api.query('merge') || '') : '';
+            if (mergeFlag === '1') {
+                try {
+                    var rawMerge = sessionStorage.getItem('procuremen_merge_rows');
+                    if (rawMerge) {
+                        var parsed = JSON.parse(rawMerge);
+                        if ($.isArray(parsed) && parsed.length) {
+                            mergeRows = parsed;
+                        }
+                    }
+                } catch (ignoreMerge) {
+                }
+            }
+            if (mergeFlag === '1' && !mergeRows.length && row) {
+                mergeRows = [row];
+            }
+            if (mergeRows.length) {
+                row = mergeRows[0];
+            } else 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 : '—');
+            function procuremenReviewQtyDisp(r) {
+                if (!r) {
+                    return '';
+                }
+                if (r.This_quantity != null && String(r.This_quantity).trim() !== '') {
+                    return String(r.This_quantity).trim();
+                }
+                if (r.this_quantity != null && String(r.this_quantity).trim() !== '') {
+                    return String(r.this_quantity).trim();
+                }
+                if (r.NGZL != null && String(r.NGZL).trim() !== '') {
+                    return String(r.NGZL).trim();
+                }
+                if (r.ngzl != null && String(r.ngzl).trim() !== '') {
+                    return String(r.ngzl).trim();
+                }
+                return '';
+            }
+
+            function procuremenReviewPriceDisp(r) {
+                if (!r) {
+                    return '';
+                }
+                if (r.ceilingPrice != null && String(r.ceilingPrice).trim() !== '') {
+                    return String(r.ceilingPrice).trim();
+                }
+                if (r.ceiling_price != null && String(r.ceiling_price).trim() !== '') {
+                    return String(r.ceiling_price).trim();
+                }
+                return '';
+            }
+
+            function applyProcuremenMergeReviewUi(rows) {
+                var list = $.isArray(rows) && rows.length ? rows : (row ? [row] : []);
+                var $tb = $('#review-merge-tbody').empty();
+                var orderKey0 = list.length ? String(list[0].CCYDH || '').trim() : '';
+                var nameKey0 = list.length ? String(list[0].CYJMC || '').trim() : '';
+                var mergeSameOrder = list.length > 1 && orderKey0 !== '';
+                if (mergeSameOrder) {
+                    $.each(list, function (i, rr) {
+                        if (String(rr.CCYDH || '').trim() !== orderKey0
+                            || String(rr.CYJMC || '').trim() !== nameKey0) {
+                            mergeSameOrder = false;
+                            return false;
+                        }
+                    });
+                }
+                $.each(list, function (idx, r) {
+                    var qty = procuremenReviewQtyDisp(r);
+                    var price = procuremenReviewPriceDisp(r);
+                    var ccydh = String(r.CCYDH != null ? r.CCYDH : '').trim();
+                    var cyjmc = String(r.CYJMC != null ? r.CYJMC : '').trim();
+                    var $tr = $('<tr/>');
+                    if (mergeSameOrder && idx === 0) {
+                        $tr.append(
+                            $('<td class="text-center"/>').attr('rowspan', list.length).text(ccydh),
+                            $('<td/>').attr('rowspan', list.length).text(cyjmc)
+                        );
+                    } else if (!mergeSameOrder) {
+                        $tr.append(
+                            $('<td class="text-center"/>').text(ccydh),
+                            $('<td/>').text(cyjmc)
+                        );
+                    }
+                    $tr.append(
+                        $('<td/>').text(r.CGYMC || ''),
+                        $('<td class="text-center"/>').text(r.CDW != null && r.CDW !== '' ? r.CDW : ''),
+                        $('<td class="text-center"/>').text(r.NGZL != null && r.NGZL !== '' ? r.NGZL : ''),
+                        $('<td class="text-center"/>').text(qty),
+                        $('<td class="text-center"/>').text(price),
+                        $('<td class="text-center"/>').text(r.CDF || '')
+                    );
+                    $tb.append($tr);
+                });
+                var isPickModeUi = String($('#review-form').data('pickMode') || $('#review-form').data('pick-mode') || '') === '1';
+                if (!isPickModeUi && list.length > 1) {
+                    $('.procuremen-review-submit-tip').html(
+                        '<i class="fa fa-exclamation-triangle"></i> '
+                        + '<strong>合并单:</strong>提交将向已勾选单位各发送<strong>一封邮件、一条短信</strong>,'
+                        + '内容含本单 <strong>' + list.length + ' 道工序</strong>明细;不可撤回,请核对后再确认。'
+                    );
+                }
+                $('#c-merge-rows-json').val(JSON.stringify(list));
+            }
+
+            applyProcuremenMergeReviewUi(mergeRows.length ? mergeRows : (row ? [row] : []));
+
+            var isPickMode = String($('#review-form').data('pickMode') || $('#review-form').data('pick-mode') || '') === '1';
+
             $('#c-row-json').val(JSON.stringify(row));
 
             var reviewCompaniesAll = [];
@@ -1362,7 +1563,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }
                 });
                 if (!selected.length) {
-                    Toastr.warning('请至少选择一个下发单位');
+                    Toastr.warning(isPickMode ? '请至少选择一家合作供应商' : '请至少选择一个下发单位');
                     return;
                 }
                 var sysRq = ($('#review-sys-rq').val() || '').trim();
@@ -1377,17 +1578,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 } catch (e) {
                     cons = {};
                 }
+                var mergePayload = ($('#c-merge-rows-json').val() || '').trim();
+                if (!mergePayload) {
+                    mergePayload = JSON.stringify([cons]);
+                }
                 Fast.api.ajax({
-                    url: 'procuremen/review',
+                    url: isPickMode ? 'procuremen/picksubmit' : 'procuremen/review',
                     type: 'POST',
                     data: {
                         __token__: $('input[name=\'__token__\']').val(),
                         row_json: JSON.stringify(cons),
+                        merge_rows_json: mergePayload,
                         companies_json: JSON.stringify(selected),
                         sys_rq: sysRq
                     }
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '操作成功';
+                    try {
+                        sessionStorage.removeItem('procuremen_merge_rows');
+                    } catch (ignoreClr) {
+                    }
                     if (typeof parent !== 'undefined' && parent.Toastr) {
                         parent.Toastr.success(msg);
                     }
@@ -1400,14 +1610,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 });
             });
 
-            Fast.api.ajax({
-                url: 'procuremen/snapshotToProcure',
-                type: 'POST',
-                data: {
-                    __token__: $('input[name=\'__token__\']').val(),
-                    row_json: JSON.stringify(row)
-                }
-            }, function () {
+            /** 加载供应商列表(弹窗内勾选单位) */
+            function loadReviewCompaniesPanel() {
                 Fast.api.ajax({
                     url: 'procuremen/reviewCompanies',
                     type: 'GET',
@@ -1554,7 +1758,109 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     syncReviewSelectedSummary();
                     return false;
                 });
-                return false;
+            }
+
+            /*
+             * 外发下发:打开弹窗仅展示供应商,不写 purchase_order(确认下发走 picksubmit)。
+             * 非外发模式:仅当列表已填本次数量或最高限价时才 snapshot(草稿)。
+             */
+            if (isPickMode) {
+                loadReviewCompaniesPanel();
+            } else if (procuremenReviewQtyDisp(row) !== '' || procuremenReviewPriceDisp(row) !== '') {
+                Fast.api.ajax({
+                    url: 'procuremen/snapshotToProcure',
+                    type: 'POST',
+                    data: {
+                        __token__: $('input[name=\'__token__\']').val(),
+                        row_json: JSON.stringify(row)
+                    }
+                }, function () {
+                    loadReviewCompaniesPanel();
+                    return false;
+                });
+            } else {
+                loadReviewCompaniesPanel();
+            }
+        },
+
+        pickreview: function () {
+            Controller.review();
+        },
+
+        pickadd: function () {
+            Controller.api.bindevent();
+            $(document).off('click.procuremenPickAddClose', '.btn-procuremen-pick-add-close').on('click.procuremenPickAddClose', '.btn-procuremen-pick-add-close', function () {
+                var index = parent.Layer.getFrameIndex(window.name);
+                parent.Layer.close(index);
+            });
+        },
+
+        auditissue: function () {
+            var auditSupplierGroups = [];
+            try {
+                auditSupplierGroups = JSON.parse($('#audit-supplier-groups-json').val() || '[]');
+            } catch (ignoreAudCo) {
+                auditSupplierGroups = [];
+            }
+            function auditIssueClose() {
+                var index = parent.Layer.getFrameIndex(window.name);
+                parent.Layer.close(index);
+            }
+            $('#btn-audit-issue-close').off('click.auditClose').on('click.auditClose', function () {
+                auditIssueClose();
+            });
+            $('#audit-pick-tbody').off('click.auditPickRow').on('click.auditPickRow', 'tr', function (e) {
+                if ($(e.target).is('input, label, a')) {
+                    return;
+                }
+                $(this).find('.audit-pick-radio').prop('checked', true);
+            });
+            $('#btn-audit-issue-submit').off('click.auditIssue').on('click.auditIssue', function () {
+                var $radio = $('input[name="audit_pick_company"]:checked');
+                if (!$radio.length) {
+                    Toastr.warning('请勾选一家供应商');
+                    return;
+                }
+                var idx = parseInt($radio.val(), 10);
+                var company = (!isNaN(idx) && auditSupplierGroups[idx]) ? auditSupplierGroups[idx] : null;
+                if (!company || typeof company !== 'object') {
+                    Toastr.warning('所选供应商无效');
+                    return;
+                }
+                var cname = company.name || company.company_name || '';
+                Layer.confirm(
+                    '确认选定「' + cname + '」?<br/>本步骤<strong>不发送短信或邮件</strong>,订单将进入第三步「采购确认」;通过/未通过通知在采购确认提交时发送。',
+                    {icon: 3, title: '确认供应商', btn: ['确认', '取消']},
+                    function (layerIdx) {
+                        Layer.close(layerIdx);
+                        var submitCo = {
+                            name: cname,
+                            company_name: cname,
+                            email: company.email || '',
+                            phone: company.phone || '',
+                            username: company.username || ''
+                        };
+                        Fast.api.ajax({
+                            url: 'procuremen/auditsubmit',
+                            type: 'POST',
+                            data: {
+                                __token__: $('input[name=\'__token__\']').val(),
+                                scydgy_id: $('input[name=\'scydgy_id\']').val(),
+                                company_json: JSON.stringify(submitCo)
+                            }
+                        }, 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');
+                            }
+                            auditIssueClose();
+                            return false;
+                        });
+                    }
+                );
             });
         },
 

+ 79 - 0
public/assets/js/backend/procuremenarchive.js

@@ -0,0 +1,79 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'procuremenarchive/index' + location.search,
+                    table: 'purchase_order',
+                }
+            });
+
+            var table = $("#table");
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'scydgy_id',
+                sortName: 'id',
+                sortOrder: 'desc',
+                queryParams: function (params) {
+                    var ccydh = ($('#filter-ccydh').val() || '').trim();
+                    if (ccydh) {
+                        params.ccydh = ccydh;
+                    }
+                    return params;
+                },
+                columns: [
+                    [
+                        {field: 'CCYDH', title: '订单号', operate: 'LIKE'},
+                        {field: 'CYJMC', title: '印件名称', operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'CGYMC', title: '工序名称', operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content},
+                        {field: 'createtime_text', title: '完结时间', operate: false, width: 165},
+                        {
+                            field: 'operate',
+                            title: '操作',
+                            width: 100,
+                            align: 'center',
+                            table: table,
+                            formatter: function (value, row) {
+                                var sid = row && row.scydgy_id;
+                                if (!sid) {
+                                    return '';
+                                }
+                                return '<a href="javascript:;" class="btn btn-xs btn-info btn-archive-details" data-scydgy-id="' + sid + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>';
+                            },
+                            events: {}
+                        }
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+
+            $(document).off('click.procuremenArchiveDetails', '.btn-archive-details').on('click.procuremenArchiveDetails', '.btn-archive-details', function (e) {
+                e.preventDefault();
+                var sid = $(this).data('scydgy-id') || $(this).attr('data-scydgy-id');
+                if (!sid) {
+                    return;
+                }
+                var url = Fast.api.fixurl('procuremen/details?ids=' + encodeURIComponent(String(sid)));
+                Backend.api.open(url, '详情', {area: ['76%', '100%']});
+            });
+
+            $('#btn-filter-ccydh').on('click', function () {
+                table.bootstrapTable('refresh');
+            });
+            $('#filter-ccydh').on('keypress', function (e) {
+                if (e.which === 13) {
+                    table.bootstrapTable('refresh');
+                }
+            });
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 75 - 0
public/assets/js/backend/procuremenexport.js

@@ -0,0 +1,75 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'procuremenexport/index' + location.search,
+                    table: 'purchase_month_export_log',
+                }
+            });
+
+            var table = $("#table");
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                sortOrder: 'desc',
+                columns: [
+                    [
+                        {field: 'id', title: __('Id')},
+                        {field: 'ym', title: '月份', operate: 'LIKE'},
+                        {field: 'admin_name', title: '导出人', operate: 'LIKE'},
+                        {field: 'row_count', title: '明细行数', operate: false},
+                        {field: 'total_amount_text', title: '金额合计', operate: false},
+                        {field: 'createtime_text', title: '导出时间', operate: false}
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+
+            $(document).off('click.procuremenExportMonth', '#btn-export-month-outward').on('click.procuremenExportMonth', '#btn-export-month-outward', function () {
+                var d = new Date();
+                var ymDefault = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
+                var escYm = String(ymDefault).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                var html = ''
+                    + '<div style="padding:14px 18px 6px;">'
+                    + '<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: ['370px', 'auto'],
+                    shadeClose: true,
+                    content: html,
+                    btn: ['导出 Excel'],
+                    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);
+                        setTimeout(function () {
+                            table.bootstrapTable('refresh');
+                        }, 1500);
+                    }
+                });
+            });
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 163 - 0
public/assets/js/backend/procuremensms.js

@@ -0,0 +1,163 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var SMS_EDIT_AREA = ['960px', '580px'];
+
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'procuremensms/index' + location.search,
+                    edit_url: 'procuremensms/edit',
+                    table: 'purchase_sms_template',
+                }
+            });
+
+            var table = $("#table");
+            var colCenter = {align: 'center', valign: 'middle'};
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                singleSelect: true,
+                clickToSelect: true,
+                columns: [
+                    [
+                        {checkbox: true, align: 'center', valign: 'middle'},
+                        $.extend({field: 'id', title: __('Id'), width: 52}, colCenter),
+                        $.extend({field: 'title', title: '模版名称', operate: 'LIKE', width: 160}, colCenter),
+                        $.extend({
+                            field: 'content',
+                            title: '模版正文',
+                            operate: 'LIKE',
+                            class: 'col-sms-content',
+                            width: 360,
+                            cellStyle: function () {
+                                return {css: {whiteSpace: 'pre-wrap', wordBreak: 'break-word', textAlign: 'center'}};
+                            },
+                            formatter: function (value) {
+                                if (value == null || value === '') {
+                                    return '';
+                                }
+                                return '<div class="sms-content-cell">' + $('<div/>').text(String(value)).html() + '</div>';
+                            }
+                        }, colCenter),
+                        $.extend({field: 'remark', title: '备注', operate: 'LIKE', width: 100}, colCenter),
+                        $.extend({
+                            field: 'status',
+                            title: __('Status'),
+                            width: 88,
+                            searchList: {'1': '正常', '0': '禁用'},
+                            formatter: function (value) {
+                                var v = String(value);
+                                if (v === '1' || v === 'normal') {
+                                    return '<span class="text-success"><i class="fa fa-circle"></i> 正常</span>';
+                                }
+                                return '<span class="text-muted"><i class="fa fa-circle"></i> 禁用</span>';
+                            },
+                            cellStyle: function () {
+                                return {css: {whiteSpace: 'nowrap', textAlign: 'center'}};
+                            }
+                        }, colCenter),
+                        $.extend({
+                            field: 'updatetime',
+                            title: __('Updatetime'),
+                            width: 158,
+                            operate: 'RANGE',
+                            addclass: 'datetimerange',
+                            autocomplete: false,
+                            formatter: function (value) {
+                                if (value == null || value === '') {
+                                    return '';
+                                }
+                                var s = String(value);
+                                if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
+                                    return s.length >= 19 ? s.substr(0, 19) : s;
+                                }
+                                return Table.api.formatter.datetime.call(this, value);
+                            },
+                            cellStyle: function () {
+                                return {css: {whiteSpace: 'nowrap', textAlign: 'center'}};
+                            }
+                        }, colCenter),
+                        $.extend({field: 'operate', title: __('Operate'), width: 72, table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}, colCenter)
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+
+            function procuremenSmsCenterTableHeader() {
+                var $wrap = table.closest('.bootstrap-table');
+                $wrap.find('.fixed-table-header thead th, .fixed-table-container thead th').css({
+                    'text-align': 'center',
+                    'vertical-align': 'middle'
+                });
+                $wrap.find('.fixed-table-header thead th .th-inner, .fixed-table-container thead th .th-inner').css({
+                    'text-align': 'center',
+                    'justify-content': 'center'
+                });
+            }
+
+            table.on('post-header.bs.table post-body.bs.table refresh.bs.table', procuremenSmsCenterTableHeader);
+
+            table.on('check.bs.table', function (e, row) {
+                var sel = table.bootstrapTable('getSelections') || [];
+                if (sel.length <= 1) {
+                    return;
+                }
+                $.each(sel, function (i, r) {
+                    if (r && row && r.id !== row.id) {
+                        table.bootstrapTable('uncheckBy', {field: 'id', values: [r.id]});
+                    }
+                });
+            });
+
+            table.on('post-body.bs.table', function () {
+                procuremenSmsCenterTableHeader();
+                table.closest('.bootstrap-table').find('.btn-editone').each(function () {
+                    $(this).data('area', SMS_EDIT_AREA);
+                });
+            });
+            $('#toolbar .btn-edit').data('area', SMS_EDIT_AREA);
+        },
+        edit: function () {
+            Controller.api.bindevent();
+
+            $(document).off('click.procuremenSmsClose', '.btn-procuremen-sms-close').on('click.procuremenSmsClose', '.btn-procuremen-sms-close', function () {
+                var index = parent.Layer.getFrameIndex(window.name);
+                parent.Layer.close(index);
+            });
+
+            $(document).off('click.smsVarInsert', '.sms-var-tag').on('click.smsVarInsert', '.sms-var-tag', function () {
+                var tag = $(this).data('tag') || $(this).text();
+                tag = String(tag || '').trim();
+                if (!tag) {
+                    return;
+                }
+                var $ta = $('#sms-template-content');
+                if (!$ta.length) {
+                    return;
+                }
+                var el = $ta[0];
+                var start = el.selectionStart;
+                var end = el.selectionEnd;
+                var val = $ta.val();
+                if (typeof start === 'number' && typeof end === 'number') {
+                    $ta.val(val.substring(0, start) + tag + val.substring(end));
+                    var pos = start + tag.length;
+                    el.setSelectionRange(pos, pos);
+                } else {
+                    $ta.val(val + tag);
+                }
+                $ta.focus();
+            });
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 91 - 0
public/assets/js/backend/purchaseemail.js

@@ -0,0 +1,91 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'purchaseemail/index' + location.search,
+                    edit_url: 'purchaseemail/edit',
+                    table: 'purchase_email',
+                }
+            });
+
+            var table = $("#table");
+            var colCenter = {align: 'center', valign: 'middle'};
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                columns: [
+                    [
+                        $.extend({checkbox: true}, colCenter),
+                        $.extend({field: 'id', title: __('Id'), width: 52}, colCenter),
+                        $.extend({field: 'email_addr', title: '发件邮箱', operate: 'LIKE'}, colCenter),
+                        $.extend({
+                            field: 'email_pass',
+                            title: '授权码',
+                            operate: false,
+                            class: 'autocontent',
+                            formatter: Table.api.formatter.content
+                        }, colCenter),
+                        $.extend({
+                            field: 'createtime',
+                            title: __('Createtime'),
+                            operate: 'RANGE',
+                            addclass: 'datetimerange',
+                            formatter: Table.api.formatter.datetime
+                        }, colCenter),
+                        $.extend({
+                            field: 'updatetime',
+                            title: __('Updatetime'),
+                            operate: 'RANGE',
+                            addclass: 'datetimerange',
+                            formatter: Table.api.formatter.datetime
+                        }, colCenter),
+                        $.extend({
+                            field: 'operate',
+                            title: __('Operate'),
+                            width: 90,
+                            table: table,
+                            events: Table.api.events.operate,
+                            formatter: Table.api.formatter.operate,
+                            buttons: [
+                                {
+                                    name: 'edit',
+                                    text: '编辑',
+                                    title: '编辑发件邮箱',
+                                    classname: 'btn btn-xs btn-success btn-editone',
+                                    icon: 'fa fa-pencil',
+                                    url: 'purchaseemail/edit'
+                                }
+                            ]
+                        }, colCenter)
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+
+            table.on('post-body.bs.table', function () {
+                table.closest('.bootstrap-table').find('.btn-editone').each(function () {
+                    $(this).data('area', ['720px', '420px']);
+                });
+            });
+            $('#toolbar .btn-edit').data('area', ['720px', '420px']);
+        },
+        edit: function () {
+            Controller.api.bindevent();
+            $(document).off('click.purchaseEmailClose', '.btn-purchase-email-close').on('click.purchaseEmailClose', '.btn-purchase-email-close', function () {
+                var index = parent.Layer.getFrameIndex(window.name);
+                parent.Layer.close(index);
+            });
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů