m0_70156489 2 ngày trước cách đây
mục cha
commit
a2234f2efe

+ 3 - 0
application/extra/mproc.php

@@ -7,6 +7,7 @@
  *   - 供应商(customer)登录后 → 报价列表(可填单价/交期)
  *   - 质检中心(auth_group)→ 质量评分(合格/投诉/中断)
  *   - 生产部/生产员(auth_group)→ 交货情况(准时/滞后)
+ *   - 业务员(auth_group)→ 新增订单询价
  *   - 其它后台账号 → 提示「账号或密码错误」(不可进手机端)
  */
 return [
@@ -36,6 +37,8 @@ return [
     // 手机端交货情况:所属角色组(默认「生产员」,展示名「生产部」)
     'mobile_delivery_group_id'  => 15,
     'mobile_delivery_dept_name' => '生产部',
+    // 手机端新增订单询价:所属角色组(默认「业务员」,与 rfq_notify_salesman_group_id 一致)
+    'mobile_rfq_group_id'       => 12,
 
     // 当前 login.html 对应哪个系统(换登录页同步改这里)
     // collab = 供应链协同;

+ 531 - 3
application/index/controller/Index.php

@@ -18,6 +18,7 @@ use think\Validate;
  * 普通用户(customer):登录后进报价列表,可填单价/交期
  * 质检中心:登录后进「质量评分」(合格/投诉/中断)
  * 生产部(生产员):登录后进「交货情况」(准时/滞后)
+ * 业务员:登录后进「新增订单询价」
  */
 class Index extends Frontend
 {
@@ -686,6 +687,7 @@ class Index extends Frontend
         if (!empty($user['is_admin'])) {
             $canQuality = $this->mprocAdminHasMobileRole($user, 'quality');
             $canDelivery = $this->mprocAdminHasMobileRole($user, 'delivery');
+            $canRfq = $this->mprocAdminHasMobileRole($user, 'rfq');
             $raw = $this->mprocSanitizeRedirectUrl($redirectPathOrUrl);
             if ($raw !== '') {
                 if ($canDelivery && stripos($raw, 'deliveryscore') !== false) {
@@ -694,6 +696,9 @@ class Index extends Frontend
                 if ($canQuality && stripos($raw, 'inboundscore') !== false) {
                     return $this->mprocAbsoluteFromSanitizedPath($raw, 'index/index/inboundscore');
                 }
+                if ($canRfq && stripos($raw, 'rfqadd') !== false) {
+                    return $this->mprocAbsoluteFromSanitizedPath($raw, 'index/index/rfqadd');
+                }
             }
             if ($canQuality) {
                 return url('index/index/inboundscore', '', '', true);
@@ -701,6 +706,9 @@ class Index extends Frontend
             if ($canDelivery) {
                 return url('index/index/deliveryscore', '', '', true);
             }
+            if ($canRfq) {
+                return url('index/index/rfqadd', '', '', true);
+            }
 
             return '';
         }
@@ -709,14 +717,15 @@ class Index extends Frontend
     }
 
     /**
-     * 操作人员是否具备手机端任一业务角色(质检 / 生产,含超管)
+     * 操作人员是否具备手机端任一业务角色(质检 / 生产 / 业务员询价,含超管)
      *
      * @param array<string, mixed> $user
      */
     protected function mprocAdminHasAnyMobileAccess(array $user): bool
     {
         return $this->mprocAdminHasMobileRole($user, 'quality')
-            || $this->mprocAdminHasMobileRole($user, 'delivery');
+            || $this->mprocAdminHasMobileRole($user, 'delivery')
+            || $this->mprocAdminHasMobileRole($user, 'rfq');
     }
 
     /**
@@ -4654,6 +4663,30 @@ class Index extends Frontend
         return $id > 0 ? $id : 15;
     }
 
+    /** 业务员(手机端新增询价)角色组 id */
+    protected function mprocResolveMobileRfqGroupId(): int
+    {
+        $id = (int)Config::get('mproc.mobile_rfq_group_id');
+        if ($id <= 0) {
+            $id = (int)Config::get('mproc.rfq_notify_salesman_group_id');
+        }
+        if ($id > 0) {
+            return $id;
+        }
+        try {
+            $found = Db::name('auth_group')
+                ->where('name', '业务员')
+                ->where('status', 'normal')
+                ->order('id', 'asc')
+                ->value('id');
+            $id = (int)$found;
+        } catch (\Throwable $e) {
+            $id = 0;
+        }
+
+        return $id > 0 ? $id : 12;
+    }
+
     /**
      * @return int[]
      */
@@ -4711,7 +4744,7 @@ class Index extends Frontend
 
     /**
      * @param array<string, mixed> $user
-     * @param string               $role quality|delivery
+     * @param string               $role quality|delivery|rfq
      */
     protected function mprocAdminHasMobileRole(array $user, string $role): bool
     {
@@ -4736,10 +4769,28 @@ class Index extends Frontend
 
             return $need > 0 && in_array($need, $gids, true);
         }
+        if ($role === 'rfq') {
+            $need = $this->mprocResolveMobileRfqGroupId();
+
+            return $need > 0 && in_array($need, $gids, true);
+        }
 
         return false;
     }
 
+    /**
+     * @return array<string, mixed>
+     */
+    protected function mprocRequireRfqUser(): array
+    {
+        $user = $this->mprocRequireAdminUser();
+        if (!$this->mprocAdminHasMobileRole($user, 'rfq')) {
+            $this->error('仅业务员账号可新增订单询价');
+        }
+
+        return $user;
+    }
+
     /**
      * @return array<string, mixed>
      */
@@ -5127,4 +5178,481 @@ class Index extends Frontend
 
         return $out;
     }
+
+    /**
+     * 业务员:新增订单询价页
+     */
+    public function rfqadd()
+    {
+        $user = $this->mprocGetUser();
+        if (!$user) {
+            $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
+            $safe = $this->mprocSanitizeRedirectUrl($uri);
+            if ($safe !== '') {
+                Session::set('mproc_intended_url', $safe);
+            }
+            $this->redirect($this->mprocBuildLoginUrl($safe));
+
+            return;
+        }
+        if (empty($user['is_admin']) || !$this->mprocAdminHasMobileRole($user, 'rfq')) {
+            $jump = $this->mprocBuildAfterLoginHomeUrl($user);
+            if ($jump === '') {
+                $this->mprocDropCurrentLogin();
+                $this->redirect(url('index/index/login'));
+
+                return;
+            }
+            $this->redirect($jump);
+
+            return;
+        }
+        $profile = $this->mprocProfileForUser($user);
+        $defaultDept = trim((string)($profile['department'] ?? ''));
+        if ($defaultDept === '' || $defaultDept === '业务员' || $defaultDept === '管理员') {
+            $defaultDept = '营销中心';
+        }
+        $this->view->assign('mprocProfile', $profile);
+        $this->view->assign('mprocBootstrapToken', trim((string)($user['token'] ?? '')));
+        $this->view->assign('mprocBootstrapKeepHours', $this->mprocKeepHours());
+        $this->view->assign('nextOrderCcydh', $this->mprocAllocateManualOrderCcydh());
+        $this->view->assign('defaultCclbmmc', $defaultDept);
+        $this->view->assign('adminNickname', trim((string)($profile['contact_name'] ?? '')) ?: '业务员');
+        $this->view->assign('rfqDeptOptions', $this->mprocLoadRfqDeptNameOptions());
+        $this->view->assign('rows', $this->mprocLoadRfqAddRows($user, ''));
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 业务员询价列表 JSON(本人发起或被通知)
+     * GET:q
+     */
+    public function rfqaddlist()
+    {
+        $user = $this->mprocRequireRfqUser();
+        $q = trim((string)$this->request->get('q', ''));
+        $rows = $this->mprocLoadRfqAddRows($user, $q);
+        $this->success('ok', null, ['rows' => $rows, 'total' => count($rows)]);
+    }
+
+    /**
+     * 业务员查看询价报价(供应商 + 单价)
+     * GET:scydgy_id
+     */
+    public function rfqaddquotes()
+    {
+        $user = $this->mprocRequireRfqUser();
+        $sid = (int)$this->request->param('scydgy_id', 0);
+        if ($sid >= 0) {
+            $this->error('仅支持手工询价单');
+        }
+        $po = $this->mprocLoadAccessibleRfqOrder($user, $sid);
+        if (!$po) {
+            $this->error('询价单不存在或无权查看');
+        }
+        $list = [];
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', $sid)
+                ->whereRaw(ProcuremenStatus::sqlPodNotVoid('status'))
+                ->whereRaw('(status_name IS NULL OR TRIM(status_name) = \'\' OR status_name <> \'' . ProcuremenStatus::POD_VOID . '\')')
+                ->field('id,company_name,amount')
+                ->order('id', 'asc')
+                ->select();
+        } catch (\Throwable $e) {
+            try {
+                $rows = Db::table('purchase_order_detail')
+                    ->where('scydgy_id', $sid)
+                    ->field('id,company_name,amount')
+                    ->order('id', 'asc')
+                    ->select();
+            } catch (\Throwable $e2) {
+                $rows = [];
+            }
+        }
+        if (!is_array($rows)) {
+            $rows = [];
+        }
+        foreach ($rows as $row) {
+            if (!is_array($row)) {
+                continue;
+            }
+            $amount = trim((string)($row['amount'] ?? ''));
+            $isQuoted = ($amount !== '' && $amount !== '0' && $amount !== '0.00') ? 1 : 0;
+            $list[] = [
+                'company_name' => trim((string)($row['company_name'] ?? '')),
+                'amount'       => $amount,
+                'amount_text'  => $isQuoted ? $amount : '',
+                'is_quoted'    => $isQuoted,
+            ];
+        }
+        $this->success('ok', null, [
+            'scydgy_id' => $sid,
+            'CCYDH'     => trim((string)($po['CCYDH'] ?? '')),
+            'CYJMC'     => trim((string)($po['CYJMC'] ?? '')),
+            'list'      => $list,
+        ]);
+    }
+
+    /**
+     * 业务员提交新增订单询价
+     * POST:CYJMC、CCLBMMC、CGYMC、CDW、This_quantity、ceilingPrice、CDF、MBZ
+     */
+    public function rfqaddsave()
+    {
+        $user = $this->mprocRequireRfqUser();
+        if (!$this->request->isPost()) {
+            $this->error('请使用 POST');
+        }
+        $profile = $this->mprocProfileForUser($user);
+        $cyjmc = trim((string)$this->request->post('CYJMC', ''));
+        $cgymc = trim((string)$this->request->post('CGYMC', ''));
+        $cclbmmc = trim((string)$this->request->post('CCLBMMC', ''));
+        if ($cyjmc === '') {
+            $this->error('请填写印件名称');
+        }
+        if ($cclbmmc === '') {
+            $cclbmmc = trim((string)($profile['department'] ?? ''));
+        }
+        if ($cclbmmc === '') {
+            $cclbmmc = '营销中心';
+        }
+        $cywyxm = trim((string)($profile['contact_name'] ?? ''));
+        if ($cywyxm === '') {
+            $cywyxm = trim((string)($user['username'] ?? ''));
+        }
+        $ccydh = $this->mprocAllocateManualOrderCcydh();
+        $sid = $this->mprocAllocateManualScydgyId();
+        $now = date('Y-m-d H:i:s');
+        $data = [
+            'scydgy_id'                => $sid,
+            'CCYDH'                    => $ccydh,
+            'CYJMC'                    => $cyjmc,
+            'CGYMC'                    => $cgymc,
+            'CCLBMMC'                  => $cclbmmc,
+            'CDW'                      => trim((string)$this->request->post('CDW', '')),
+            'NGZL'                     => '',
+            'CDF'                      => trim((string)$this->request->post('CDF', '')),
+            'cGzzxMc'                  => '',
+            'MBZ'                      => trim((string)$this->request->post('MBZ', '')),
+            'cywyxm'                   => $cywyxm,
+            'notify_salesman'          => '',
+            'rfq_salesman_notified'    => 0,
+            'rfq_salesman_notify_time' => null,
+            'This_quantity'            => trim((string)$this->request->post('This_quantity', '')),
+            'ceilingPrice'             => trim((string)$this->request->post('ceilingPrice', '')),
+            'wflow_status'             => ProcuremenStatus::WFLOW_PENDING_ISSUE,
+            'status'                   => ProcuremenStatus::PO_IN_PROGRESS,
+            'createtime'               => $now,
+            'dStamp'                   => $now,
+            'dputrecord'               => $now,
+        ];
+        try {
+            $this->mprocEnsureRfqSalesmanColumns();
+            Db::table('purchase_order')->insert($data);
+        } catch (\Throwable $e) {
+            $this->error('新增失败:' . $e->getMessage());
+        }
+        $this->success('新增成功', '', [
+            'scydgy_id'      => $sid,
+            'CCYDH'          => $ccydh,
+            'nextOrderCcydh' => $this->mprocAllocateManualOrderCcydh(),
+        ]);
+    }
+
+    protected function mprocAllocateManualScydgyId(): int
+    {
+        try {
+            $min = Db::table('purchase_order')->where('scydgy_id', '<', 0)->min('scydgy_id');
+            $min = (int)$min;
+
+            return $min < 0 ? $min - 1 : -1;
+        } catch (\Throwable $e) {
+            return -1;
+        }
+    }
+
+    protected function mprocAllocateManualOrderCcydh(): string
+    {
+        $prefix = 'YW' . date('Ymd');
+        $maxSeq = 0;
+        try {
+            $rows = Db::table('purchase_order')
+                ->where('CCYDH', 'like', $prefix . '%')
+                ->column('CCYDH');
+            if (is_array($rows)) {
+                foreach ($rows as $ccydh) {
+                    $ccydh = trim((string)$ccydh);
+                    if (preg_match('/^' . preg_quote($prefix, '/') . '(\d{3})$/', $ccydh, $m)) {
+                        $maxSeq = max($maxSeq, (int)$m[1]);
+                    }
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+
+        return $prefix . str_pad((string)($maxSeq + 1), 3, '0', STR_PAD_LEFT);
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function mprocLoadRfqDeptNameOptions(): array
+    {
+        $out = [];
+        try {
+            $vals = Db::table('mcyd')
+                ->where('CCLBMMC', '<>', '')
+                ->whereNotNull('CCLBMMC')
+                ->distinct(true)
+                ->limit(300)
+                ->column('CCLBMMC');
+        } catch (\Throwable $e) {
+            $vals = [];
+        }
+        if (is_array($vals)) {
+            foreach ($vals as $v) {
+                $v = trim((string)$v);
+                if ($v === '' || isset($out[$v])) {
+                    continue;
+                }
+                $out[$v] = $v;
+            }
+        }
+        try {
+            $poVals = Db::table('purchase_order')
+                ->where('CCLBMMC', '<>', '')
+                ->whereNotNull('CCLBMMC')
+                ->distinct(true)
+                ->limit(200)
+                ->column('CCLBMMC');
+        } catch (\Throwable $e) {
+            $poVals = [];
+        }
+        if (is_array($poVals)) {
+            foreach ($poVals as $v) {
+                $v = trim((string)$v);
+                if ($v === '' || isset($out[$v])) {
+                    continue;
+                }
+                $out[$v] = $v;
+            }
+        }
+        foreach (['营销中心', '业务部', '市场部', '外贸部', '新华技术业务部', '新华广告', '数字印刷中心'] as $preset) {
+            if (!isset($out[$preset])) {
+                $out[$preset] = $preset;
+            }
+        }
+        $list = array_values($out);
+        usort($list, static function ($a, $b) {
+            $weight = [
+                '营销中心'       => 1,
+                '业务部'         => 2,
+                '市场部'         => 3,
+                '外贸部'         => 4,
+                '新华技术业务部' => 5,
+            ];
+            $wa = $weight[$a] ?? 100;
+            $wb = $weight[$b] ?? 100;
+            if ($wa !== $wb) {
+                return $wa <=> $wb;
+            }
+
+            return strcmp((string)$a, (string)$b);
+        });
+
+        return $list;
+    }
+
+    protected function mprocEnsureRfqSalesmanColumns(): void
+    {
+        static $ok = false;
+        if ($ok) {
+            return;
+        }
+        $alters = [
+            'notify_salesman' => "ADD COLUMN `notify_salesman` varchar(500) NOT NULL DEFAULT '' COMMENT '通知业务员' AFTER `cywyxm`",
+            'rfq_salesman_notified' => "ADD COLUMN `rfq_salesman_notified` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '是否已通知业务员' AFTER `notify_salesman`",
+            'rfq_salesman_notify_time' => "ADD COLUMN `rfq_salesman_notify_time` datetime DEFAULT NULL COMMENT '通知业务员时间' AFTER `rfq_salesman_notified`",
+        ];
+        foreach ($alters as $col => $sql) {
+            try {
+                $exists = Db::query("SHOW COLUMNS FROM `purchase_order` LIKE '{$col}'");
+                if (is_array($exists) && $exists !== []) {
+                    continue;
+                }
+                Db::execute('ALTER TABLE `purchase_order` ' . $sql);
+            } catch (\Throwable $e) {
+            }
+        }
+        $ok = true;
+    }
+
+    /**
+     * 校验业务员是否可访问该手工询价单
+     *
+     * @param array<string, mixed> $user
+     * @return array<string, mixed>|null
+     */
+    protected function mprocLoadAccessibleRfqOrder(array $user, int $sid): ?array
+    {
+        if ($sid >= 0) {
+            return null;
+        }
+        $profile = $this->mprocProfileForUser($user);
+        $adminName = trim((string)($profile['contact_name'] ?? ''));
+        if ($adminName === '') {
+            $adminName = trim((string)($user['username'] ?? ''));
+        }
+        $isSuper = $this->mprocAdminIsMobileSuper((int)($user['admin_id'] ?? 0));
+        $this->mprocEnsureRfqSalesmanColumns();
+        try {
+            $query = Db::table('purchase_order')
+                ->where('scydgy_id', $sid)
+                ->whereRaw(
+                    '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
+                );
+            if (!$isSuper) {
+                if ($adminName === '') {
+                    return null;
+                }
+                $query->where(function ($q2) use ($adminName) {
+                    $q2->where('cywyxm', $adminName)
+                        ->whereOr('notify_salesman', $adminName)
+                        ->whereOr('notify_salesman', 'like', '%、' . $adminName . '、%')
+                        ->whereOr('notify_salesman', 'like', $adminName . '、%')
+                        ->whereOr('notify_salesman', 'like', '%、' . $adminName)
+                        ->whereOr('notify_salesman', 'like', '%,' . $adminName . ',%')
+                        ->whereOr('notify_salesman', 'like', $adminName . ',%')
+                        ->whereOr('notify_salesman', 'like', '%,' . $adminName);
+                });
+            }
+            $row = $query->find();
+        } catch (\Throwable $e) {
+            return null;
+        }
+
+        return is_array($row) ? $row : null;
+    }
+
+    /**
+     * 手机端业务员询价列表(本人发起 / 被通知)
+     *
+     * @param array<string, mixed> $user
+     * @return array<int, array<string, mixed>>
+     */
+    protected function mprocLoadRfqAddRows(array $user, string $q): array
+    {
+        $profile = $this->mprocProfileForUser($user);
+        $adminName = trim((string)($profile['contact_name'] ?? ''));
+        if ($adminName === '') {
+            $adminName = trim((string)($user['username'] ?? ''));
+        }
+        $isSuper = $this->mprocAdminIsMobileSuper((int)($user['admin_id'] ?? 0));
+        $this->mprocEnsureRfqSalesmanColumns();
+        try {
+            $query = Db::table('purchase_order')
+                ->where('scydgy_id', '<', 0)
+                ->whereRaw(
+                    '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
+                );
+            if (!$isSuper) {
+                if ($adminName === '') {
+                    return [];
+                }
+                $query->where(function ($q2) use ($adminName) {
+                    $q2->where('cywyxm', $adminName)
+                        ->whereOr('notify_salesman', $adminName)
+                        ->whereOr('notify_salesman', 'like', '%、' . $adminName . '、%')
+                        ->whereOr('notify_salesman', 'like', $adminName . '、%')
+                        ->whereOr('notify_salesman', 'like', '%、' . $adminName)
+                        ->whereOr('notify_salesman', 'like', '%,' . $adminName . ',%')
+                        ->whereOr('notify_salesman', 'like', $adminName . ',%')
+                        ->whereOr('notify_salesman', 'like', '%,' . $adminName);
+                });
+            }
+            $q = trim($q);
+            if ($q !== '') {
+                $like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $q) . '%';
+                $query->where(function ($q2) use ($like) {
+                    $q2->where('CCYDH', 'like', $like)
+                        ->whereOr('CYJMC', 'like', $like)
+                        ->whereOr('CGYMC', 'like', $like)
+                        ->whereOr('CCLBMMC', 'like', $like)
+                        ->whereOr('MBZ', 'like', $like);
+                });
+            }
+            $rows = $query
+                ->field('id,scydgy_id,CCYDH,CYJMC,CCLBMMC,CGYMC,CDW,CDF,This_quantity,ceilingPrice,cywyxm,MBZ,status,wflow_status,rfq_salesman_notified,createtime,dStamp')
+                ->order('id', 'desc')
+                ->limit(200)
+                ->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (!is_array($rows)) {
+            $rows = [];
+        }
+        $out = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $created = trim((string)($r['createtime'] ?? ''));
+            if ($created === '' || stripos($created, '0000-00-00') === 0) {
+                $created = trim((string)($r['dStamp'] ?? ''));
+            }
+            if (preg_match('/^\d{10,}$/', $created)) {
+                $created = date('Y-m-d H:i:s', (int)$created);
+            }
+            $out[] = [
+                'id'            => (int)($r['id'] ?? 0),
+                'scydgy_id'     => (int)($r['scydgy_id'] ?? 0),
+                'CCYDH'         => trim((string)($r['CCYDH'] ?? '')),
+                'CYJMC'         => trim((string)($r['CYJMC'] ?? '')),
+                'CCLBMMC'       => trim((string)($r['CCLBMMC'] ?? '')),
+                'CGYMC'         => trim((string)($r['CGYMC'] ?? '')),
+                'CDW'           => trim((string)($r['CDW'] ?? '')),
+                'CDF'           => trim((string)($r['CDF'] ?? '')),
+                'This_quantity' => trim((string)($r['This_quantity'] ?? '')),
+                'ceilingPrice'  => trim((string)($r['ceilingPrice'] ?? '')),
+                'cywyxm'        => trim((string)($r['cywyxm'] ?? '')),
+                'MBZ'           => trim((string)($r['MBZ'] ?? '')),
+                'progress_text' => $this->mprocFormatRfqProgressText($r),
+                'createtime'    => $created,
+            ];
+        }
+
+        return $out;
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected function mprocFormatRfqProgressText(array $row): string
+    {
+        if (ProcuremenStatus::isPoCompleted($row['status'] ?? '')) {
+            return ProcuremenStatus::PO_COMPLETED;
+        }
+        if ((int)($row['rfq_salesman_notified'] ?? 0) === 1) {
+            return '已通知业务员';
+        }
+        $wflow = ProcuremenStatus::normalizeWflowStatus($row['wflow_status'] ?? '');
+        if ($wflow !== ProcuremenStatus::WFLOW_PENDING_ISSUE) {
+            return '待报价';
+        }
+        $sid = (int)($row['scydgy_id'] ?? 0);
+        if ($sid < 0) {
+            try {
+                $cnt = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->count();
+                if ($cnt > 0) {
+                    return '待报价';
+                }
+            } catch (\Throwable $e) {
+            }
+        }
+
+        return '待询价';
+    }
 }

+ 910 - 0
application/index/view/index/rfqadd.html

@@ -0,0 +1,910 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>订单询价</title>
+    <meta property="og:title" content="订单询价">
+    <meta property="og:image" content="{:htmlentities(default_admin_avatar_url())}">
+    <link rel="shortcut icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="icon" href="{:htmlentities(default_admin_avatar_url())}" type="image/png">
+    <link rel="apple-touch-icon" href="{:htmlentities(default_admin_avatar_url())}">
+    <style>
+        * { box-sizing: border-box; }
+        html, body { margin: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
+        :root {
+            --bar-h: 52px;
+            --tabbar-h: 52px;
+            --safe-bottom: env(safe-area-inset-bottom, 0px);
+        }
+        body.layout-orders { overflow: hidden; }
+        body.layout-me { overflow: hidden; }
+        .bar {
+            position: fixed; top: 0; left: 0; right: 0; z-index: 50;
+            height: var(--bar-h); padding: 0 14px;
+            background: #3c8dbc; color: #fff;
+            display: flex; align-items: center; justify-content: center;
+        }
+        .bar h1 {
+            position: absolute; left: 0; right: 0;
+            margin: 0; font-size: 1.05rem; font-weight: 600;
+            text-align: center; pointer-events: none;
+        }
+        .bar a {
+            margin-left: auto; position: relative; z-index: 1;
+            color: #fff; text-decoration: none; font-size: 13px;
+            padding: 5px 10px; border: 1px solid rgba(255,255,255,.6); border-radius: 6px;
+        }
+        body.layout-orders .bar-logout { display: none !important; }
+        #pane-orders {
+            position: fixed; left: 0; right: 0; z-index: 10;
+            top: var(--bar-h); bottom: calc(var(--tabbar-h) + var(--safe-bottom));
+            display: flex; flex-direction: column; overflow: hidden; background: #f5f5f5;
+        }
+        #pane-me {
+            position: fixed; left: 0; right: 0; z-index: 10;
+            top: var(--bar-h); bottom: calc(var(--tabbar-h) + var(--safe-bottom));
+            overflow-y: auto; -webkit-overflow-scrolling: touch; background: #f5f5f5;
+            display: none;
+        }
+        .toolbar {
+            flex-shrink: 0; padding: 10px 12px; background: #e8f4fc;
+            border-bottom: 1px solid #d2e7f4; display: flex; gap: 8px; align-items: center;
+        }
+        .toolbar input[type="search"] {
+            flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid #bcd8ea;
+            border-radius: 8px; font-size: 15px; background: #fff;
+        }
+        .toolbar input:focus { outline: none; border-color: #3c8dbc; }
+        .toolbar .btn-search,
+        .toolbar .btn-add {
+            flex-shrink: 0; padding: 10px 12px; border: none; border-radius: 8px;
+            font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer;
+        }
+        .toolbar .btn-search { background: #3c8dbc; color: #fff; }
+        .toolbar .btn-add { background: #27ae60; color: #fff; }
+        .list-wrap {
+            flex: 1 1 0%; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
+            padding: 10px 12px 20px;
+        }
+        .card {
+            background: #fff; border-radius: 10px; padding: 12px 14px;
+            margin-bottom: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06);
+        }
+        .card .ord { font-size: 15px; font-weight: 700; color: #222; line-height: 1.4; word-break: break-all; }
+        .card .meta { margin-top: 8px; font-size: 13px; color: #555; line-height: 1.55; }
+        .card .meta b { color: #888; font-weight: 500; }
+        .card .tags {
+            margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px 10px;
+            font-size: 12px; color: #666; align-items: center;
+        }
+        .card .progress {
+            display: inline-block; padding: 2px 8px; border-radius: 10px;
+            background: #eef6fb; color: #3c8dbc; font-weight: 600;
+        }
+        .card .progress.is-done { background: #eaf7ee; color: #27ae60; }
+        .card .time { color: #999; }
+        .card .btn-view-quote {
+            margin-left: auto; padding: 4px 10px; border: 1px solid #3c8dbc;
+            border-radius: 6px; background: #fff; color: #3c8dbc;
+            font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer;
+        }
+        .card .btn-view-quote:active { background: #eef6fb; }
+        .empty { text-align: center; color: #999; padding: 48px 16px; font-size: 14px; }
+        .rfq-mask {
+            display: none; position: fixed; inset: 0; z-index: 2000;
+            background: rgba(0,0,0,.45); align-items: center; justify-content: center;
+            padding: 24px 28px;
+            padding-bottom: calc(24px + var(--safe-bottom));
+        }
+        .rfq-mask.show { display: flex; }
+        .rfq-sheet {
+            width: 100%; max-width: 340px; max-height: min(72vh, 560px);
+            background: #fff; border-radius: 12px;
+            display: flex; flex-direction: column; overflow: hidden;
+            box-shadow: 0 8px 28px rgba(0,0,0,.18);
+        }
+        .rfq-sheet-head {
+            flex: 0 0 auto; padding: 10px 12px; border-bottom: 1px solid #eee;
+            display: flex; align-items: center; justify-content: space-between;
+        }
+        .rfq-sheet-head h2 { margin: 0; font-size: 15px; font-weight: 700; color: #222; }
+        .rfq-sheet-head .rfq-close {
+            border: none; background: transparent; color: #888; font-size: 20px;
+            line-height: 1; padding: 0 4px; cursor: pointer;
+        }
+        .rfq-sheet-body {
+            flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
+            padding: 10px 12px 6px;
+        }
+        .rfq-field { margin-bottom: 8px; }
+        .rfq-field label {
+            display: block; font-size: 12px; color: #666; margin-bottom: 4px; font-weight: 600;
+        }
+        .rfq-field label .req { color: #e74c3c; margin-left: 2px; }
+        .rfq-field input,
+        .rfq-field textarea {
+            width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 8px;
+            font-size: 14px; font-family: inherit; background: #fff; color: #333;
+        }
+        .rfq-field input:focus,
+        .rfq-field textarea:focus { outline: none; border-color: #3c8dbc; }
+        .rfq-field input[readonly] { background: #f5f5f5; color: #555; }
+        .rfq-field textarea { resize: vertical; min-height: 64px; }
+        .rfq-dept-combo { position: relative; }
+        .rfq-dept-combo .rfq-dept-row {
+            display: flex; align-items: stretch; gap: 0;
+            border: 1px solid #ddd; border-radius: 8px; background: #fff; overflow: hidden;
+        }
+        .rfq-dept-combo .rfq-dept-row:focus-within { border-color: #3c8dbc; }
+        .rfq-dept-combo #cclbmmc {
+            flex: 1 1 auto; min-width: 0; border: none; border-radius: 0;
+            padding: 8px 10px; font-size: 14px;
+        }
+        .rfq-dept-combo #cclbmmc:focus { outline: none; }
+        .rfq-dept-combo .rfq-dept-toggle {
+            flex: 0 0 38px; border: none; border-left: 1px solid #e5e5e5;
+            background: #fafafa; color: #666; font-size: 11px; cursor: pointer;
+        }
+        .rfq-dept-combo .rfq-dept-menu {
+            display: none; position: absolute; left: 0; right: 0; top: calc(100% + 4px);
+            z-index: 5; max-height: 160px; overflow-y: auto;
+            background: #fff; border: 1px solid #ddd; border-radius: 8px;
+            box-shadow: 0 6px 18px rgba(0,0,0,.12);
+        }
+        .rfq-dept-combo.open .rfq-dept-menu { display: block; }
+        .rfq-dept-combo .rfq-dept-item {
+            display: block; width: 100%; padding: 9px 10px; border: none;
+            background: #fff; text-align: left; font-size: 13px; color: #333;
+            font-family: inherit; cursor: pointer; border-bottom: 1px solid #f3f3f3;
+        }
+        .rfq-dept-combo .rfq-dept-item:last-child { border-bottom: none; }
+        .rfq-dept-combo .rfq-dept-item:active,
+        .rfq-dept-combo .rfq-dept-item:hover { background: #f0f7fb; color: #3c8dbc; }
+        .rfq-dept-combo .rfq-dept-empty {
+            display: none; padding: 10px; color: #999; font-size: 12px; text-align: center;
+        }
+        .rfq-sheet-acts {
+            flex: 0 0 auto; display: flex; gap: 8px; padding: 10px 12px;
+            border-top: 1px solid #eee; background: #fff;
+        }
+        .rfq-sheet-acts button {
+            flex: 1; padding: 10px 8px; border: none; border-radius: 8px;
+            font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer;
+        }
+        .rfq-quote-table {
+            width: 100%; border-collapse: collapse; font-size: 13px;
+        }
+        .rfq-quote-table th,
+        .rfq-quote-table td {
+            padding: 8px 6px; border-bottom: 1px solid #eee; text-align: left;
+            vertical-align: middle; word-break: break-all;
+        }
+        .rfq-quote-table th {
+            color: #888; font-weight: 600; font-size: 12px; background: #fafafa;
+        }
+        .rfq-quote-table td.col-amt { text-align: right; white-space: nowrap; width: 88px; }
+        .rfq-quote-table .muted { color: #bbb; }
+        .rfq-quote-empty { text-align: center; color: #999; padding: 28px 8px; font-size: 13px; }
+        .rfq-sheet-acts .btn-reset { background: #f0f0f0; color: #555; }
+        .rfq-sheet-acts .btn-submit { background: #3c8dbc; color: #fff; }
+        .rfq-sheet-acts .btn-submit:disabled { opacity: .55; }
+        .me-panel { padding: 16px 14px 24px; max-width: 480px; margin: 0 auto; }
+        .me-card { background: #fff; border-radius: 12px; padding: 18px 16px; box-shadow: 0 1px 6px rgba(0,0,0,.06); }
+        .me-row { font-size: 14px; line-height: 1.7; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
+        .me-row span { display: inline-block; min-width: 5em; color: #888; }
+        .btn-me-pwd {
+            display: block; width: 100%; margin-top: 16px; padding: 12px;
+            border: 1px solid #3c8dbc; border-radius: 8px; background: #fff;
+            color: #3c8dbc; font-size: 15px; font-weight: 600; text-align: center;
+            cursor: pointer; font-family: inherit;
+        }
+        .pwd-mask {
+            display: none; position: fixed; inset: 0; z-index: 2100;
+            background: rgba(0,0,0,.45); align-items: center; justify-content: center;
+            padding: 24px 28px; padding-bottom: calc(24px + var(--safe-bottom));
+        }
+        .pwd-mask.show { display: flex; }
+        .pwd-sheet {
+            width: 100%; max-width: 360px; background: #fff; border-radius: 14px;
+            padding: 18px 18px 20px; box-shadow: 0 8px 32px rgba(0,0,0,.12);
+        }
+        .pwd-sheet .pwd-head { font-size: 16px; font-weight: 600; margin: 0 0 14px; color: #222; }
+        .pwd-field { margin-bottom: 14px; }
+        .pwd-field label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
+        .pwd-field input {
+            width: 100%; padding: 11px 12px; border: 1px solid #ddd; border-radius: 8px;
+            font-size: 16px; font-family: inherit;
+        }
+        .pwd-acts { display: flex; gap: 10px; margin-top: 18px; }
+        .pwd-acts button {
+            flex: 1; padding: 12px; border-radius: 8px; font-size: 15px; border: none;
+            cursor: pointer; font-family: inherit;
+        }
+        .pwd-acts .pwd-cancel { background: #f0f0f0; color: #555; }
+        .pwd-acts .pwd-ok { background: #3c8dbc; color: #fff; font-weight: 600; }
+        .tabbar {
+            position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
+            display: flex; background: #fff; border-top: 1px solid #e5e5e5;
+            box-shadow: 0 -2px 10px rgba(0,0,0,.05);
+            padding-bottom: var(--safe-bottom);
+        }
+        .tabbar .tabbar-btn {
+            flex: 1; text-align: center; padding: 10px 4px 12px; color: #666;
+            font-size: 14px; line-height: 1.25; border: none; border-top: 3px solid transparent;
+            margin-top: -1px; background: #fff; cursor: pointer; font-family: inherit;
+        }
+        .tabbar .tabbar-btn.active { color: #3c8dbc; font-weight: 600; border-top-color: #3c8dbc; background: #f8fbfd; }
+        .toast {
+            position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%);
+            max-width: 86vw; padding: 10px 16px; border-radius: 8px;
+            background: rgba(0,0,0,.78); color: #fff; font-size: 14px;
+            z-index: 3000; opacity: 0; pointer-events: none; transition: opacity .2s;
+        }
+        .toast.show { opacity: 1; }
+        .toast.ok { background: #27ae60; }
+        .toast.err { background: #c0392b; }
+    </style>
+</head>
+<body class="layout-orders">
+<div class="bar">
+    <h1 id="barTitle">订单询价</h1>
+    <a href="{:url('index/index/logout')}" class="bar-logout" id="barLogout">退出</a>
+</div>
+
+<div id="pane-orders">
+    <div class="toolbar">
+        <input type="search" id="qInput" placeholder="请输入搜索内容" maxlength="120" enterkeyhint="search" autocomplete="off">
+        <button type="button" class="btn-search" id="btnSearch">搜索</button>
+        <button type="button" class="btn-add" id="btnOpenAdd">新增询价</button>
+    </div>
+    <div class="list-wrap" id="listMain">
+        {if $rows}
+        {volist name="rows" id="row"}
+        <div class="card">
+            <div class="ord">{$row.CCYDH|default=''|htmlentities}{if $row.CYJMC} {$row.CYJMC|htmlentities}{/if}</div>
+            <div class="meta">
+                <div><b>需求部门:</b>{$row.CCLBMMC|default=''|htmlentities}</div>
+                <div><b>工序:</b>{$row.CGYMC|default=''|htmlentities}</div>
+                <div><b>发起人:</b>{$row.cywyxm|default=''|htmlentities}</div>
+            </div>
+            <div class="tags">
+                <span class="progress{if condition="$row.progress_text eq '已完结'"} is-done{/if}">{$row.progress_text|default='待询价'|htmlentities}</span>
+                {if $row.createtime}<span class="time">{$row.createtime|htmlentities}</span>{/if}
+                <button type="button" class="btn-view-quote" data-scydgy-id="{$row.scydgy_id}">查看报价</button>
+            </div>
+        </div>
+        {/volist}
+        {else/}
+        <div class="empty" id="emptyTip">暂无询价单,点击右上角「新增询价」</div>
+        {/if}
+    </div>
+</div>
+
+<div id="pane-me" class="me-panel">
+    <div class="me-card">
+        <div class="me-row"><span>账号</span>{$mprocProfile.account|default=''|htmlentities}</div>
+        <div class="me-row"><span>姓名</span>{$mprocProfile.contact_name|default=''|htmlentities}</div>
+        <div class="me-row"><span>部门</span>{$mprocProfile.department|default=''|htmlentities}</div>
+        <div class="me-row"><span>手机号</span>{$mprocProfile.phone|default=''|htmlentities}</div>
+        <div class="me-row"><span>邮箱</span>{$mprocProfile.email|default=''|htmlentities}</div>
+        <button type="button" class="btn-me-pwd" id="btn-me-change-pwd">修改密码</button>
+    </div>
+</div>
+
+<nav class="tabbar" id="tabbar" aria-label="主导航">
+    <button type="button" class="tabbar-btn active" data-main-tab="orders">首页</button>
+    <button type="button" class="tabbar-btn" data-main-tab="me">我的</button>
+</nav>
+
+<div class="rfq-mask" id="rfqMask" aria-hidden="true">
+    <div class="rfq-sheet" id="rfqSheet">
+        <div class="rfq-sheet-head">
+            <h2>新增询价</h2>
+            <button type="button" class="rfq-close" id="rfqClose" aria-label="关闭">&times;</button>
+        </div>
+        <div class="rfq-sheet-body">
+            <div class="rfq-field">
+                <label>需求编号</label>
+                <input type="text" id="ccydh" value="{$nextOrderCcydh|default=''|htmlentities}" readonly>
+            </div>
+            <div class="rfq-field">
+                <label>印件名称<span class="req">*</span></label>
+                <input type="text" id="cyjmc" maxlength="200" autocomplete="off" placeholder="请输入印件名称">
+            </div>
+            <div class="rfq-field">
+                <label>需求部门<span class="req">*</span></label>
+                <div class="rfq-dept-combo" id="rfqDeptCombo">
+                    <div class="rfq-dept-row">
+                        <input type="text" id="cclbmmc" value="{$defaultCclbmmc|default='营销中心'|htmlentities}" maxlength="100" autocomplete="off" placeholder="可下拉选择或自行输入">
+                        <button type="button" class="rfq-dept-toggle" id="rfqDeptToggle" aria-label="选择部门">▼</button>
+                    </div>
+                    <div class="rfq-dept-menu" id="rfqDeptMenu">
+                        {volist name="rfqDeptOptions" id="deptName"}
+                        <button type="button" class="rfq-dept-item" data-value="{$deptName|htmlentities}">{$deptName|htmlentities}</button>
+                        {/volist}
+                        <div class="rfq-dept-empty">无匹配部门,可直接输入</div>
+                    </div>
+                </div>
+            </div>
+            <div class="rfq-field">
+                <label>工序名称</label>
+                <input type="text" id="cgymc" maxlength="200" autocomplete="off">
+            </div>
+            <div class="rfq-field">
+                <label>单位</label>
+                <input type="text" id="cdw" maxlength="50" autocomplete="off">
+            </div>
+            <div class="rfq-field">
+                <label>本次数量</label>
+                <input type="text" id="thisQty" maxlength="50" autocomplete="off" inputmode="decimal">
+            </div>
+            <div class="rfq-field">
+                <label>最高限价</label>
+                <input type="text" id="ceilingPrice" maxlength="50" autocomplete="off" inputmode="decimal">
+            </div>
+            <div class="rfq-field">
+                <label>订法</label>
+                <input type="text" id="cdf" maxlength="100" autocomplete="off">
+            </div>
+            <div class="rfq-field">
+                <label>需求发起人</label>
+                <input type="text" id="cywyxm" value="{$adminNickname|default=''|htmlentities}" readonly>
+            </div>
+            <div class="rfq-field">
+                <label>备注</label>
+                <textarea id="mbz" maxlength="500"></textarea>
+            </div>
+        </div>
+        <div class="rfq-sheet-acts">
+            <button type="button" class="btn-reset" id="btnReset">重置</button>
+            <button type="button" class="btn-submit" id="btnSubmit">新增</button>
+        </div>
+    </div>
+</div>
+
+<div class="rfq-mask" id="quoteMask" aria-hidden="true">
+    <div class="rfq-sheet" id="quoteSheet" style="max-height:min(68vh,480px);">
+        <div class="rfq-sheet-head">
+            <h2 id="quoteTitle">查看报价</h2>
+            <button type="button" class="rfq-close" id="quoteClose" aria-label="关闭">&times;</button>
+        </div>
+        <div class="rfq-sheet-body" id="quoteBody">
+            <div class="rfq-quote-empty">加载中…</div>
+        </div>
+    </div>
+</div>
+
+<div class="toast" id="toast"></div>
+<div class="pwd-mask" id="pwdMask" aria-hidden="true">
+    <div class="pwd-sheet" id="pwdSheet">
+        <p class="pwd-head">修改密码</p>
+        <div class="pwd-field">
+            <label for="inp-old-pwd">原密码</label>
+            <input type="password" id="inp-old-pwd" autocomplete="current-password" maxlength="20">
+        </div>
+        <div class="pwd-field">
+            <label for="inp-new-pwd">新密码</label>
+            <input type="password" id="inp-new-pwd" autocomplete="new-password" maxlength="20" placeholder="8~20位,须含字母和数字">
+        </div>
+        <div class="pwd-field">
+            <label for="inp-renew-pwd">确认新密码</label>
+            <input type="password" id="inp-renew-pwd" autocomplete="new-password" maxlength="20" placeholder="再次输入新密码">
+        </div>
+        <div class="pwd-acts">
+            <button type="button" class="pwd-cancel" id="pwdCancel">取消</button>
+            <button type="button" class="pwd-ok" id="pwdSave">确定</button>
+        </div>
+    </div>
+</div>
+
+<script>
+(function () {
+    var listUrl = "{:url('index/index/rfqaddlist')}";
+    var saveUrl = "{:url('index/index/rfqaddsave')}";
+    var quotesUrl = "{:url('index/index/rfqaddquotes')}";
+    var loginUrl = "{:url('index/index/login')}";
+    var changePwdUrl = "{:url('index/index/mprocChangePwd')}";
+    var token = {:json_encode(isset($mprocBootstrapToken) ? $mprocBootstrapToken : '', JSON_UNESCAPED_UNICODE)};
+    var keepHours = {$mprocBootstrapKeepHours|default=72};
+    var defaultDept = {:json_encode(isset($defaultCclbmmc) ? (string)$defaultCclbmmc : '营销中心', JSON_UNESCAPED_UNICODE)};
+    var defaultStarter = {:json_encode(isset($adminNickname) ? (string)$adminNickname : '', JSON_UNESCAPED_UNICODE)};
+    var nextCcydh = {:json_encode(isset($nextOrderCcydh) ? (string)$nextOrderCcydh : '', JSON_UNESCAPED_UNICODE)};
+
+    try {
+        if (token) {
+            localStorage.setItem('mproc_token', token);
+            localStorage.setItem('mproc_token_exp', String(Date.now() + keepHours * 3600 * 1000));
+        }
+    } catch (e) {}
+
+    function clearLocalLoginToken() {
+        try {
+            localStorage.removeItem('mproc_token');
+            localStorage.removeItem('mproc_token_exp');
+        } catch (e) {}
+    }
+
+    document.querySelectorAll('a.bar-logout').forEach(function (a) {
+        a.addEventListener('click', function () { clearLocalLoginToken(); });
+    });
+
+    function showToast(msg, type) {
+        var el = document.getElementById('toast');
+        if (!el) return;
+        el.textContent = msg || '';
+        el.className = 'toast show' + (type === 'ok' ? ' ok' : (type === 'err' ? ' err' : ''));
+        clearTimeout(showToast._t);
+        showToast._t = setTimeout(function () { el.className = 'toast'; }, 2800);
+    }
+
+    function esc(s) {
+        return String(s == null ? '' : s)
+            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;');
+    }
+
+    function postForm(url, data) {
+        var body = Object.keys(data).map(function (k) {
+            return encodeURIComponent(k) + '=' + encodeURIComponent(data[k] == null ? '' : data[k]);
+        }).join('&');
+        var headers = {
+            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+            'X-Requested-With': 'XMLHttpRequest'
+        };
+        try {
+            var t = localStorage.getItem('mproc_token') || '';
+            if (t) headers['X-Mproc-Token'] = t;
+        } catch (e) {}
+        return fetch(url, {
+            method: 'POST',
+            headers: headers,
+            body: body,
+            credentials: 'same-origin'
+        }).then(function (r) { return r.json(); });
+    }
+
+    function getJson(url) {
+        var headers = { 'X-Requested-With': 'XMLHttpRequest' };
+        try {
+            var t = localStorage.getItem('mproc_token') || '';
+            if (t) headers['X-Mproc-Token'] = t;
+        } catch (e) {}
+        return fetch(url, { method: 'GET', headers: headers, credentials: 'same-origin' })
+            .then(function (r) { return r.json(); });
+    }
+
+    var paneOrders = document.getElementById('pane-orders');
+    var paneMe = document.getElementById('pane-me');
+    var barTitle = document.getElementById('barTitle');
+    var barLogout = document.getElementById('barLogout');
+    document.querySelectorAll('#tabbar .tabbar-btn').forEach(function (btn) {
+        btn.addEventListener('click', function () {
+            var tab = btn.getAttribute('data-main-tab') || 'orders';
+            document.querySelectorAll('#tabbar .tabbar-btn').forEach(function (b) {
+                b.classList.toggle('active', b === btn);
+            });
+            if (tab === 'me') {
+                document.body.className = 'layout-me';
+                if (paneOrders) paneOrders.style.display = 'none';
+                if (paneMe) paneMe.style.display = 'block';
+                if (barTitle) barTitle.textContent = '我的';
+                if (barLogout) barLogout.style.display = '';
+            } else {
+                document.body.className = 'layout-orders';
+                if (paneOrders) paneOrders.style.display = 'flex';
+                if (paneMe) paneMe.style.display = 'none';
+                if (barTitle) barTitle.textContent = '订单询价';
+                if (barLogout) barLogout.style.display = 'none';
+            }
+        });
+    });
+
+    function renderRows(rows) {
+        var main = document.getElementById('listMain');
+        if (!main) return;
+        rows = Array.isArray(rows) ? rows : [];
+        if (!rows.length) {
+            main.innerHTML = '<div class="empty" id="emptyTip">暂无询价单,点击右上角「新增询价」</div>';
+            return;
+        }
+        var html = '';
+        rows.forEach(function (row) {
+            var title = String(row.CCYDH || '').trim();
+            var name = String(row.CYJMC || '').trim();
+            if (title && name) title += ' ' + name;
+            else title = title || name || '';
+            var progress = String(row.progress_text || '待询价');
+            html += '<div class="card">'
+                + '<div class="ord">' + esc(title) + '</div>'
+                + '<div class="meta">'
+                + '<div><b>需求部门:</b>' + esc(row.CCLBMMC || '') + '</div>'
+                + '<div><b>工序:</b>' + esc(row.CGYMC || '') + '</div>'
+                + '<div><b>发起人:</b>' + esc(row.cywyxm || '') + '</div>'
+                + '</div>'
+                + '<div class="tags">'
+                + '<span class="progress' + (progress === '已完结' ? ' is-done' : '') + '">' + esc(progress) + '</span>'
+                + (row.createtime ? '<span class="time">' + esc(row.createtime) + '</span>' : '')
+                + '<button type="button" class="btn-view-quote" data-scydgy-id="' + esc(row.scydgy_id || '') + '">查看报价</button>'
+                + '</div></div>';
+        });
+        main.innerHTML = html;
+    }
+
+    function loadList() {
+        var q = (document.getElementById('qInput').value || '').trim();
+        var url = listUrl + (listUrl.indexOf('?') >= 0 ? '&' : '?') + 'q=' + encodeURIComponent(q);
+        getJson(url).then(function (ret) {
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                var data = ret.data || {};
+                renderRows(data.rows || []);
+            } else if (ret && (ret.code === 401 || ret.code === '401')) {
+                clearLocalLoginToken();
+                window.location.href = loginUrl;
+            } else {
+                showToast((ret && ret.msg) ? ret.msg : '加载失败', 'err');
+            }
+        }).catch(function () {
+            showToast('网络错误', 'err');
+        });
+    }
+
+    document.getElementById('btnSearch').addEventListener('click', loadList);
+    document.getElementById('qInput').addEventListener('keydown', function (e) {
+        if (e.key === 'Enter') {
+            e.preventDefault();
+            loadList();
+        }
+    });
+
+    var rfqMask = document.getElementById('rfqMask');
+    var rfqSheet = document.getElementById('rfqSheet');
+    function openAddModal() {
+        resetForm(false);
+        if (rfqMask) {
+            rfqMask.classList.add('show');
+            rfqMask.setAttribute('aria-hidden', 'false');
+        }
+    }
+    function closeAddModal() {
+        if (rfqMask) {
+            rfqMask.classList.remove('show');
+            rfqMask.setAttribute('aria-hidden', 'true');
+        }
+    }
+    document.getElementById('btnOpenAdd').addEventListener('click', openAddModal);
+    document.getElementById('rfqClose').addEventListener('click', closeAddModal);
+    if (rfqMask) {
+        rfqMask.addEventListener('click', function (e) {
+            if (e.target === rfqMask) closeAddModal();
+        });
+    }
+    if (rfqSheet) rfqSheet.addEventListener('click', function (e) { e.stopPropagation(); });
+
+    var quoteMask = document.getElementById('quoteMask');
+    var quoteSheet = document.getElementById('quoteSheet');
+    var quoteBody = document.getElementById('quoteBody');
+    var quoteTitle = document.getElementById('quoteTitle');
+    function closeQuoteModal() {
+        if (quoteMask) {
+            quoteMask.classList.remove('show');
+            quoteMask.setAttribute('aria-hidden', 'true');
+        }
+    }
+    function openQuoteModal(sid) {
+        if (!quoteMask || !quoteBody) return;
+        if (quoteTitle) quoteTitle.textContent = '查看报价';
+        quoteBody.innerHTML = '<div class="rfq-quote-empty">加载中…</div>';
+        quoteMask.classList.add('show');
+        quoteMask.setAttribute('aria-hidden', 'false');
+        var url = quotesUrl + (quotesUrl.indexOf('?') >= 0 ? '&' : '?') + 'scydgy_id=' + encodeURIComponent(sid);
+        getJson(url).then(function (ret) {
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                var data = ret.data || {};
+                var list = Array.isArray(data.list) ? data.list : [];
+                var head = String(data.CCYDH || '').trim();
+                var name = String(data.CYJMC || '').trim();
+                if (quoteTitle) {
+                    quoteTitle.textContent = head ? ('报价 · ' + head) : '查看报价';
+                }
+                if (!list.length) {
+                    quoteBody.innerHTML = '<div class="rfq-quote-empty">暂无报价</div>';
+                    return;
+                }
+                var html = '<table class="rfq-quote-table"><thead><tr>'
+                    + '<th>供应商</th><th class="col-amt">单价</th>'
+                    + '</tr></thead><tbody>';
+                list.forEach(function (item) {
+                    var cn = item && item.company_name != null ? String(item.company_name) : '';
+                    var quoted = item && (parseInt(item.is_quoted, 10) === 1);
+                    var amt = item && item.amount_text != null ? String(item.amount_text) : '';
+                    html += '<tr><td>' + esc(cn) + '</td><td class="col-amt">'
+                        + (quoted ? esc(amt) : '<span class="muted">未填写</span>')
+                        + '</td></tr>';
+                });
+                html += '</tbody></table>';
+                if (name) {
+                    html = '<div style="margin:0 0 8px;font-size:12px;color:#888;">' + esc(name) + '</div>' + html;
+                }
+                quoteBody.innerHTML = html;
+            } else if (ret && (ret.code === 401 || ret.code === '401')) {
+                clearLocalLoginToken();
+                window.location.href = loginUrl;
+            } else {
+                quoteBody.innerHTML = '<div class="rfq-quote-empty">' + esc((ret && ret.msg) ? ret.msg : '加载失败') + '</div>';
+            }
+        }).catch(function () {
+            quoteBody.innerHTML = '<div class="rfq-quote-empty">网络错误</div>';
+        });
+    }
+    var listMain = document.getElementById('listMain');
+    if (listMain) {
+        listMain.addEventListener('click', function (e) {
+            var btn = e.target && e.target.closest ? e.target.closest('.btn-view-quote') : null;
+            if (!btn) return;
+            e.preventDefault();
+            var sid = btn.getAttribute('data-scydgy-id') || '';
+            if (!sid) {
+                showToast('缺少询价单标识', 'err');
+                return;
+            }
+            openQuoteModal(sid);
+        });
+    }
+    var quoteClose = document.getElementById('quoteClose');
+    if (quoteClose) quoteClose.addEventListener('click', closeQuoteModal);
+    if (quoteMask) {
+        quoteMask.addEventListener('click', function (e) {
+            if (e.target === quoteMask) closeQuoteModal();
+        });
+    }
+    if (quoteSheet) quoteSheet.addEventListener('click', function (e) { e.stopPropagation(); });
+
+    function resetForm(keepCcydh) {
+        document.getElementById('cyjmc').value = '';
+        document.getElementById('cclbmmc').value = defaultDept || '营销中心';
+        document.getElementById('cgymc').value = '';
+        document.getElementById('cdw').value = '';
+        document.getElementById('thisQty').value = '';
+        document.getElementById('ceilingPrice').value = '';
+        document.getElementById('cdf').value = '';
+        document.getElementById('mbz').value = '';
+        document.getElementById('cywyxm').value = defaultStarter || '';
+        if (!keepCcydh) {
+            document.getElementById('ccydh').value = nextCcydh || '';
+        }
+    }
+
+    document.getElementById('btnReset').addEventListener('click', function () {
+        resetForm(true);
+    });
+
+    (function bindDeptCombo() {
+        var combo = document.getElementById('rfqDeptCombo');
+        var input = document.getElementById('cclbmmc');
+        var toggle = document.getElementById('rfqDeptToggle');
+        var menu = document.getElementById('rfqDeptMenu');
+        if (!combo || !input || !menu) return;
+        var items = menu.querySelectorAll('.rfq-dept-item');
+        var emptyEl = menu.querySelector('.rfq-dept-empty');
+
+        function openMenu() {
+            combo.classList.add('open');
+            filterMenu(input.value || '');
+        }
+        function closeMenu() {
+            combo.classList.remove('open');
+        }
+        function filterMenu(keyword) {
+            var kw = String(keyword || '').trim().toLowerCase();
+            var hit = 0;
+            items.forEach(function (btn) {
+                var text = String(btn.getAttribute('data-value') || btn.textContent || '').trim();
+                var show = !kw || text.toLowerCase().indexOf(kw) !== -1;
+                btn.style.display = show ? '' : 'none';
+                if (show) hit++;
+            });
+            if (emptyEl) emptyEl.style.display = hit === 0 ? 'block' : 'none';
+        }
+
+        if (toggle) {
+            toggle.addEventListener('click', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+                if (combo.classList.contains('open')) closeMenu();
+                else openMenu();
+            });
+        }
+        input.addEventListener('focus', function () {
+            openMenu();
+        });
+        input.addEventListener('click', function () {
+            openMenu();
+        });
+        input.addEventListener('input', function () {
+            openMenu();
+            filterMenu(input.value || '');
+        });
+        items.forEach(function (btn) {
+            btn.addEventListener('click', function (e) {
+                e.preventDefault();
+                var v = String(btn.getAttribute('data-value') || btn.textContent || '').trim();
+                input.value = v;
+                closeMenu();
+            });
+        });
+        document.addEventListener('click', function (e) {
+            if (!combo.contains(e.target)) closeMenu();
+        });
+        if (rfqSheet) {
+            rfqSheet.addEventListener('scroll', closeMenu, true);
+        }
+        var sheetBody = document.querySelector('.rfq-sheet-body');
+        if (sheetBody) {
+            sheetBody.addEventListener('scroll', closeMenu);
+        }
+    })();
+
+    document.getElementById('btnSubmit').addEventListener('click', function () {
+        var cyjmc = (document.getElementById('cyjmc').value || '').trim();
+        var cclbmmc = (document.getElementById('cclbmmc').value || '').trim();
+        if (!cyjmc) {
+            showToast('请填写印件名称', 'err');
+            return;
+        }
+        if (!cclbmmc) {
+            showToast('请填写需求部门', 'err');
+            return;
+        }
+        var btn = document.getElementById('btnSubmit');
+        btn.disabled = true;
+        postForm(saveUrl, {
+            CYJMC: cyjmc,
+            CCLBMMC: cclbmmc,
+            CGYMC: (document.getElementById('cgymc').value || '').trim(),
+            CDW: (document.getElementById('cdw').value || '').trim(),
+            This_quantity: (document.getElementById('thisQty').value || '').trim(),
+            ceilingPrice: (document.getElementById('ceilingPrice').value || '').trim(),
+            CDF: (document.getElementById('cdf').value || '').trim(),
+            MBZ: (document.getElementById('mbz').value || '').trim()
+        }).then(function (ret) {
+            btn.disabled = false;
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                var data = ret.data || {};
+                if (data.nextOrderCcydh) {
+                    nextCcydh = String(data.nextOrderCcydh);
+                    document.getElementById('ccydh').value = nextCcydh;
+                }
+                showToast((ret.msg || '新增成功') + (data.CCYDH ? (':' + data.CCYDH) : ''), 'ok');
+                closeAddModal();
+                loadList();
+            } else if (ret && (ret.code === 401 || ret.code === '401')) {
+                clearLocalLoginToken();
+                window.location.href = loginUrl;
+            } else {
+                showToast((ret && ret.msg) ? ret.msg : '新增失败', 'err');
+            }
+        }).catch(function () {
+            btn.disabled = false;
+            showToast('网络错误', 'err');
+        });
+    });
+
+    function passwordHasSequentialRun(pwd, runLen) {
+        runLen = runLen || 4;
+        var lower = String(pwd || '').toLowerCase();
+        var n = lower.length;
+        if (n < runLen) return false;
+        for (var i = 0; i <= n - runLen; i++) {
+            var asc = true, desc = true;
+            for (var j = 1; j < runLen; j++) {
+                var prev = lower.charCodeAt(i + j - 1);
+                var cur = lower.charCodeAt(i + j);
+                if (cur !== prev + 1) asc = false;
+                if (cur !== prev - 1) desc = false;
+                if (!asc && !desc) break;
+            }
+            if (asc || desc) return true;
+        }
+        return false;
+    }
+    function validateStrongPassword(pwd) {
+        pwd = String(pwd || '');
+        if (pwd.length < 8 || pwd.length > 20) return '密码长度须为8~20位';
+        if (/\s/.test(pwd)) return '密码不能包含空格';
+        if (/^\d+$/.test(pwd)) return '密码不能为纯数字,请同时包含字母';
+        if (/^[a-zA-Z]+$/.test(pwd)) return '密码不能为纯字母,请同时包含数字';
+        if (!/[a-zA-Z]/.test(pwd) || !/\d/.test(pwd)) return '密码须同时包含字母和数字';
+        if (/(.)\1{3,}/.test(pwd)) return '密码不能包含过多重复字符(如1111、aaaa)';
+        if (passwordHasSequentialRun(pwd, 4)) return '密码不能包含连续字符(如1234、abcd)';
+        var weak = {
+            '12345678': 1, '123456789': 1, '1234567890': 1, '87654321': 1, '01234567': 1,
+            'password': 1, 'password1': 1, 'passw0rd': 1, 'qwerty12': 1, 'qwertyui': 1,
+            'abc12345': 1, 'abcd1234': 1, 'a1b2c3d4': 1, '11111111': 1, '00000000': 1,
+            '88888888': 1, '66666666': 1, '11223344': 1, '1qaz2wsx': 1, 'qazwsxed': 1
+        };
+        if (weak[pwd.toLowerCase()]) return '密码过于简单,请重新设置';
+        return '';
+    }
+    (function bindChangePwd() {
+        var pwdMask = document.getElementById('pwdMask');
+        var pwdSheet = document.getElementById('pwdSheet');
+        var btnMePwd = document.getElementById('btn-me-change-pwd');
+        var inpOldPwd = document.getElementById('inp-old-pwd');
+        var inpNewPwd = document.getElementById('inp-new-pwd');
+        var inpRenewPwd = document.getElementById('inp-renew-pwd');
+        var btnPwdSave = document.getElementById('pwdSave');
+        var btnPwdCancel = document.getElementById('pwdCancel');
+        function openPwdModal() {
+            if (inpOldPwd) inpOldPwd.value = '';
+            if (inpNewPwd) inpNewPwd.value = '';
+            if (inpRenewPwd) inpRenewPwd.value = '';
+            if (pwdMask) {
+                pwdMask.classList.add('show');
+                pwdMask.setAttribute('aria-hidden', 'false');
+            }
+        }
+        function closePwdModal() {
+            if (pwdMask) {
+                pwdMask.classList.remove('show');
+                pwdMask.setAttribute('aria-hidden', 'true');
+            }
+        }
+        if (btnMePwd) btnMePwd.addEventListener('click', openPwdModal);
+        if (btnPwdCancel) btnPwdCancel.addEventListener('click', closePwdModal);
+        if (pwdMask) {
+            pwdMask.addEventListener('click', function (e) {
+                if (e.target === pwdMask) closePwdModal();
+            });
+        }
+        if (pwdSheet) pwdSheet.addEventListener('click', function (e) { e.stopPropagation(); });
+        if (btnPwdSave) {
+            btnPwdSave.addEventListener('click', function () {
+                var oldP = inpOldPwd ? inpOldPwd.value : '';
+                var newP = inpNewPwd ? inpNewPwd.value : '';
+                var renP = inpRenewPwd ? inpRenewPwd.value : '';
+                if (!oldP || !newP || !renP) {
+                    showToast('请填写完整', 'err');
+                    return;
+                }
+                var tip = validateStrongPassword(newP);
+                if (tip) {
+                    showToast(tip, 'err');
+                    return;
+                }
+                if (newP !== renP) {
+                    showToast('两次输入的新密码不一致', 'err');
+                    return;
+                }
+                if (oldP === newP) {
+                    showToast('新密码不能与旧密码相同', 'err');
+                    return;
+                }
+                btnPwdSave.disabled = true;
+                postForm(changePwdUrl, {
+                    old_password: oldP,
+                    new_password: newP,
+                    renew_password: renP
+                }).then(function (ret) {
+                    btnPwdSave.disabled = false;
+                    if (ret && (ret.code === 1 || ret.code === '1')) {
+                        closePwdModal();
+                        clearLocalLoginToken();
+                        showToast(ret.msg || '密码已修改,请重新登录', 'ok');
+                        setTimeout(function () {
+                            window.location.href = ret.url || loginUrl;
+                        }, 900);
+                    } else {
+                        showToast((ret && ret.msg) ? ret.msg : '修改失败', 'err');
+                    }
+                }).catch(function () {
+                    btnPwdSave.disabled = false;
+                    showToast('网络错误', 'err');
+                });
+            });
+        }
+    })();
+})();
+</script>
+</body>
+</html>