12 Commits 9e47b2113e ... c62a5550cc

Tác giả SHA1 Thông báo Ngày
  PC-7\Administrator c62a5550cc 添加功能 10 tháng trước cách đây
  “陈博论” dc2eec2cfb 注册和忘记密码发送验证码逻辑优化 10 tháng trước cách đây
  “陈博论” a89a50e5ae 注册修改 10 tháng trước cách đây
  “陈博论” a511deb462 交易配置 10 tháng trước cách đây
  “陈博论” 26bda0f854 交易管理修改 10 tháng trước cách đây
  “陈博论” 9ec0a13055 交易模块编写 10 tháng trước cách đây
  “陈博论” 5c333706ae 应用模块编写完成 10 tháng trước cách đây
  “陈博论” 4446e4a96d 应用模块调整 10 tháng trước cách đây
  “陈博论” a53045d162 交易管理 10 tháng trước cách đây
  “陈博论” 351deb21d4 套餐模块修改 10 tháng trước cách đây
  “陈博论” aa2d5b9b22 应用管理修改 10 tháng trước cách đây
  “陈博论” 238adb805e 模块修改 10 tháng trước cách đây
55 tập tin đã thay đổi với 6887 bổ sung và 1708 xóa
  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>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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: 接入后端发送重置验证码接口

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác