12 Commits 9e47b2113e ... c62a5550cc

Auteur SHA1 Message Date
  PC-7\Administrator c62a5550cc 添加功能 il y a 10 mois
  “陈博论” dc2eec2cfb 注册和忘记密码发送验证码逻辑优化 il y a 10 mois
  “陈博论” a89a50e5ae 注册修改 il y a 10 mois
  “陈博论” a511deb462 交易配置 il y a 10 mois
  “陈博论” 26bda0f854 交易管理修改 il y a 10 mois
  “陈博论” 9ec0a13055 交易模块编写 il y a 10 mois
  “陈博论” 5c333706ae 应用模块编写完成 il y a 10 mois
  “陈博论” 4446e4a96d 应用模块调整 il y a 10 mois
  “陈博论” a53045d162 交易管理 il y a 10 mois
  “陈博论” 351deb21d4 套餐模块修改 il y a 10 mois
  “陈博论” aa2d5b9b22 应用管理修改 il y a 10 mois
  “陈博论” 238adb805e 模块修改 il y a 10 mois
55 fichiers modifiés avec 6887 ajouts et 1708 suppressions
  1. 441 0
      app/admin/controller/distribution/Distribution.php
  2. 441 0
      app/admin/controller/order/Distribution.php
  3. 426 0
      app/admin/controller/order/Order.php
  4. 353 0
      app/admin/controller/order/Withdrawal.php
  5. 101 135
      app/admin/controller/package/Package.php
  6. 51 85
      app/admin/controller/package/Product.php
  7. 539 0
      app/admin/controller/pay/AccountConfig.php
  8. 455 0
      app/admin/controller/pay/Config.php
  9. 20 0
      app/admin/controller/userCenter/Index.php
  10. 13 6
      app/admin/controller/userCenter/User.php
  11. 2 1
      composer.json
  12. 1 1
      web/.env
  13. 1 1
      web/.env.development
  14. 26 0
      web/src/api/backend/order/distribution.ts
  15. 10 0
      web/src/api/backend/order/order.ts
  16. 40 0
      web/src/api/backend/order/withdrawal.ts
  17. 96 0
      web/src/api/backend/package/package.ts
  18. 51 0
      web/src/api/backend/package/product.ts
  19. 19 0
      web/src/api/backend/pay/accountConfig.ts
  20. 28 0
      web/src/api/backend/pay/config.ts
  21. 12 3
      web/src/api/backend/userCenter/index.ts
  22. 21 7
      web/src/api/backend/userCenter/user.ts
  23. 1 0
      web/src/assets/icons/pay-list.svg
  24. 0 0
      web/src/assets/icons/withdrawal-configuration.svg
  25. 2 1
      web/src/components/baInput/components/baUpload.vue
  26. 147 0
      web/src/views/backend/order/distribution/index.vue
  27. 224 0
      web/src/views/backend/order/distribution/settingDialog.vue
  28. 156 0
      web/src/views/backend/order/order/index.vue
  29. 177 0
      web/src/views/backend/order/withdrawal/index.vue
  30. 163 0
      web/src/views/backend/order/withdrawal/reviewDialog.vue
  31. 507 0
      web/src/views/backend/package/package/detail.vue
  32. 231 0
      web/src/views/backend/package/package/index.vue
  33. 379 0
      web/src/views/backend/package/package/popupForm.vue
  34. 510 0
      web/src/views/backend/package/package/specDialog.vue
  35. 98 84
      web/src/views/backend/package/product/index.vue
  36. 120 0
      web/src/views/backend/package/product/popupForm.vue
  37. 247 0
      web/src/views/backend/pay/accountConfig/index.vue
  38. 227 0
      web/src/views/backend/pay/accountConfig/popupForm.vue
  39. 251 0
      web/src/views/backend/pay/config/index.vue
  40. 131 0
      web/src/views/backend/pay/config/popupForm.vue
  41. 0 13
      web/src/views/backend/payment/alipay/index.vue
  42. 0 330
      web/src/views/backend/product/combo/detail.vue
  43. 0 357
      web/src/views/backend/product/combo/index.vue
  44. 0 436
      web/src/views/backend/product/combo/popupForm.vue
  45. 0 152
      web/src/views/backend/product/product/popupForm.vue
  46. 0 14
      web/src/views/backend/transaction/distribution/index.vue
  47. 0 14
      web/src/views/backend/transaction/order/index.vue
  48. 0 14
      web/src/views/backend/transaction/withdraw/index.vue
  49. 6 1
      web/src/views/backend/user/group/index.vue
  50. 44 20
      web/src/views/backend/userCenter/index/index.vue
  51. 36 11
      web/src/views/backend/userCenter/index/popupForm.vue
  52. 15 5
      web/src/views/backend/userCenter/user/index.vue
  53. 41 8
      web/src/views/backend/userCenter/user/popupForm.vue
  54. 15 6
      web/src/views/frontend/Register.vue
  55. 12 3
      web/src/views/frontend/Reset.vue

+ 441 - 0
app/admin/controller/distribution/Distribution.php

@@ -0,0 +1,441 @@
+<?php
+/**
+ * 分销管理控制器
+ */
+
+namespace app\admin\controller\distribution;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class Distribution extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 分销订单列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 分佣产品搜索
+        if (!empty($this->request->get('product_name'))) {
+            $productName = $this->request->get('product_name');
+            $where[] = ['p.name', 'like', '%' . $productName . '%'];
+        }
+
+        // 分佣人搜索
+        if (!empty($this->request->get('distributor_name'))) {
+            $distributorName = $this->request->get('distributor_name');
+            $where[] = ['du.name', 'like', '%' . $distributorName . '%'];
+        }
+
+        // 购买人搜索
+        if (!empty($this->request->get('buyer_name'))) {
+            $buyerName = $this->request->get('buyer_name');
+            $where[] = ['bu.name', 'like', '%' . $buyerName . '%'];
+        }
+
+        // 关联订单号搜索
+        if (!empty($this->request->get('order_no'))) {
+            $orderNo = $this->request->get('order_no');
+            $where[] = ['o.order_no', 'like', '%' . $orderNo . '%'];
+        }
+
+        // 状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['dr.status'] = $status;
+        }
+
+        // 创建时间范围筛选
+        if (!empty($this->request->get('create_time_range'))) {
+            $timeRange = $this->request->get('create_time_range');
+            $timeArr = explode(' - ', $timeRange);
+            if (count($timeArr) == 2) {
+                $startTime = $timeArr[0];
+                $endTime = $timeArr[1];
+                $where[] = ['dr.create_time', '>=', $startTime . ' 00:00:00'];
+                $where[] = ['dr.create_time', '<=', $endTime . ' 23:59:59'];
+            }
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['dr.create_time'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询分销记录
+        $distributionList = Db::name('distribution_record')
+            ->alias('dr')
+            ->leftJoin('order o', 'dr.order_id = o.id')
+            ->leftJoin('product p', 'dr.product_id = p.id')
+            ->leftJoin('user bu', 'dr.buyer_id = bu.id')
+            ->leftJoin('user du', 'dr.distributor_id = du.id')
+            ->field('
+                dr.id as 编号,
+                o.order_no as 关联订单号,
+                p.name as 分佣产品,
+                bu.name as 购买人,
+                du.name as 分佣人,
+                dr.commission_amount as 分佣金额,
+                dr.create_time as 创建时间,
+                dr.status,
+                dr.commission_rate
+            ')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 状态转换
+        $list = $distributionList->items();
+        foreach ($list as &$item) {
+            $statusMap = [
+                1 => '待结算',
+                2 => '已结算',
+                3 => '已取消'
+            ];
+            $item['status_text'] = $statusMap[$item['status']] ?? '未知';
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $distributionList->total()
+        ]);
+    }
+
+    /**
+     * 获取分销设置
+     * @return void
+     */
+    public function getSetting(): void
+    {
+        $setting = Db::name('distribution_setting')
+            ->order('id', 'desc')
+            ->find();
+
+        if (!$setting) {
+            // 如果不存在,创建默认设置
+            $defaultSetting = [
+                'default_rate' => 15.00,
+                'min_withdrawal' => 20.00,
+                'max_withdrawal' => 50000.00,
+                'withdrawal_fee_rate' => 0.00,
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            Db::name('distribution_setting')->insert($defaultSetting);
+            $setting = $defaultSetting;
+        }
+
+        $this->success('获取成功', $setting);
+    }
+
+    /**
+     * 更新分销设置
+     * @return void
+     */
+    public function updateSetting(): void
+    {
+        $defaultRate = $this->request->post('default_rate', 15.00);
+        $minWithdrawal = $this->request->post('min_withdrawal', 20.00);
+        $maxWithdrawal = $this->request->post('max_withdrawal', 50000.00);
+        $withdrawalFeeRate = $this->request->post('withdrawal_fee_rate', 0.00);
+
+        if ($defaultRate <= 0 || $defaultRate > 100) {
+            $this->error(__('分销佣金比例必须在0-100之间'));
+        }
+
+        if ($minWithdrawal <= 0) {
+            $this->error(__('最低提现金额必须大于0'));
+        }
+
+        if ($maxWithdrawal <= $minWithdrawal) {
+            $this->error(__('最高提现金额必须大于最低提现金额'));
+        }
+
+        $data = [
+            'default_rate' => $defaultRate,
+            'min_withdrawal' => $minWithdrawal,
+            'max_withdrawal' => $maxWithdrawal,
+            'withdrawal_fee_rate' => $withdrawalFeeRate,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            // 检查是否已有设置
+            $existingSetting = Db::name('distribution_setting')->find();
+
+            if ($existingSetting) {
+                // 更新
+                $result = Db::name('distribution_setting')
+                    ->where('id', $existingSetting['id'])
+                    ->update($data);
+            } else {
+                // 新增
+                $result = Db::name('distribution_setting')
+                    ->insert($data);
+            }
+
+            if ($result) {
+                $this->success('更新成功');
+            } else {
+                $this->error(__('更新失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 手动结算分销佣金
+     * @return void
+     */
+    public function settleCommission(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取分销记录
+            $distributionRecord = Db::name('distribution_record')
+                ->where('id', $id)
+                ->lock(true)
+                ->find();
+
+            if (!$distributionRecord) {
+                throw new \Exception('分销记录不存在');
+            }
+
+            if ($distributionRecord['status'] != 1) {
+                throw new \Exception('只有待结算的记录才能结算');
+            }
+
+            // 更新分销记录状态
+            $updateResult = Db::name('distribution_record')
+                ->where('id', $id)
+                ->update([
+                    'status' => 2,
+                    'settle_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if (!$updateResult) {
+                throw new \Exception('更新分销记录失败');
+            }
+
+            // 更新用户佣金
+            $userCommission = Db::name('user_commission')
+                ->where('user_id', $distributionRecord['distributor_id'])
+                ->lock(true)
+                ->find();
+
+            if ($userCommission) {
+                // 更新现有记录
+                $commissionResult = Db::name('user_commission')
+                    ->where('user_id', $distributionRecord['distributor_id'])
+                    ->update([
+                        'total_commission' => Db::raw('total_commission + ' . $distributionRecord['commission_amount']),
+                        'current_commission' => Db::raw('current_commission + ' . $distributionRecord['commission_amount']),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            } else {
+                // 创建新记录
+                $commissionResult = Db::name('user_commission')
+                    ->insert([
+                        'user_id' => $distributionRecord['distributor_id'],
+                        'total_commission' => $distributionRecord['commission_amount'],
+                        'current_commission' => $distributionRecord['commission_amount'],
+                        'promotion_link' => 'xxxx.com?from=' . $distributionRecord['distributor_id'],
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+            if (!$commissionResult) {
+                throw new \Exception('更新用户佣金失败');
+            }
+
+            Db::commit();
+            $this->success('结算成功');
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('结算失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 批量结算分销佣金
+     * @return void
+     */
+    public function batchSettle(): void
+    {
+        $ids = $this->request->post('ids');
+
+        if (empty($ids)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        $successCount = 0;
+        $failCount = 0;
+        $messages = [];
+
+        foreach ($ids as $id) {
+            try {
+                Db::startTrans();
+
+                $distributionRecord = Db::name('distribution_record')
+                    ->where('id', $id)
+                    ->lock(true)
+                    ->find();
+
+                if (!$distributionRecord) {
+                    throw new \Exception('分销记录不存在');
+                }
+
+                if ($distributionRecord['status'] != 1) {
+                    throw new \Exception('只有待结算的记录才能结算');
+                }
+
+                // 更新分销记录状态
+                Db::name('distribution_record')
+                    ->where('id', $id)
+                    ->update([
+                        'status' => 2,
+                        'settle_time' => date('Y-m-d H:i:s')
+                    ]);
+
+                // 更新用户佣金
+                $userCommission = Db::name('user_commission')
+                    ->where('user_id', $distributionRecord['distributor_id'])
+                    ->lock(true)
+                    ->find();
+
+                if ($userCommission) {
+                    Db::name('user_commission')
+                        ->where('user_id', $distributionRecord['distributor_id'])
+                        ->update([
+                            'total_commission' => Db::raw('total_commission + ' . $distributionRecord['commission_amount']),
+                            'current_commission' => Db::raw('current_commission + ' . $distributionRecord['commission_amount']),
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                } else {
+                    Db::name('user_commission')
+                        ->insert([
+                            'user_id' => $distributionRecord['distributor_id'],
+                            'total_commission' => $distributionRecord['commission_amount'],
+                            'current_commission' => $distributionRecord['commission_amount'],
+                            'promotion_link' => 'xxxx.com?from=' . $distributionRecord['distributor_id'],
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                }
+
+                Db::commit();
+                $successCount++;
+
+            } catch (\Exception $e) {
+                Db::rollback();
+                $failCount++;
+                $messages[] = "记录ID {$id}: " . $e->getMessage();
+            }
+        }
+
+        $message = "结算完成,成功: {$successCount}条,失败: {$failCount}条";
+        if (!empty($messages)) {
+            $message .= "。失败原因:" . implode('; ', $messages);
+        }
+
+        $this->success($message);
+    }
+
+    /**
+     * 分销统计
+     * @return void
+     */
+    public function statistics(): void
+    {
+        // 总分销记录数
+        $totalCount = Db::name('distribution_record')->count();
+
+        // 待结算记录数
+        $pendingCount = Db::name('distribution_record')
+            ->where('status', 1)
+            ->count();
+
+        // 已结算记录数
+        $settledCount = Db::name('distribution_record')
+            ->where('status', 2)
+            ->count();
+
+        // 总分销金额
+        $totalCommission = Db::name('distribution_record')
+            ->where('status', 2)
+            ->sum('commission_amount');
+
+        // 今日分销统计
+        $todayStart = date('Y-m-d 00:00:00');
+        $todayEnd = date('Y-m-d 23:59:59');
+        $todayCount = Db::name('distribution_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->count();
+
+        $todayCommission = Db::name('distribution_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->sum('commission_amount');
+
+        // 分销人数统计
+        $distributorCount = Db::name('distribution_record')
+            ->distinct(true)
+            ->field('distributor_id')
+            ->count();
+
+        $stats = [
+            'total_count' => $totalCount,
+            'pending_count' => $pendingCount,
+            'settled_count' => $settledCount,
+            'total_commission' => $totalCommission ?? 0,
+            'today_count' => $todayCount,
+            'today_commission' => $todayCommission ?? 0,
+            'distributor_count' => $distributorCount
+        ];
+
+        $this->success('获取成功', $stats);
+    }
+
+    /**
+     * 获取分销状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 1, 'label' => '待结算'],
+            ['value' => 2, 'label' => '已结算'],
+            ['value' => 3, 'label' => '已取消']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+}

+ 441 - 0
app/admin/controller/order/Distribution.php

@@ -0,0 +1,441 @@
+<?php
+/**
+ * 分销管理控制器
+ */
+
+namespace app\admin\controller\order;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class Distribution extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 分销订单列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 分佣产品搜索
+        if (!empty($this->request->get('product_name'))) {
+            $productName = $this->request->get('product_name');
+            $where[] = ['p.name', 'like', '%' . $productName . '%'];
+        }
+
+        // 分佣人搜索
+        if (!empty($this->request->get('distributor_name'))) {
+            $distributorName = $this->request->get('distributor_name');
+            $where[] = ['du.name', 'like', '%' . $distributorName . '%'];
+        }
+
+        // 购买人搜索
+        if (!empty($this->request->get('buyer_name'))) {
+            $buyerName = $this->request->get('buyer_name');
+            $where[] = ['bu.name', 'like', '%' . $buyerName . '%'];
+        }
+
+        // 关联订单号搜索
+        if (!empty($this->request->get('order_no'))) {
+            $orderNo = $this->request->get('order_no');
+            $where[] = ['o.order_no', 'like', '%' . $orderNo . '%'];
+        }
+
+        // 状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['dr.status'] = $status;
+        }
+
+        // 创建时间范围筛选
+        if (!empty($this->request->get('create_time_range'))) {
+            $timeRange = $this->request->get('create_time_range');
+            $timeArr = explode(' - ', $timeRange);
+            if (count($timeArr) == 2) {
+                $startTime = $timeArr[0];
+                $endTime = $timeArr[1];
+                $where[] = ['dr.create_time', '>=', $startTime . ' 00:00:00'];
+                $where[] = ['dr.create_time', '<=', $endTime . ' 23:59:59'];
+            }
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['dr.create_time'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询分销记录
+        $distributionList = Db::name('distribution_record')
+            ->alias('dr')
+            ->leftJoin('order o', 'dr.order_id = o.id')
+            ->leftJoin('product p', 'dr.product_id = p.id')
+            ->leftJoin('user_on bu', 'dr.buyer_id = bu.id')
+            ->leftJoin('user_on du', 'dr.distributor_id = du.id')
+            ->field('
+                dr.id,
+                o.order_no,
+                p.name as commission_product,
+                bu.name as g_name,
+                du.name as f_name,
+                dr.commission_amount,
+                dr.create_time,
+                dr.status,
+                dr.commission_rate
+            ')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 状态转换
+        $list = $distributionList->items();
+        foreach ($list as &$item) {
+            $statusMap = [
+                1 => '待结算',
+                2 => '已结算',
+                3 => '已取消'
+            ];
+            $item['status_text'] = $statusMap[$item['status']] ?? '未知';
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $distributionList->total()
+        ]);
+    }
+
+    /**
+     * 获取分销设置
+     * @return void
+     */
+    public function getSetting(): void
+    {
+        $setting = Db::name('distribution_setting')
+            ->order('id', 'desc')
+            ->find();
+
+        if (!$setting) {
+            // 如果不存在,创建默认设置
+            $defaultSetting = [
+                'default_rate' => 15.00,
+                'min_withdrawal' => 20.00,
+                'max_withdrawal' => 50000.00,
+                'withdrawal_fee_rate' => 0.00,
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            Db::name('distribution_setting')->insert($defaultSetting);
+            $setting = $defaultSetting;
+        }
+
+        $this->success('获取成功', $setting);
+    }
+
+    /**
+     * 更新分销设置
+     * @return void
+     */
+    public function updateSetting(): void
+    {
+        $defaultRate = $this->request->post('default_rate', 15.00);
+        $minWithdrawal = $this->request->post('min_withdrawal', 20.00);
+        $maxWithdrawal = $this->request->post('max_withdrawal', 50000.00);
+        $withdrawalFeeRate = $this->request->post('withdrawal_fee_rate', 0.00);
+
+        if ($defaultRate <= 0 || $defaultRate > 100) {
+            $this->error(__('分销佣金比例必须在0-100之间'));
+        }
+
+        if ($minWithdrawal <= 0) {
+            $this->error(__('最低提现金额必须大于0'));
+        }
+
+        if ($maxWithdrawal <= $minWithdrawal) {
+            $this->error(__('最高提现金额必须大于最低提现金额'));
+        }
+
+        $data = [
+            'default_rate' => $defaultRate,
+            'min_withdrawal' => $minWithdrawal,
+            'max_withdrawal' => $maxWithdrawal,
+            'withdrawal_fee_rate' => $withdrawalFeeRate,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            // 检查是否已有设置
+            $existingSetting = Db::name('distribution_setting')->value('id');
+            if ($existingSetting) {
+                // 更新
+                $result = Db::name('distribution_setting')
+                    ->where('id', $existingSetting)
+                    ->update($data);
+            } else {
+                // 新增
+                $result = Db::name('distribution_setting')
+                    ->insert($data);
+            }
+
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success('更新成功');
+        } else {
+            $this->error(__('更新失败'));
+        }
+    }
+
+    /**
+     * 手动结算分销佣金
+     * @return void
+     */
+    public function settleCommission(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取分销记录
+            $distributionRecord = Db::name('distribution_record')
+                ->where('id', $id)
+                ->lock(true)
+                ->find();
+
+            if (!$distributionRecord) {
+                throw new \Exception('分销记录不存在');
+            }
+
+            if ($distributionRecord['status'] != 1) {
+                throw new \Exception('只有待结算的记录才能结算');
+            }
+
+            // 更新分销记录状态
+            $updateResult = Db::name('distribution_record')
+                ->where('id', $id)
+                ->update([
+                    'status' => 2,
+                    'settle_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if (!$updateResult) {
+                throw new \Exception('更新分销记录失败');
+            }
+
+            // 更新用户佣金
+            $userCommission = Db::name('user_commission')
+                ->where('user_id', $distributionRecord['distributor_id'])
+                ->lock(true)
+                ->find();
+
+            if ($userCommission) {
+                // 更新现有记录
+                $commissionResult = Db::name('user_commission')
+                    ->where('user_id', $distributionRecord['distributor_id'])
+                    ->update([
+                        'total_commission' => Db::raw('total_commission + ' . $distributionRecord['commission_amount']),
+                        'current_commission' => Db::raw('current_commission + ' . $distributionRecord['commission_amount']),
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            } else {
+                // 创建新记录
+                $commissionResult = Db::name('user_commission')
+                    ->insert([
+                        'user_id' => $distributionRecord['distributor_id'],
+                        'total_commission' => $distributionRecord['commission_amount'],
+                        'current_commission' => $distributionRecord['commission_amount'],
+                        'promotion_link' => 'xxxx.com?from=' . $distributionRecord['distributor_id'],
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+            if (!$commissionResult) {
+                throw new \Exception('更新用户佣金失败');
+            }
+
+            Db::commit();
+            $this->success('结算成功');
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('结算失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 批量结算分销佣金
+     * @return void
+     */
+    public function batchSettle(): void
+    {
+        $ids = $this->request->post('ids');
+
+        if (empty($ids)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        $successCount = 0;
+        $failCount = 0;
+        $messages = [];
+
+        foreach ($ids as $id) {
+            try {
+                Db::startTrans();
+
+                $distributionRecord = Db::name('distribution_record')
+                    ->where('id', $id)
+                    ->lock(true)
+                    ->find();
+
+                if (!$distributionRecord) {
+                    throw new \Exception('分销记录不存在');
+                }
+
+                if ($distributionRecord['status'] != 1) {
+                    throw new \Exception('只有待结算的记录才能结算');
+                }
+
+                // 更新分销记录状态
+                Db::name('distribution_record')
+                    ->where('id', $id)
+                    ->update([
+                        'status' => 2,
+                        'settle_time' => date('Y-m-d H:i:s')
+                    ]);
+
+                // 更新用户佣金
+                $userCommission = Db::name('user_commission')
+                    ->where('user_id', $distributionRecord['distributor_id'])
+                    ->lock(true)
+                    ->find();
+
+                if ($userCommission) {
+                    Db::name('user_commission')
+                        ->where('user_id', $distributionRecord['distributor_id'])
+                        ->update([
+                            'total_commission' => Db::raw('total_commission + ' . $distributionRecord['commission_amount']),
+                            'current_commission' => Db::raw('current_commission + ' . $distributionRecord['commission_amount']),
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                } else {
+                    Db::name('user_commission')
+                        ->insert([
+                            'user_id' => $distributionRecord['distributor_id'],
+                            'total_commission' => $distributionRecord['commission_amount'],
+                            'current_commission' => $distributionRecord['commission_amount'],
+                            'promotion_link' => 'xxxx.com?from=' . $distributionRecord['distributor_id'],
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                }
+
+                Db::commit();
+                $successCount++;
+
+            } catch (\Exception $e) {
+                Db::rollback();
+                $failCount++;
+                $messages[] = "记录ID {$id}: " . $e->getMessage();
+            }
+        }
+
+        $message = "结算完成,成功: {$successCount}条,失败: {$failCount}条";
+        if (!empty($messages)) {
+            $message .= "。失败原因:" . implode('; ', $messages);
+        }
+
+        $this->success($message);
+    }
+
+    /**
+     * 分销统计
+     * @return void
+     */
+    public function statistics(): void
+    {
+        // 总分销记录数
+        $totalCount = Db::name('distribution_record')->count();
+
+        // 待结算记录数
+        $pendingCount = Db::name('distribution_record')
+            ->where('status', 1)
+            ->count();
+
+        // 已结算记录数
+        $settledCount = Db::name('distribution_record')
+            ->where('status', 2)
+            ->count();
+
+        // 总分销金额
+        $totalCommission = Db::name('distribution_record')
+            ->where('status', 2)
+            ->sum('commission_amount');
+
+        // 今日分销统计
+        $todayStart = date('Y-m-d 00:00:00');
+        $todayEnd = date('Y-m-d 23:59:59');
+        $todayCount = Db::name('distribution_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->count();
+
+        $todayCommission = Db::name('distribution_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->sum('commission_amount');
+
+        // 分销人数统计
+        $distributorCount = Db::name('distribution_record')
+            ->distinct(true)
+            ->field('distributor_id')
+            ->count();
+
+        $stats = [
+            'total_count' => $totalCount,
+            'pending_count' => $pendingCount,
+            'settled_count' => $settledCount,
+            'total_commission' => $totalCommission ?? 0,
+            'today_count' => $todayCount,
+            'today_commission' => $todayCommission ?? 0,
+            'distributor_count' => $distributorCount
+        ];
+
+        $this->success('获取成功', $stats);
+    }
+
+    /**
+     * 获取分销状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 1, 'label' => '待结算'],
+            ['value' => 2, 'label' => '已结算'],
+            ['value' => 3, 'label' => '已取消']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+}

+ 426 - 0
app/admin/controller/order/Order.php

@@ -0,0 +1,426 @@
+<?php
+/**
+ * 订单管理控制器
+ */
+
+namespace app\admin\controller\order;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+
+class Order extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 订单列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 订单编号搜索
+        if (!empty($this->request->get('order_no'))) {
+            $orderNo = $this->request->get('order_no');
+            $where[] = ['o.order_no', 'like', '%' . $orderNo . '%'];
+        }
+
+        // 商品名称搜索
+        if (!empty($this->request->get('product_name'))) {
+            $productName = $this->request->get('product_name');
+            $where[] = ['oi.item_name', 'like', '%' . $productName . '%'];
+        }
+
+        // 会员名称搜索
+        if (!empty($this->request->get('user_name'))) {
+            $userName = $this->request->get('user_name');
+            $where[] = ['u.name', 'like', '%' . $userName . '%'];
+        }
+
+        // 分佣人搜索
+        if (!empty($this->request->get('distributor_name'))) {
+            $distributorName = $this->request->get('distributor_name');
+            $where[] = ['du.name', 'like', '%' . $distributorName . '%'];
+        }
+
+        // 支付状态筛选
+        if (!empty($this->request->get('pay_status'))) {
+            $payStatus = $this->request->get('pay_status');
+            $where['o.pay_status'] = $payStatus;
+        }
+
+        // 支付方式筛选
+        if (!empty($this->request->get('pay_method'))) {
+            $payMethod = $this->request->get('pay_method');
+            $where['o.pay_method'] = $payMethod;
+        }
+
+        // 日期范围筛选
+        if (!empty($this->request->get('date_range'))) {
+            $dateRange = $this->request->get('date_range');
+            $dateArr = explode(' - ', $dateRange);
+            if (count($dateArr) == 2) {
+                $startDate = $dateArr[0];
+                $endDate = $dateArr[1];
+                $where[] = ['o.order_time', '>=', $startDate . ' 00:00:00'];
+                $where[] = ['o.order_time', '<=', $endDate . ' 23:59:59'];
+            }
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['o.order_time'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据(使用订单商品表作为主表,一个订单可能对应多个商品)
+        $orderList = Db::name('order_item')
+            ->alias('oi')
+            ->leftJoin('order o', 'oi.order_id = o.id')
+            ->leftJoin('user_on u', 'o.user_id = u.id')
+            ->leftJoin('user_on du', 'o.distributor_id = du.id')
+            ->field('
+                o.order_time,
+                o.order_no,
+                oi.item_name,
+                oi.quantity,
+                oi.unit_price,
+                u.name,
+                o.pay_amount,
+                o.pay_method,
+                o.pay_status,
+                o.commission_amount,
+                du.name,
+                o.order_time,
+                o.pay_time,
+                oi.id,
+                o.id as order_id
+            ')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 转换字段格式
+        $list = $orderList->items();
+        foreach ($list as &$item) {
+            // 支付方式转换
+            $payMethodMap = [
+                'wechat' => '微信',
+                'alipay' => '支付宝',
+                'bank' => '银行卡',
+                'balance' => '余额'
+            ];
+            $item['pay_method'] = $payMethodMap[$item['pay_method']] ?? $item['pay_method'];
+
+            // 支付状态转换
+            $payStatusMap = [
+                0 => '待支付',
+                1 => '已支付',
+                2 => '支付失败',
+                3 => '已取消'
+            ];
+            $item['pay_status_text'] = $payStatusMap[$item['pay_status']] ?? $item['pay_status'];
+
+            // 格式化日期
+            if (!empty($item['order_time'])) {
+                $item['order_time'] = date('Y-m-d', strtotime($item['order_time']));
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $orderList->total()
+        ]);
+    }
+
+
+    /**
+     * 构建查询条件
+     * @return array
+     */
+    private function buildWhere(): array
+    {
+        $where = [];
+
+        if (!empty($this->request->get('order_no'))) {
+            $where[] = ['o.order_no', 'like', '%' . $this->request->get('order_no') . '%'];
+        }
+
+        if (!empty($this->request->get('item_name'))) {
+            $where[] = ['oi.item_name', 'like', '%' . $this->request->get('item_name') . '%'];
+        }
+
+        if (!empty($this->request->get('user_name'))) {
+            $where[] = ['u.name', 'like', '%' . $this->request->get('user_name') . '%'];
+        }
+
+        if (!empty($this->request->get('distributor_name'))) {
+            $where[] = ['du.name', 'like', '%' . $this->request->get('distributor_name') . '%'];
+        }
+
+        if ($this->request->get('pay_status') !== '' && $this->request->get('pay_status') !== null) {
+            $where['o.pay_status'] = $this->request->get('pay_status');
+        }
+
+        if (!empty($this->request->get('pay_method'))) {
+            $where['o.pay_method'] = $this->request->get('pay_method');
+        }
+
+        if (!empty($this->request->get('date_range'))) {
+            $dateRange = $this->request->get('date_range');
+            $dateArr = explode(' - ', $dateRange);
+            if (count($dateArr) == 2) {
+                $startDate = $dateArr[0];
+                $endDate = $dateArr[1];
+                $where[] = ['o.order_time', '>=', $startDate . ' 00:00:00'];
+                $where[] = ['o.order_time', '<=', $endDate . ' 23:59:59'];
+            }
+        }
+
+        return $where;
+    }
+
+    /**
+     * 构建排序
+     * @return array
+     */
+    private function buildOrder(): array
+    {
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['o.order_time'] = 'desc';
+        }
+        return $order;
+    }
+
+    /**
+     * 使用PhpSpreadsheet导出Excel文件
+     * @return void
+     */
+    public function export(): void
+    {
+        // 获取查询条件
+        $where = $this->buildWhere();
+        $order = $this->buildOrder();
+
+        // 查询所有数据
+        $data = Db::name('order_item')
+            ->alias('oi')
+            ->leftJoin('order o', 'oi.order_id = o.id')
+            ->leftJoin('user_on u', 'o.user_id = u.id')
+            ->leftJoin('user_on du', 'o.distributor_id = du.id')
+            ->field('
+            o.order_no as 订单编号,
+            DATE(o.order_time) as 日期,
+            oi.item_name as 商品名称,
+            oi.quantity as 购买数量,
+            oi.unit_price as 商品单价,
+            u.name as 会员名称,
+            o.pay_amount as 支付金额,
+            o.pay_method as 支付方式,
+            o.pay_status as 支付状态,
+            o.commission_amount as 最后佣金,
+            du.name as 分佣人名称,
+            o.order_time as 下单时间,
+            o.pay_time as 支付时间
+        ')
+            ->where($where)
+            ->order($order)
+            ->select()
+            ->toArray();
+
+        if (empty($data)) {
+            $this->error(__('没有找到符合条件的数据'));
+        }
+
+        // 转换字段格式
+        foreach ($data as &$item) {
+            // 支付方式转换
+            $payMethodMap = [
+                'wechat' => '微信支付',
+                'alipay' => '支付宝',
+                'bank' => '银行卡',
+                'balance' => '余额支付'
+            ];
+            $item['支付方式'] = $payMethodMap[$item['支付方式']] ?? $item['支付方式'];
+
+            // 支付状态转换
+            $payStatusMap = [
+                0 => '待支付',
+                1 => '已支付',
+                2 => '支付失败',
+                3 => '已取消'
+            ];
+            $item['支付状态'] = $payStatusMap[$item['支付状态']] ?? $item['支付状态'];
+
+            // 格式化金额(PhpSpreadsheet会自动处理数字格式)
+            $item['商品单价'] = (float)$item['商品单价'];
+            $item['支付金额'] = (float)$item['支付金额'];
+            $item['最后佣金'] = (float)$item['最后佣金'];
+
+            // 格式化时间
+            if ($item['下单时间']) {
+                $item['下单时间'] = date('Y-m-d H:i:s', strtotime($item['下单时间']));
+            }
+            if ($item['支付时间']) {
+                $item['支付时间'] = date('Y-m-d H:i:s', strtotime($item['支付时间']));
+            }
+        }
+
+        try {
+            $spreadsheet = new Spreadsheet();
+            $sheet = $spreadsheet->getActiveSheet();
+
+            // 定义表头
+            $headers = [
+                '订单编号', '日期', '商品名称', '购买数量', '商品单价',
+                '会员名称', '支付金额', '支付方式', '支付状态',
+                '最后佣金', '分佣人名称', '下单时间', '支付时间'
+            ];
+
+            // 设置表头
+            $col = 'A';
+            foreach ($headers as $header) {
+                $sheet->setCellValue($col . '1', $header);
+
+                // 设置表头加粗和背景色
+                $sheet->getStyle($col . '1')->getFont()->setBold(true);
+                $sheet->getStyle($col . '1')->getFill()
+                    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                    ->getStartColor()->setRGB('D9D9D9');
+
+                $col++;
+            }
+
+            // 填充数据
+            $row = 2;
+            foreach ($data as $item) {
+                $col = 'A';
+                foreach ($item as $value) {
+                    $cellAddress = $col . $row;
+
+                    // 特殊处理订单编号(防止科学计数法)
+                    if ($col === 'A') {
+                        $sheet->setCellValueExplicit($cellAddress, $value, DataType::TYPE_STRING);
+                        $sheet->getStyle($cellAddress)->getNumberFormat()->setFormatCode('@');
+                    } else {
+                        $sheet->setCellValue($cellAddress, $value);
+                    }
+                    $col++;
+                }
+                $row++;
+            }
+
+            // 设置列宽自适应
+            $lastColumn = $sheet->getHighestColumn();
+            for ($col = 'A'; $col <= $lastColumn; $col++) {
+                $sheet->getColumnDimension($col)->setAutoSize(true);
+            }
+
+            // 生成文件名
+            $fileName = '订单数据_' . date('YmdHis') . '.xlsx';
+
+            // 清除输出缓冲区
+            ob_clean();
+
+            // 设置HTTP头
+            header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+            header('Content-Disposition: attachment;filename="' . $fileName . '"');
+            header('Cache-Control: max-age=0');
+
+            // 输出到浏览器
+            $writer = new Xlsx($spreadsheet);
+            $writer->save('php://output');
+
+            exit;
+
+        } catch (\Exception $e) {
+            $this->error('导出失败:' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取订单状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 0, 'label' => '待支付'],
+            ['value' => 1, 'label' => '已支付'],
+            ['value' => 2, 'label' => '支付失败'],
+            ['value' => 3, 'label' => '已取消']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取支付方式选项
+     * @return void
+     */
+    public function payMethodOptions(): void
+    {
+        $options = [
+            ['value' => 'wechat', 'label' => '微信'],
+            ['value' => 'alipay', 'label' => '支付宝'],
+            ['value' => 'bank', 'label' => '银行卡'],
+            ['value' => 'balance', 'label' => '余额']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+
+    /**
+     * 获取支付方式文本
+     * @param string $method
+     * @return string
+     */
+    private function getPayMethodText(string $method): string
+    {
+        $map = [
+            'wechat' => '微信支付',
+            'alipay' => '支付宝',
+            'bank' => '银行卡',
+            'balance' => '余额支付'
+        ];
+        return $map[$method] ?? $method;
+    }
+
+    /**
+     * 获取支付状态文本
+     * @param int $status
+     * @return string
+     */
+    private function getPayStatusText(int $status): string
+    {
+        $map = [
+            0 => '待支付',
+            1 => '已支付',
+            2 => '支付失败',
+            3 => '已取消'
+        ];
+        return $map[$status] ?? '未知';
+    }
+
+}

+ 353 - 0
app/admin/controller/order/Withdrawal.php

@@ -0,0 +1,353 @@
+<?php
+
+namespace app\admin\controller\order;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class Withdrawal extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 提现申请列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 申请人搜索
+        if (!empty($this->request->get('applicant_name'))) {
+            $applicantName = $this->request->get('applicant_name');
+            $where[] = ['u.name', 'like', '%' . $applicantName . '%'];
+        }
+
+        // 状态筛选
+        if ($this->request->get('status') !== '' && $this->request->get('status') !== null) {
+            $where['wr.status'] = $this->request->get('status');
+        }
+
+        // 提现方式筛选
+        if (!empty($this->request->get('withdrawal_method'))) {
+            $where['wr.withdrawal_method'] = $this->request->get('withdrawal_method');
+        }
+
+        //查询时间范围
+        $star_time = $this->request->get('star_time');
+        $end_time = $this->request->get('end_time');
+        if (!empty($star_time) && !empty($end_time)) {
+        $where[] = ['wr.create_time', 'between', [$star_time, $end_time], 'AND'];
+        }
+
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['wr.create_time'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据
+        $withdrawalList = Db::name('withdrawal_record')
+            ->alias('wr')
+            ->leftJoin('user_on u', 'wr.user_id = u.id')
+            ->field('
+                wr.id,
+                u.name,
+                wr.amount,
+                wr.current_balance,
+                wr.withdrawal_method,
+                wr.status,
+                wr.create_time,
+                wr.audit_time,
+                wr.audit_remark,
+                wr.user_id,
+                wr.withdrawal_no
+            ')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 转换字段格式
+        $list = $withdrawalList->items();
+        foreach ($list as &$item) {
+            // 提现方式转换
+            $item['withdrawal_method'] = $this->getWithdrawalMethodText($item['withdrawal_method']);
+
+            // 状态转换
+            $item['status_text'] = $this->getStatusText($item['status']);
+
+            // 格式化金额
+            $item['amount'] = number_format($item['amount'], 2);
+            $item['current_balance'] = number_format($item['current_balance'], 2);
+
+            // 格式化时间
+            if ($item['create_time']) {
+                $item['create_time'] = date('Y-m-d H:i:s', strtotime($item['create_time']));
+            }
+            if ($item['audit_time']) {
+                $item['audit_time'] = date('Y-m-d H:i:s', strtotime($item['audit_time']));
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $withdrawalList->total()
+        ]);
+    }
+
+    /**
+     * 审核通过
+     * @return void
+     */
+    public function approve(): void
+    {
+        $id = $this->request->post('id');
+        $auditRemark = $this->request->post('audit_remark', '');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取提现记录
+            $withdrawalRecord = Db::name('withdrawal_record')
+                ->where('id', $id)
+                ->lock(true)
+                ->find();
+
+            if (!$withdrawalRecord) {
+                throw new \Exception('提现记录不存在');
+            }
+
+            if ($withdrawalRecord['status'] != 0) {
+                throw new \Exception('只有待审核的记录才能审核');
+            }
+
+            // 检查用户佣金是否足够
+            $userCommission = Db::name('user_commission')
+                ->where('user_id', $withdrawalRecord['user_id'])
+                ->lock(true)
+                ->find();
+            if (!$userCommission || $userCommission['current_commission'] < $withdrawalRecord['amount']) {
+                throw new \Exception('用户佣金不足');
+            }
+
+            // 更新提现记录状态
+            $updateData = [
+                'status' => 1, // 审核通过
+                'auditor_id' => $this->auth->id ?? 0,
+                'audit_time' => date('Y-m-d H:i:s'),
+                'audit_remark' => $auditRemark,
+                'balance_after' => Db::raw('current_balance - ' . $withdrawalRecord['amount']),
+                'update_time' => date('Y-m-d H:i:s')
+            ];
+
+            $result = Db::name('withdrawal_record')
+                ->where('id', $id)
+                ->update($updateData);
+
+            if (!$result) {
+                throw new \Exception('更新提现记录失败');
+            }
+
+            // 更新用户佣金(扣除已提现金额)
+            Db::name('user_commission')
+                ->where('user_id', $withdrawalRecord['user_id'])
+                ->update([
+                    'current_commission' => Db::raw('current_commission - ' . $withdrawalRecord['amount']),
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            Db::commit();
+
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('审核失败: ' . $e->getMessage());
+        }
+
+        $this->success('审核通过');
+    }
+
+    /**
+     * 审核拒绝
+     * @return void
+     */
+    public function reject(): void
+    {
+        $id = $this->request->post('id');
+        $auditRemark = $this->request->post('audit_remark', '');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (empty($auditRemark)) {
+            $this->error(__('请填写拒绝原因'));
+        }
+
+        try {
+            // 获取提现记录
+            $withdrawalRecord = Db::name('withdrawal_record')
+                ->where('id', $id)
+                ->find();
+            if (!$withdrawalRecord) {
+                throw new \Exception ('提现记录不存在');
+            }
+
+            if ($withdrawalRecord['status'] != 0) {
+                throw new \Exception ('只有待审核的记录才能审核');
+            }
+            // 更新提现记录状态
+            $result = Db::name('withdrawal_record')
+                ->where('id', $id)
+                ->update([
+                    'status' => 2, // 审核拒绝
+                    'auditor_id' => $this->auth->id ?? 0,
+                    'audit_time' => date('Y-m-d H:i:s'),
+                    'audit_remark' => $auditRemark,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success('审核拒绝成功');
+        } else {
+            $this->error(__('审核拒绝失败'));
+        }
+    }
+
+    /**
+     * 获取提现方式文本
+     * @param string $method
+     * @return string
+     */
+    private function getWithdrawalMethodText(string $method): string
+    {
+        $map = [
+            'wechat' => '微信',
+            'alipay' => '支付宝',
+            'bank' => '银行卡'
+        ];
+        return $map[$method] ?? $method;
+    }
+
+    /**
+     * 获取状态文本
+     * @param int $status
+     * @return string
+     */
+    private function getStatusText(int $status): string
+    {
+        $map = [
+            0 => '待审核',
+            1 => '审核通过',
+            2 => '审核拒绝',
+            3 => '已打款',
+            4 => '打款失败'
+        ];
+        return $map[$status] ?? '未知';
+    }
+
+    /**
+     * 获取提现状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 0, 'label' => '待审核'],
+            ['value' => 1, 'label' => '审核通过'],
+            ['value' => 2, 'label' => '审核拒绝'],
+            ['value' => 3, 'label' => '已打款'],
+            ['value' => 4, 'label' => '打款失败']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取提现方式选项
+     * @return void
+     */
+    public function methodOptions(): void
+    {
+        $options = [
+            ['value' => 'wechat', 'label' => '微信'],
+            ['value' => 'alipay', 'label' => '支付宝'],
+            ['value' => 'bank', 'label' => '银行卡']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 提现统计
+     * @return void
+     */
+    public function statistics(): void
+    {
+        // 总提现申请数
+        $totalCount = Db::name('withdrawal_record')->count();
+
+        // 待审核数
+        $pendingCount = Db::name('withdrawal_record')
+            ->where('status', 0)
+            ->count();
+
+        // 已通过数
+        $approvedCount = Db::name('withdrawal_record')
+            ->where('status', 1)
+            ->count();
+
+        // 已拒绝数
+        $rejectedCount = Db::name('withdrawal_record')
+            ->where('status', 2)
+            ->count();
+
+        // 总提现金额
+        $totalAmount = Db::name('withdrawal_record')
+            ->where('status', 1) // 只计算审核通过的
+            ->sum('amount');
+
+        // 今日提现统计
+        $todayStart = date('Y-m-d 00:00:00');
+        $todayEnd = date('Y-m-d 23:59:59');
+        $todayCount = Db::name('withdrawal_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->count();
+
+        $todayAmount = Db::name('withdrawal_record')
+            ->whereBetween('create_time', [$todayStart, $todayEnd])
+            ->where('status', 1)
+            ->sum('amount');
+
+        $stats = [
+            'total_count' => $totalCount,
+            'pending_count' => $pendingCount,
+            'approved_count' => $approvedCount,
+            'rejected_count' => $rejectedCount,
+            'total_amount' => number_format($totalAmount ?? 0, 2),
+            'today_count' => $todayCount,
+            'today_amount' => number_format($todayAmount ?? 0, 2)
+        ];
+
+        $this->success('获取成功', $stats);
+    }
+}

+ 101 - 135
app/admin/controller/package/Package.php

@@ -44,25 +44,17 @@ class Package extends Backend
             $order_a = explode(',', $order_a);
             $order[$order_a[0]] = $order_a[1];
         } else {
-            $order['p.sort'] = 'asc';
-            $order['p.id'] = 'desc';
+            $order['sort'] = 'asc';
+            $order['id'] = 'desc';
         }
 
         // 分页
         $page = $this->request->get('page', 1);
         $limit = $this->request->get('limit', 20);
 
-        // 查询数据(统计规格数量和已售总数)
-        $specCountSubQuery = Db::name('package_price')
-            ->field('package_id, COUNT(*) as spec_count, SUM(sold_count) as total_sold')
-            ->where('status', 1)
-            ->group('package_id')
-            ->buildSql();
 
+        // 查询套餐列表并计算库存状态
         $packageList = Db::name('package')
-            ->alias('p')
-            ->leftJoin([$specCountSubQuery => 'sc'], 'p.id = sc.package_id')
-            ->field('p.*, IFNULL(sc.spec_count, 0) as spec_count, IFNULL(sc.total_sold, 0) as total_sold')
             ->where($where)
             ->order($order)
             ->paginate([
@@ -70,14 +62,35 @@ class Package extends Backend
                 'page' => $page
             ]);
 
-        // 转换功能特性字段
+        // 为每个套餐计算库存状态
         $list = $packageList->items();
-        foreach ($list as &$item) {
-            if (!empty($item['features'])) {
-                $item['features'] = json_decode($item['features'], true);
+        foreach ($list as &$package) {
+            // 获取该套餐的所有规格
+            $specs = Db::name('package_price')
+                ->where('package_id', $package['id'])
+                ->select()
+                ->toArray();
+
+            // 计算总库存和总已售
+            $totalStock = 0;
+            $totalSold = 0;
+            foreach ($specs as $spec) {
+                $totalStock += $spec['stock'];
+                $totalSold += $spec['sold_count'];
+            }
+
+            // 根据库存设置库存状态
+            if ($totalStock == 0) {
+                $package['stock_status'] = '售罄';
+            } elseif ($totalStock <= 10) {
+                $package['stock_status'] = '即将售罄';
             } else {
-                $item['features'] = [];
+                $package['stock_status'] = '充足';
             }
+
+            $package['remaining_stock'] = $totalStock;
+            $package['sold_count'] = $totalSold;
+            $package['spec_count'] = count($specs);
         }
 
         $this->success('查询成功', [
@@ -94,9 +107,7 @@ class Package extends Backend
     {
         $id = $this->request->post('id');
         $name = $this->request->post('name');
-        $type = $this->request->post('type', 1);
         $description = $this->request->post('description', '');
-        $features = $this->request->post('features', []);
         $sort = $this->request->post('sort', 0);
         $status = $this->request->post('status', 1);
 
@@ -116,16 +127,9 @@ class Package extends Backend
             $this->error(__('套餐名称已存在'));
         }
 
-        // 处理功能特性字段
-        if (!is_array($features)) {
-            $features = [];
-        }
-
         $data = [
             'name' => $name,
-            'type' => $type,
             'description' => $description,
-            'features' => json_encode($features, JSON_UNESCAPED_UNICODE),
             'sort' => $sort,
             'status' => $status,
             'update_time' => date('Y-m-d H:i:s')
@@ -250,13 +254,6 @@ class Package extends Backend
             $this->error(__('套餐不存在'));
         }
 
-        // 处理功能特性字段
-        if (!empty($packageInfo['features'])) {
-            $packageInfo['features'] = json_decode($packageInfo['features'], true);
-        } else {
-            $packageInfo['features'] = [];
-        }
-
         // 获取套餐规格列表
         $specList = Db::name('package_price')
             ->where('package_id', $id)
@@ -264,43 +261,58 @@ class Package extends Backend
             ->select()
             ->toArray();
 
-        // 统计库存和销售情况
+        // 计算规格库存状态和汇总数据
         $totalStock = 0;
         $totalSold = 0;
-        foreach ($specList as $spec) {
-            $totalStock += $spec['stock'];
-            $totalSold += $spec['sold_count'];
-        }
+        $cardTypeMap = [
+            1 => '次卡',
+            2 => '按积分',
+            3 => '日卡',
+            4 => '周卡',
+            5 => '季卡',
+            6 => '月卡',
+            7 => '年卡'
+        ];
 
-        // 统计用户购买数量(如果user_package表存在)
-        $userPurchaseCount = 0;
-        try {
-            $userPurchaseCount = Db::name('user_package')
-                ->where('package_id', $id)
-                ->count();
-        } catch (\Exception $e) {
-            // 表不存在,设为0
-        }
+        $typeMap = [
+            1 => '个人套餐',
+            2 => '团队套餐',
+        ];
 
-        // 统计企业购买数量(如果company_package表存在)
-        $companyPurchaseCount = 0;
-        try {
-            $companyPurchaseCount = Db::name('company_package')
-                ->where('package_id', $id)
-                ->count();
-        } catch (\Exception $e) {
-            // 表不存在,设为0
+        foreach ($specList as &$spec) {
+            // 计算库存状态
+            if ($spec['stock'] == 0) {
+                $spec['stock_status'] = '售罄';
+            } elseif ($spec['stock'] <= 10) {
+                $spec['stock_status'] = '即将售罄';
+            } else {
+                $spec['stock_status'] = '充足';
+            }
+
+            // 翻译类型和卡类型
+            $spec['type_text'] = $typeMap[$spec['type']] ?? '未知';
+            $spec['card_type_text'] = $cardTypeMap[$spec['card_type']] ?? '未知';
+
+            // 格式化收费方式显示
+            if ($spec['card_type'] == 1 && $spec['unit_value'] > 0) {
+                $spec['charge_text'] = '按次数:' . $spec['unit_value'] . '次';
+            } elseif ($spec['card_type'] == 2 && $spec['points_value'] > 0) {
+                $spec['charge_text'] = '按积分:' . $spec['points_value'] . '积分';
+            } else {
+                $spec['charge_text'] = $spec['card_type_text'];
+            }
+
+            $totalStock += $spec['stock'];
+            $totalSold += $spec['sold_count'];
         }
 
         $result = [
             'package_info' => $packageInfo,
             'spec_list' => $specList,
             'statistics' => [
-                'spec_count' => count($specList),
                 'total_stock' => $totalStock,
                 'total_sold' => $totalSold,
-                'user_purchase_count' => $userPurchaseCount,
-                'company_purchase_count' => $companyPurchaseCount
+                'spec_count' => count($specList)
             ]
         ];
 
@@ -317,9 +329,11 @@ class Package extends Backend
         $packageId = $this->request->post('package_id');
         $specName = $this->request->post('spec_name');
         $price = $this->request->post('price', 0);
-        $originalPrice = $this->request->post('original_price', 0);
         $stock = $this->request->post('stock', 0);
-        $unitValue = $this->request->post('unit_value', '');
+        $type = $this->request->post('type', 1);
+        $cardType = $this->request->post('card_type', 1);
+        $unitValue = $this->request->post('unit_value', 0);
+        $pointsValue = $this->request->post('points_value', 0);
         $description = $this->request->post('description', '');
         $sort = $this->request->post('sort', 0);
 
@@ -331,6 +345,15 @@ class Package extends Backend
             $this->error(__('价格必须大于0'));
         }
 
+        // 验证条件
+        if ($cardType == 1 && $unitValue <= 0) {
+            $this->error(__('次卡类型必须填写有效次数'));
+        }
+
+        if ($cardType == 2 && $pointsValue <= 0) {
+            $this->error(__('积分类型必须填写有效积分数量'));
+        }
+
         // 检查套餐是否存在
         $packageInfo = Db::name('package')
             ->where('id', $packageId)
@@ -357,9 +380,11 @@ class Package extends Backend
             'package_id' => $packageId,
             'spec_name' => $specName,
             'price' => $price,
-            'original_price' => $originalPrice,
             'stock' => $stock,
+            'type' => $type,
+            'card_type' => $cardType,
             'unit_value' => $unitValue,
+            'points_value' => $pointsValue,
             'description' => $description,
             'sort' => $sort,
             'update_time' => date('Y-m-d H:i:s')
@@ -381,10 +406,10 @@ class Package extends Backend
                 $message = '新增成功';
             }
 
-
         } catch (\Exception $e) {
             $this->error('操作失败: ' . $e->getMessage());
         }
+
         if ($result) {
             $this->success($message, ['id' => $id]);
         } else {
@@ -435,10 +460,10 @@ class Package extends Backend
                 ->where('id', $id)
                 ->delete();
 
-
         } catch (\Exception $e) {
             $this->error('删除失败: ' . $e->getMessage());
         }
+
         if ($result) {
             $this->success('删除成功');
         } else {
@@ -492,10 +517,10 @@ class Package extends Backend
                     'update_time' => date('Y-m-d H:i:s')
                 ]);
 
-
         } catch (\Exception $e) {
             $this->error('操作失败: ' . $e->getMessage());
         }
+
         if ($result) {
             $statusText = $status == 1 ? '启用' : '禁用';
             $this->success("成功{$statusText}{$result}个套餐");
@@ -529,10 +554,10 @@ class Package extends Backend
                     'update_time' => date('Y-m-d H:i:s')
                 ]);
 
-
         } catch (\Exception $e) {
             $this->error('操作失败: ' . $e->getMessage());
         }
+
         if ($result) {
             $this->success("成功更新{$result}个规格的库存");
         } else {
@@ -570,14 +595,19 @@ class Package extends Backend
     }
 
     /**
-     * 获取规格状态选项
+     * 获取卡类型选项
      * @return void
      */
-    public function specStatusOptions():void
+    public function cardTypeOptions():void
     {
         $options = [
-            ['value' => 1, 'label' => '正常'],
-            ['value' => 2, 'label' => '售罄']
+            ['value' => 1, 'label' => '次卡'],
+            ['value' => 2, 'label' => '按积分'],
+            ['value' => 3, 'label' => '日卡'],
+            ['value' => 4, 'label' => '周卡'],
+            ['value' => 5, 'label' => '季卡'],
+            ['value' => 6, 'label' => '月卡'],
+            ['value' => 7, 'label' => '年卡']
         ];
 
         $this->success('获取成功', $options);
@@ -608,79 +638,17 @@ class Package extends Backend
         $totalSold = Db::name('package_price')
             ->sum('sold_count');
 
-        // 按类型统计
-        $typeStats = Db::name('package')
-            ->field('type, COUNT(*) as count')
-            ->where('status', 1)
-            ->group('type')
-            ->select()
-            ->toArray();
-
         $stats = [
             'total_package' => $totalPackage,
             'active_package' => $activePackage,
             'total_spec' => $totalSpec,
             'total_stock' => $totalStock,
-            'total_sold' => $totalSold,
-            'type_stats' => $typeStats
+            'total_sold' => $totalSold
         ];
 
         $this->success('获取成功', $stats);
     }
 
-    /**
-     * 导出套餐数据
-     * @return void
-     */
-    public function export():void
-    {
-        $where = [];
-
-        // 根据筛选条件构建查询条件
-        if (!empty($this->request->get('name'))) {
-            $name = $this->request->get('name');
-            $where[] = ['name', 'like', '%' . $name . '%'];
-        }
-
-        if (!empty($this->request->get('status'))) {
-            $status = $this->request->get('status');
-            $where['status'] = $status;
-        }
-
-        // 查询数据
-        $packageList = Db::name('package')
-            ->where($where)
-            ->order('sort', 'asc')
-            ->order('id', 'desc')
-            ->select()
-            ->toArray();
-
-        // 状态映射
-        $typeMap = [1 => '个人套餐', 2 => '团队套餐', 3 => '企业套餐'];
-        $statusMap = [1 => '启用', 2 => '禁用'];
-
-        foreach ($packageList as &$item) {
-            $item['type'] = $typeMap[$item['type']] ?? '未知';
-            $item['status'] = $statusMap[$item['status']] ?? '未知';
-
-            // 获取规格信息
-            $specs = Db::name('package_price')
-                ->where('package_id', $item['id'])
-                ->order('sort', 'asc')
-                ->select()
-                ->toArray();
-
-            $specInfo = [];
-            foreach ($specs as $spec) {
-                $specInfo[] = "{$spec['spec_name']} - ¥{$spec['price']} (库存:{$spec['stock']})";
-            }
-
-            $item['spec_info'] = implode('; ', $specInfo);
-        }
-
-        $this->success('查询成功', $packageList);
-    }
-
     /**
      * 获取所有套餐选项(用于下拉选择)
      * @return void
@@ -689,7 +657,7 @@ class Package extends Backend
     {
         $packageList = Db::name('package')
             ->where('status', 1)
-            ->field('id as value, name as label, type')
+            ->field('id as value, name as label')
             ->order('sort', 'asc')
             ->order('id', 'asc')
             ->select()
@@ -712,16 +680,14 @@ class Package extends Backend
 
         $specList = Db::name('package_price')
             ->where('package_id', $packageId)
-            ->where('status', 1)
             ->where('stock', '>', 0) // 只显示有库存的规格
-            ->field('id as value, spec_name as label, price, stock, unit_value, description')
+            ->field('id as value, spec_name as label, price, stock, type, card_type, unit_value, points_value, description')
             ->order('sort', 'asc')
             ->select()
             ->toArray();
 
         $this->success('获取成功', $specList);
     }
-
     /**
      * 更新规格销售数量(模拟用户购买)
      * @return void

+ 51 - 85
app/admin/controller/package/Product.php

@@ -6,6 +6,7 @@
 namespace app\admin\controller\package;
 
 use app\common\controller\Backend;
+use ba\Filesystem;
 use think\facade\Db;
 
 class Product extends Backend
@@ -31,6 +32,11 @@ class Product extends Backend
             $code = $this->request->get('code');
             $where[] = ['p.code', 'like', '%' . $code . '%'];
         }
+        // 产品类型筛选
+        if (!empty($this->request->get('type'))) {
+            $type = $this->request->get('type');
+            $where['p.type'] = $type;
+        }
 
         // 分类筛选
         if (!empty($this->request->get('category_id'))) {
@@ -60,38 +66,35 @@ class Product extends Backend
             $order['p.sort'] = 'asc';
             $order['p.id'] = 'desc';
         }
-
         // 分页
-        $page = $this->request->get('page', 1);
-        $limit = $this->request->get('limit', 20);
-
+        $page = $this->request->param('page', 1);
+        $limit = $this->request->param('limit', 20);
         // 查询数据
         $productList = Db::name('product')
             ->alias('p')
-            ->leftJoin('product_category pc', 'p.category_id = pc.id')
-            ->field('p.*, pc.name as category_name')
+            ->leftJoin('package pk', 'p.package_id = pk.id')
+            ->field('p.*, pk.name as package_name')
             ->where($where)
             ->order($order)
             ->paginate([
                 'list_rows' => $limit,
                 'page' => $page
             ]);
-
         // 转换图片字段
         $list = $productList->items();
-        foreach ($list as &$item) {
-            if (!empty($item['image'])) {
-                $item['image'] = json_decode($item['image'], true);
-            } else {
-                $item['image'] = [];
-            }
-
-            if (!empty($item['key_attributes'])) {
-                $item['key_attributes'] = json_decode($item['key_attributes'], true);
-            } else {
-                $item['key_attributes'] = [];
-            }
-        }
+//        foreach ($list as &$item) {
+//            if (!empty($item['image'])) {
+//                $item['image'] = json_decode($item['image'], true);
+//            } else {
+//                $item['image'] = [];
+//            }
+//
+//            if (!empty($item['key_attributes'])) {
+//                $item['key_attributes'] = json_decode($item['key_attributes'], true);
+//            } else {
+//                $item['key_attributes'] = [];
+//            }
+//        }
 
         $this->success('查询成功', [
             'list' => $list,
@@ -107,8 +110,8 @@ class Product extends Backend
     {
         $id = $this->request->post('id');
         $name = $this->request->post('name');
-        $code = $this->request->post('code');
-        $categoryId = $this->request->post('category_id');
+//        $code = $this->request->post('code');
+        $packageId = $this->request->post('package_id');
         $description = $this->request->post('description', '');
         $detail = $this->request->post('detail', '');
         $keyAttributes = $this->request->post('key_attributes', []);
@@ -116,27 +119,28 @@ class Product extends Backend
         $stockStatus = $this->request->post('stock_status', 1);
         $status = $this->request->post('status', 1);
         $sort = $this->request->post('sort', 0);
+        $type = $this->request->post('type', 0);
+        $link = $this->request->post('link', 0);
 
         if (empty($name)) {
             $this->error(__('产品名称不能为空'));
         }
 
-        if (empty($code)) {
-            $this->error(__('产品编码不能为空'));
+        if (empty($link)) {
+            $this->error(__('产品链接不能为空'));
         }
-
-        // 验证产品编码是否已存在(编辑时排除自己)
-        $checkCode = Db::name('product')
-            ->where('code', $code);
-
-        if (!empty($id)) {
-            $checkCode->where('id', '<>', $id);
+        if (empty($packageId)) {
+            $this->error(__('请选择套餐'));
         }
 
-        if ($checkCode->find()) {
-            $this->error(__('产品编码已存在'));
-        }
+        // 验证套餐是否存在
+        $packageInfo = Db::name('package')
+            ->where('id', $packageId)
+            ->find();
 
+        if (!$packageInfo) {
+            $this->error(__('选择的套餐不存在'));
+        }
         // 处理图片和属性字段
         if (empty($image)) {
             $image = "";
@@ -148,8 +152,8 @@ class Product extends Backend
 
         $data = [
             'name' => $name,
-            'code' => $code,
-            'category_id' => $categoryId ?: null,
+//            'code' => $code,
+            'package_id' => $packageId ?: null,
             'description' => $description,
             'detail' => $detail,
             'key_attributes' => json_encode($keyAttributes, JSON_UNESCAPED_UNICODE),
@@ -157,6 +161,8 @@ class Product extends Backend
             'stock_status' => $stockStatus,
             'status' => $status,
             'sort' => $sort,
+            'type' => $type,
+            'link' =>$link,
             'update_time' => date('Y-m-d H:i:s')
         ];
         try {
@@ -205,15 +211,6 @@ class Product extends Backend
             $this->error(__('参数错误'));
         }
 
-        // 检查是否有套餐关联
-        $hasPackage = Db::name('package')
-            ->where('product_id', $id)
-            ->count();
-
-        if ($hasPackage > 0) {
-            $this->error(__('该产品下有关联的套餐,请先删除套餐'));
-        }
-
         // 检查是否有订单关联
         $hasOrder = Db::name('order_item')
             ->where('product_id', $id)
@@ -224,24 +221,14 @@ class Product extends Backend
         }
 
         try {
-            Db::startTrans();
-
-            // 删除产品
             $result = Db::name('product')
                 ->where('id', $id)
                 ->delete();
 
-            // 删除产品规格
-            Db::name('product_spec')
-                ->where('product_id', $id)
-                ->delete();
-
-            Db::commit();
-
         } catch (\Exception $e) {
-            Db::rollback();
             $this->error('删除失败: ' . $e->getMessage());
         }
+
         if ($result) {
             $this->success('删除成功');
         } else {
@@ -264,45 +251,25 @@ class Product extends Backend
         // 获取产品基础信息
         $productInfo = Db::name('product')
             ->alias('p')
-            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->leftJoin('package pk', 'p.package_id = pk.id')
             ->where('p.id', $id)
-            ->field('p.*, pc.name as category_name')
+            ->field('p.*, pk.name as package_name, pk.type as package_type')
             ->find();
 
         if (!$productInfo) {
             $this->error(__('产品不存在'));
         }
 
-        if (empty($productInfo['image'])) {
-            $productInfo['image'] = [];
-        }
 
-        if (!empty($productInfo['key_attributes'])) {
-            $productInfo['key_attributes'] = json_decode($productInfo['key_attributes'], true);
-        } else {
-            $productInfo['key_attributes'] = [];
-        }
-
-        // 获取产品规格
-        $specList = Db::name('product_spec')
-            ->where('product_id', $id)
+        // 获取所属套餐下的其他产品
+        $samePackageProducts = Db::name('product')
+            ->where('package_id', $productInfo['package_id'])
+            ->where('id', '<>', $id)
+            ->field('id, name, code')
             ->order('sort', 'asc')
             ->select()
             ->toArray();
 
-        foreach ($specList as &$spec) {
-            if (!empty($spec['value'])) {
-                $spec['value'] = json_decode($spec['value'], true);
-            } else {
-                $spec['value'] = [];
-            }
-        }
-
-        // 获取关联套餐数量
-        $packageCount = Db::name('package')
-            ->where('product_id', $id)
-            ->count();
-
         // 获取订单数量
         $orderCount = Db::name('order_item')
             ->where('product_id', $id)
@@ -310,8 +277,7 @@ class Product extends Backend
 
         $result = [
             'product_info' => $productInfo,
-            'spec_list' => $specList,
-            'package_count' => $packageCount,
+            'same_package_products' => $samePackageProducts,
             'order_count' => $orderCount
         ];
 

+ 539 - 0
app/admin/controller/pay/AccountConfig.php

@@ -0,0 +1,539 @@
+<?php
+/**
+ * 提现账户配置控制器(客户银行卡配置)
+ */
+namespace app\admin\controller\pay;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class AccountConfig extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 提现账户列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 用户ID搜索
+        if (!empty($this->request->get('user_id'))) {
+            $where['wac.user_id'] = $this->request->get('user_id');
+        }
+
+        // 账户类型筛选
+        if (!empty($this->request->get('account_type'))) {
+            $where['wac.account_type'] = $this->request->get('account_type');
+        }
+
+        // 银行名称搜索
+        if (!empty($this->request->get('bank_name'))) {
+            $where[] = ['wac.bank_name', 'like', '%' . $this->request->get('bank_name') . '%'];
+        }
+
+        // 账户姓名搜索
+        if (!empty($this->request->get('account_name'))) {
+            $where[] = ['wac.account_name', 'like', '%' . $this->request->get('account_name') . '%'];
+        }
+
+        // 状态筛选
+        if ($this->request->get('status') !== '' && $this->request->get('status') !== null) {
+            $where['wac.status'] = $this->request->get('status');
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['wac.create_time'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据
+        $accountList = Db::name('withdrawal_account_config')
+            ->alias('wac')
+            ->leftJoin('user_on u', 'wac.user_id = u.id')
+            ->field('
+                wac.id,
+                wac.user_id,
+                u.name as user_name,
+                wac.account_type,
+                wac.account_name,
+                wac.bank_name,
+                wac.bank_branch,
+                wac.bank_province,
+                wac.bank_city,
+                wac.card_number,
+                wac.alipay_account,
+                wac.wechat_account,
+                wac.is_default,
+                wac.status,
+                wac.remark,
+                wac.create_time,
+                wac.update_time
+            ')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        $list = $accountList->items();
+        foreach ($list as &$item) {
+            // 添加账户类型文本
+            $item['account_type_text'] = $this->getAccountTypeText($item['account_type']);
+
+            // 添加状态文本
+            $item['status_text'] = $item['status'] ? '启用' : '禁用';
+
+            // 添加默认账户文本
+            $item['is_default_text'] = $item['is_default'] ? '是' : '否';
+
+            // 部分掩码显示(保护隐私)
+            if ($item['card_number']) {
+                $item['card_number_masked'] = substr($item['card_number'], 0, 4) . '****' . substr($item['card_number'], -4);
+            }
+            if ($item['alipay_account']) {
+                // 支付宝邮箱掩码
+                if (strpos($item['alipay_account'], '@') !== false) {
+                    list($prefix, $domain) = explode('@', $item['alipay_account']);
+                    if (strlen($prefix) > 3) {
+                        $item['alipay_account_masked'] = substr($prefix, 0, 3) . '***@' . $domain;
+                    } else {
+                        $item['alipay_account_masked'] = $prefix . '***@' . $domain;
+                    }
+                } else {
+                    $item['alipay_account_masked'] = substr($item['alipay_account'], 0, 3) . '***';
+                }
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $accountList->total()
+        ]);
+    }
+
+    /**
+     * 获取账户详情
+     * @return void
+     */
+    public function detail(): void
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $account = Db::name('withdrawal_account_config')
+            ->alias('wac')
+            ->leftJoin('user_on u', 'wac.user_id = u.id')
+            ->where('wac.id', $id)
+            ->field('wac.*, u.name as user_name')
+            ->find();
+
+        if (!$account) {
+            $this->error(__('账户不存在'));
+        }
+
+        // 添加账户类型文本
+        $account['account_type_text'] = $this->getAccountTypeText($account['account_type']);
+
+        $this->success('查询成功', $account);
+    }
+
+    /**
+     * 新增/编辑提现账户
+     * @return void
+     */
+    public function edit(): void
+    {
+        $id = $this->request->post('id');
+        $userId = $this->request->post('user_id');
+        $accountType = $this->request->post('account_type');
+        $accountName = $this->request->post('account_name');
+        $isDefault = $this->request->post('is_default', 0);
+        $status = $this->request->post('status', 1);
+        $remark = $this->request->post('remark', '');
+
+        // 根据账户类型接收不同的字段
+        $bankName = $this->request->post('bank_name', '');
+        $bankBranch = $this->request->post('bank_branch', '');
+        $bankProvince = $this->request->post('bank_province', '');
+        $bankCity = $this->request->post('bank_city', '');
+        $cardNumber = $this->request->post('card_number', '');
+        $alipayAccount = $this->request->post('alipay_account', '');
+        $wechatAccount = $this->request->post('wechat_account', '');
+
+        if (empty($userId)) {
+            $this->error(__('用户ID不能为空'));
+        }
+
+        if (empty($accountType)) {
+            $this->error(__('请选择账户类型'));
+        }
+
+        if (empty($accountName)) {
+            $this->error(__('账户姓名不能为空'));
+        }
+
+        // 验证用户是否存在
+        $userInfo = Db::name('user')
+            ->where('id', $userId)
+            ->find();
+
+        if (!$userInfo) {
+            $this->error(__('用户不存在'));
+        }
+
+        // 验证账户类型对应的必填字段
+        $validationResult = $this->validateAccountFields($accountType, [
+            'bank_name' => $bankName,
+            'card_number' => $cardNumber,
+            'alipay_account' => $alipayAccount,
+            'wechat_account' => $wechatAccount
+        ]);
+
+        if ($validationResult !== true) {
+            $this->error($validationResult);
+        }
+
+        // 验证银行卡号格式(如果是银行卡)
+        if ($accountType == 'bank' && !empty($cardNumber)) {
+            if (!preg_match('/^[0-9]{16,19}$/', $cardNumber)) {
+                $this->error(__('银行卡号格式不正确,应为16-19位数字'));
+            }
+        }
+
+        // 验证支付宝账号格式
+        if ($accountType == 'alipay' && !empty($alipayAccount)) {
+            if (!filter_var($alipayAccount, FILTER_VALIDATE_EMAIL) && !preg_match('/^1[3-9]\d{9}$/', $alipayAccount)) {
+                $this->error(__('支付宝账号应为邮箱或手机号格式'));
+            }
+        }
+
+        $data = [
+            'user_id' => $userId,
+            'account_type' => $accountType,
+            'account_name' => $accountName,
+            'bank_name' => $accountType == 'bank' ? $bankName : '',
+            'bank_branch' => $accountType == 'bank' ? $bankBranch : '',
+            'bank_province' => $accountType == 'bank' ? $bankProvince : '',
+            'bank_city' => $accountType == 'bank' ? $bankCity : '',
+            'card_number' => $accountType == 'bank' ? $cardNumber : '',
+            'alipay_account' => $accountType == 'alipay' ? $alipayAccount : '',
+            'wechat_account' => $accountType == 'wechat' ? $wechatAccount : '',
+            'is_default' => $isDefault,
+            'status' => $status,
+            'remark' => $remark,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $accountInfo = Db::name('withdrawal_account_config')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$accountInfo) {
+                    $this->error(__('账户不存在'));
+                }
+
+                // 如果设置为默认账户,需要取消该用户的其他默认账户
+                if ($isDefault == 1) {
+                    Db::name('withdrawal_account_config')
+                        ->where('user_id', $userId)
+                        ->where('id', '<>', $id)
+                        ->where('account_type', $accountType)
+                        ->update(['is_default' => 0]);
+                }
+
+                $result = Db::name('withdrawal_account_config')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+
+                // 检查是否已存在相同账户
+                $existCheck = Db::name('withdrawal_account_config')
+                    ->where('user_id', $userId)
+                    ->where('account_type', $accountType);
+
+                if ($accountType == 'bank') {
+                    $existCheck->where('card_number', $cardNumber);
+                } elseif ($accountType == 'alipay') {
+                    $existCheck->where('alipay_account', $alipayAccount);
+                } elseif ($accountType == 'wechat') {
+                    $existCheck->where('wechat_account', $wechatAccount);
+                }
+
+                if ($existCheck->find()) {
+                    throw new \Exception('该账户已存在');
+                }
+
+                // 如果设置为默认账户,需要取消该用户的其他默认账户
+                if ($isDefault == 1) {
+                    Db::name('withdrawal_account_config')
+                        ->where('user_id', $userId)
+                        ->where('account_type', $accountType)
+                        ->update(['is_default' => 0]);
+                }
+
+                $result = Db::name('withdrawal_account_config')
+                    ->insert($data);
+                $id = Db::name('withdrawal_account_config')->getLastInsID();
+                $message = '新增成功';
+            }
+
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+
+
+        if ($result) {
+            $this->success($message, ['id' => $id]);
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 删除提现账户
+     * @return void
+     */
+    public function delete(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $accountInfo = Db::name('withdrawal_account_config')
+            ->where('id', $id)
+            ->find();
+
+        if (!$accountInfo) {
+            $this->error(__('账户不存在'));
+        }
+
+        // 检查是否正在使用(如果有提现记录)
+        $hasWithdrawal = Db::name('withdrawal_record')
+            ->where('account_info', 'like', '%' . $accountInfo['card_number'] . '%')
+//            ->whereOr('account_info', 'like', '%' . $accountInfo['alipay_account'] . '%')
+//            ->whereOr('account_info', 'like', '%' . $accountInfo['wechat_account'] . '%')
+            ->find();
+        if ($hasWithdrawal) {
+            $this->error(__('该账户已有提现记录,不能删除'));
+        }
+
+        $result = Db::name('withdrawal_account_config')
+            ->where('id', $id)
+            ->delete();
+
+        if ($result) {
+            $this->success('删除成功');
+        } else {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 设置默认账户
+     * @return void
+     */
+    public function setDefault(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $accountInfo = Db::name('withdrawal_account_config')
+            ->where('id', $id)
+            ->find();
+
+        if (!$accountInfo) {
+            $this->error(__('账户不存在'));
+        }
+
+        try {
+            // 取消该用户同类型账户的其他默认设置
+            Db::name('withdrawal_account_config')
+                ->where('user_id', $accountInfo['user_id'])
+                ->where('account_type', $accountInfo['account_type'])
+                ->where('id', '<>', $id)
+                ->update(['is_default' => 0]);
+
+            // 设置当前账户为默认
+            $result = Db::name('withdrawal_account_config')
+                ->where('id', $id)
+                ->update([
+                    'is_default' => 1,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result) {
+                $this->success('设置默认成功');
+            } else {
+                $this->error(__('设置失败'));
+            }
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取用户的提现账户列表
+     * @return void
+     */
+    public function getUserAccounts(): void
+    {
+        $userId = $this->request->get('user_id');
+
+        if (empty($userId)) {
+            $this->error(__('用户ID不能为空'));
+        }
+
+        $accounts = Db::name('withdrawal_account_config')
+            ->where('user_id', $userId)
+            ->where('status', 1)
+            ->order('is_default', 'desc')
+            ->order('create_time', 'desc')
+            ->select()
+            ->toArray();
+
+        foreach ($accounts as &$account) {
+            // 添加账户类型文本
+            $account['account_type_text'] = $this->getAccountTypeText($account['account_type']);
+
+            // 部分掩码显示
+            if ($account['card_number']) {
+                $account['card_number_masked'] = substr($account['card_number'], 0, 4) . '****' . substr($account['card_number'], -4);
+            }
+        }
+
+        $this->success('查询成功', $accounts);
+    }
+
+    /**
+     * 验证账户字段
+     * @param string $accountType 账户类型
+     * @param array $fields 字段值
+     * @return bool|string
+     */
+    private function validateAccountFields(string $accountType, array $fields): bool|string
+    {
+        if ($accountType == 'bank') {
+            if (empty($fields['bank_name'])) {
+                return '银行名称不能为空';
+            }
+            if (empty($fields['card_number'])) {
+                return '银行卡号不能为空';
+            }
+        } elseif ($accountType == 'alipay') {
+            if (empty($fields['alipay_account'])) {
+                return '支付宝账号不能为空';
+            }
+        } elseif ($accountType == 'wechat') {
+            if (empty($fields['wechat_account'])) {
+                return '微信账号不能为空';
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * 获取账户类型文本
+     * @param string $type
+     * @return string
+     */
+    private function getAccountTypeText(string $type): string
+    {
+        $map = [
+            'bank' => '银行卡',
+            'alipay' => '支付宝',
+            'wechat' => '微信'
+        ];
+        return $map[$type] ?? '未知类型';
+    }
+
+    /**
+     * 获取账户类型选项
+     * @return void
+     */
+    public function accountTypeOptions(): void
+    {
+        $options = [
+            ['value' => 'bank', 'label' => '银行卡'],
+            ['value' => 'alipay', 'label' => '支付宝'],
+            ['value' => 'wechat', 'label' => '微信']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 0, 'label' => '禁用'],
+            ['value' => 1, 'label' => '启用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取银行列表(常见银行)
+     * @return void
+     */
+    public function bankList(): void
+    {
+        $banks = [
+            '中国工商银行',
+            '中国农业银行',
+            '中国银行',
+            '中国建设银行',
+            '交通银行',
+            '招商银行',
+            '中信银行',
+            '中国民生银行',
+            '中国光大银行',
+            '华夏银行',
+            '兴业银行',
+            '浦发银行',
+            '平安银行',
+            '广发银行',
+            '邮政储蓄银行'
+        ];
+
+        $options = [];
+        foreach ($banks as $bank) {
+            $options[] = ['value' => $bank, 'label' => $bank];
+        }
+
+        $this->success('获取成功', $options);
+    }
+}

+ 455 - 0
app/admin/controller/pay/Config.php

@@ -0,0 +1,455 @@
+<?php
+/**
+ * 支付配置控制器 - 字段单独存储版
+ */
+
+namespace app\admin\controller\pay;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class Config extends Backend
+{
+    protected array $noNeedLogin = ['*'];
+
+    /**
+     * 支付配置列表
+     * @return void
+     */
+    public function index(): void
+    {
+        $where = [];
+
+        // 配置名称搜索
+        if (!empty($this->request->get('name'))) {
+            $where[] = ['name', 'like', '%' . $this->request->get('name') . '%'];
+        }
+
+        // 支付类型筛选
+        if (!empty($this->request->get('type'))) {
+            $where['type'] = $this->request->get('type');
+        }
+
+        // 状态筛选
+        if ($this->request->get('status') !== '' && $this->request->get('status') !== null) {
+            $where['status'] = $this->request->get('status');
+        }
+
+        // 排序
+        $order = ['id' => 'desc'];
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        $configList = Db::name('pay_config')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        $list = $configList->items();
+        foreach ($list as &$item) {
+            // 添加类型文本
+            $item['type_text'] = $this->getTypeText($item['type']);
+            $item['status_text'] = $item['status'] ? '启用' : '禁用';
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $configList->total()
+        ]);
+    }
+
+    /**
+     * 获取支付配置详情
+     * @return void
+     */
+    public function detail(): void
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $config = Db::name('pay_config')
+            ->where('id', $id)
+            ->find();
+
+        if (!$config) {
+            $this->error(__('配置不存在'));
+        }
+
+        // 添加类型文本
+        $config['type_text'] = $this->getTypeText($config['type']);
+
+        $this->success('查询成功', $config);
+    }
+
+    /**
+     * 新增/编辑支付配置
+     * @return void
+     */
+    public function edit(): void
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $type = $this->request->post('type', 1);
+        $status = $this->request->post('status', 0);
+
+        // 通用字段
+        $appId = $this->request->post('app_id', '');
+        $mchId = $this->request->post('mch_id', '');
+        $key = $this->request->post('key', '');
+        $notifyUrl = $this->request->post('notify_url', '');
+        $h5NotifyUrl = $this->request->post('h5_notify_url', '');
+
+        // 支付宝专用字段
+        $publicKeyPath = $this->request->post('public_key_path', '');
+        $privateKeyPath = $this->request->post('private_key_path', '');
+
+        if (empty($name)) {
+            $this->error(__('配置名称不能为空'));
+        }
+
+        if (empty($type)) {
+            $this->error(__('请选择支付类型'));
+        }
+
+        // 验证配置名称是否已存在(编辑时排除自己)
+        $checkName = Db::name('pay_config')
+            ->where('name', $name);
+
+        if (!empty($id)) {
+            $checkName->where('id', '<>', $id);
+        }
+
+        if ($checkName->find()) {
+            $this->error(__('配置名称已存在'));
+        }
+
+        // 验证必填字段
+        $validationResult = $this->validateFields($type, [
+            'app_id' => $appId,
+            'mch_id' => $mchId,
+            'key' => $key,
+            'notify_url' => $notifyUrl,
+            'h5_notify_url' => $h5NotifyUrl,
+            'public_key_path' => $publicKeyPath,
+            'private_key_path' => $privateKeyPath
+        ]);
+
+        if ($validationResult !== true) {
+            $this->error($validationResult);
+        }
+
+        $data = [
+            'name' => $name,
+            'type' => $type,
+            'app_id' => $appId,
+            'mch_id' => $mchId,
+            'key' => $key,
+            'notify_url' => $notifyUrl,
+            'h5_notify_url' => $h5NotifyUrl,
+            'public_key_path' => $type == 2 ? $publicKeyPath : '',
+            'private_key_path' => $type == 2 ? $privateKeyPath : '',
+            'status' => $status,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $configInfo = Db::name('pay_config')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$configInfo) {
+                    $this->error(__('配置不存在'));
+                }
+
+                // 如果启用此配置,需要禁用同类型的其他配置
+                if ($status == 1 && $configInfo['status'] == 0) {
+                    Db::name('pay_config')
+                        ->where('type', $type)
+                        ->where('id', '<>', $id)
+                        ->update(['status' => 0]);
+                }
+
+                $result = Db::name('pay_config')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+
+                // 如果启用此配置,需要禁用同类型的其他配置
+                if ($status == 1) {
+                    Db::name('pay_config')
+                        ->where('type', $type)
+                        ->update(['status' => 0]);
+                }
+
+                $result = Db::name('pay_config')
+                    ->insert($data);
+                $id = Db::name('pay_config')->getLastInsID();
+                $message = '新增成功';
+            }
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success($message, ['id' => $id]);
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 验证字段
+     * @param int $type 支付类型
+     * @param array $fields 字段值
+     * @return bool|string
+     */
+    private function validateFields(int $type, array $fields)
+    {
+        // 通用验证
+        if (empty($fields['app_id'])) {
+            return '应用ID不能为空';
+        }
+
+        if (empty($fields['mch_id'])) {
+            return '商户ID不能为空';
+        }
+
+        if (empty($fields['key'])) {
+            return 'API密钥不能为空';
+        }
+
+        // 类型特定验证
+        if ($type == 1) { // 微信支付
+            if (strlen($fields['key']) < 32) {
+                return '微信支付API密钥长度必须至少32位';
+            }
+        } elseif ($type == 2) { // 支付宝
+            if (empty($fields['private_key_path'])) {
+                return '支付宝私钥地址不能为空';
+            }
+        }
+
+        // URL验证
+        if (!empty($fields['notify_url']) && !filter_var($fields['notify_url'], FILTER_VALIDATE_URL)) {
+            return '支付通知地址格式不正确';
+        }
+
+        if (!empty($fields['h5_notify_url']) && !filter_var($fields['h5_notify_url'], FILTER_VALIDATE_URL)) {
+            return 'H5支付通知地址格式不正确';
+        }
+
+        return true;
+    }
+
+    /**
+     * 删除支付配置
+     * @return void
+     */
+    public function delete(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $configInfo = Db::name('pay_config')
+            ->where('id', $id)
+            ->find();
+
+        if (!$configInfo) {
+            $this->error(__('配置不存在'));
+        }
+
+        if ($configInfo['status'] == 1) {
+            $this->error(__('当前配置已启用,请先禁用后再删除'));
+        }
+
+        $result = Db::name('pay_config')
+            ->where('id', $id)
+            ->delete();
+
+        if ($result) {
+            $this->success('删除成功');
+        } else {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 启用/禁用配置
+     * @return void
+     */
+    public function toggleStatus(): void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $configInfo = Db::name('pay_config')
+            ->where('id', $id)
+            ->find();
+
+        if (!$configInfo) {
+            $this->error(__('配置不存在'));
+        }
+
+        $newStatus = $configInfo['status'] == 1 ? 0 : 1;
+        $statusText = $newStatus ? '启用' : '禁用';
+
+        try {
+            // 如果启用此配置,需要禁用同类型的其他配置
+            if ($newStatus == 1) {
+                Db::name('pay_config')
+                    ->where('type', $configInfo['type'])
+                    ->where('id', '<>', $id)
+                    ->update(['status' => 0]);
+            }
+
+            $result = Db::name('pay_config')
+                ->where('id', $id)
+                ->update([
+                    'status' => $newStatus,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success($statusText . '成功');
+        } else {
+            $this->error($statusText . '失败');
+        }
+    }
+
+    /**
+     * 获取支付类型文本
+     * @param int $type
+     * @return string
+     */
+    private function getTypeText(int $type): string
+    {
+        $map = [
+            1 => '微信支付',
+            2 => '支付宝支付'
+        ];
+        return $map[$type] ?? '未知类型';
+    }
+
+    /**
+     * 获取支付类型选项
+     * @return void
+     */
+    public function typeOptions(): void
+    {
+        $options = [
+            ['value' => 1, 'label' => '微信支付'],
+            ['value' => 2, 'label' => '支付宝支付']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取状态选项
+     * @return void
+     */
+    public function statusOptions(): void
+    {
+        $options = [
+            ['value' => 0, 'label' => '禁用'],
+            ['value' => 1, 'label' => '启用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取支付配置模板
+     * @return void
+     */
+    public function getTemplate(): void
+    {
+        $type = $this->request->get('type', 1);
+
+        if ($type == 1) { // 微信支付模板
+            $template = [
+                'name' => '微信支付配置',
+                'type' => 1,
+                'app_id' => 'wx1234567890abcdef',
+                'mch_id' => '1234567890',
+                'key' => 'your_wechat_api_key_32_characters_long',
+                'notify_url' => 'https://your-domain.com/api/pay/wechat/notify',
+                'h5_notify_url' => 'https://your-domain.com/api/pay/wechat/h5-notify',
+                'public_key_path' => '',
+                'private_key_path' => '',
+                'status' => 0
+            ];
+        } elseif ($type == 2) { // 支付宝模板
+            $template = [
+                'name' => '支付宝支付配置',
+                'type' => 2,
+                'app_id' => '2021000123456789',
+                'mch_id' => '2088101123456789',
+                'key' => 'your_alipay_api_key',
+                'notify_url' => 'https://your-domain.com/api/pay/alipay/notify',
+                'h5_notify_url' => 'https://your-domain.com/api/pay/alipay/h5-notify',
+                'public_key_path' => '/path/to/alipay/public_key.pem',
+                'private_key_path' => '/path/to/alipay/private_key.pem',
+                'status' => 0
+            ];
+        } else {
+            $template = [
+                'name' => '',
+                'type' => $type,
+                'app_id' => '',
+                'mch_id' => '',
+                'key' => '',
+                'notify_url' => '',
+                'h5_notify_url' => '',
+                'public_key_path' => '',
+                'private_key_path' => '',
+                'status' => 0
+            ];
+        }
+
+        $this->success('获取成功', $template);
+    }
+
+    /**
+     * 获取当前启用的配置
+     * @return void
+     */
+    public function getActiveConfig(): void
+    {
+        $type = $this->request->get('type', 1);
+
+        $config = Db::name('pay_config')
+            ->where('type', $type)
+            ->where('status', 1)
+            ->find();
+
+        if (!$config) {
+            $this->error('未找到启用的支付配置');
+        }
+
+        $this->success('获取成功', $config);
+    }
+}

+ 20 - 0
app/admin/controller/userCenter/Index.php

@@ -94,6 +94,9 @@ class Index extends  Backend
                 $result = Db::name('company')
                     ->where('id', $id)
                     ->update($data);
+                Db::name('user_on')
+                    ->where('company_id', $id)
+                    ->update(['company'=>$name]);
                 $message = '编辑成功';
             } else {
                 // 新增
@@ -143,6 +146,23 @@ class Index extends  Backend
         }
     }
 
+
+    /**
+     * 获取启用企业选项(用于下拉选择)
+     * @return void
+     */
+    public function selectOptions():void
+    {
+        $packageList = Db::name('company')
+            ->where('status', 1)
+            ->field('id,name')
+            ->select()
+            ->toArray();
+
+        $this->success('获取成功', $packageList);
+    }
+
+
     /**
      * 禁用/启用企业
      * @return void

+ 13 - 6
app/admin/controller/userCenter/User.php

@@ -21,25 +21,25 @@ class User extends Backend
         // 姓名搜索
         if (!empty($this->request->get('name'))) {
             $name = $this->request->get('name');
-            $where[] = ['name', 'like', '%' . $name . '%'];
+            $where[] = ['wac.name', 'like', '%' . $name . '%'];
         }
 
         // 邮箱搜索
         if (!empty($this->request->get('email'))) {
             $email = $this->request->get('email');
-            $where[] = ['email', 'like', '%' . $email . '%'];
+            $where[] = ['wac.email', 'like', '%' . $email . '%'];
         }
 
         // 企业搜索
         if (!empty($this->request->get('company'))) {
             $company = $this->request->get('company');
-            $where[] = ['company', 'like', '%' . $company . '%'];
+            $where[] = ['wac.company', 'like', '%' . $company . '%'];
         }
 
         // 状态筛选
         if (!empty($this->request->get('status'))) {
             $status = $this->request->get('status');
-            $where['status'] = $status;
+            $where['wac.status'] = $status;
         }
 
         // 排序
@@ -49,7 +49,7 @@ class User extends Backend
             $order_a = explode(',', $order_a);
             $order[$order_a[0]] = $order_a[1];
         } else {
-            $order['id'] = 'desc';
+            $order['wac.id'] = 'desc';
         }
 
         // 分页
@@ -57,7 +57,10 @@ class User extends Backend
         $limit = $this->request->get('limit', 20);
 
         $userList = Db::name('user_on')
+            ->alias('wac')
+            ->leftJoin('company u', 'wac.company_id = u.id')
             ->where($where)
+            ->field('wac.*, u.name as company_name')
             ->order($order)
             ->paginate([
                 'list_rows' => $limit,
@@ -70,6 +73,8 @@ class User extends Backend
         ]);
     }
 
+
+
     /**
      * 新增/编辑用户
      * @return void
@@ -81,6 +86,7 @@ class User extends Backend
         $email = $this->request->post('email');
         $password = $this->request->post('password');
         $company = $this->request->post('company');
+        $company_id = $this->request->post('company_id');
         $status = $this->request->post('status', 1);
 
         if (empty($name) || empty($email)) {
@@ -107,7 +113,8 @@ class User extends Backend
         $data = [
             'name' => $name,
             'email' => $email,
-            'company' => $company ?: null,
+            'company' => $company?: null,
+            'company_id' => $company_id?: null,
             'status' => $status,
             'update_time' => date('Y-m-d H:i:s')
         ];

+ 2 - 1
composer.json

@@ -29,7 +29,8 @@
         "ext-bcmath": "*",
         "ext-iconv": "*",
         "ext-json": "*",
-        "ext-gd": "*"
+        "ext-gd": "*",
+        "phpoffice/phpspreadsheet": "^2.1"
     },
     "require-dev": {
         "symfony/var-dumper": "^5.4",

+ 1 - 1
web/.env

@@ -1,5 +1,5 @@
 # port 端口号
-VITE_PORT = 1818
+VITE_PORT = 1919
 
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN = false

+ 1 - 1
web/.env.development

@@ -5,4 +5,4 @@ ENV = 'development'
 VITE_BASE_PATH = './'
 
 # 本地环境接口地址 - 尾部无需带'/'
-VITE_AXIOS_BASE_URL = 'http://user.adjdds.com'
+VITE_AXIOS_BASE_URL = 'https://user.adjdds.com'

+ 26 - 0
web/src/api/backend/order/distribution.ts

@@ -0,0 +1,26 @@
+import createAxios from '/@/utils/axios'
+
+// 获取分销设置
+export function getSetting(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/order.Distribution/getSetting',
+            method: 'get',
+            params: data,
+        },
+    )
+}
+
+// 更新分销设置
+export function updateSetting(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/order.Distribution/updateSetting',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true
+        }
+    )
+}

+ 10 - 0
web/src/api/backend/order/order.ts

@@ -0,0 +1,10 @@
+import createAxios from '/@/utils/axios'
+
+// 列表
+export function handlexport(data: anyObj) {
+    return createAxios({
+        url: '/admin/order.Order/export',
+        method: 'get',
+        params: data,
+    })
+}

+ 40 - 0
web/src/api/backend/order/withdrawal.ts

@@ -0,0 +1,40 @@
+import createAxios from '/@/utils/axios'
+
+// 提现审核通过
+export function approve(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/order.Withdrawal/approve',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true
+        }
+    )
+}
+
+// 审核拒绝
+export function reject(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/order.Withdrawal/reject',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true
+        }
+    )
+}
+
+// 提现统计
+export function statistics(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/order.Withdrawal/statistics',
+            method: 'get',
+            params: data,
+        },
+    )
+}

+ 96 - 0
web/src/api/backend/package/package.ts

@@ -0,0 +1,96 @@
+import createAxios from '/@/utils/axios'
+
+// 列表
+export function list(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/index',
+        method: 'get',
+        params: data,
+    })
+}
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/delete',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 更新产品状态
+export function batchStatus(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/package.Package/batchStatus',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true
+        }
+    )
+}
+
+// 详情
+export function detail(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/detail',
+        method: 'GET',
+        params: data,
+    })
+}
+
+// 套餐统计信息
+export function statistics(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/statistics',
+        method: 'GET',
+        params: data,
+    })
+}
+
+// 规格列表
+export function specList(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/specList',
+        method: 'get',
+        params: data,
+    })
+}
+
+// 添加/编辑套餐规格
+export function editSpec(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/editSpec',
+        method: 'post',
+        data: data,
+    })
+}
+
+//删除套餐规格
+export function deleteSpec(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/deleteSpec',
+        method: 'post',
+        data: data,
+    })
+}
+
+//删除套餐规格
+export function batchUpdateStock(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.Package/batchUpdateStock',
+        method: 'get',
+        params: data,
+    })
+}

+ 51 - 0
web/src/api/backend/package/product.ts

@@ -0,0 +1,51 @@
+import createAxios from '/@/utils/axios'
+
+// 列表
+export function list(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.product/index',
+        method: 'get',
+        params: data,
+    })
+}
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.product/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.product/delete',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 更新产品状态
+export function batchStatus(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/package.product/batchStatus',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true
+        }
+    )
+}
+
+// 批量更新库存状态
+export function batchStockStatus(data: anyObj) {
+    return createAxios({
+        url: '/admin/package.product/batchStockStatus',
+        method: 'post',
+        data: data,
+    })
+}

+ 19 - 0
web/src/api/backend/pay/accountConfig.ts

@@ -0,0 +1,19 @@
+import createAxios from '/@/utils/axios'
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/pay.AccountConfig/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data: anyObj) {
+    return createAxios({
+        url: '/admin/pay.AccountConfig/delete',
+        method: 'post',
+        data: data,
+    })
+}

+ 28 - 0
web/src/api/backend/pay/config.ts

@@ -0,0 +1,28 @@
+import createAxios from '/@/utils/axios'
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/pay.Config/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data: anyObj) {
+    return createAxios({
+        url: '/admin/pay.Config/delete',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 启用/禁用配置(启用此配置,需要禁用同类型的其他配置)
+export function toggleStatus(data: anyObj) {
+    return createAxios({
+        url: '/admin/pay.Config/toggleStatus',
+        method: 'post',
+        data: data,
+    })
+}

+ 12 - 3
web/src/api/backend/company/index.ts → web/src/api/backend/userCenter/index.ts

@@ -1,10 +1,19 @@
 import createAxios from '/@/utils/axios'
 
+// 添加、编辑
+export function list(data: anyObj) {
+    return createAxios({
+        url: '/admin/userCenter.index/index',
+        method: 'get',
+        params: data,
+    })
+}
+
 
 // 添加、编辑
 export function edit(data: anyObj) {
     return createAxios({
-        url: '/admin/company.index/edit',
+        url: '/admin/userCenter.index/edit',
         method: 'post',
         data: data,
     })
@@ -13,7 +22,7 @@ export function edit(data: anyObj) {
 // 删除
 export function del(data:anyObj) {
     return createAxios({
-        url: '/admin/company.index/delete',
+        url: '/admin/userCenter.index/delete',
         method: 'post',
         data: data,
     })
@@ -22,7 +31,7 @@ export function del(data:anyObj) {
 // 禁用/启用企业 传id
 export function toggleStatus(data:anyObj) {
     return createAxios({
-        url: '/admin/company.index/toggleStatus',
+        url: '/admin/userCenter.index/toggleStatus',
         method: 'post',
         data: data,
     })

+ 21 - 7
web/src/api/backend/userCenter/user.ts

@@ -1,6 +1,15 @@
 import createAxios from '/@/utils/axios'
 
 
+// 列表
+export function list(data: anyObj) {
+    return createAxios({
+        url: '/admin/userCenter.user/index',
+        method: 'get',
+        params: data,
+    })
+}
+
 // 添加、编辑
 export function edit(data: anyObj) {
     return createAxios({
@@ -11,16 +20,21 @@ export function edit(data: anyObj) {
 }
 
 // 删除
-export function del(data:anyObj) {
-    return createAxios({
-        url: '/admin/userCenter.user/delete',
-        method: 'post',
-        data: data,
-    })
+export function del(data: anyObj) {
+    return createAxios(
+        {
+            url: '/admin/userCenter.user/delete',
+            method: 'post',
+            data: data,
+        },
+        {
+            showSuccessMessage: true,
+        }
+    )
 }
 
 // 解除上下级关系
-export function removeSubordinate(data:anyObj) {
+export function removeSubordinate(data: anyObj) {
     return createAxios({
         url: '/userCenter.user/removeSubordinate',
         method: 'get',

+ 1 - 0
web/src/assets/icons/pay-list.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766990560067" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7081" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M832 256H192a32 32 0 0 1 0-64h640a32 32 0 1 1 0 64zM342.4 448H192a32 32 0 0 1 0-64h150.4a32 32 0 0 1 0 64zM342.4 640H192a32 32 0 0 1 0-64h150.4a32 32 0 0 1 0 64zM342.4 832H192a32 32 0 0 1 0-64h150.4a32 32 0 1 1 0 64zM810.88 579.2H512a32 32 0 0 1-32-32 32 32 0 0 1 32-32h296.32a32 32 0 0 1 32 32 32.64 32.64 0 0 1-29.44 32zM810.88 704H512a32 32 0 1 1 0-64h296.32a32 32 0 0 1 0 64z" fill="#333333" p-id="7082"></path><path d="M662.4 547.84a31.36 31.36 0 0 1-22.4-9.6L523.52 421.76a32 32 0 0 1 0-45.44 32 32 0 0 1 44.8 0l117.12 117.12a32 32 0 0 1 0 44.8 32.64 32.64 0 0 1-23.04 9.6z" fill="#333333" p-id="7083"></path><path d="M662.4 547.84a32 32 0 0 1-22.4-54.4l116.48-117.12a32 32 0 1 1 45.44 45.44l-116.48 116.48a32.64 32.64 0 0 1-23.04 9.6z" fill="#333333" p-id="7084"></path><path d="M662.4 832a32 32 0 0 1-32-32V512a32 32 0 0 1 32-32 32 32 0 0 1 32 32v288a32 32 0 0 1-32 32z" fill="#333333" p-id="7085"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
web/src/assets/icons/withdrawal-configuration.svg


+ 2 - 1
web/src/components/baInput/components/baUpload.vue

@@ -196,7 +196,8 @@ const onElChange = (file: UploadFileExt, files: UploadFiles) => {
     })
         .then((res) => {
             if (res.code == 1) {
-                file.serverUrl = res.data.file.url
+                // 如果 returnFullUrl 为 true,优先使用 full_url 字段,否则使用 url 字段
+                file.serverUrl = props.returnFullUrl && res.data.file.full_url ? res.data.file.full_url : res.data.file.url
                 file.status = 'success'
                 emits('update:modelValue', getAllUrls())
                 triggerEvent('success', [res, file, files])

+ 147 - 0
web/src/views/backend/order/distribution/index.vue

@@ -0,0 +1,147 @@
+<template>
+	<div class="default-main ba-table-box">
+		<!-- 表格顶部菜单 -->
+		<TableHeader :buttons="['refresh', 'columnDisplay',]">
+			<el-button type="primary" @click="handleGetSetting" plain v-auth="'getSetting'">分销配置</el-button>
+		</TableHeader>
+		<!-- 表格 -->
+		<!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+		<Table ref="tableRef"></Table>
+
+		<!-- 分销配置弹框 -->
+		<SettingDialog ref="settingDialogRef" @success="handleSettingSuccess" />
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, provide, useTemplateRef } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import SettingDialog from './settingDialog.vue'
+
+defineOptions({
+    name: 'order/distribution',
+})
+
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+	new baTableApi('/admin/order.Distribution/'),
+	{
+		column: [
+			{ label: '编号', prop: 'id', align: 'center', width: 70, operator: '=' },
+			{ 
+				label: '关联订单编号', 
+				prop: 'order_no', 
+				align: 'center', 
+				minWidth: 160,
+			},
+			{ 
+				label: '分佣产品', 
+				prop: 'commission_product', 
+				align: 'center', 
+				minWidth: 140,
+			},
+			{ 
+				label: '购买人', 
+				prop: 'g_name', 
+				align: 'center', 
+				minWidth: 100,
+			},
+			{ 
+				label: '分佣人', 
+				prop: 'f_name', 
+				align: 'center', 
+				minWidth: 100,
+			},
+			{ 
+				label: '分佣金额', 
+				prop: 'commission_amount', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{ 
+				label: '佣金比例', 
+				prop: 'commission_rate', 
+				align: 'center', 
+				minWidth: 110,
+				formatter: (row: any, column: any, cellValue: any) => {
+					return cellValue ? `${cellValue}%` : '-';
+				},
+			},
+			{ 
+				label: '创建时间', 
+				prop: 'create_time', 
+				align: 'center',
+				minWidth: 170,
+			},
+			{
+				label: '状态',
+				prop: 'status',
+				align: 'center',
+				render: 'tag',
+				custom: {
+					'1': 'warning',
+					'2': 'success',
+					'3': 'info',
+				},
+				replaceValue: {
+					'1': '待结算',
+					'2': '已结算',
+					'3': '已取消',
+				},
+				minWidth: 100,
+			},
+			{
+				label: '操作',
+				align: 'center',
+				width: '180',
+				render: 'slot',
+				slotName: 'operate',
+				operator: false,
+			},
+		],
+		dblClickNotEditColumn: [undefined,],
+		filter: {
+			limit: 20,
+		},
+	},
+	{
+		defaultItems: {
+		},
+	},
+)
+
+baTable.mount()
+baTable.getIndex()
+provide('baTable', baTable)
+
+// 搜索
+const seachList = () => {
+	baTable.table.filter!.page = 1
+	baTable.table.loading = true
+}
+
+// 分销配置弹框引用
+const settingDialogRef = useTemplateRef('settingDialogRef')
+
+// 打开分销设置弹框
+const handleGetSetting = () => {
+	if (settingDialogRef.value) {
+		settingDialogRef.value.open()
+	}
+}
+
+// 配置成功回调
+const handleSettingSuccess = () => {
+	// 配置更新后可以刷新表格,如果需要的话
+	// baTable.getIndex()
+}
+</script>
+<style scoped lang="scss">
+
+</style>

+ 224 - 0
web/src/views/backend/order/distribution/settingDialog.vue

@@ -0,0 +1,224 @@
+<template>
+    <!-- 分销配置对话框 -->
+    <el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
+        @close="handleClose" width="600px" style="height: 400px;">
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                分销配置
+            </div>
+        </template>
+        <el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
+            <div class="ba-operate-form">
+                <el-form ref="formRef" :model="formData" :rules="rules" :label-width="140">
+                    <el-form-item label="默认分销佣金比例" prop="default_rate">
+                        <div class="input-with-unit">
+                            <el-input-number v-model="formData.default_rate" :min="0" :max="100" :precision="2"
+                                placeholder="请输入默认分销佣金比例" style="width: 100%" controls-position="right" />
+                            <span class="form-unit">%</span>
+                        </div>
+                    </el-form-item>
+
+                    <el-form-item label="最低提现金额" prop="min_withdrawal">
+                        <div class="input-with-unit">
+                            <el-input-number v-model="formData.min_withdrawal" :min="0" :precision="2"
+                                placeholder="请输入最低提现金额" style="width: 100%" controls-position="right"
+                                @change="handleMinWithdrawalChange" />
+                            <span class="form-unit">元</span>
+                        </div>
+                    </el-form-item>
+
+                    <el-form-item label="最高提现金额" prop="max_withdrawal">
+                        <div class="input-with-unit">
+                            <el-input-number v-model="formData.max_withdrawal" :min="0" :precision="2"
+                                placeholder="请输入最高提现金额" style="width: 100%" controls-position="right" />
+                            <span class="form-unit">元</span>
+                        </div>
+                    </el-form-item>
+
+                    <el-form-item label="提现手续费比例" prop="withdrawal_fee_rate">
+                        <div class="input-with-unit">
+                            <el-input-number v-model="formData.withdrawal_fee_rate" :min="0" :max="100" :precision="2"
+                                placeholder="请输入提现手续费比例" style="width: 100%" controls-position="right" />
+                            <span class="form-unit">%</span>
+                        </div>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="handleClose" size="default">取消</el-button>
+                <el-button v-blur :loading="loading" @click="handleSubmit" type="primary" size="default">
+                    保存
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type { FormInstance, FormRules } from 'element-plus'
+import { getSetting, updateSetting } from '/@/api/backend/order/distribution'
+
+const { t } = useI18n()
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+const formRef = ref<FormInstance>()
+
+const formData = reactive({
+    default_rate: 0,
+    min_withdrawal: 0,
+    max_withdrawal: 0,
+    withdrawal_fee_rate: 0,
+})
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+    default_rate: [
+        { required: true, message: '请输入默认分销佣金比例', trigger: 'blur' },
+        {
+            validator: (rule: any, value: number, callback: Function) => {
+                if (value < 0 || value > 100) {
+                    callback(new Error('默认分销佣金比例必须在0-100之间'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur',
+        },
+    ],
+    min_withdrawal: [
+        { required: true, message: '请输入最低提现金额', trigger: 'blur' },
+        {
+            validator: (rule: any, value: number, callback: Function) => {
+                if (value < 0) {
+                    callback(new Error('最低提现金额必须大于等于0'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur',
+        },
+    ],
+    max_withdrawal: [
+        { required: true, message: '请输入最高提现金额', trigger: 'blur' },
+        {
+            validator: (rule: any, value: number, callback: Function) => {
+                if (value < 0) {
+                    callback(new Error('最高提现金额必须大于等于0'))
+                } else if (value < formData.min_withdrawal) {
+                    callback(new Error('最高提现金额必须大于等于最低提现金额'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur',
+        },
+    ],
+    withdrawal_fee_rate: [
+        { required: true, message: '请输入提现手续费比例', trigger: 'blur' },
+        {
+            validator: (rule: any, value: number, callback: Function) => {
+                if (value < 0 || value > 100) {
+                    callback(new Error('提现手续费比例必须在0-100之间'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur',
+        },
+    ],
+})
+
+// 打开对话框
+const open = () => {
+    dialogVisible.value = true
+    loadSetting()
+}
+
+// 加载分销设置
+const loadSetting = () => {
+    loading.value = true
+    getSetting({}).then((res: any) => {
+        formData.default_rate = res.data.default_rate || 0
+        formData.min_withdrawal = res.data.min_withdrawal || 0
+        formData.max_withdrawal = res.data.max_withdrawal || 0
+        formData.withdrawal_fee_rate = res.data.withdrawal_fee_rate || 0
+    }).finally(() => {
+        loading.value = false
+    })
+}
+
+// 提交表单
+const handleSubmit = () => {
+    if (!formRef.value) return
+
+    formRef.value.validate((valid: boolean) => {
+        if (valid) {
+            loading.value = true
+
+            const params = {
+                default_rate: formData.default_rate,
+                min_withdrawal: formData.min_withdrawal,
+                max_withdrawal: formData.max_withdrawal,
+                withdrawal_fee_rate: formData.withdrawal_fee_rate,
+            }
+
+            updateSetting(params).then(() => {
+                dialogVisible.value = false
+                emit('success')
+            }).finally(() => {
+                loading.value = false
+            })
+        }
+    })
+}
+
+// 最低提现金额改变时,重新验证最高提现金额
+const handleMinWithdrawalChange = () => {
+    if (formRef.value) {
+        formRef.value.validateField('max_withdrawal', () => { })
+    }
+}
+
+// 关闭对话框
+const handleClose = () => {
+    dialogVisible.value = false
+    formRef.value?.clearValidate()
+}
+
+const emit = defineEmits(['success'])
+
+// 导出方法供父组件调用
+defineExpose({
+    open,
+})
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+}
+
+.input-with-unit {
+    display: flex;
+    align-items: center;
+    width: 100%;
+
+    :deep(.el-input-number) {
+        flex: 1;
+    }
+
+    .form-unit {
+        margin-left: 8px;
+        color: var(--el-text-color-regular);
+        font-size: 14px;
+        white-space: nowrap;
+    }
+}
+</style>

+ 156 - 0
web/src/views/backend/order/order/index.vue

@@ -0,0 +1,156 @@
+<template>
+	<div class="default-main ba-table-box">
+		<!-- 表格顶部菜单 -->
+		<TableHeader :buttons="['refresh', 'columnDisplay',]">
+			<el-button type="warning" :icon="Download" plain @click="onExport" v-auth="'export'">导出</el-button>
+		</TableHeader>
+		<!-- 表格 -->
+		<!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+		<Table ref="tableRef"></Table>
+
+	</div>
+</template>
+
+<script setup lang="ts">
+import {  provide, } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import { handlexport } from '/@/api/backend/order/order'
+import { Download } from '@element-plus/icons-vue'
+
+defineOptions({
+    name: 'order/order',
+})
+
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+	new baTableApi('/admin/order.Order/'),
+	{
+		column: [
+			{ 
+				label: '编号', 
+				prop: 'id', 
+				align: 'center', 
+				width: 70,
+			},
+			{ 
+				label: '订单编号', 
+				prop: 'order_no', 
+				align: 'center', 
+				minWidth: 180,
+			},
+			{ 
+				label: '商品名称', 
+				prop: 'item_name', 
+				align: 'center', 
+				minWidth: 140,
+			},
+			{ 
+				label: '购买数量', 
+				prop: 'quantity', 
+				align: 'center', 
+				minWidth: 100,
+			},
+			{ 
+				label: '商品单价', 
+				prop: 'unit_price', 
+				align: 'center', 
+				minWidth: 110,
+			},
+			{ 
+				label: '会员名称', 
+				prop: 'name', 
+				align: 'center', 
+				minWidth: 100,
+			},
+			{ 
+				label: '支付金额', 
+				prop: 'pay_amount', 
+				align: 'center', 
+				minWidth: 110,
+			},
+			{ 
+				label: '最后佣金', 
+				prop: 'commission_amount', 
+				align: 'center', 
+				minWidth: 110,
+			},
+			{ 
+				label: '支付方式', 
+				prop: 'pay_method', 
+				align: 'center', 
+				minWidth: 100,
+			},
+			{ 
+				label: '订单状态', 
+				prop: 'pay_status', 
+				align: 'center', 
+				render: 'tag',
+				custom: {
+					'已支付': 'success',
+					'待支付': 'warning',
+					'已取消': 'info',
+					'已退款': 'danger',
+				},
+				minWidth: 100,
+			},
+			
+			{ 
+				label: '支付时间', 
+				prop: 'pay_time', 
+				align: 'center',
+			},
+			{ 
+				label: '订单日期', 
+				prop: 'order_time', 
+				align: 'center',
+				minWidth: 120,
+			},
+			{
+				label: '操作',
+				align: 'center',
+				width: '180',
+				render: 'slot',
+				slotName: 'operate',
+				operator: false,
+			},
+		],
+		dblClickNotEditColumn: [undefined,],
+		filter: {
+			limit: 20,
+		},
+	},
+	{
+		defaultItems: {
+		
+		},
+	},
+)
+
+baTable.mount()
+baTable.getIndex()
+provide('baTable', baTable)
+
+// 搜索
+const seachList = () => {
+	baTable.table.filter!.page = 1
+	baTable.table.loading = true
+}
+
+// 导出
+const onExport = ()=>{
+	handlexport({}).then((res)=>{
+		console.log(res,'ddddd');
+		
+	})
+}
+
+</script>
+<style scoped lang="scss">
+
+</style>

+ 177 - 0
web/src/views/backend/order/withdrawal/index.vue

@@ -0,0 +1,177 @@
+<template>
+	<div class="default-main ba-table-box">
+		<!-- 表格顶部菜单 -->
+		<TableHeader :buttons="['refresh', 'columnDisplay', 'comSearch']"></TableHeader>
+		<!-- 表格 -->
+		<!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+		<Table ref="tableRef">
+			<template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="150">
+                    <template #default="scope">
+                        <el-button type="primary" size="small" @click="handleApprove(scope.row)" v-auth="'approve'">同意</el-button>
+                        <el-button type="danger" size="small" @click="handleReject(scope.row)" v-auth="'reject'">拒绝</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+		</Table>
+
+		<!-- 审核弹框 -->
+		<ReviewDialog ref="reviewDialogRef" @success="handleReviewSuccess" />
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, provide, useTemplateRef } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import ReviewDialog from './reviewDialog.vue'
+
+defineOptions({
+    name: 'order/withdrawal',
+})
+
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+	new baTableApi('/admin/order.Withdrawal/'),
+	{
+		column: [
+			{ 
+				label: '编号', 
+				prop: 'id', 
+				align: 'center', 
+				width: 70,
+			},
+			{ 
+				label: '提现单号', 
+				prop: 'withdrawal_no', 
+				align: 'center', 
+				minWidth: 160,
+			},
+			{ 
+				label: '申请人', 
+				prop: 'name', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{ 
+				label: '提现金额', 
+				prop: 'amount', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{ 
+				label: '当前账号剩余金额', 
+				prop: 'current_balance', 
+				align: 'center', 
+				minWidth: 160,
+			},
+			{ 
+				label: '提现方式', 
+				prop: 'withdrawal_method', 
+				align: 'center', 
+				minWidth: 120,
+				replaceValue: {
+					'微信': '微信',
+					'支付宝': '支付宝',
+					'银行卡': '银行卡',
+				},
+			},
+			{ 
+				label: '状态', 
+				prop: 'status', 
+				align: 'center',
+				render: 'tag',
+				custom: {
+					'待审核': 'warning',
+					'已通过': 'success',
+					'已拒绝': 'danger',
+					'已取消': 'info',
+				},
+				replaceValue: {
+					'待审核': '待审核',
+					'已通过': '已通过',
+					'已拒绝': '已拒绝',
+					'已取消': '已取消',
+				},
+				minWidth: 100,
+			},
+			{ 
+				label: '创建时间', 
+				prop: 'create_time', 
+				align: 'center',
+				minWidth: 170,
+			},
+			{ 
+				label: '审核时间', 
+				prop: 'audit_time', 
+				align: 'center',
+				minWidth: 170,
+			},
+			{ 
+				label: '备注', 
+				prop: 'audit_remark', 
+				align: 'center', 
+				minWidth: 150,
+			},
+			{
+				label: '操作',
+				align: 'center',
+				width: '180',
+				render: 'slot',
+				slotName: 'operate',
+				operator: false,
+			},
+		],
+		dblClickNotEditColumn: [undefined,],
+		filter: {
+			limit: 20,
+		},
+	},
+	{
+		defaultItems: {
+		},
+	},
+)
+
+baTable.mount()
+baTable.getIndex()
+provide('baTable', baTable)
+
+// 搜索
+const seachList = () => {
+	baTable.table.filter!.page = 1
+	baTable.table.loading = true
+}
+
+// 审核弹框引用
+const reviewDialogRef = useTemplateRef('reviewDialogRef')
+
+// 同意
+const handleApprove = (row: any) => {
+	if (reviewDialogRef.value) {
+		reviewDialogRef.value.openApprove(row)
+	}
+}
+
+// 拒绝
+const handleReject = (row: any) => {
+	if (reviewDialogRef.value) {
+		reviewDialogRef.value.openReject(row)
+	}
+}
+
+// 审核成功回调
+const handleReviewSuccess = () => {
+	baTable.getIndex()
+}
+
+
+</script>
+<style scoped lang="scss">
+
+</style>

+ 163 - 0
web/src/views/backend/order/withdrawal/reviewDialog.vue

@@ -0,0 +1,163 @@
+<template>
+    <!-- 审核对话框 -->
+    <el-dialog
+        class="ba-operate-dialog"
+        :close-on-click-modal="false"
+        :destroy-on-close="true"
+        v-model="dialogVisible"
+        @close="handleClose"
+        width="500px"
+        style="height: 300px;"
+    >
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ dialogTitle }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
+            <div class="ba-operate-form">
+                <el-form
+                    ref="formRef"
+                    :model="formData"
+                    :rules="rules"
+                    :label-width="100"
+                >
+                    <el-form-item label="审核备注" prop="audit_remark">
+                        <el-input
+                            v-model="formData.audit_remark"
+                            type="textarea"
+                            :rows="5"
+                            :placeholder="dialogType === 'approve' ? '请输入审核通过备注(可选)' : '请输入审核拒绝原因'"
+                            maxlength="200"
+                            show-word-limit
+                        />
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="handleClose" size="default">取消</el-button>
+                <el-button
+                    v-blur
+                    :loading="loading"
+                    @click="handleSubmit"
+                    :type="dialogType === 'approve' ? 'primary' : 'danger'"
+                    size="default"
+                >
+                    {{ dialogType === 'approve' ? '确认通过' : '确认拒绝' }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type { FormInstance, FormRules } from 'element-plus'
+import { approve, reject } from '/@/api/backend/order/withdrawal'
+
+const { t } = useI18n()
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+const dialogType = ref<'approve' | 'reject'>('approve')
+const currentId = ref<string | number>('')
+
+const formRef = ref<FormInstance>()
+
+const formData = reactive({
+    id: '',
+    audit_remark: '',
+})
+
+// 表单验证规则(动态规则)
+const rules = computed<FormRules>(() => {
+    return {
+        audit_remark: [
+            {
+                required: dialogType.value === 'reject',
+                message: dialogType.value === 'reject' ? '请输入审核拒绝原因' : '请输入审核备注',
+                trigger: 'blur',
+            },
+        ],
+    }
+})
+
+// 对话框标题
+const dialogTitle = computed(() => {
+    return dialogType.value === 'approve' ? '审核通过' : '审核拒绝'
+})
+
+// 打开审核对话框(同意)
+const openApprove = (row: { id: string | number }) => {
+    dialogType.value = 'approve'
+    currentId.value = row.id
+    formData.id = String(row.id)
+    formData.audit_remark = ''
+    dialogVisible.value = true
+    formRef.value?.clearValidate()
+}
+
+// 打开审核对话框(拒绝)
+const openReject = (row: { id: string | number }) => {
+    dialogType.value = 'reject'
+    currentId.value = row.id
+    formData.id = String(row.id)
+    formData.audit_remark = ''
+    dialogVisible.value = true
+    formRef.value?.clearValidate()
+}
+
+// 提交审核
+const handleSubmit = () => {
+    if (!formRef.value) return
+
+    formRef.value.validate((valid: boolean) => {
+        if (valid) {
+            loading.value = true
+
+            const params = {
+                id: formData.id,
+                audit_remark: formData.audit_remark || (dialogType.value === 'approve' ? '审核通过' : ''),
+            }
+
+            const apiCall = dialogType.value === 'approve' ? approve(params) : reject(params)
+
+            apiCall
+                .then(() => {
+                    dialogVisible.value = false
+                    emit('success')
+                })
+                .finally(() => {
+                    loading.value = false
+                })
+        }
+    })
+}
+
+// 关闭对话框
+const handleClose = () => {
+    dialogVisible.value = false
+    formData.audit_remark = ''
+    formRef.value?.clearValidate()
+}
+
+const emit = defineEmits(['success'])
+
+// 导出方法供父组件调用
+defineExpose({
+    openApprove,
+    openReject,
+})
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+}
+</style>
+

+ 507 - 0
web/src/views/backend/package/package/detail.vue

@@ -0,0 +1,507 @@
+<template>
+    <el-dialog
+        v-model="visible"
+        class="ba-operate-dialog"
+        width="1200px"
+        :close-on-click-modal="false"
+        :destroy-on-close="true"
+        @close="handleClose"
+    >
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                套餐详情 --- {{ packageInfo?.name || '-' }}
+            </div>
+        </template>
+
+        <el-scrollbar class="ba-table-form-scrollbar" v-loading="loading">
+            <div class="detail-wrapper" v-if="packageInfo">
+
+                <!-- 统计信息卡片 -->
+                <div class="statistics-card">
+                    <div class="stat-item">
+                        <div class="stat-icon spec-icon">
+                            <el-icon><List /></el-icon>
+                        </div>
+                        <div class="stat-content">
+                            <div class="stat-label">规格总数</div>
+                            <div class="stat-value">{{ statistics.spec_count }}</div>
+                        </div>
+                    </div>
+                    <div class="stat-item">
+                        <div class="stat-icon stock-icon">
+                            <el-icon><Box /></el-icon>
+                        </div>
+                        <div class="stat-content">
+                            <div class="stat-label">总库存</div>
+                            <div class="stat-value">{{ statistics.total_stock }}</div>
+                        </div>
+                    </div>
+                    <div class="stat-item">
+                        <div class="stat-icon sold-icon">
+                            <el-icon><TrendCharts /></el-icon>
+                        </div>
+                        <div class="stat-content">
+                            <div class="stat-label">已售数量</div>
+                            <div class="stat-value">{{ statistics.total_sold }}</div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 规格明细表格 -->
+                <div class="table-card">
+                    <div class="table-header">
+                        <h3 class="table-title">
+                            <el-icon class="title-icon"><Grid /></el-icon>
+                            套餐规格明细
+                            <span class="spec-count">(共 {{ specList.length }} 项)</span>
+                        </h3>
+                    </div>
+                    <el-table
+                        v-if="specList.length"
+                        :data="specList"
+                        class="spec-table"
+                        size="default"
+                        stripe
+                    >
+                        <el-table-column
+                            type="index"
+                            label="序号"
+                            align="center"
+                            width="70"
+                        />
+                        <el-table-column
+                            prop="spec_name"
+                            label="规格名称"
+                            align="center"
+                            min-width="140"
+                        >
+                            <template #default="{ row }">
+                                <span class="spec-name">{{ row.spec_name }}</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="price"
+                            label="价格"
+                            align="center"
+                            min-width="110"
+                        >
+                            <template #default="{ row }">
+                                <span class="price-text">¥{{ row.price }}</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="stock"
+                            label="库存"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                <span class="stock-number">{{ row.stock }}</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            label="库存状态"
+                            align="center"
+                            min-width="120"
+                        >
+                            <template #default="{ row }">
+                                <el-tag :type="getStockStatusTag(row.stock_status)" effect="dark" size="small">
+                                    {{ row.stock_status }}
+                                </el-tag>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="sold_count"
+                            label="已售数量"
+                            align="center"
+                            min-width="110"
+                        >
+                            <template #default="{ row }">
+                                <span class="sold-number">{{ row.sold_count }}</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="type_text"
+                            label="套餐类型"
+                            align="center"
+                            min-width="120"
+                        >
+                            <template #default="{ row }">
+                                <el-tag type="primary" effect="plain" size="small">{{ row.type_text }}</el-tag>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="card_type_text"
+                            label="卡类型"
+                            align="center"
+                            min-width="110"
+                        >
+                            <template #default="{ row }">
+                                <el-tag type="warning" effect="plain" size="small">{{ row.card_type_text }}</el-tag>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            label="次数/积分"
+                            align="center"
+                            min-width="120"
+                        >
+                            <template #default="{ row }">
+                                <span v-if="String(row.card_type) === '1'">
+                                    <span class="unit-value">{{ row.unit_value || '-' }}</span>
+                                    <span class="unit-label">次</span>
+                                </span>
+                                <span v-else-if="String(row.card_type) === '2'">
+                                    <span class="points-value">{{ row.points_value || '-' }}</span>
+                                    <span class="unit-label">分</span>
+                                </span>
+                                <span v-else class="no-value">-</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="description"
+                            label="说明描述"
+                            align="center"
+                            min-width="150"
+                            show-overflow-tooltip
+                        >
+                            <template #default="{ row }">
+                                <span class="description-cell">{{ row.description || '-' }}</span>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="create_time"
+                            label="创建时间"
+                            align="center"
+                            min-width="180"
+                        />
+                    </el-table>
+                    <el-empty v-else description="暂无规格数据" :image-size="100" />
+                </div>
+            </div>
+        </el-scrollbar>
+
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button size="large" class="cancel-btn" @click="handleClose">
+                    关闭
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { detail } from '/@/api/backend/package/package'
+import { ElMessage } from 'element-plus'
+import { Document, List, Box, TrendCharts, Grid } from '@element-plus/icons-vue'
+
+// 套餐基本信息接口
+interface PackageInfo {
+    id: number
+    name: string
+    description: string
+    sort: number
+    status: number
+    create_time: string
+    update_time: string
+}
+
+// 规格列表接口
+interface SpecItem {
+    id: number
+    package_id: number
+    spec_name: string
+    price: string
+    stock: number
+    type: number
+    card_type: number
+    unit_value: number
+    points_value: number
+    description: string
+    sold_count: number
+    sort: number
+    create_time: string
+    update_time: string
+    stock_status: string
+    type_text: string
+    card_type_text: string
+    charge_text: string
+}
+
+// 统计信息接口
+interface Statistics {
+    total_stock: number
+    total_sold: number
+    spec_count: number
+}
+
+// 详情数据接口
+interface DetailResponse {
+    package_info: PackageInfo
+    spec_list: SpecItem[]
+    statistics: Statistics
+}
+
+const visible = ref(false)
+const loading = ref(false)
+const packageInfo = ref<PackageInfo | null>(null)
+const specList = ref<SpecItem[]>([])
+const statistics = ref<Statistics>({
+    total_stock: 0,
+    total_sold: 0,
+    spec_count: 0,
+})
+
+const open = async (id: number) => {
+    visible.value = true
+    await fetchDetail(id)
+}
+
+// 获取详情数据
+const fetchDetail = async (id: number) => {
+    try {
+        loading.value = true
+        const res = await detail({ id })
+        const data: DetailResponse = res.data
+
+        if (data) {
+            packageInfo.value = data.package_info || null
+            specList.value = data.spec_list || []
+            statistics.value = data.statistics || {
+                total_stock: 0,
+                total_sold: 0,
+                spec_count: 0,
+            }
+        }
+    } catch (error: any) {
+        ElMessage.error(error?.msg || '获取详情失败')
+        handleClose()
+    } finally {
+        loading.value = false
+    }
+}
+
+const handleClose = () => {
+    visible.value = false
+    // 重置数据
+    packageInfo.value = null
+    specList.value = []
+    statistics.value = {
+        total_stock: 0,
+        total_sold: 0,
+        spec_count: 0,
+    }
+}
+
+// 获取库存状态标签类型
+const getStockStatusTag = (status: string) => {
+    if (status === '售罄') return 'danger'
+    if (status === '即将售罄') return 'warning'
+    return 'success'
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss">
+.detail-wrapper {
+    padding: 20px;
+}
+
+// 统计信息卡片
+.statistics-card {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 16px;
+    margin-bottom: 20px;
+
+    .stat-item {
+        background: #fff;
+        border-radius: 12px;
+        padding: 20px;
+        display: flex;
+        align-items: center;
+        gap: 16px;
+        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+        transition: all 0.3s ease;
+        border: 1px solid #f0f0f0;
+
+        &:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+        }
+
+        .stat-icon {
+            width: 56px;
+            height: 56px;
+            border-radius: 12px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 24px;
+            color: #fff;
+            flex-shrink: 0;
+
+            &.spec-icon {
+                background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+            }
+
+            &.stock-icon {
+                background: linear-gradient(135deg, #36a3f7 0%, #409eff 100%);
+            }
+
+            &.sold-icon {
+                background: linear-gradient(135deg, #79bbff 0%, #409eff 100%);
+            }
+        }
+
+        .stat-content {
+            flex: 1;
+
+            .stat-label {
+                font-size: 13px;
+                color: #909399;
+                margin-bottom: 8px;
+                font-weight: 500;
+            }
+
+            .stat-value {
+                font-size: 28px;
+                font-weight: 700;
+                color: #303133;
+                line-height: 1;
+            }
+        }
+    }
+}
+
+// 表格卡片
+.table-card {
+    background: #fff;
+    border-radius: 12px;
+    padding: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+    border: 1px solid #f0f0f0;
+
+    .table-header {
+        margin-bottom: 16px;
+        padding-bottom: 12px;
+        border-bottom: 2px solid #f5f7fa;
+
+        .table-title {
+            margin: 0;
+            font-size: 18px;
+            font-weight: 600;
+            color: #303133;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+
+            .title-icon {
+                font-size: 20px;
+                color: #409eff;
+            }
+
+            .spec-count {
+                font-size: 14px;
+                font-weight: 400;
+                color: #909399;
+                margin-left: 4px;
+            }
+        }
+    }
+
+    .spec-table {
+        :deep(.el-table__header) {
+            th {
+                background: #f8f9fa;
+                color: #606266;
+                font-weight: 600;
+                border-bottom: 2px solid #ebeef5;
+            }
+        }
+
+        :deep(.el-table__body) {
+            tr {
+                transition: background-color 0.2s;
+
+                &:hover {
+                    background-color: #f5f7fa;
+                }
+            }
+        }
+
+        .spec-name {
+            font-weight: 600;
+            color: #303133;
+        }
+
+        .price-text {
+            color: #f56c6c;
+            font-weight: 600;
+            font-size: 15px;
+        }
+
+        .stock-number {
+            font-weight: 600;
+            color: #409eff;
+        }
+
+        .sold-number {
+            font-weight: 500;
+            color: #67c23a;
+        }
+
+        .charge-text {
+            color: #606266;
+            font-size: 13px;
+        }
+
+        .unit-value {
+            font-weight: 600;
+            color: #409eff;
+            font-size: 15px;
+        }
+
+        .points-value {
+            font-weight: 600;
+            color: #e6a23c;
+            font-size: 15px;
+        }
+
+        .unit-label {
+            color: #909399;
+            font-size: 13px;
+            margin-left: 4px;
+        }
+
+        .no-value {
+            color: #c0c4cc;
+            font-size: 13px;
+        }
+
+        .description-cell {
+            color: #606266;
+            font-size: 13px;
+            line-height: 1.5;
+        }
+    }
+}
+
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 10px;
+}
+
+// 响应式设计
+@media (max-width: 1200px) {
+    .statistics-card {
+        grid-template-columns: 1fr;
+    }
+}
+</style>
+
+

+ 231 - 0
web/src/views/backend/package/package/index.vue

@@ -0,0 +1,231 @@
+<template>
+    <div class="default-main ba-table-box">
+        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info"
+            show-icon />
+
+        <!-- 表格顶部菜单 -->
+        <TableHeader :buttons="['refresh', 'quickSearch', 'columnDisplay']"
+            :quick-search-placeholder="t('Quick search placeholder', { fields: '套餐名称' })">
+            <el-button type="primary" @click="onAdd" v-auth="'edit'">添加</el-button>
+        </TableHeader>
+
+        <!-- 表格 -->
+        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+        <Table>
+            <template #status>
+                <el-table-column prop="status" align="center" label="状态" min-width="100">
+                    <template #default="scope">
+                        <el-switch v-model="scope.row.status" @change="changeStatus(scope.row)"
+                            :active-value="1" :inactive-value="2" />
+                    </template>
+                </el-table-column>
+            </template>
+            <template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="220">
+                    <template #default="scope">
+                        <el-button type="info" size="small" @click="onDetail(scope.row)"  v-auth="'detail'">详情</el-button>
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                        <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+
+        <!-- 表单 -->
+        <PopupForm ref="popupFormRef" @success="handleFormSuccess" />
+
+        <!-- 详情弹框 -->
+        <Detail ref="detailRef" />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { provide, useTemplateRef } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import PopupForm from './popupForm.vue'
+import Detail from './detail.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import { del, edit,batchStatus } from '/@/api/backend/package/package'
+import { ElMessageBox, ElMessage } from 'element-plus'
+
+type TableRow = Record<string, any>
+
+defineOptions({
+    name: 'package/package',
+})
+
+const { t } = useI18n()
+
+const popupFormRef = useTemplateRef<InstanceType<typeof PopupForm>>('popupFormRef')
+const detailRef = useTemplateRef<InstanceType<typeof Detail>>('detailRef')
+
+const baTable = new baTableClass(
+    new baTableApi('/admin/package.package/'),
+    {
+        column: [
+            { label: '编号', prop: 'id', align: 'center', width: 70 },
+            {
+                label: '套餐名称',
+                prop: 'name',
+                align: 'center',
+                minWidth: 160,
+            },
+            {
+                label: '状态',
+                prop: 'status',
+                align: 'center',
+                render: 'slot',
+                slotName: 'status',
+                minWidth: 100,
+            },
+            {
+                label: '总库存数',
+                prop: 'remaining_stock',
+                align: 'center',
+                minWidth: 110,
+            },
+            
+            {
+                label: '已售数量',
+                prop: 'sold_count',
+                align: 'center',
+                minWidth: 110,
+            },
+            {
+                label: '规格数量',
+                prop: 'spec_count',
+                align: 'center',
+                minWidth: 110,
+            },
+            {
+                label: '套餐描述',
+                prop: 'description',
+                align: 'center',
+                showOverflowTooltip: true,
+                minWidth: 200,
+            },
+            {
+                label: '创建时间',
+                prop: 'create_time',
+                align: 'center',
+                minWidth: 170,
+            },
+            {
+                label: '操作',
+                align: 'center',
+                render: 'slot',
+                slotName: 'operate',
+                operator: false,
+            },
+        ],
+        dblClickNotEditColumn: [undefined,],
+    },
+    {
+        defaultItems: {
+            id: undefined,
+            name: '',
+            description: '',
+            status: '1',
+        },
+    },
+    {
+		onTableDblclick: () => {
+			return false
+		},
+	},
+)
+
+baTable.mount()
+baTable.getData()
+provide('baTable', baTable)
+
+
+// 删除
+const handleDel = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除套餐"${row.name}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
+}
+
+// 改变状态
+const changeStatus = (row: TableRow) => {
+    batchStatus({ ids: row.id, status: row.status })
+}
+
+const onAdd = () => {
+    popupFormRef.value?.openDialog()
+}
+
+const onEdit = (row: TableRow) => {
+    popupFormRef.value?.editDialog(row)
+}
+
+const onDetail = (row: TableRow) => {
+    detailRef.value?.open(row.id)
+}
+
+// 处理表单提交成功
+const handleFormSuccess = async (submitData: any) => {
+    try {
+        const res = await edit({
+            id: submitData.id,
+            name: submitData.name,
+            description: submitData.description,
+            status: submitData.status,
+        })
+        
+        // 如果是新增,获取返回的套餐ID,并切换到编辑模式
+        if (submitData.type === 'add') {
+            // 从响应中获取套餐ID,通常后端会返回新创建的记录ID
+            // 尝试多种可能的响应结构
+            const packageId = (res?.data as any)?.id || (res?.data as any)?.data?.id || (res as any)?.id
+            
+            if (packageId) {
+                // 通知 popupForm 保存成功,切换到编辑模式
+                popupFormRef.value?.handleSaveSuccess(packageId)
+                // 刷新列表
+                baTable.getIndex()
+            } else {
+                // 如果后端没有返回ID,刷新列表后通过名称查找(备用方案)
+                ElMessage.success('添加成功')
+                await baTable.getIndex()
+                
+                // 从刷新后的列表中查找刚创建的套餐
+                const tableData = baTable.table.data
+                if (tableData && Array.isArray(tableData)) {
+                    const newPackage = tableData.find((item: any) => item.name === submitData.name)
+                    if (newPackage && newPackage.id) {
+                        popupFormRef.value?.handleSaveSuccess(newPackage.id)
+                    } else {
+                        ElMessage.info('请点击编辑按钮添加规格')
+                    }
+                } else {
+                    ElMessage.info('请点击编辑按钮添加规格')
+                }
+            }
+        } else {
+            // 编辑模式,直接刷新列表
+            ElMessage.success('编辑成功')
+            baTable.getIndex()
+        }
+    } catch (error: any) {
+        ElMessage({
+            type: 'error',
+            message: error?.msg || (submitData.type === 'add' ? '添加失败' : '编辑失败'),
+        })
+    }
+}
+ 
+</script>
+
+<style scoped lang="scss"></style>
+

+ 379 - 0
web/src/views/backend/package/package/popupForm.vue

@@ -0,0 +1,379 @@
+<template>
+	<!-- 对话框表单 -->
+	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
+		@close="handleClose" width="880px">
+		<template #header>
+			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+				套餐配置
+			</div>
+		</template>
+		<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
+			<div class="ba-operate-form">
+				<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" class="form-container">
+					<el-form-item label="套餐名称" prop="name">
+						<el-input v-model="formData.name" placeholder="请输入套餐名称" />
+					</el-form-item>
+					<el-form-item label="状态" prop="status">
+						<el-radio-group v-model="formData.status">
+							<el-radio :value="1">启用</el-radio>
+							<el-radio :value="2">禁用</el-radio>
+						</el-radio-group>
+					</el-form-item>
+					<!-- 套餐规格列表 -->
+					<el-form-item label="套餐规格" v-if="currentEditId">
+						<div class="spec-section">
+							<div class="spec-header">
+								<span class="spec-title">规格列表:</span>
+								<el-button type="primary" size="small" plain class="spec-add-btn"
+									@click="openSpecDialog">
+									<span class="spec-add-icon">+</span>
+									<span>新增规格</span>
+								</el-button>
+							</div>
+							<div v-loading="specLoading" class="spec-list">
+								<el-table :data="specListData" border stripe style="width: 100%">
+									<el-table-column prop="spec_name" align="center" label="规格名称" min-width="120" />
+									<el-table-column prop="price" align="center" label="价格" min-width="100">
+										<template #default="scope">
+											<span>¥{{ scope.row.price }}</span>
+										</template>
+									</el-table-column>
+									<el-table-column prop="stock" align="center" label="库存" min-width="80" />
+									<el-table-column prop="type" align="center" label="套餐类型" min-width="100">
+										<template #default="scope">
+											<el-tag :type="scope.row.type === '1' ? 'primary' : 'success'">
+												{{ scope.row.type === '1' ? '个人套餐' : '企业套餐' }}
+											</el-tag>
+										</template>
+									</el-table-column>
+									<el-table-column prop="card_type" align="center" label="卡类型" min-width="100">
+										<template #default="scope">
+											{{ getCardTypeLabel(scope.row.card_type) }}
+										</template>
+									</el-table-column>
+									<el-table-column label="次数/积分" align="center" min-width="100">
+										<template #default="scope">
+											<span v-if="scope.row.card_type == '1' && scope.row.unit_value">
+												{{ scope.row.unit_value }}次
+											</span>
+											<span v-else-if="scope.row.card_type == '2' && scope.row.points_value">
+												{{ scope.row.points_value }}分
+											</span>
+											<span v-else>-</span>
+										</template>
+									</el-table-column>
+									<el-table-column prop="description" align="center" label="说明" min-width="150"
+										show-overflow-tooltip />
+									<el-table-column label="操作" align="center" width="150" fixed="right">
+										<template #default="scope">
+											<el-button type="primary" size="small" @click="editSpecItem(scope.row)">
+												编辑
+											</el-button>
+											<el-button type="danger" size="small" @click="deleteSpecItem(scope.row)">
+												删除
+											</el-button>
+										</template>
+									</el-table-column>
+								</el-table>
+								<div v-if="!specLoading && specListData.length === 0" class="spec-empty">
+									<el-empty description="暂无规格,请添加规格" :image-size="80" />
+								</div>
+							</div>
+						</div>
+					</el-form-item>
+					<el-form-item label="说明" prop="description">
+						<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入套餐说明" />
+					</el-form-item>
+				</el-form>
+			</div>
+		</el-scrollbar>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="handleClose" size="large" class="cancel-btn">{{ t('Cancel') }}</el-button>
+				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading"
+					class="submit-btn">确定</el-button>
+			</div>
+		</template>
+	</el-dialog>
+
+	<!-- 规格弹框 -->
+	<SpecDialog ref="specDialogRef" @success="loadSpecList" />
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, nextTick } from 'vue'
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { specList, deleteSpec } from '/@/api/backend/package/package'
+import SpecDialog from './specDialog.vue'
+
+const { t } = useI18n()
+
+// 显示隐藏
+const dialogVisible = ref(false)
+// 类型:add、edit
+const dialogType = ref<'add' | 'edit'>('add')
+// 当前编辑的套餐 ID(新增时为 null)
+const currentEditId = ref<number | null>(null)
+
+const loading = ref(false)
+const specLoading = ref(false)
+// 防止重复请求的标志
+const specListLoading = ref(false)
+
+const formRef = ref<FormInstance>()
+const specDialogRef = ref<InstanceType<typeof SpecDialog>>()
+
+const emits = defineEmits(['success'])
+
+const formData = reactive({
+	name: '',
+	description: '',
+	status: 1, // 1.启用 2.禁用
+})
+
+// 规格列表
+const specListData = ref<any[]>([])
+
+// 表单验证规则
+const rules: FormRules = {
+	name: [
+		{ required: true, message: '请输入套餐名称', trigger: 'change' }
+	],
+	status: [
+		{ required: true, message: '请选择状态', trigger: 'change' }
+	],
+}
+
+// 打开对话框(新增)
+const openDialog = () => {
+	dialogVisible.value = true
+	dialogType.value = 'add'
+	currentEditId.value = null
+	resetForm()
+}
+
+// 编辑:接收列表行数据,回填到表单
+const editDialog = (row: TableRow & { id?: number; name?: string; description?: string; status?: number }) => {
+	dialogType.value = 'edit'
+	currentEditId.value = row.id ?? null
+
+	// 清理校验状态
+	formRef.value?.clearValidate()
+
+	// 回填基础信息
+	formData.name = row.name ?? ''
+	formData.description = row.description ?? ''
+	formData.status = row.status ?? 1
+
+	// 最后设置 dialogVisible,让 watch 统一处理规格列表加载
+	dialogVisible.value = true
+}
+
+// 重置表单
+const resetForm = () => {
+	formRef.value?.clearValidate()
+	formData.name = ''
+	formData.description = ''
+	formData.status = 1
+	specListData.value = []
+	// 重置加载标志
+	specListLoading.value = false
+}
+
+// 加载规格列表
+const loadSpecList = async () => {
+	if (!currentEditId.value) return
+
+	// 防止重复请求
+	if (specListLoading.value) return
+	specListLoading.value = true
+	specLoading.value = true
+
+	try {
+		const res = await specList({ package_id: currentEditId.value })
+		if (res.data && Array.isArray(res.data)) {
+			specListData.value = res.data
+		} else {
+			specListData.value = []
+		}
+	} catch (error: any) {
+		// 如果是请求被取消的错误,不显示错误消息
+		if (error?.message && error.message.includes('canceled')) {
+			return
+		}
+		ElMessage({
+			type: 'error',
+			message: error?.msg || '加载规格列表失败',
+		})
+		specListData.value = []
+	} finally {
+		specLoading.value = false
+		specListLoading.value = false
+	}
+}
+
+// 打开规格弹框(新增)
+const openSpecDialog = () => {
+	if (!currentEditId.value) {
+		ElMessage.warning('请先保存套餐')
+		return
+	}
+	specDialogRef.value?.openDialog(currentEditId.value)
+}
+
+// 编辑规格
+const editSpecItem = (row: any) => {
+	if (!currentEditId.value) return
+	specDialogRef.value?.editDialog(currentEditId.value, row)
+}
+
+// 删除规格
+const deleteSpecItem = (row: any) => {
+	ElMessageBox.confirm(`确定删除规格"${row.spec_name}"吗?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		try {
+			await deleteSpec({ id: row.id })
+			ElMessage.success('删除成功')
+			loadSpecList()
+		} catch (error: any) {
+			ElMessage({
+				type: 'error',
+				message: error?.msg || '删除失败,请重试',
+			})
+		}
+	})
+}
+
+// 获取卡类型标签
+const getCardTypeLabel = (cardType: string) => {
+	const map: Record<string, string> = {
+		'1': '次卡',
+		'2': '按积分',
+		'3': '日卡',
+		'4': '周卡',
+		'5': '季卡',
+		'6': '月卡',
+		'7': '年卡',
+	}
+	return map[cardType] || '-'
+}
+
+// 提交表单
+const onSubmit = async () => {
+	if (!formRef.value) return
+
+	try {
+		await formRef.value.validate()
+		loading.value = true
+
+		// 构建提交数据,传给父组件(index.vue)
+		const submitData = {
+			id: currentEditId.value,
+			type: dialogType.value,
+			name: formData.name,
+			description: formData.description,
+			status: formData.status,
+		}
+
+		emits('success', submitData)
+		// 注意:不在这里关闭弹框,由父组件处理成功后回调
+	} catch (error) {
+		ElMessage({
+			type: 'error',
+			message: '保存失败,请重试!',
+		})
+	} finally {
+		loading.value = false
+	}
+}
+
+// 处理保存成功后的回调(由父组件调用)
+const handleSaveSuccess = (packageId: number) => {
+	// 切换到编辑模式
+	dialogType.value = 'edit'
+	currentEditId.value = packageId
+	// 加载规格列表
+	loadSpecList()
+	ElMessage.success('套餐保存成功,现在可以添加规格了')
+}
+
+// 关闭弹框
+const handleClose = () => {
+	dialogVisible.value = false
+	resetForm()
+}
+
+// 监听对话框打开和ID变化,如果是编辑模式且有ID,加载规格列表
+watch([dialogVisible, currentEditId], ([visible, id], [oldVisible, oldId]) => {
+	// 只在以下情况加载规格列表:
+	// 1. 对话框从关闭变为打开,且是编辑模式,且有ID
+	// 2. ID发生变化(例如从新增切换到编辑),且对话框是打开的,且是编辑模式
+	const shouldLoad = visible &&
+		dialogType.value === 'edit' &&
+		id &&
+		((!oldVisible && visible) || (id !== oldId && visible))
+
+	if (shouldLoad) {
+		// 使用 nextTick 确保所有状态都已更新
+		nextTick(() => {
+			loadSpecList()
+		})
+	}
+})
+
+// 导出方法
+defineExpose({ openDialog, editDialog, handleSaveSuccess })
+</script>
+
+<style scoped lang="scss">
+.form-container {
+	.spec-section {
+		width: 100%;
+		border: 1px solid #f0f0f0;
+		padding: 12px 16px;
+		border-radius: 4px;
+		background-color: #fafafa;
+
+		.spec-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			margin-bottom: 12px;
+
+			.spec-title {
+				font-weight: 600;
+				color: #333;
+			}
+
+			.spec-add-btn {
+				display: inline-flex;
+				align-items: center;
+				gap: 4px;
+				border-radius: 999px;
+				padding: 0 10px;
+
+				.spec-add-icon {
+					font-size: 14px;
+					transform: translateY(-1px);
+				}
+			}
+		}
+
+		.spec-list {
+			min-height: 200px;
+		}
+
+		.spec-empty {
+			padding: 40px 0;
+		}
+	}
+
+	.spec-tip {
+		width: 100%;
+	}
+}
+</style>

+ 510 - 0
web/src/views/backend/package/package/specDialog.vue

@@ -0,0 +1,510 @@
+<template>
+	<!-- 规格对话框表单 -->
+	<el-dialog 
+		class="ba-operate-dialog spec-dialog" 
+		:close-on-click-modal="false" 
+		:destroy-on-close="true" 
+		v-model="dialogVisible"
+		@close="handleClose" 
+		width="750px"
+	>
+		<template #header>
+			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+				{{ dialogType === 'add' ? '添加规格' : '编辑规格' }}
+			</div>
+		</template>
+		<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
+			<div class="ba-operate-form">
+				<el-form 
+					ref="formRef" 
+					:model="formData" 
+					:rules="rules" 
+					label-width="110px" 
+					class="form-container"
+				>
+					<el-form-item label="规格名称" prop="spec_name">
+						<el-input 
+							v-model="formData.spec_name" 
+							placeholder="请输入规格名称"
+							clearable
+							maxlength="50"
+							show-word-limit
+						/>
+					</el-form-item>
+					
+					<el-row :gutter="20">
+						<el-col :span="12">
+							<el-form-item label="价格" prop="price">
+								<div class="input-with-unit">
+									<el-input-number
+										v-model.number="formData.price"
+										:min="0"
+										:precision="2"
+										placeholder="请输入价格"
+										style="width: 100%"
+										controls-position="right"
+									/>
+									<span class="form-unit">元</span>
+								</div>
+							</el-form-item>
+						</el-col>
+						<el-col :span="12">
+							<el-form-item label="库存" prop="stock">
+								<div class="input-with-unit">
+									<el-input-number
+										v-model.number="formData.stock"
+										:min="0"
+										placeholder="请输入库存"
+										style="width: 100%"
+										controls-position="right"
+									/>
+									<span class="form-unit">件</span>
+								</div>
+							</el-form-item>
+						</el-col>
+					</el-row>
+					
+					<el-row :gutter="20">
+						<el-col :span="12">
+							<el-form-item label="套餐类型" prop="type">
+								<el-select 
+									v-model="formData.type" 
+									placeholder="请选择套餐类型" 
+									style="width: 100%"
+									clearable
+								>
+									<el-option
+										v-for="opt in packageTypeOptions"
+										:key="opt.value"
+										:label="opt.label"
+										:value="opt.value"
+									/>
+								</el-select>
+							</el-form-item>
+						</el-col>
+						<el-col :span="12">
+							<el-form-item label="卡类型" prop="card_type">
+								<el-select
+									v-model="formData.card_type"
+									placeholder="请选择卡类型"
+									style="width: 100%"
+									@change="handleCardTypeChange"
+									clearable
+								>
+									<el-option
+										v-for="opt in cardTypeOptions"
+										:key="opt.value"
+										:label="opt.label"
+										:value="opt.value"
+									/>
+								</el-select>
+							</el-form-item>
+						</el-col>
+					</el-row>
+					
+					<el-form-item
+						v-if="formData.card_type === '1'"
+						label="次卡次数"
+						prop="unit_value"
+					>
+						<div class="input-with-unit">
+							<el-input-number
+								v-model.number="formData.unit_value"
+								:min="1"
+								placeholder="请输入次卡次数"
+								style="width: 100%"
+								controls-position="right"
+							/>
+							<span class="form-unit">次</span>
+						</div>
+						<div class="form-tip">当选择次卡时必填</div>
+					</el-form-item>
+					
+					<el-form-item
+						v-if="formData.card_type === '2'"
+						label="积分数"
+						prop="points_value"
+					>
+						<div class="input-with-unit">
+							<el-input-number
+								v-model.number="formData.points_value"
+								:min="1"
+								placeholder="请输入积分数"
+								style="width: 100%"
+								controls-position="right"
+							/>
+							<span class="form-unit">分</span>
+						</div>
+						<div class="form-tip">当选择按积分时必填</div>
+					</el-form-item>
+					
+					<el-form-item label="说明描述" prop="description">
+						<el-input
+							v-model="formData.description"
+							type="textarea"
+							:rows="4"
+							placeholder="请输入说明描述"
+							maxlength="500"
+							show-word-limit
+						/>
+					</el-form-item>
+				</el-form>
+			</div>
+		</el-scrollbar>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="handleClose" size="default">{{ t('Cancel') }}</el-button>
+				<el-button @click="onSubmit()" type="primary" size="default" :loading="loading">
+					确定
+				</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { editSpec } from '/@/api/backend/package/package'
+
+const { t } = useI18n()
+
+// 显示隐藏
+const dialogVisible = ref(false)
+// 类型:add、edit
+const dialogType = ref<'add' | 'edit'>('add')
+// 当前套餐 ID
+const packageId = ref<number | null>(null)
+// 当前编辑的规格 ID(新增时为 null)
+const currentSpecId = ref<number | null>(null)
+
+const loading = ref(false)
+
+const formRef = ref<FormInstance>()
+
+const emits = defineEmits(['success'])
+
+const formData = reactive({
+	spec_name: '',
+	price: undefined as number | undefined,
+	stock: undefined as number | undefined,
+	type: '',
+	card_type: '',
+	unit_value: undefined as number | undefined,
+	points_value: undefined as number | undefined,
+	description: '',
+})
+
+// 表单验证规则
+const rules: FormRules = {
+	spec_name: [
+		{ required: true, message: '请输入规格名称', trigger: 'change' }
+	],
+	price: [
+		{ required: true, message: '请输入价格', trigger: 'change' }
+	],
+	stock: [
+		{ required: true, message: '请输入库存', trigger: 'change' }
+	],
+	type: [
+		{ required: true, message: '请选择套餐类型', trigger: 'change' }
+	],
+	card_type: [
+		{ required: true, message: '请选择卡类型', trigger: 'change' }
+	],
+	unit_value: [
+		{
+			validator: (_rule, value, callback) => {
+				if (formData.card_type === '1' && (value === undefined || value === null || value <= 0)) {
+					return callback(new Error('次卡次数为必填项,且必须大于0'))
+				}
+				return callback()
+			},
+			trigger: 'change',
+		},
+	],
+	points_value: [
+		{
+			validator: (_rule, value, callback) => {
+				if (formData.card_type === '2' && (value === undefined || value === null || value <= 0)) {
+					return callback(new Error('积分数为必填项,且必须大于0'))
+				}
+				return callback()
+			},
+			trigger: 'change',
+		},
+	],
+}
+
+// 套餐类型选项
+const packageTypeOptions = [
+	{ label: '个人套餐', value: '1' },
+	{ label: '企业套餐', value: '2' },
+]
+
+// 卡类型选项
+const cardTypeOptions = [
+	{ label: '次卡', value: '1' },
+	{ label: '按积分', value: '2' },
+	{ label: '日卡', value: '3' },
+	{ label: '周卡', value: '4' },
+	{ label: '季卡', value: '5' },
+	{ label: '月卡', value: '6' },
+	{ label: '年卡', value: '7' },
+]
+
+// 打开对话框(新增)
+const openDialog = (pkgId: number) => {
+	dialogVisible.value = true
+	dialogType.value = 'add'
+	packageId.value = pkgId
+	currentSpecId.value = null
+	resetForm()
+}
+
+// 编辑:接收规格数据,回填到表单
+const editDialog = (pkgId: number, specData: any) => {
+	dialogVisible.value = true
+	dialogType.value = 'edit'
+	packageId.value = pkgId
+	currentSpecId.value = specData.id ?? null
+
+	// 清理校验状态
+	formRef.value?.clearValidate()
+
+	// 辅助函数:安全地将值转换为数字
+	const toNumber = (value: any): number | undefined => {
+		if (value == null || value === '') return undefined
+		const num = Number(value)
+		return isNaN(num) ? undefined : num
+	}
+
+	// 回填数据,确保数字类型正确转换
+	formData.spec_name = specData.spec_name || specData.specName || ''
+	// 价格:转换为数字,如果为空或无效则设为 undefined
+	formData.price = toNumber(specData.price)
+	// 库存:转换为数字,如果为空或无效则设为 undefined
+	formData.stock = toNumber(specData.stock)
+	formData.type = String(specData.type || specData.packageType || '')
+	formData.card_type = String(specData.card_type || specData.chargeType || '')
+	// 次卡次数:转换为数字,如果为空或无效则设为 undefined
+	formData.unit_value = toNumber(specData.unit_value) || 
+		(specData.card_type === '1' ? toNumber(specData.quantity) : undefined)
+	// 积分数:转换为数字,如果为空或无效则设为 undefined
+	formData.points_value = toNumber(specData.points_value) || 
+		(specData.card_type === '2' ? toNumber(specData.quantity) : undefined)
+	formData.description = specData.description || specData.remark || ''
+}
+
+// 重置表单
+const resetForm = () => {
+	formRef.value?.clearValidate()
+	formData.spec_name = ''
+	formData.price = undefined
+	formData.stock = undefined
+	formData.type = ''
+	formData.card_type = ''
+	formData.unit_value = undefined
+	formData.points_value = undefined
+	formData.description = ''
+}
+
+// 卡类型变化处理
+const handleCardTypeChange = () => {
+	// 切换卡类型时,清空对应的值
+	if (formData.card_type !== '1') {
+		formData.unit_value = undefined
+	}
+	if (formData.card_type !== '2') {
+		formData.points_value = undefined
+	}
+}
+
+// 提交表单
+const onSubmit = async () => {
+	if (!formRef.value) return
+
+	try {
+		await formRef.value.validate()
+		loading.value = true
+
+		// 构建提交数据(按照用户要求的字段)
+		const submitData: any = {
+			id: currentSpecId.value || '',
+			package_id: packageId.value,
+			spec_name: formData.spec_name,
+			// 确保数字类型正确
+			price: formData.price != null ? Number(formData.price) : undefined,
+			stock: formData.stock != null ? Number(formData.stock) : undefined,
+			type: formData.type,
+			card_type: formData.card_type,
+			description: formData.description,
+		}
+
+		// 根据卡类型添加对应的值
+		// 当选择次卡时必填 unit_value,选择其他天数的卡可以不传
+		if (formData.card_type === '1' && formData.unit_value !== undefined && formData.unit_value != null) {
+			submitData.unit_value = Number(formData.unit_value)
+		}
+		// 当选择积分时必填 points_value,选择其他天数的卡可以不传
+		if (formData.card_type === '2' && formData.points_value !== undefined && formData.points_value != null) {
+			submitData.points_value = Number(formData.points_value)
+		}
+
+		// 调用接口
+		await editSpec(submitData)
+		
+		ElMessage({
+			type: 'success',
+			message: dialogType.value === 'add' ? '添加成功' : '编辑成功',
+		})
+
+		emits('success')
+		handleClose()
+	} catch (error: any) {
+		ElMessage({
+			type: 'error',
+			message: error?.msg || (dialogType.value === 'add' ? '添加失败,请重试!' : '编辑失败,请重试!'),
+		})
+	} finally {
+		loading.value = false
+	}
+}
+
+// 关闭弹框
+const handleClose = () => {
+	dialogVisible.value = false
+	resetForm()
+}
+
+// 导出方法
+defineExpose({ openDialog, editDialog })
+</script>
+
+<style scoped lang="scss">
+.spec-dialog {
+	:deep(.el-dialog__body) {
+		padding: 20px 28px;
+		max-height: 70vh;
+		overflow: hidden;
+	}
+	
+	:deep(.el-dialog__footer) {
+		padding: 16px 28px;
+		border-top: 1px solid #ebeef5;
+	}
+	
+	:deep(.ba-table-form-scrollbar) {
+		max-height: calc(70vh - 100px);
+	}
+}
+
+.form-container {
+	padding: 4px 0;
+	
+	:deep(.el-form-item) {
+		margin-bottom: 20px;
+		
+		&:last-child {
+			margin-bottom: 0;
+		}
+		
+		.el-form-item__label {
+			font-weight: 500;
+			color: #606266;
+			padding-right: 12px;
+			padding-bottom: 0;
+			line-height: 32px;
+		}
+		
+		.el-form-item__content {
+			line-height: 32px;
+		}
+	}
+	
+	:deep(.el-row) {
+		margin-bottom: 20px;
+		
+		&:last-child {
+			margin-bottom: 0;
+		}
+		
+		.el-col {
+			.el-form-item {
+				margin-bottom: 0;
+			}
+		}
+	}
+	
+	.input-with-unit {
+		display: flex;
+		align-items: center;
+		gap: 8px;
+		
+		.el-input-number {
+			flex: 1;
+		}
+	}
+	
+	.form-unit {
+		color: #909399;
+		font-size: 14px;
+		white-space: nowrap;
+		min-width: 24px;
+	}
+	
+	.form-tip {
+		margin-top: 4px;
+		font-size: 12px;
+		color: #909399;
+		line-height: 1.5;
+	}
+	
+	:deep(.el-input__wrapper) {
+		border-radius: 4px;
+		transition: all 0.2s;
+		
+		&:hover {
+			box-shadow: 0 0 0 1px #c0c4cc inset;
+		}
+	}
+	
+	:deep(.el-textarea__inner) {
+		border-radius: 4px;
+		transition: all 0.2s;
+		
+		&:hover {
+			border-color: #c0c4cc;
+		}
+	}
+	
+	:deep(.el-select) {
+		.el-select__wrapper {
+			border-radius: 4px;
+			transition: all 0.2s;
+			
+			&:hover {
+				box-shadow: 0 0 0 1px #c0c4cc inset;
+			}
+		}
+	}
+	
+	:deep(.el-input-number) {
+		.el-input__wrapper {
+			border-radius: 4px;
+		}
+	}
+}
+
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 12px;
+	
+	.el-button {
+		min-width: 88px;
+		border-radius: 4px;
+	}
+}
+</style>

+ 98 - 84
web/src/views/backend/product/product/index.vue → web/src/views/backend/package/product/index.vue

@@ -4,9 +4,29 @@
             show-icon />
 
         <!-- 表格顶部菜单 -->
-        <TableHeader :buttons="['refresh', 'quickSearch', 'columnDisplay']"
+        <TableHeader :buttons="['refresh','columnDisplay']"
             :quick-search-placeholder="t('Quick search placeholder', { fields: '产品名称' })">
-            <el-button type="primary" @click="onAdd">添加</el-button>
+            <el-form style="margin-left: 5px;padding-top: 20px;" inline :model="seachForm" @submit.prevent="handleSearch">
+                <el-form-item >
+                    <el-input v-model="seachForm.name" placeholder="请输入产品名称" @blur="handleSearch" />
+                </el-form-item>
+                <el-form-item>
+                    <el-select v-model="seachForm.stock_status" placeholder="请选择库存状态" style="width: 150px;" clearable filterable @change="handleSearch">
+                        <el-option label="充足" value="1" />
+                        <el-option label="即将售罄" value="2" />
+                        <el-option label="售罄" value="3" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-select v-model="seachForm.status" placeholder="请选择产品状态" style="width: 150px;" clearable filterable @change="handleSearch">
+                        <el-option label="启用" value="1" />
+                        <el-option label="禁用" value="2" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="onAdd" v-auth="'edit'">添加</el-button>
+                </el-form-item>
+            </el-form>
         </TableHeader>
 
         <!-- 表格 -->
@@ -15,28 +35,29 @@
             <template #status>
                 <el-table-column prop="status" align="center" label="状态" min-width="100">
                     <template #default="scope">
-                        <el-switch v-model="scope.row.status" 
-                            active-value="1" inactive-value="0" />
+                        <el-switch v-model="scope.row.status" @change="changeStatus(scope.row)"
+                            :active-value="1" :inactive-value="2" />
                     </template>
                 </el-table-column>
             </template>
             <template #operate>
                 <el-table-column prop="operate" align="center" label="操作" min-width="150">
                     <template #default="scope">
-                        <el-button type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
-                        <el-button type="danger" size="small" @click="del(scope.row)">删除</el-button>
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                        <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
                     </template>
                 </el-table-column>
             </template>
         </Table>
 
         <!-- 表单 -->
-        <PopupForm />
+        <PopupForm @success="baTable.getIndex()" :packageOptions="packageOptions" />
+
     </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, provide } from 'vue'
+import { ref,reactive,provide, onMounted } from 'vue'
 import { cloneDeep } from 'lodash-es'
 import baTableClass from '/@/utils/baTable'
 import Table from '/@/components/table/index.vue'
@@ -44,17 +65,32 @@ import TableHeader from '/@/components/table/header/index.vue'
 import PopupForm from './popupForm.vue'
 import { baTableApi } from '/@/api/common'
 import { useI18n } from 'vue-i18n'
-
-type TableRow = Record<string, any>
+import { batchStatus,del } from '/@/api/backend/package/product'
+import { list } from '/@/api/backend/package/package'
+import { ElMessageBox } from 'element-plus'
 
 defineOptions({
-    name: 'application/product',
+    name: 'package/product',
 })
-
 const { t } = useI18n()
 
+const seachForm = reactive({
+    name:"",
+    stock_status:"",
+    status:""
+})
+
+// 搜索
+const handleSearch = () =>{
+    baTable.table.filter!.page = 1
+    baTable.table.filter!.name = seachForm.name
+    baTable.table.filter!.stock_status = seachForm.stock_status
+    baTable.table.filter!.status = seachForm.status
+    baTable.getIndex()
+}
+
 const baTable = new baTableClass(
-    new baTableApi('/admin/auth.Admin/'),
+    new baTableApi('/admin/package.product/'),
     {
         column: [
             { label: '编号', prop: 'id', align: 'center', width: 70 },
@@ -74,13 +110,13 @@ const baTable = new baTableClass(
             },
             {
                 label: '关联套餐',
-                prop: 'package',
+                prop: 'package_name',
                 align: 'center',
                 minWidth: 150
             },
             {
                 label: '库存状态',
-                prop: 'inventory_status',
+                prop: 'stock_status',
                 align: 'center',
                 render: 'tag',
                 custom: {
@@ -144,84 +180,59 @@ const baTable = new baTableClass(
     },
     {
         defaultItems: {
-            id: undefined,
+            id: null,
             name: '',
-            package: '',
+            package_id: '',
             description: '',
             image: '',
-            inventory_status: '1',
             status: '1',
             type: '1',
             link: '',
         },
-    }
+    },
+    {
+		onTableDblclick: () => {
+			return false
+		},
+	},
 )
 
-// 设置假数据
-baTable.getData = () => {
-    baTable.table.loading = true
-    // 模拟异步请求
-    return new Promise<void>((resolve) => {
-        setTimeout(() => {
-            baTable.table.data = [
-                {
-                    id: 1,
-                    name: '聚德神笔',
-                    image: 'https://picsum.photos/200/300',
-                    package: '聚德神笔套餐A',
-                    inventory_status: '1',
-                    status: '1',
-                    description: '这是聚德神笔',
-                    create_time: '2025-10-13 10:00:00',
-                    type: 1,
-                },
-                {
-                    id: 2,
-                    name: '棱镜',
-                    image: 'https://picsum.photos/300',
-                    package: '棱镜套餐B',
-                    inventory_status: '2',
-                    status: '0',
-                    description: '这是棱镜',
-                    create_time: '2025-10-13 11:00:00',
-                    type: 2,
-                },
-                {
-                    id: 3,
-                    name: '测试产品',
-                    image: 'https://picsum.photos/100',
-                    package: '测试套餐2',
-                    inventory_status: '3',
-                    status: '0',
-                    description: '我是描述内容',
-                    create_time: '2025-10-13 11:00:00',
-                    type: 1,
-                },
-                {
-                    id: 4,
-                    name: '测试产品',
-                    image: 'https://picsum.photos/400',
-                    package: '测试套餐',
-                    inventory_status: '1',
-                    status: '1',
-                    description: '这是一个测试产品',
-                    create_time: '2025-10-14 09:00:00',
-                    type: 2,
-                },
-            ]
-            baTable.table.total = 4
-            baTable.table.loading = false
-            resolve()
-        }, 300)
+baTable.mount()
+baTable.getData()
+provide('baTable', baTable)
+
+interface packageType {
+    value: string | number
+    label: string | number
+}
+const packageOptions = ref<packageType[]>([])
+// 获取套餐列表
+const getPackageList = () => {
+    list({ limit: 9999 }).then((res: any) => {
+        packageOptions.value = res.data.list.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+        }))
     })
 }
 
-provide('baTable', baTable)
-
-const del = (row: TableRow) => {
-    baTable.onTableAction('delete', { row })
+// 删除
+const handleDel = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除产品"${row.name}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
 }
 
+// 改变状态
+const changeStatus = (row: TableRow) => {
+    batchStatus({ ids: [row.id], status: row.status })
+}
 const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
     if (type === 'Edit' && !row) return
     baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
@@ -231,12 +242,15 @@ const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
 
 const onAdd = () => openForm('Add')
 const onEdit = (row: TableRow) => openForm('Edit', row)
-
-onMounted(() => {
-    baTable.mount()
-    baTable.getData()
+ 
+onMounted(()=>{
+    getPackageList()
 })
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.el-form--inline .el-form-item {
+    margin-right: 8px;
+}
+</style>
 

+ 120 - 0
web/src/views/backend/package/product/popupForm.vue

@@ -0,0 +1,120 @@
+<template>
+    <!-- 对话框表单 -->
+    <el-dialog class="ba-operate-dialog" :close-on-click-modal="false"
+        :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm"
+        :destroy-on-close="true">
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
+            <div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'"
+                :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
+                <el-form ref="formRef" @keyup.enter="btnSubmit" :model="baTable.form.items"
+                    :label-position="config.layout.shrink ? 'top' : 'right'"
+                    :label-width="baTable.form.labelWidth + 'px'" :rules="rules" v-if="!baTable.form.loading">
+                    <FormItem label="产品名称" prop="name" v-model="baTable.form.items!.name" type="string"
+                        :placeholder="t('Please input field', { field: '产品名称' })" />
+                    <el-form-item label="关联套餐" prop="package_id">
+                        <el-select v-model="baTable.form.items!.package_id" placeholder="请选择关联套餐" style="width: 100%">
+                            <el-option v-for="item in packageOptions" :key="item.value" :label="item.label"
+                                :value="item.value" />
+                        </el-select>
+                    </el-form-item>
+                    <FormItem label="产品图片" prop="image" type="image" v-model="baTable.form.items!.image" :input-attr="{ returnFullUrl: true }" />
+                    <FormItem label="产品状态" prop="status" type="radio" v-model="baTable.form.items!.status"
+                        :input-attr="{ border: true, content: statusOptions }" />
+                    <FormItem label="产品类型" prop="type" type="radio" v-model="baTable.form.items!.type"
+                        :input-attr="{ border: true, content: typeOptions }" />
+                    <el-form-item label="产品链接" prop="link">
+                        <el-input v-model="baTable.form.items!.link" placeholder="请输入产品链接" />
+                    </el-form-item>
+
+                    <el-form-item label="产品描述" prop="description">
+                        <el-input v-model="baTable.form.items!.description" type="textarea" :rows="4"
+                            placeholder="请输入产品描述" />
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
+                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') :
+                    t('Save') }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, inject, useTemplateRef, ref, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type baTableClass from '/@/utils/baTable'
+import FormItem from '/@/components/formItem/index.vue'
+import type { FormItemRule } from 'element-plus'
+import { useConfig } from '/@/stores/config'
+import { buildValidatorData } from '/@/utils/validate'
+import { edit } from '/@/api/backend/package/product'
+
+const config = useConfig()
+const formRef = useTemplateRef('formRef')
+const baTable = inject('baTable') as baTableClass
+const emit = defineEmits(['success'])
+const prop = defineProps(['packageOptions'])
+
+const { t } = useI18n()
+
+const rules: Partial<Record<string, FormItemRule[]>> = reactive({
+    name: [buildValidatorData({ name: 'required', title: '产品名称' })],
+    package_id: [buildValidatorData({ name: 'required', trigger: 'change', title: '关联套餐' })],
+    description: [buildValidatorData({ name: 'required', title: '产品描述' })],
+    image: [buildValidatorData({ name: 'required', title: '产品图片' })],
+    type: [buildValidatorData({ name: 'required', title: '产品类型' })],
+    link: [buildValidatorData({ name: 'required', title: '产品链接' })],
+    status: [buildValidatorData({ name: 'required', title: '产品状态' })],
+})
+
+const typeOptions = {
+    "1": '网页',
+    "2": '软件',
+}
+
+const statusOptions = {
+    "1": '启用',
+    "2": '禁用',
+}
+
+// 提交
+const btnSubmit = () => {
+    formRef.value?.validate((valid: boolean) => {
+        if (valid) {
+            baTable.table.loading = true
+
+            let params = {
+                id: baTable.form.items!.id,
+                name: baTable.form.items!.name,
+                package_id: baTable.form.items!.package_id,
+                image: baTable.form.items!.image,
+                type: baTable.form.items!.type,
+                link: baTable.form.items!.link,
+                status: baTable.form.items!.status,
+                description: baTable.form.items!.description,
+            }
+
+            edit(params).then(() => {
+                baTable.toggleForm()
+                emit('success')
+            }).finally(() => {
+                baTable.table.loading = false
+            })
+
+        }
+    })
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 247 - 0
web/src/views/backend/pay/accountConfig/index.vue

@@ -0,0 +1,247 @@
+<template>
+	<div class="default-main ba-table-box">
+		<!-- 表格顶部菜单 -->
+		<TableHeader :buttons="['refresh','columnDisplay']">
+			<el-form style="margin-left: 5px;padding-top: 20px;" inline :model="seachForm" @submit.prevent="handleSearch">
+				<el-form-item>
+					<el-input v-model="seachForm.user_name" placeholder="请输入用户名称" @blur="handleSearch" clearable />
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="seachForm.account_type" placeholder="请选择账户类型" style="width: 150px;" clearable filterable @change="handleSearch">
+						<el-option label="银行卡" value="bank" />
+						<el-option label="支付宝" value="alipay" />
+						<el-option label="微信" value="wechat" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="seachForm.status" placeholder="请选择状态" style="width: 150px;" clearable filterable @change="handleSearch">
+						<el-option label="禁用" value="0" />
+						<el-option label="启用" value="1" />
+					</el-select>
+				</el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="onAdd" v-auth="'edit'">添加</el-button>
+                </el-form-item>
+			</el-form>
+		</TableHeader>
+		<!-- 表格 -->
+		<!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+		<Table ref="tableRef">
+            <template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="150" fixed="right">
+                    <template #default="scope">
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                        <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+        <!-- 表单 -->
+        <PopupForm @success="baTable.getIndex()" />
+
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, provide, useTemplateRef } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import PopupForm from './popupForm.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import { cloneDeep } from 'lodash-es'
+import { ElMessageBox } from 'element-plus'
+import { del } from '/@/api/backend/pay/accountConfig'
+
+defineOptions({
+    name: 'pay/accountConfig',
+})
+
+
+const { t } = useI18n()
+
+const seachForm = reactive({
+	user_name: "",
+	account_type: "",
+	status: ""
+})
+
+// 搜索
+const handleSearch = () => {
+	baTable.table.filter!.page = 1
+	baTable.table.filter!.user_name = seachForm.user_name
+	baTable.table.filter!.account_type = seachForm.account_type
+	baTable.table.filter!.status = seachForm.status
+	baTable.getIndex()
+}
+
+const baTable = new baTableClass(
+	new baTableApi('/admin/pay.AccountConfig/'),
+	{
+		column: [
+			{ 
+				label: '编号', 
+				prop: 'id', 
+				align: 'center', 
+				width: 70,
+			},
+			{ 
+				label: '用户名称', 
+				prop: 'user_name', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{
+				label: '账户类型',
+				prop: 'account_type_text',
+				align: 'center',
+				minWidth: 100,
+				render: 'tag',
+				custom: {
+					'银行卡': 'warning',
+					'支付宝': '',
+					'微信': 'success',
+				},
+			},
+			{ 
+				label: '账户姓名', 
+				prop: 'account_name', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{ 
+				label: '银行名称', 
+				prop: 'bank_name', 
+				align: 'center', 
+				minWidth: 150,
+			},
+			{ 
+				label: '卡号', 
+				prop: 'card_number_masked', 
+				align: 'center', 
+				minWidth: 180,
+			},
+			{
+				label: '是否默认',
+				prop: 'is_default_text',
+				align: 'center',
+				render: 'tag',
+				custom: {
+					'是': '',
+					'否': 'info',
+				},
+				minWidth: 100,
+			},
+			{
+				label: '状态',
+				prop: 'status_text',
+				align: 'center',
+				render: 'tag',
+				custom: {
+					'禁用': 'danger',
+					'启用': 'success',
+				},
+				minWidth: 100,
+			},
+			{ 
+				label: '备注', 
+				prop: 'remark', 
+				align: 'center', 
+				minWidth: 150,
+			},
+			{ 
+				label: '创建时间', 
+				prop: 'create_time', 
+				align: 'center',
+				minWidth: 170,
+				operator: false,
+			},
+			{ 
+				label: '更新时间', 
+				prop: 'update_time', 
+				align: 'center',
+				minWidth: 170,
+				operator: false,
+			},
+			{
+				label: '操作',
+				align: 'center',
+				width: '180',
+				render: 'slot',
+				slotName: 'operate',
+			},
+		],
+		dblClickNotEditColumn: [undefined,],
+		filter: {
+			limit: 20,
+		},
+	},
+	{
+		defaultItems: {
+			id: null,
+			user_id: '',
+			account_type: 'bank',
+			account_name: '',
+			is_default: '0',
+			status: '1',
+			remark: '',
+			bank_name: '',
+			bank_branch: '',
+			bank_province: '',
+			bank_city: '',
+			card_number: '',
+			alipay_account: '',
+			wechat_account: '',
+		},
+	},
+)
+
+baTable.mount()
+baTable.getIndex()
+provide('baTable', baTable)
+
+// 删除
+const handleDel = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除账户配置"${row.account_name || row.id}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
+}
+
+const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
+    if (type === 'Edit' && !row) return
+    baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
+    // 统一状态和是否默认为字符串,避免下拉框/单选框显示问题
+    if (baTable.form.items) {
+        if (baTable.form.items.status !== undefined && baTable.form.items.status !== null) {
+            baTable.form.items.status = String(baTable.form.items.status)
+        }
+        if (baTable.form.items.is_default !== undefined && baTable.form.items.is_default !== null) {
+            baTable.form.items.is_default = String(baTable.form.items.is_default)
+        }
+        if (baTable.form.items.account_type !== undefined && baTable.form.items.account_type !== null) {
+            baTable.form.items.account_type = String(baTable.form.items.account_type)
+        }
+        if (baTable.form.items.user_id !== undefined && baTable.form.items.user_id !== null) {
+            baTable.form.items.user_id = String(baTable.form.items.user_id)
+        }
+    }
+    baTable.form.operate = type
+    baTable.form.operateIds = row && baTable.table.pk ? [row[baTable.table.pk]] : []
+}
+
+const onAdd = () => openForm('Add')
+const onEdit = (row: TableRow) => openForm('Edit', row)
+
+</script>
+<style scoped lang="scss">
+.el-form--inline .el-form-item {
+	margin-right: 8px;
+}
+</style>

+ 227 - 0
web/src/views/backend/pay/accountConfig/popupForm.vue

@@ -0,0 +1,227 @@
+<template>
+    <!-- 对话框表单 -->
+    <el-dialog class="ba-operate-dialog" :close-on-click-modal="false"
+        :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm"
+        :destroy-on-close="true">
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
+            <div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'"
+                :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
+                <el-form ref="formRef" @keyup.enter="btnSubmit" :model="baTable.form.items"
+                    :label-position="config.layout.shrink ? 'top' : 'right'"
+                    :label-width="baTable.form.labelWidth + 'px'" :rules="rules" v-if="!baTable.form.loading">
+                    
+                    <FormItem
+                        type="remoteSelect"
+                        prop="user_id"
+                        label="用户"
+                        v-model="baTable.form.items!.user_id"
+                        placeholder="请选择用户"
+                        :input-attr="{
+                            pk: 'user.id',
+                            field: 'name',
+                            remoteUrl: '/admin/userCenter.user/index',
+                        }"
+                    />
+                    
+                    <el-form-item label="账户类型" prop="account_type">
+                        <el-select v-model="baTable.form.items!.account_type" placeholder="请选择账户类型" style="width: 100%">
+                            <el-option label="银行卡" value="bank" />
+                            <el-option label="支付宝" value="alipay" />
+                            <el-option label="微信" value="wechat" />
+                        </el-select>
+                    </el-form-item>
+                    
+                    <FormItem label="账户姓名" prop="account_name" v-model="baTable.form.items!.account_name" type="string"
+                        placeholder="请输入账户姓名" />
+                    
+                    <FormItem label="是否默认" prop="is_default" type="radio" v-model="baTable.form.items!.is_default"
+                        :input-attr="{ border: true, content: isDefaultOptions }" />
+                    
+                    <FormItem label="状态" prop="status" type="radio" v-model="baTable.form.items!.status"
+                        :input-attr="{ border: true, content: statusOptions }" />
+                    
+                    <!-- 银行卡相关字段 -->
+                    <template v-if="baTable.form.items!.account_type === 'bank'">
+                        <FormItem label="银行名称" prop="bank_name" v-model="baTable.form.items!.bank_name" type="string"
+                            placeholder="请输入银行名称" />
+                        <FormItem label="支行名称" prop="bank_branch" v-model="baTable.form.items!.bank_branch" type="string"
+                            placeholder="请输入支行名称" />
+                        <FormItem label="省份" prop="bank_province" v-model="baTable.form.items!.bank_province" type="string"
+                            placeholder="请输入省份" />
+                        <FormItem label="城市" prop="bank_city" v-model="baTable.form.items!.bank_city" type="string"
+                            placeholder="请输入城市" />
+                        <FormItem label="银行卡号" prop="card_number" v-model="baTable.form.items!.card_number" type="string"
+                            placeholder="请输入银行卡号" />
+                    </template>
+                    
+                    <!-- 支付宝相关字段 -->
+                    <template v-if="baTable.form.items!.account_type === 'alipay'">
+                        <FormItem label="支付宝账号" prop="alipay_account" v-model="baTable.form.items!.alipay_account" type="string"
+                            placeholder="请输入支付宝账号" />
+                    </template>
+                    
+                    <!-- 微信相关字段 -->
+                    <template v-if="baTable.form.items!.account_type === 'wechat'">
+                        <FormItem label="微信账号" prop="wechat_account" v-model="baTable.form.items!.wechat_account" type="string"
+                            placeholder="请输入微信账号" />
+                    </template>
+                    
+                    <el-form-item label="备注" prop="remark">
+                        <el-input
+                            v-model="baTable.form.items!.remark"
+                            type="textarea"
+                            :rows="3"
+                            placeholder="请输入备注"
+                        />
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
+                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') :
+                    t('Save') }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, inject, useTemplateRef, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type baTableClass from '/@/utils/baTable'
+import FormItem from '/@/components/formItem/index.vue'
+import type { FormItemRule } from 'element-plus'
+import { useConfig } from '/@/stores/config'
+import { buildValidatorData } from '/@/utils/validate'
+import { edit } from '/@/api/backend/pay/accountConfig'
+
+const config = useConfig()
+const formRef = useTemplateRef('formRef')
+const baTable = inject('baTable') as baTableClass
+const emit = defineEmits(['success'])
+
+const { t } = useI18n()
+
+const statusOptions = {
+    '0': '禁用',
+    '1': '启用',
+}
+
+const isDefaultOptions = {
+    '0': '否',
+    '1': '是',
+}
+
+// 验证规则
+const rules: Partial<Record<string, FormItemRule[]>> = reactive({
+    user_id: [buildValidatorData({ name: 'required', title: '用户' })],
+    account_type: [buildValidatorData({ name: 'required', trigger: 'change', title: '账户类型' })],
+    account_name: [buildValidatorData({ name: 'required', title: '账户姓名' })],
+    is_default: [buildValidatorData({ name: 'required', trigger: 'change', title: '是否默认' })],
+    status: [buildValidatorData({ name: 'required', trigger: 'change', title: '状态' })],
+})
+
+// 监听账户类型变化,动态更新验证规则
+const updateValidationRules = (accountType: string) => {
+    // 移除条件字段的验证规则(如果存在)
+    if (rules.bank_name) delete rules.bank_name
+    if (rules.card_number) delete rules.card_number
+    if (rules.alipay_account) delete rules.alipay_account
+    if (rules.wechat_account) delete rules.wechat_account
+    
+    // 根据账户类型添加相应的验证规则
+    if (accountType === 'bank') {
+        rules.bank_name = [buildValidatorData({ name: 'required', title: '银行名称' })]
+        rules.card_number = [buildValidatorData({ name: 'required', title: '银行卡号' })]
+    } else if (accountType === 'alipay') {
+        rules.alipay_account = [buildValidatorData({ name: 'required', title: '支付宝账号' })]
+    } else if (accountType === 'wechat') {
+        rules.wechat_account = [buildValidatorData({ name: 'required', title: '微信账号' })]
+    }
+}
+
+watch(
+    () => baTable.form.items!.account_type,
+    (newType) => {
+        if (newType) {
+            updateValidationRules(newType)
+        }
+    },
+    { immediate: true }
+)
+
+// 监听表单打开,确保验证规则正确初始化
+watch(
+    () => baTable.form.operate,
+    (newOperate) => {
+        if (newOperate && baTable.form.items!.account_type) {
+            updateValidationRules(baTable.form.items!.account_type)
+        }
+    }
+)
+
+// 提交
+const btnSubmit = () => {
+    formRef.value?.validate((valid: boolean) => {
+        if (valid) {
+            baTable.table.loading = true
+
+            let params: any = {
+                id: baTable.form.items!.id,
+                user_id: baTable.form.items!.user_id,
+                account_type: baTable.form.items!.account_type,
+                account_name: baTable.form.items!.account_name,
+                is_default: baTable.form.items!.is_default,
+                status: baTable.form.items!.status,
+                remark: baTable.form.items!.remark || '',
+            }
+            
+            // 根据账户类型添加相应字段
+            if (baTable.form.items!.account_type === 'bank') {
+                params.bank_name = baTable.form.items!.bank_name || ''
+                params.bank_branch = baTable.form.items!.bank_branch || ''
+                params.bank_province = baTable.form.items!.bank_province || ''
+                params.bank_city = baTable.form.items!.bank_city || ''
+                params.card_number = baTable.form.items!.card_number || ''
+                params.alipay_account = ''
+                params.wechat_account = ''
+            } else if (baTable.form.items!.account_type === 'alipay') {
+                params.alipay_account = baTable.form.items!.alipay_account || ''
+                params.bank_name = ''
+                params.bank_branch = ''
+                params.bank_province = ''
+                params.bank_city = ''
+                params.card_number = ''
+                params.wechat_account = ''
+            } else if (baTable.form.items!.account_type === 'wechat') {
+                params.wechat_account = baTable.form.items!.wechat_account || ''
+                params.bank_name = ''
+                params.bank_branch = ''
+                params.bank_province = ''
+                params.bank_city = ''
+                params.card_number = ''
+                params.alipay_account = ''
+            }
+
+            edit(params).then(() => {
+                baTable.toggleForm()
+                emit('success')
+            }).finally(() => {
+                baTable.table.loading = false
+            })
+
+        }
+    })
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 251 - 0
web/src/views/backend/pay/config/index.vue

@@ -0,0 +1,251 @@
+<template>
+	<div class="default-main ba-table-box">
+		<!-- 表格顶部菜单 -->
+		<TableHeader :buttons="['refresh','columnDisplay']">
+			<el-form style="margin-left: 5px;padding-top: 20px;" inline :model="seachForm" @submit.prevent="handleSearch">
+				<el-form-item>
+					<el-input v-model="seachForm.name" placeholder="请输入配置名称" @blur="handleSearch" clearable />
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="seachForm.type" placeholder="请选择支付类型" style="width: 150px;" clearable filterable @change="handleSearch">
+						<el-option value="1" label="微信支付" />
+						<el-option value="2" label="支付宝支付" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="seachForm.status" placeholder="请选择状态" style="width: 150px;" clearable filterable @change="handleSearch">
+						<el-option label="禁用" value="0" />
+						<el-option label="启用" value="1" />
+					</el-select>
+				</el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="onAdd" v-auth="'edit'">添加</el-button>
+                </el-form-item>
+			</el-form>
+		</TableHeader>
+		<!-- 表格 -->
+		<!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+		<Table ref="tableRef">
+            <template #operate>
+                <el-table-column prop="operate" align="center" label="操作" min-width="150" fixed="right">
+                    <template #default="scope">
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                        <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+        <!-- 表单 -->
+        <PopupForm @success="baTable.getIndex()" />
+
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, provide, useTemplateRef } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+import PopupForm from './popupForm.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import { cloneDeep } from 'lodash-es'
+import { ElMessageBox } from 'element-plus'
+import { del } from '/@/api/backend/pay/config'
+
+defineOptions({
+    name: 'pay/config',
+})
+
+
+const { t } = useI18n()
+
+const seachForm = reactive({
+	name: "",
+	type: "",
+	status: ""
+})
+
+// 搜索
+const handleSearch = () => {
+	baTable.table.filter!.page = 1
+	baTable.table.filter!.name = seachForm.name
+	baTable.table.filter!.type = seachForm.type
+	baTable.table.filter!.status = seachForm.status
+	baTable.getIndex()
+}
+
+const baTable = new baTableClass(
+	new baTableApi('/admin/pay.Config/'),
+	{
+		column: [
+			{ 
+				label: '编号', 
+				prop: 'id', 
+				align: 'center', 
+				width: 70,
+			},
+			{ 
+				label: '配置名称', 
+				prop: 'name', 
+				align: 'center', 
+				minWidth: 160,
+			},
+			{
+				label: '支付类型',
+				prop: 'type',
+				align: 'center',
+				minWidth: 120,
+				render: 'tag',
+				custom: {
+					'1': 'success',
+					'2': '',
+				},
+				replaceValue: {
+					'1': '微信支付',
+					'2': '支付宝支付',
+				},
+			},
+			{ 
+				label: '应用ID/AppId', 
+				prop: 'app_id', 
+				align: 'center', 
+				minWidth: 180,
+			},
+			{ 
+				label: '商户ID', 
+				prop: 'mch_id', 
+				align: 'center', 
+				minWidth: 120,
+			},
+			{ 
+				label: '商户密钥/API密钥', 
+				prop: 'key', 
+				align: 'center', 
+				minWidth: 200,
+			},
+			{ 
+				label: '支付通知地址', 
+				prop: 'notify_url', 
+				align: 'center', 
+				minWidth: 250,
+			},
+			{ 
+				label: 'H5支付通知地址', 
+				prop: 'h5_notify_url', 
+				align: 'center', 
+				minWidth: 250,
+			},
+			{ 
+				label: '支付宝公钥地址', 
+				prop: 'public_key_path', 
+				align: 'center', 
+				minWidth: 180,
+			},
+			{ 
+				label: '支付宝私钥地址', 
+				prop: 'private_key_path', 
+				align: 'center', 
+				minWidth: 180,
+			},
+			{
+				label: '状态',
+				prop: 'status',
+				align: 'center',
+				render: 'tag',
+				custom: {
+					'0': 'danger',
+					'1': 'success',
+				},
+				replaceValue: {
+					'0': '禁用',
+					'1': '启用',
+				},
+				minWidth: 100,
+			},
+			{ 
+				label: '创建时间', 
+				prop: 'create_time', 
+				align: 'center',
+				minWidth: 170,
+				operator: false,
+			},
+			{ 
+				label: '更新时间', 
+				prop: 'update_time', 
+				align: 'center',
+				minWidth: 170,
+				operator: false,
+			},
+			{
+				label: '操作',
+				align: 'center',
+				width: '180',
+				render: 'slot',
+				slotName: 'operate',
+			},
+		],
+		dblClickNotEditColumn: [undefined,],
+		filter: {
+			limit: 20,
+		},
+	},
+	{
+		defaultItems: {
+            id: null,
+            name: '',
+            type: '',
+            status: '1',
+            app_id: '',
+            mch_id: '',
+            key: '',
+            notify_url: '',
+            h5_notify_url: '',
+            public_key_path: '',
+            private_key_path: '',
+		},
+	},
+)
+
+baTable.mount()
+baTable.getIndex()
+provide('baTable', baTable)
+
+// 删除
+const handleDel = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除配置"${row.name}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
+}
+
+const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
+    if (type === 'Edit' && !row) return
+    baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
+    // 统一支付类型和状态为字符串,避免下拉框/单选框显示为“1”而不是对应中文
+    if (baTable.form.items) {
+        if (baTable.form.items.type !== undefined && baTable.form.items.type !== null) {
+            baTable.form.items.type = String(baTable.form.items.type)
+        }
+        if (baTable.form.items.status !== undefined && baTable.form.items.status !== null) {
+            baTable.form.items.status = String(baTable.form.items.status)
+        }
+    }
+    baTable.form.operate = type
+    baTable.form.operateIds = row && baTable.table.pk ? [row[baTable.table.pk]] : []
+}
+
+const onAdd = () => openForm('Add')
+const onEdit = (row: TableRow) => openForm('Edit', row)
+
+</script>
+<style scoped lang="scss">
+.el-form--inline .el-form-item {
+	margin-right: 8px;
+}
+</style>

+ 131 - 0
web/src/views/backend/pay/config/popupForm.vue

@@ -0,0 +1,131 @@
+<template>
+    <!-- 对话框表单 -->
+    <el-dialog class="ba-operate-dialog" :close-on-click-modal="false"
+        :model-value="['Add', 'Edit'].includes(baTable.form.operate!)" @close="baTable.toggleForm"
+        :destroy-on-close="true">
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
+            <div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'"
+                :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
+                <el-form ref="formRef" @keyup.enter="btnSubmit" :model="baTable.form.items"
+                    :label-position="config.layout.shrink ? 'top' : 'right'"
+                    :label-width="baTable.form.labelWidth + 'px'" :rules="rules" v-if="!baTable.form.loading">
+                    <FormItem label="配置名称" prop="name" v-model="baTable.form.items!.name" type="string"
+                        :placeholder="t('Please input field', { field: '配置名称' })" />
+                    <el-form-item label="支付类型" prop="type">
+                        <el-select v-model="baTable.form.items!.type" placeholder="请选择支付类型" style="width: 100%">
+                            <el-option label="微信支付" value="1" />
+                            <el-option label="支付宝支付" value="2" />
+                        </el-select>
+                    </el-form-item>
+                    <FormItem label="状态" prop="status" type="radio" v-model="baTable.form.items!.status"
+                        :input-attr="{ border: true, content: statusOptions }" />
+                    <el-form-item label="应用ID/AppId" prop="app_id">
+                        <el-input v-model="baTable.form.items!.app_id" placeholder="请输入应用ID/AppId" />
+                    </el-form-item>
+                    <el-form-item label="商户ID" prop="mch_id">
+                        <el-input v-model="baTable.form.items!.mch_id" placeholder="请输入商户ID(商户号/PID)" />
+                    </el-form-item>
+                    <el-form-item label="商户密钥/API密钥" prop="key">
+                        <el-input v-model="baTable.form.items!.key" placeholder="请输入商户密钥或API密钥" />
+                    </el-form-item>
+                    <el-form-item label="支付通知地址" prop="notify_url">
+                        <el-input v-model="baTable.form.items!.notify_url"
+                            placeholder="https://your-domain.com/api/pay/notify" />
+                    </el-form-item>
+                    <el-form-item label="H5支付通知地址" prop="h5_notify_url">
+                        <el-input v-model="baTable.form.items!.h5_notify_url"
+                            placeholder="https://your-domain.com/api/pay/h5-notify" />
+                    </el-form-item>
+                    <el-form-item label="支付宝公钥地址" prop="public_key_path">
+                        <el-input v-model="baTable.form.items!.public_key_path"
+                            placeholder="请输入支付宝公钥证书路径或内容标识" />
+                    </el-form-item>
+                    <el-form-item label="支付宝私钥地址" prop="private_key_path">
+                        <el-input v-model="baTable.form.items!.private_key_path"
+                            placeholder="请输入支付宝私钥证书路径或内容标识" />
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
+                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') :
+                    t('Save') }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, inject, useTemplateRef, } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type baTableClass from '/@/utils/baTable'
+import FormItem from '/@/components/formItem/index.vue'
+import type { FormItemRule } from 'element-plus'
+import { useConfig } from '/@/stores/config'
+import { buildValidatorData } from '/@/utils/validate'
+import { edit } from '/@/api/backend/pay/config'
+
+const config = useConfig()
+const formRef = useTemplateRef('formRef')
+const baTable = inject('baTable') as baTableClass
+const emit = defineEmits(['success'])
+
+const { t } = useI18n()
+
+const rules: Partial<Record<string, FormItemRule[]>> = reactive({
+    name: [buildValidatorData({ name: 'required', title: '配置名称' })],
+    type: [buildValidatorData({ name: 'required', trigger: 'change', title: '支付类型' })],
+    status: [buildValidatorData({ name: 'required', trigger: 'change', title: '状态' })],
+    app_id: [buildValidatorData({ name: 'required', title: '应用ID/AppId' })],
+    mch_id: [buildValidatorData({ name: 'required', title: '商户ID' })],
+    key: [buildValidatorData({ name: 'required', title: '商户密钥/API密钥' })],
+    notify_url: [buildValidatorData({ name: 'required', title: '支付通知地址' })],
+})
+
+const statusOptions = {
+    '0': '禁用',
+    '1': '启用',
+}
+
+// 提交
+const btnSubmit = () => {
+    formRef.value?.validate((valid: boolean) => {
+        if (valid) {
+            baTable.table.loading = true
+
+            let params = {
+                id: baTable.form.items!.id,
+                name: baTable.form.items!.name,
+                type: baTable.form.items!.type,
+                status: baTable.form.items!.status,
+                app_id: baTable.form.items!.app_id,
+                mch_id: baTable.form.items!.mch_id,
+                key: baTable.form.items!.key,
+                notify_url: baTable.form.items!.notify_url,
+                h5_notify_url: baTable.form.items!.h5_notify_url,
+                public_key_path: baTable.form.items!.public_key_path,
+                private_key_path: baTable.form.items!.private_key_path,
+            }
+
+            edit(params).then(() => {
+                baTable.toggleForm()
+                emit('success')
+            }).finally(() => {
+                baTable.table.loading = false
+            })
+
+        }
+    })
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 0 - 13
web/src/views/backend/payment/alipay/index.vue

@@ -1,13 +0,0 @@
-<template>
-    <div class="default-main ba-table-box">
-        <el-card shadow="never">
-            支付宝配置
-        </el-card>
-    </div>
-</template>
-
-<script setup lang="ts">
-defineOptions({
-    name: 'payment/alipay',
-})
-</script>

+ 0 - 330
web/src/views/backend/product/combo/detail.vue

@@ -1,330 +0,0 @@
-<template>
-    <el-dialog
-        v-model="visible"
-        class="ba-operate-dialog"
-        width="950px"
-        :close-on-click-modal="false"
-        :destroy-on-close="true"
-        @close="handleClose"
-    >
-        <template #header>
-            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
-                套餐详情 --- {{ detailData?.name || '-' }}
-            </div>
-        </template>
-
-        <el-scrollbar class="ba-table-form-scrollbar">
-            <div class="detail-wrapper">
-                <el-descriptions
-                    v-if="detailData"
-                    :column="3"
-                    border
-                    class="detail-base-info"
-                    size="small"
-                >
-                    <el-descriptions-item label="套餐名称">
-                        {{ detailData.name }}
-                    </el-descriptions-item>
-                    <el-descriptions-item label="当前库存状态">
-                        <el-tag :type="inventoryStatusTag" effect="plain">
-                            {{ inventoryStatusText }}
-                        </el-tag>
-                    </el-descriptions-item>
-                    <el-descriptions-item label="总剩余库存">
-                        {{ totalStock }}
-                    </el-descriptions-item>
-                    <el-descriptions-item label="已卖数量">
-                        {{ detailData.sold_quantity }}
-                    </el-descriptions-item>
-                    <el-descriptions-item label="创建时间">
-                        {{ detailData.create_time }}
-                    </el-descriptions-item>
-                </el-descriptions>
-
-                <!-- 规格明细表格 -->
-                <div class="spec-table-title">
-                    套餐规格明细
-                </div>
-                <el-table
-                    v-if="specList.length"
-                    :data="specList"
-                    border
-                    size="small"
-                    style="width: 100%"
-                >
-                    <el-table-column
-                        prop="specName"
-                        label="规格名称"
-                        align="center"
-                        min-width="120"
-                    />
-                    <el-table-column
-                        label="库存状态"
-                        align="center"
-                        min-width="120"
-                    >
-                        <template #default="{ row }">
-                            <el-tag :type="getRowStatusTag(row.stock)" effect="plain">
-                                {{ getRowStatusText(row.stock) }}
-                            </el-tag>
-                        </template>
-                    </el-table-column>
-                    <el-table-column
-                        label="剩余库存"
-                        align="center"
-                        min-width="140"
-                    >
-                        <template #default="{ row, $index }">
-                            <el-input-number
-                                v-model.number="specList[$index].stock"
-                                :min="0"
-                                :max="999999"
-                                controls-position="right"
-                                size="small"
-                            />
-                        </template>
-                    </el-table-column>
-                    <el-table-column
-                        prop="price"
-                        label="价格(元)"
-                        align="center"
-                        min-width="120"
-                    />
-                    <el-table-column
-                        label="套餐类型"
-                        align="center"
-                        min-width="120"
-                    >
-                        <template #default="{ row }">
-                            {{ packageTypeText(row.packageType) }}
-                        </template>
-                    </el-table-column>
-                    <el-table-column
-                        label="收费方式"
-                        align="center"
-                        min-width="140"
-                    >
-                        <template #default="{ row }">
-                            {{ chargeTypeText(row.chargeType) }}
-                            <span v-if="showQuantity(row.chargeType) && row.quantity">
-                                ({{ row.quantity }}{{ getUnitLabel(row.chargeType) }})
-                            </span>
-                        </template>
-                    </el-table-column>
-                    <el-table-column
-                        prop="remark"
-                        label="说明"
-                        align="center"
-                        min-width="200"
-                        show-overflow-tooltip
-                    />
-                </el-table>
-
-                <!-- 汇总信息 -->
-                <div class="summary-row">
-                    <div class="summary-item">
-                        <span class="label">规格数量:</span>
-                        <span>{{ specList.length }}</span>
-                    </div>
-                    <div class="summary-item">
-                        <span class="label">总剩余库存:</span>
-                        <span class="value-number">{{ totalStock }}</span>
-                    </div>
-                    <div class="summary-item">
-                        <span class="label">整体库存状态:</span>
-                        <el-tag :type="inventoryStatusTag" effect="plain">
-                            {{ inventoryStatusText }}
-                        </el-tag>
-                    </div>
-                </div>
-            </div>
-        </el-scrollbar>
-
-        <template #footer>
-            <div class="dialog-footer">
-                <el-button size="large" class="cancel-btn" @click="handleClose">
-                    取消
-                </el-button>
-                <el-button
-                    size="large"
-                    type="primary"
-                    class="submit-btn"
-                    @click="handleConfirm"
-                >
-                    确认修改库存
-                </el-button>
-            </div>
-        </template>
-    </el-dialog>
-</template>
-
-<script setup lang="ts">
-import { computed, reactive, ref } from 'vue'
-
-interface SpecificationItem {
-    specName: string
-    price: number | undefined
-    stock: number | undefined
-    packageType: string
-    chargeType: string
-    quantity?: number | undefined
-    remark?: string
-}
-
-interface DetailRow {
-    id: number
-    name: string
-    inventory_status: string
-    remaining_stock: number
-    sold_quantity: number
-    create_time: string
-    specificationsList?: SpecificationItem[]
-}
-
-const visible = ref(false)
-const detailData = ref<DetailRow | null>(null)
-const specList = reactive<SpecificationItem[]>([])
-
-const emits = defineEmits<{
-    (e: 'confirm', payload: { id: number; specificationsList: SpecificationItem[]; totalStock: number }): void
-}>()
-
-const open = (row: DetailRow) => {
-    detailData.value = { ...row }
-
-    // 重置规格明细
-    specList.splice(0, specList.length)
-    const list = (row.specificationsList || []) as SpecificationItem[]
-    list.forEach((item) => {
-        specList.push({ ...item })
-    })
-
-    visible.value = true
-}
-
-const handleClose = () => {
-    visible.value = false
-}
-
-const totalStock = computed(() =>
-    specList.reduce((sum, item) => sum + (Number(item.stock) || 0), 0)
-)
-
-const inventoryStatusText = computed(() => {
-    if (totalStock.value === 0) return '售罄'
-    if (totalStock.value < 10) return '即将售罄'
-    return '充足'
-})
-
-const inventoryStatusTag = computed(() => {
-    if (totalStock.value === 0) return 'danger'
-    if (totalStock.value < 10) return 'warning'
-    return 'success'
-})
-
-const getRowStatusText = (stock?: number) => {
-    const value = Number(stock) || 0
-    if (value === 0) return '售罄'
-    if (value < 10) return '即将售罄'
-    return '充足'
-}
-
-const getRowStatusTag = (stock?: number) => {
-    const value = Number(stock) || 0
-    if (value === 0) return 'danger'
-    if (value < 10) return 'warning'
-    return 'success'
-}
-
-const packageTypeText = (type: string) => {
-    if (type === '2') return '团队套餐'
-    return '个人套餐'
-}
-
-const chargeTypeText = (type: string) => {
-    const map: Record<string, string> = {
-        '1': '按次数',
-        '2': '按积分',
-        '3': '日卡',
-        '4': '周卡',
-        '5': '月卡',
-        '6': '季卡',
-        '7': '年卡',
-    }
-    return map[type] || '-'
-}
-
-const showQuantity = (chargeType: string) => {
-    return chargeType === '1' || chargeType === '2'
-}
-
-const getUnitLabel = (chargeType: string) => {
-    if (chargeType === '1') return '次'
-    if (chargeType === '2') return '分'
-    return ''
-}
-
-const handleConfirm = () => {
-    if (!detailData.value) return
-    emits('confirm', {
-        id: detailData.value.id,
-        specificationsList: specList.map((item) => ({ ...item })),
-        totalStock: totalStock.value,
-    })
-    handleClose()
-}
-
-defineExpose({ open })
-</script>
-
-<style scoped lang="scss">
-.detail-wrapper {
-    padding: 12px 16px 4px;
-}
-
-.detail-base-info {
-    margin-bottom: 16px;
-}
-
-.spec-table-title {
-    margin-bottom: 8px;
-    font-weight: 600;
-    font-size: 14px;
-    color: #333;
-}
-
-.summary-row {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: flex-end;
-    gap: 16px;
-    margin-top: 12px;
-    padding-top: 8px;
-    border-top: 1px dashed #e5e5e5;
-
-    .summary-item {
-        display: inline-flex;
-        align-items: center;
-        font-size: 13px;
-
-        .label {
-            color: #666;
-            margin-right: 4px;
-        }
-
-        .value-number {
-            font-weight: 600;
-            color: #333;
-        }
-    }
-}
-
-.dialog-footer {
-    display: flex;
-    justify-content: flex-end;
-    align-items: center;
-    gap: 10px;
-}
-</style>
-
-

+ 0 - 357
web/src/views/backend/product/combo/index.vue

@@ -1,357 +0,0 @@
-<template>
-    <!-- 外层容器,保持默认的表格布局框架 -->
-    <div class="default-main ba-table-box">
-        <!-- 备注提示,当有 remark 时展示 -->
-        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
-
-        <!-- 顶部筛选与新增区域,左侧输入与下拉,右侧添加按钮 -->
-         <!-- 表格顶部菜单 -->
-        <TableHeader :buttons="['refresh', 'columnDisplay']"
-            :quick-search-placeholder="t('Quick search placeholder', { fields: '产品名称' })">
-            <!-- 套餐名称输入框,支持清空 -->
-            <el-input
-                class="filter-item"
-                v-model="searchForm.name"
-                placeholder="套餐名称"
-                clearable
-                @input="handleFilter"
-                @clear="handleFilter"
-            />
-            <!-- 库存状态选择框,模拟原型的状态下拉 -->
-            <el-select
-                class="filter-item"
-                v-model="searchForm.inventory_status"
-                placeholder="库存状态"
-                clearable
-                @change="handleFilter"
-                @clear="handleFilter"
-            >
-                <el-option
-                    v-for="item in inventoryOptions"
-                    :key="item.value"
-                    :label="item.label"
-                    :value="item.value"
-                />
-            </el-select>
-            <!-- 右侧添加按钮 -->
-            <el-button type="primary" @click="onAdd">添加</el-button>
-        </TableHeader>
-        <!-- 表格主体,使用通用 Table 组件并自定义操作列 -->
-        <Table>
-            <!-- 自定义操作列插槽,呈现编辑/删除/详情 -->
-            <template #operate>
-                <el-table-column prop="operate" align="center" label="操作" min-width="180">
-                    <template #default="scope">
-                        <el-button  type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
-                        <el-button  type="danger" size="small" @click="del(scope.row)">删除</el-button>
-                        <el-button style="color: #fff;"  color="#9ca0fa" size="small" @click="viewDetail(scope.row)">套餐详情</el-button>
-                    </template>
-                </el-table-column>
-            </template>
-        </Table>
-
-        <!-- 添加编辑弹框 -->
-        <PopupForm ref="popupFormRef" @success="handleFormSuccess" />
-
-        <!-- 套餐详情 / 库存调整弹框 -->
-        <Detail ref="detailRef" @confirm="handleDetailConfirm" />
-
-    </div>
-</template>
-
-<script setup lang="ts">
-// 引入响应式工具与生命周期钩子
-import { onMounted, provide, reactive, ref } from 'vue'
-// 引入表格封装类
-import baTableClass from '/@/utils/baTable'
-// 引入通用表格组件
-import Table from '/@/components/table/index.vue'
-import TableHeader from '/@/components/table/header/index.vue'
-import PopupForm from './popupForm.vue'
-import Detail from './detail.vue'
-// 引入接口封装
-import { baTableApi } from '/@/api/common'
-// 引入国际化
-import { useI18n } from 'vue-i18n'
-// 引入消息提示
-import { ElMessage } from 'element-plus'
-
-// 组件名称定义
-defineOptions({
-    name: 'application/combo',
-})
-
-// 国际化方法
-const { t } = useI18n()
-
-const popupFormRef = ref<InstanceType<typeof PopupForm>>()
-const detailRef = ref<InstanceType<typeof Detail>>()
-
-// 筛选表单数据,包含套餐名称与库存状态
-const searchForm = reactive({
-    name: '', // 套餐名称关键词
-    inventory_status: '', // 库存状态筛选值
-})
-
-// 库存状态选项,映射下拉显示
-const inventoryOptions = [
-    { label: '充足', value: '1' },
-    { label: '即将售罄', value: '2' },
-    { label: '售罄', value: '3' },
-]
-
-// 静态模拟数据,来源于原型图 + 规格示例数据
-// 规格字段与 popupForm 中的 SpecificationItem 一致,方便后续联动编辑
-const baseData = [
-    {
-        id: 1,
-        name: '聚德神笔',
-        inventory_status: '1',
-        remaining_stock: 120,
-        sold_quantity: 12,
-        create_time: '2025-10-19',
-        specificationsList: [
-            {
-                specName: '个人基础版',
-                price: 99,
-                stock: 80,
-                packageType: '1', // 个人套餐
-                chargeType: '1', // 按次数
-                quantity: 10,
-                remark: '适合个人日常使用',
-            },
-            {
-                specName: '个人进阶版',
-                price: 199,
-                stock: 40,
-                packageType: '1',
-                chargeType: '2', // 按积分
-                quantity: 100,
-                remark: '附赠额外积分',
-            },
-        ],
-    },
-    {
-        id: 2,
-        name: '棱镜',
-        inventory_status: '2',
-        remaining_stock: 12,
-        sold_quantity: 88,
-        create_time: '2025-10-19',
-        specificationsList: [
-            {
-                specName: '团队标准版',
-                price: 599,
-                stock: 8,
-                packageType: '2', // 团队套餐
-                chargeType: '3', // 日卡
-                quantity: undefined,
-                remark: '适合小团队短期使用',
-            },
-            {
-                specName: '团队增强版',
-                price: 1599,
-                stock: 4,
-                packageType: '2',
-                chargeType: '5', // 月卡
-                quantity: undefined,
-                remark: '按月计费,支持多人使用',
-            },
-        ],
-    },
-    {
-        id: 3,
-        name: '套装示例',
-        inventory_status: '3',
-        remaining_stock: 0,
-        sold_quantity: 120,
-        create_time: '2025-10-19',
-        specificationsList: [
-            {
-                specName: '体验版',
-                price: 49,
-                stock: 0,
-                packageType: '1',
-                chargeType: '1',
-                quantity: 3,
-                remark: '促销活动已售罄',
-            },
-        ],
-    },
-]
-
-// 初始化表格实例,配置列信息
-const baTable = new baTableClass(
-    new baTableApi('/admin/application.combo'),
-    {
-        column: [
-            { label: '编号', prop: 'id', align: 'center', width: 70 },
-            { label: '套餐名称', prop: 'name', align: 'center', minWidth: 140 },
-            {
-                label: '库存状态',
-                prop: 'inventory_status',
-                align: 'center',
-                render: 'tag',
-                custom: { '1': 'primary', '2': 'warning', '3': 'danger' },
-                replaceValue: { '1': '充足', '2': '即将售罄', '3': '售罄' },
-                effect: 'plain',
-                minWidth: 120,
-            },
-            {
-                label: '剩余库存',
-                prop: 'remaining_stock',
-                align: 'center',
-                minWidth: 140,
-            },
-            { label: '已卖数量', prop: 'sold_quantity', align: 'center', minWidth: 100 },
-            { label: '创建时间', prop: 'create_time', align: 'center', minWidth: 160 },
-            { label: '操作', align: 'center', render: 'slot', slotName: 'operate', operator: false, minWidth: 180 },
-        ],
-        dblClickNotEditColumn: [undefined],
-    },
-    {
-        defaultItems: {
-            name: '',
-            inventory_status: '1',
-            remaining_stock: 0,
-            sold_quantity: 0,
-        },
-    }
-)
-
-// 覆写获取数据方法,基于筛选条件过滤静态数据
-baTable.getData = () => {
-    baTable.table.loading = true // 开启加载状态
-    return new Promise<void>((resolve) => {
-        setTimeout(() => {
-            // 根据输入与下拉值过滤数据
-            const filtered = baseData.filter((item) => {
-                const matchName = searchForm.name ? item.name.includes(searchForm.name.trim()) : true
-                const matchStatus = searchForm.inventory_status ? item.inventory_status === searchForm.inventory_status : true
-                return matchName && matchStatus
-            })
-            baTable.table.data = filtered // 写入表格数据
-            baTable.table.total = filtered.length // 写入总数
-            baTable.table.loading = false // 关闭加载状态
-            resolve() // 完成 Promise
-        }, 200)
-    })
-}
-
-// 删除动作,展示提示
-const del = (row:TableRow) => {
-    ElMessage.success(`已删除套餐:${row.name}`)
-}
-
-// 编辑动作:打开弹框并带入当前行
-const onEdit = (row:TableRow ) => {
-    popupFormRef.value?.editDialog(row as any)
-}
-
-// 新增动作,展示提示
-const onAdd = () => {
-    popupFormRef.value?.openDialog()
-}
-
-// 处理弹框提交成功(新增 / 编辑)
-const handleFormSuccess = (payload: {
-    id: number | null
-    type: 'add' | 'edit'
-    name: string
-    specificationsList: any[]
-}) => {
-    const { id, type, name, specificationsList } = payload
-
-    // 根据规格列表计算总库存
-    const totalStock = (specificationsList || []).reduce(
-        (sum, item) => sum + (Number(item.stock) || 0),
-        0
-    )
-
-    // 根据库存计算库存状态
-    let inventoryStatus = '1' // 充足
-    if (totalStock === 0) {
-        inventoryStatus = '3' // 售罄
-    } else if (totalStock < 10) {
-        inventoryStatus = '2' // 即将售罄
-    }
-
-    if (type === 'edit' && id != null) {
-        // 编辑:更新静态数据中的对应项
-        const target = baseData.find((item) => item.id === id)
-        if (target) {
-            target.name = name
-            target.remaining_stock = totalStock
-            target.inventory_status = inventoryStatus
-            // 把规格明细也一起带上,方便下次再编辑
-            ;(target as any).specificationsList = specificationsList
-            ElMessage.success(`已更新套餐:${name}`)
-        }
-    } else {
-        // 新增:简单提示(此处仍以静态数据为主,不追加行)
-        ElMessage.success(`已添加套餐:${name}`)
-    }
-
-    // 重新根据筛选条件刷新一次表格
-    baTable.getData()
-}
-
-// 套餐详情动作,弹出详情弹框,可从按钮或库存单元格进入
-const viewDetail = (row: Record<string, any>, fromStockClick = false) => {
-    if (!row) return
-    detailRef.value?.open(row as any)
-    if (!fromStockClick) {
-        ElMessage.success(`查看套餐详情:${row.name}`)
-    }
-}
-
-// 详情弹框确认修改库存
-const handleDetailConfirm = (payload: {
-    id: number
-    specificationsList: any[]
-    totalStock: number
-}) => {
-    const target = baseData.find((item) => item.id === payload.id)
-    if (!target) return
-
-    // 更新规格明细
-    ;(target as any).specificationsList = payload.specificationsList
-
-    // 更新总库存
-    target.remaining_stock = payload.totalStock
-
-    // 根据总库存刷新库存状态
-    if (payload.totalStock === 0) {
-        target.inventory_status = '3'
-    } else if (payload.totalStock < 10) {
-        target.inventory_status = '2'
-    } else {
-        target.inventory_status = '1'
-    }
-
-    ElMessage.success(`已更新「${target.name}」的库存信息`)
-    baTable.getData()
-}
-
-// 输入或选择变化时触发过滤
-const handleFilter = () => {
-    baTable.getData()
-}
-
-// 注入表格实例供子组件使用
-provide('baTable', baTable)
-
-// 初始化挂载表格并拉取数据
-onMounted(() => {
-    baTable.mount()
-    baTable.getData()
-})
-</script>
-
-<style scoped lang="scss">
-.filter-item {
-    width: 180px;
-    margin: 0 6px;
-}
-</style>
-

+ 0 - 436
web/src/views/backend/product/combo/popupForm.vue

@@ -1,436 +0,0 @@
-<template>
-	<!-- 对话框表单 -->
-	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
-		@close="handleClose" width="880px">
-		<template #header>
-			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
-				套餐配置
-			</div>
-		</template>
-		<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
-			<div class="ba-operate-form">
-				<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" class="form-container">
-					<el-form-item label="套餐名称" prop="name">
-						<el-input v-model="formData.name" placeholder="请输入套餐名称" />
-					</el-form-item>
-					<!-- 套餐规格配置 -->
-					<el-form-item prop="specificationsList">
-						<div class="spec-section">
-							<div class="spec-header">
-								<span class="spec-title">套餐规格:</span>
-								<el-button
-									type="primary"
-									size="small"
-									plain
-									class="spec-add-btn"
-									@click="addSpecification"
-								>
-									<span class="spec-add-icon">+</span>
-									<span>新增规格</span>
-								</el-button>
-							</div>
-							<div class="spec-row" v-for="(item, index) in formData.specificationsList" :key="index">
-								<!-- 第一行:规格名称 / 价格 / 库存 / 套餐类型 -->
-								<div class="spec-row-line spec-row-line--top">
-									<el-input
-										v-model="item.specName"
-										placeholder="规格名称"
-										class="spec-input spec-name"
-									/>
-									<div class="spec-group">
-										<el-input-number
-											v-model.number="item.price"
-											:min="0"
-											placeholder="价格"
-											class="spec-input spec-price"
-										/>
-										<span class="spec-unit">元</span>
-									</div>
-									<el-select v-model="item.packageType" placeholder="套餐类型"
-										class="spec-input spec-select">
-										<el-option v-for="opt in packageTypeOptions" :key="opt.value" :label="opt.label"
-											:value="opt.value" />
-									</el-select>
-									<div class="spec-group">
-										<span class="spec-label">库存:</span>
-										<el-input-number v-model.number="item.stock" :min="0" placeholder="库存数量"
-											class="spec-input"  />
-									</div>
-
-								</div>
-
-								<!-- 第二行:计费方式 / 次数 / 说明 / 删除 -->
-								<div class="spec-row-line spec-row-line--bottom">
-									<el-select
-										v-model="item.chargeType"
-										placeholder="计费方式"
-										class="spec-input spec-select"
-									>
-										<el-option
-											v-for="opt in chargeTypeOptions"
-											:key="opt.value"
-											:label="opt.label"
-											:value="opt.value"
-										/>
-									</el-select>
-									<div class="spec-group" v-if="showQuantity(item.chargeType)">
-										<el-input-number
-											v-model.number="item.quantity"
-											:min="0"
-											placeholder="数量"
-											class="spec-input spec-quantity"
-										/>
-										<span class="spec-unit">{{ getUnitLabel(item.chargeType) }}</span>
-									</div>
-									<el-input
-										v-model="item.remark"
-										placeholder="说明"
-										class="spec-input spec-remark"
-									/>
-									<el-button
-										type="danger"
-										size="small"
-										text
-										class="spec-delete-btn"
-										@click="removeSpecification(index)"
-									>
-										<span class="spec-delete-icon">×</span>
-										<span>删除</span>
-									</el-button>
-								</div>
-							</div>
-						</div>
-					</el-form-item>
-				</el-form>
-			</div>
-		</el-scrollbar>
-		<template #footer>
-			<div class="dialog-footer">
-				<el-button @click="handleClose" size="large" class="cancel-btn">{{ t('Cancel') }}</el-button>
-				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading"
-					class="submit-btn">确定</el-button>
-			</div>
-		</template>
-	</el-dialog>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, computed } from 'vue'
-import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
-import { useI18n } from 'vue-i18n'
-
-const { t } = useI18n()
-
-// 显示隐藏
-const dialogVisible = ref(false)
-// 类型:add、edit
-const dialogType = ref<'add' | 'edit'>('add')
-// 当前编辑的套餐 ID(新增时为 null)
-const currentEditId = ref<number | null>(null)
-
-const loading = ref(false)
-
-const formRef = ref<FormInstance>()
-
-const emits = defineEmits(['success'])
-
-
-const formData = reactive({
-	name: '',
-	specificationsList: [] as SpecificationItem[],
-})
-
-
-
-// 表单验证规则
-const rules: FormRules = {
-	name: [
-		{ required: true, message: '请输入套餐名称', trigger: 'change' }
-	],
-	specificationsList: [
-		{
-			validator: (_rule, value, callback) => {
-				if (!value || !value.length) {
-					return callback(new Error('请至少添加一个套餐规格'))
-				}
-				const invalid = value.some((item: SpecificationItem) => {
-					const needQuantity = showQuantity(item.chargeType)
-					return (
-						!item.specName ||
-						item.price == null ||
-						item.stock == null ||
-						!item.packageType ||
-						!item.chargeType ||
-						(needQuantity && item.quantity == null)
-					)
-				})
-				if (invalid) {
-					return callback(new Error('请完整填写每一行套餐规格'))
-				}
-				return callback()
-			},
-			trigger: 'change',
-		},
-	]
-}
-
-// 打开对话框(新增)
-const openDialog = () => {
-	dialogVisible.value = true
-	dialogType.value = 'add'
-	currentEditId.value = null
-	resetForm()
-
-}
-
-// 编辑:接收列表行数据,回填到表单
-const editDialog = (row: TableRow & { id?: number; name?: string; specificationsList?: SpecificationItem[] }) => {
-	dialogVisible.value = true
-	dialogType.value = 'edit'
-	currentEditId.value = row.id ?? null
-
-	// 清理校验状态
-	formRef.value?.clearValidate()
-
-	// 回填基础信息
-	formData.name = row.name ?? ''
-
-	// 回填规格列表
-	if (row.specificationsList && row.specificationsList.length) {
-		formData.specificationsList = row.specificationsList.map((item) => ({ ...item }))
-	} else {
-		formData.specificationsList = []
-		addSpecification()
-	}
-}
-
-// 重置表单
-const resetForm = () => {
-	formRef.value?.clearValidate()
-	formData.name = ''
-	formData.specificationsList = []
-	addSpecification()
-}
-
-// 提交表单
-const onSubmit = async () => {
-	if (!formRef.value) return
-
-	try {
-		await formRef.value.validate()
-		loading.value = true
-
-		// 构建提交数据,传给父组件(index.vue)
-		const submitData = {
-			id: currentEditId.value,
-			type: dialogType.value,
-			name: formData.name,
-			specificationsList: formData.specificationsList
-		}
-
-		emits('success', submitData)
-		handleClose()
-	} catch (error) {
-		ElMessage({
-			type: 'error',
-			message: '账户添加失败,请重试!',
-		})
-	} finally {
-		loading.value = false
-	}
-}
-
-// 关闭弹框
-const handleClose = () => {
-	dialogVisible.value = false
-	resetForm()
-}
-
-// 规格项类型
-interface SpecificationItem {
-	specName: string
-	price: number | undefined
-	stock: number | undefined
-	packageType: string
-	chargeType: string
-	quantity: number | undefined
-	remark: string
-}
-
-// 套餐类型选项(值改为数字编码,方便与后端枚举对应)
-const packageTypeOptions = [
-	{ label: '个人套餐', value: '1' },
-	{ label: '团队套餐', value: '2' },
-]
-
-// 计费方式选项(值使用数字编码,便于与后端对接)
-const chargeTypeOptions = [
-	{ label: '按次数', value: '1' },
-	{ label: '按积分', value: '2' },
-	{ label: '日卡', value: '3' },
-	{ label: '周卡', value: '4' },
-	{ label: '月卡', value: '5' },
-	{ label: '季卡', value: '6' },
-	{ label: '年卡', value: '7' },
-]
-
-// 新增一行规格
-const addSpecification = () => {
-	formData.specificationsList.push({
-		specName: '',
-		price: undefined,
-		stock: undefined,
-		packageType: packageTypeOptions[0].value,
-		chargeType: chargeTypeOptions[0].value,
-		quantity: undefined,
-		remark: '',
-	})
-}
-
-// 删除一行规格
-const removeSpecification = (index: number) => {
-	formData.specificationsList.splice(index, 1)
-}
-
-// 是否需要展示“次数 / 积分”等数量输入框
-const showQuantity = (chargeType: string) => {
-	// 1: 按次数,2: 按积分
-	return chargeType === '1' || chargeType === '2'
-}
-
-// 根据计费方式返回数量单位
-const getUnitLabel = (chargeType: string) => {
-	if (chargeType === '1') return '次'
-	if (chargeType === '2') return '分'
-	return ''
-}
-
-// 导出方法
-defineExpose({ openDialog, editDialog });
-</script>
-
-<style scoped lang="scss">
-.form-container {
-	.spec-section {
-		width: 100%;
-		border: 1px solid #f0f0f0;
-		padding: 12px 16px;
-		border-radius: 4px;
-		background-color: #fafafa;
-
-		.spec-header {
-			display: flex;
-			justify-content: space-between;
-			align-items: center;
-			margin-bottom: 8px;
-
-			.spec-title {
-				font-weight: 600;
-				color: #333;
-			}
-
-			.spec-add-btn {
-				display: inline-flex;
-				align-items: center;
-				gap: 4px;
-				border-radius: 999px;
-				padding: 0 10px;
-
-				.spec-add-icon {
-					font-size: 14px;
-					transform: translateY(-1px);
-				}
-			}
-		}
-
-		.spec-row {
-			position: relative;
-			display: flex;
-			flex-direction: column;
-			row-gap: 6px;
-			margin-bottom: 12px;
-			padding: 10px 12px 10px 36px;
-			border-radius: 6px;
-			background-color: #fff;
-			box-shadow: 0 0 0 1px #f0f0f0;
-
-			.spec-row-line {
-				display: flex;
-				align-items: center;
-				column-gap: 10px;
-				flex-wrap: wrap;
-			}
-
-			.spec-input {
-				margin-right: 0;
-			}
-
-			.spec-name {
-				width: 140px;
-			}
-
-			.spec-price {
-				width: 120px;
-			}
-
-			.spec-stock {
-				width: 100px;
-			}
-
-			.spec-select {
-				width: 140px;
-			}
-
-			.spec-quantity {
-				width: 120px;
-			}
-
-			.spec-remark {
-				flex: 1;
-				min-width: 260px;
-			}
-
-			.spec-group {
-				display: inline-flex;
-				align-items: center;
-				column-gap: 4px;
-			}
-
-			.spec-delete-btn {
-				display: inline-flex;
-				align-items: center;
-				gap: 2px;
-				padding: 0 6px;
-
-				.spec-delete-icon {
-					font-size: 14px;
-					transform: translateY(-1px);
-				}
-			}
-
-			.spec-unit,
-			.spec-label {
-				color: #666;
-				white-space: nowrap;
-			}
-
-			.spec-badge {
-				position: absolute;
-				left: 10px;
-				top: 50%;
-				transform: translateY(-50%);
-				width: 20px;
-				height: 20px;
-				border-radius: 10px;
-				background: linear-gradient(135deg, #409eff, #66b1ff);
-				color: #fff;
-				font-size: 12px;
-				display: flex;
-				align-items: center;
-				justify-content: center;
-				box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);
-			}
-		}
-	}
-}
-</style>

+ 0 - 152
web/src/views/backend/product/product/popupForm.vue

@@ -1,152 +0,0 @@
-<template>
-    <!-- 对话框表单 -->
-    <el-dialog
-        class="ba-operate-dialog"
-        :close-on-click-modal="false"
-        :model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
-        @close="baTable.toggleForm"
-        :destroy-on-close="true"
-    >
-        <template #header>
-            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
-                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
-            </div>
-        </template>
-        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
-            <div
-                class="ba-operate-form"
-                :class="'ba-' + baTable.form.operate + '-form'"
-                :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
-            >
-                <el-form
-                    ref="formRef"
-                    @keyup.enter="baTable.onSubmit(formRef)"
-                    :model="baTable.form.items"
-                    :label-position="config.layout.shrink ? 'top' : 'right'"
-                    :label-width="baTable.form.labelWidth + 'px'"
-                    :rules="rules"
-                    v-if="!baTable.form.loading"
-                >
-                    <FormItem
-                        label="产品名称"
-                        prop="name"
-                        v-model="baTable.form.items!.name"
-                        type="string"
-                        :placeholder="t('Please input field', { field: '产品名称' })"
-                    />
-                    <el-form-item label="关联套餐" prop="package">
-                        <el-select
-                            v-model="baTable.form.items!.package"
-                            placeholder="请选择关联套餐"
-                            style="width: 100%"
-                        >
-                            <el-option
-                                v-for="item in packageOptions"
-                                :key="item.value"
-                                :label="item.label"
-                                :value="item.value"
-                            />
-                        </el-select>
-                    </el-form-item>
-                    <FormItem
-                        label="产品图片"
-                        prop="image"
-                        type="image"
-                        v-model="baTable.form.items!.image"
-                    />
-                    <FormItem
-                        label="产品状态"
-                        prop="status"
-                        type="radio"
-                        v-model="baTable.form.items!.status"
-                        :input-attr="{ border: true, content: statusOptions }"
-                    />
-                    <FormItem
-                        label="产品类型"
-                        prop="type"
-                        type="radio"
-                        v-model="baTable.form.items!.type"
-                        :input-attr="{ border: true, content: typeOptions }"
-                    />
-                    <el-form-item label="产品链接" prop="link">
-                        <el-input
-                            v-model="baTable.form.items!.link"
-                            placeholder="请输入产品链接"
-                        />
-                    </el-form-item>
-                    
-                    <el-form-item label="产品描述" prop="description">
-                        <el-input
-                            v-model="baTable.form.items!.description"
-                            type="textarea"
-                            :rows="4"
-                            placeholder="请输入产品描述"
-                        />
-                    </el-form-item>
-                </el-form>
-            </div>
-        </el-scrollbar>
-        <template #footer>
-            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
-                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
-                <el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
-                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
-                </el-button>
-            </div>
-        </template>
-    </el-dialog>
-</template>
-
-<script setup lang="ts">
-import { reactive, inject,  useTemplateRef } from 'vue'
-import { useI18n } from 'vue-i18n'
-import type baTableClass from '/@/utils/baTable'
-import FormItem from '/@/components/formItem/index.vue'
-import type { FormItemRule } from 'element-plus'
-import { useConfig } from '/@/stores/config'
-import { buildValidatorData } from '/@/utils/validate'
-
-const config = useConfig()
-const formRef = useTemplateRef('formRef')
-const baTable = inject('baTable') as baTableClass
-
-const { t } = useI18n()
-
-const rules: Partial<Record<string, FormItemRule[]>> = reactive({
-    name: [buildValidatorData({ name: 'required', title: '产品名称' })],
-    package: [buildValidatorData({ name: 'required', trigger: 'change', title: '关联套餐' })],
-    description: [buildValidatorData({ name: 'required', title: '产品描述' })],
-    image: [buildValidatorData({ name: 'required', title: '产品图片' })],
-    type: [buildValidatorData({ name: 'required', title: '产品类型' })],
-    link: [buildValidatorData({ name: 'required', title: '产品链接' })],
-    inventory_status: [buildValidatorData({ name: 'required', title: '库存状态' })],
-    status: [buildValidatorData({ name: 'required', title: '产品状态' })],
-})
-
-const packageOptions = [
-    { label: '聚德神笔A套餐', value: '聚德神笔A套餐' },
-    { label: '聚德神笔B套餐', value: '聚德神笔B套餐' },
-    { label: '企业定制套餐', value: '企业定制套餐' },
-]
-
-const typeOptions = {
-    '1': '网页',
-    '2': '软件',
-}
-
-const inventoryOptions = {
-    '1': '充足',
-    '2': '即将售罄',
-    '3': '售罄',
-}
-
-const statusOptions = {
-    '1': '启用',
-    '0': '禁用',
-}
-
-</script>
-
-<style scoped lang="scss">
-
-</style>

+ 0 - 14
web/src/views/backend/transaction/distribution/index.vue

@@ -1,14 +0,0 @@
-<template>
-    <div class="default-main ba-table-box">
-        <el-card shadow="never">
-            分润记录页面待开发
-        </el-card>
-    </div>
-</template>
-
-<script setup lang="ts">
-defineOptions({
-    name: 'transaction/distribution',
-})
-</script>
-

+ 0 - 14
web/src/views/backend/transaction/order/index.vue

@@ -1,14 +0,0 @@
-<template>
-    <div class="default-main ba-table-box">
-        <el-card shadow="never">
-            订单管理页面待开发
-        </el-card>
-    </div>
-</template>
-
-<script setup lang="ts">
-defineOptions({
-    name: 'transaction/order',
-})
-</script>
-

+ 0 - 14
web/src/views/backend/transaction/withdraw/index.vue

@@ -1,14 +0,0 @@
-<template>
-    <div class="default-main ba-table-box">
-        <el-card shadow="never">
-            提现审核页面待开发
-        </el-card>
-    </div>
-</template>
-
-<script setup lang="ts">
-defineOptions({
-    name: 'transaction/withdraw',
-})
-</script>
-

+ 6 - 1
web/src/views/backend/user/group/index.vue

@@ -69,7 +69,12 @@ const baTable = new baTableClass(
         defaultItems: {
             status: 1,
         },
-    }
+    },
+    {
+		onTableDblclick: () => {
+			return false
+		},
+	},
 )
 
 // 利用提交前钩子重写提交操作

+ 44 - 20
web/src/views/backend/company/index.vue → web/src/views/backend/userCenter/index/index.vue

@@ -9,12 +9,10 @@
                 placeholder="企业名称" clearable />
             <el-select class="form-input" v-model="baTable.table.filter!.status" @change="onSearchInput"
                 placeholder="状态" clearable>
-                <el-option label="正常" value="1" />
-                <el-option label="禁用" value="0" />
+                <el-option label="正常" :value="1" />
+                <el-option label="禁用" :value="0" />
             </el-select>
-            <template #quickSearchPrepend>
-                <el-button type="primary" @click="onAdd">添加</el-button>
-            </template>
+            <!-- <el-button type="primary" @click="onAdd" v-auth="'edit'">添加</el-button> -->
         </TableHeader>
 
         <!-- 表格 -->
@@ -23,27 +21,27 @@
             <template #status>
                 <el-table-column prop="status" align="center" label="状态" min-width="150">
                     <template #default="scope">
-                        <el-switch v-model="scope.row.status" active-value="1" inactive-value="0" />
+                        <el-switch @change="changeStatus(scope.row)" v-model="scope.row.status" :active-value="1" :inactive-value="0" />
                     </template>
                 </el-table-column>
             </template>
             <template #operate>
                 <el-table-column prop="operate" align="center" label="操作" min-width="150">
                     <template #default="scope">
-                        <el-button type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
-                        <el-button type="danger" size="small" @click="del(scope.row)">删除</el-button>
+                        <el-button type="primary" size="small" @click="onEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                        <el-button type="danger" size="small" @click="handleDel(scope.row)" v-auth="'delete'">删除</el-button>
                     </template>
                 </el-table-column>
             </template>
         </Table>
 
         <!-- 表单 -->
-        <PopupForm />
+        <PopupForm @success="baTable.getIndex()" />
     </div>
 </template>
 
 <script setup lang="ts">
-import { onMounted, provide, } from 'vue'
+import { provide, } from 'vue'
 import { cloneDeep } from 'lodash-es'
 import baTableClass from '/@/utils/baTable'
 import Table from '/@/components/table/index.vue'
@@ -51,6 +49,8 @@ import TableHeader from '/@/components/table/header/index.vue'
 import PopupForm from './popupForm.vue'
 import { baTableApi } from '/@/api/common'
 import { useI18n } from 'vue-i18n'
+import { toggleStatus,del } from '/@/api/backend/userCenter/index'
+import { ElMessageBox } from 'element-plus'
 
 defineOptions({
     name: 'company/index',
@@ -89,21 +89,27 @@ const baTable = new baTableClass(
                 align: 'center',
                 minWidth: 160
             },
-            {
-                label: '操作',
-                align: 'center',
-                render: 'slot',
-                slotName: 'operate',
-                operator: false,
-            },
+            // {
+            //     label: '操作',
+            //     align: 'center',
+            //     render: 'slot',
+            //     slotName: 'operate',
+            //     operator: false,
+            // },
         ],
         dblClickNotEditColumn: [undefined,],
     },
     {
         defaultItems: {
             status: '1',
+            id: null,
         },
-    }
+    },
+    {
+		onTableDblclick: () => {
+			return false
+		},
+	},
 )
 
 
@@ -117,9 +123,27 @@ const onSearchInput = () => {
     baTable.getData()
 }
 
+// 改变企业状态
+const changeStatus = (row:TableRow)=>{
+    baTable.table.loading = true
+    toggleStatus({id:row.id}).then(()=>{
+        baTable.getIndex()
+    }).finally(()=>{
+        baTable.table.loading = false
+    })
+}
+
 // 删除
-const del = (row: TableRow) => {
-    baTable.onTableAction('delete', { row })
+const handleDel = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除企业"${row.name}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
 }
 
 const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {

+ 36 - 11
web/src/views/backend/company/popupForm.vue → web/src/views/backend/userCenter/index/popupForm.vue

@@ -41,6 +41,7 @@
                             v-model="baTable.form.items!.creator"
                             placeholder="请选择所属人"
                             style="width: 100%"
+                            filterable
                         >
                             <el-option
                                 v-for="item in userOptions"
@@ -72,19 +73,20 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, inject,  useTemplateRef } from 'vue'
+import { ref,reactive, inject,  useTemplateRef,onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import type baTableClass from '/@/utils/baTable'
 import type { FormItemRule } from 'element-plus'
 import { useConfig } from '/@/stores/config'
 import FormItem from '/@/components/formItem/index.vue'
 import { buildValidatorData } from '/@/utils/validate'
-import { edit } from '/@/api/backend/company/index'
+import { edit } from '/@/api/backend/userCenter/index'
+import { list } from '/@/api/backend/userCenter/user'
 
 const config = useConfig()
 const formRef = useTemplateRef('formRef')
 const baTable = inject('baTable') as baTableClass
-
+const emit = defineEmits(['success'])
 const { t } = useI18n()
 
 const rules: Partial<Record<string, FormItemRule[]>> = reactive({
@@ -92,13 +94,21 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
     
 })
 
-const userOptions = [
-    { label: '张三', value: '张三' },
-    { label: '李四', value: '李四' },
-    { label: '王五', value: '王五' },
-    { label: '赵六', value: '赵六' },
-    { label: '刘七', value: '刘七' },
-]
+interface UserOption {
+    label: string
+    value: string
+}
+
+const userOptions = ref<UserOption[]>([])
+
+const getUserList = () => {
+    list({ page: 1, limit: 1000 }).then((res: any) => {
+        userOptions.value = res.data.list.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+        }))
+    })
+}
 
 const statusOptions = {
     '0': '禁用',
@@ -110,11 +120,26 @@ const btnSubmit = () => {
 	formRef.value?.validate((valid: boolean) => {
 		if (valid) {
             baTable.table.loading = true
-			
+
+            let params = {
+                id: baTable.form.items!.id,
+                name: baTable.form.items!.name,
+                status: baTable.form.items!.status,
+                creator: baTable.form.items!.creator,
+            }
+
+            edit(params).then(() => {
+                baTable.table.loading = false
+                baTable.toggleForm()
+                emit('success')
+            })
 		}
 	})
 }
 
+onMounted(() => {
+    getUserList()
+})
 </script>
 
 <style scoped lang="scss">

+ 15 - 5
web/src/views/backend/userCenter/user/index.vue

@@ -24,7 +24,7 @@
                 <el-table-column prop="operate" align="center" label="操作" min-width="210">
                     <template #default="scope">
                         <el-button type="primary" size="small" @click="onEdit(scope.row)">编辑</el-button>
-                        <el-button type="danger" size="small" @click="del(scope.row)">删除</el-button>
+                        <el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
                         <el-button type="primary" plain size="small" @click="detail(scope.row)">详情</el-button>
                     </template>
                 </el-table-column>
@@ -32,7 +32,7 @@
         </Table>
 
         <!-- 表单 -->
-        <PopupForm />
+        <PopupForm @success="baTable.getIndex()" />
 
         <!-- 详情弹框 -->
         <DetailDialog ref="detailDialogRef" />
@@ -49,6 +49,8 @@ import PopupForm from './popupForm.vue'
 import DetailDialog from './detailDialog.vue'
 import { baTableApi } from '/@/api/common'
 import { useI18n } from 'vue-i18n'
+import { del } from '/@/api/backend/userCenter/user'
+import { ElMessageBox } from 'element-plus'
 
 defineOptions({
     name: 'userCenter/user',
@@ -71,7 +73,7 @@ const baTable = new baTableClass(
                 label: '邮箱',
                 prop: 'email',
                 align: 'center',
-                minWidth: 150
+                minWidth: 170
             },
             {
                 label: '企业',
@@ -134,8 +136,16 @@ const onSearchInput = () => {
 }
 
 // 删除
-const del = (row: TableRow) => {
-    baTable.onTableAction('delete', { row })
+const handleDelete = (row: TableRow) => {
+    ElMessageBox.confirm(`确定删除用户"${row.name}"吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+    }).then(() => {
+        del({ id: row.id }).then(() => {
+            baTable.getIndex()
+        })
+    })
 }
 
 const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {

+ 41 - 8
web/src/views/backend/userCenter/user/popupForm.vue

@@ -6,6 +6,8 @@
         :model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
         @close="baTable.toggleForm"
         :destroy-on-close="true"
+        width="600px"
+        style="height: 400px;"
     >
         <template #header>
             <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
@@ -53,6 +55,8 @@
                             v-model="baTable.form.items!.company"
                             placeholder="请选择企业"
                             style="width: 100%"
+                            clearable
+                            filterable
                         >
                             <el-option
                                 v-for="item in companyOptions"
@@ -84,7 +88,7 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, inject,  useTemplateRef } from 'vue'
+import { reactive,ref, inject,  useTemplateRef ,onMounted} from 'vue'
 import { useI18n } from 'vue-i18n'
 import type baTableClass from '/@/utils/baTable'
 import FormItem from '/@/components/formItem/index.vue'
@@ -92,10 +96,12 @@ import type { FormItemRule } from 'element-plus'
 import { useConfig } from '/@/stores/config'
 import { buildValidatorData } from '/@/utils/validate'
 import { edit } from '/@/api/backend/userCenter/user'
+import { list } from '/@/api/backend/userCenter/index'
 
 const config = useConfig()
 const formRef = useTemplateRef('formRef')
 const baTable = inject('baTable') as baTableClass
+const emit = defineEmits(['success'])
 
 const { t } = useI18n()
 
@@ -121,29 +127,56 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
     ],
 })
 
-const companyOptions = [
-    { label: '腾达', value: '腾达' },
-    { label: '聚德科技', value: '聚德科技' },
-    { label: '棱镜科技', value: '棱镜科技' },
-    { label: '测试公司', value: '测试公司' },
-    { label: '示例企业', value: '示例企业' },
-]
+interface CompanyOption {
+    label: string
+    value: string
+}
+
+const companyOptions = ref<CompanyOption[]>([])
 
 const statusOptions = {
     '0': '禁用',
     '1': '启用',
 }
 
+const getCompanyList = () => {
+    list({ page: 1, limit: 1000 }).then((res: any) => {
+        companyOptions.value = res.data.list.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+        }))
+    })
+}
+
 // 提交
 const btnSubmit = () => {
 	formRef.value?.validate((valid: boolean) => {
 		if (valid) {
             baTable.table.loading = true
+
+            let params = {
+                id: baTable.form.items!.id,
+                name: baTable.form.items!.name,
+                email:baTable.form.items!.email,
+                company:baTable.form.items!.company,
+                password:baTable.form.items!.password,
+                status: baTable.form.items!.status,
+            }
+
+            edit(params).then(() => {
+                baTable.toggleForm()
+                emit('success')
+            }).finally(()=>{
+                baTable.table.loading = false
+            })
 			
 		}
 	})
 }
 
+onMounted(() => {
+    getCompanyList()
+})
 </script>
 
 <style scoped lang="scss">

+ 15 - 6
web/src/views/frontend/Register.vue

@@ -53,7 +53,7 @@
           <el-input v-model="registerForm.confirmPassword" type="password" placeholder="再次输入密码" show-password />
         </el-form-item>
 
-        <el-form-item label="邀请码(选填)" prop="inviteCode">
+        <el-form-item label="邀请码(选填)" prop="inviteCode">
           <el-input v-model.trim="registerForm.inviteCode" placeholder="没有可留空" clearable />
         </el-form-item>
 
@@ -164,7 +164,7 @@ const handleRegister = () => {
         registerType: 'email',
         name: registerForm.name.trim(),
         email: registerForm.email.trim(),
-        username: registerForm.email.trim(),
+        username: registerForm.name.trim(),
         password: registerForm.password,
         captcha: registerForm.emailCode.trim(),
         captchaId: registerForm.captchaId,
@@ -195,10 +195,19 @@ const handleRegister = () => {
 }
 
 const handleSendEmailCode = () => {
-  if (codeDisabled.value || !registerFormRef.value) return
-  registerFormRef.value.validateField('email', async error => {
-    if (error) return
-    // 发送验证码前先进行点选验证码验证
+  if (!registerFormRef.value) return
+  if (codeDisabled.value) return
+  if (!registerForm.email.trim()) {
+    ElMessage.warning('请输入邮箱地址')
+    return
+  }
+  
+  registerFormRef.value.validateField('email').then((valid) => {
+    if (!valid) {
+      // 验证失败,Element Plus 会自动显示错误信息
+      return
+    }
+    // 邮箱验证通过,发送验证码前先进行点选验证码验证
     clickCaptcha(registerForm.captchaId, async (captchaInfo) => {
       sendingCode.value = true
       registerForm.captchaInfo = captchaInfo

+ 12 - 3
web/src/views/frontend/Reset.vue

@@ -156,9 +156,18 @@ const handleReset = () => {
 }
 
 const handleSendEmailCode = () => {
-  if (codeDisabled.value || !resetFormRef.value) return
-  resetFormRef.value.validateField('email', async error => {
-    if (error) return
+  if (!resetFormRef.value) return
+  if (codeDisabled.value) return
+  if (!resetForm.email.trim()) {
+    ElMessage.warning('请输入邮箱地址')
+    return
+  }
+  
+  resetFormRef.value.validateField('email').then(async (valid) => {
+    if (!valid) {
+      // 验证失败,Element Plus 会自动显示错误信息
+      return
+    }
     sendingCode.value = true
     try {
       // TODO: 接入后端发送重置验证码接口

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff