6 Commity fb43dccf11 ... 097a549ddc

Autor SHA1 Wiadomość Data
  PC-7\Administrator 097a549ddc 用户和套餐管理 10 miesięcy temu
  “陈博论” ece2278b5e 用户列表编写 10 miesięcy temu
  “陈博论” 8aed8aa7e1 企业列表编写 10 miesięcy temu
  “陈博论” 29a256462d 用户模块编写 10 miesięcy temu
  “陈博论” a00297ebe9 套餐模块编写 10 miesięcy temu
  “陈博论” c8b9d6a666 套餐配置编写 10 miesięcy temu

+ 780 - 0
app/admin/controller/package/Package.php

@@ -0,0 +1,780 @@
+<?php
+/**
+ * 套餐管理控制器
+ */
+
+namespace app\admin\controller\package;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+use think\facade\Validate;
+
+class Package extends Backend
+{
+    /**
+     * 套餐列表
+     * @return void
+     */
+    public function index()
+    {
+        $where = [];
+
+        // 套餐名称搜索
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        // 套餐类型筛选
+        if (!empty($this->request->get('type'))) {
+            $type = $this->request->get('type');
+            $where['p.type'] = $type;
+        }
+
+        // 状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $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['p.sort'] = 'asc';
+            $order['p.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([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 转换功能特性字段
+        $list = $packageList->items();
+        foreach ($list as &$item) {
+            if (!empty($item['features'])) {
+                $item['features'] = json_decode($item['features'], true);
+            } else {
+                $item['features'] = [];
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $packageList->total()
+        ]);
+    }
+
+    /**
+     * 新增/编辑套餐
+     * @return void
+     */
+    public function edit()
+    {
+        $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);
+
+        if (empty($name)) {
+            $this->error(__('套餐名称不能为空'));
+        }
+
+        // 验证套餐名称是否已存在(编辑时排除自己)
+        $checkName = Db::name('package')
+            ->where('name', $name);
+
+        if (!empty($id)) {
+            $checkName->where('id', '<>', $id);
+        }
+
+        if ($checkName->find()) {
+            $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')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $packageInfo = Db::name('package')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$packageInfo) {
+                    $this->error(__('套餐不存在'));
+                }
+
+                $result = Db::name('package')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('package')
+                    ->insert($data);
+                $id = Db::name('package')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除套餐
+     * @return void
+     */
+    public function delete()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有规格存在
+        $hasSpec = Db::name('package_price')
+            ->where('package_id', $id)
+            ->count();
+
+        if ($hasSpec > 0) {
+            $this->error(__('该套餐下有关联的规格,请先删除规格'));
+        }
+
+        // 检查是否有用户购买记录(如果user_package表存在)
+        try {
+            $hasUserPackage = Db::name('user_package')
+                ->where('package_id', $id)
+                ->count();
+
+            if ($hasUserPackage > 0) {
+                $this->error(__('该套餐已有用户购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        // 检查是否有企业购买记录(如果company_package表存在)
+        try {
+            $hasCompanyPackage = Db::name('company_package')
+                ->where('package_id', $id)
+                ->count();
+
+            if ($hasCompanyPackage > 0) {
+                $this->error(__('该套餐已有企业购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        try {
+            $result = Db::name('package')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 套餐详情
+     * @return void
+     */
+    public function detail()
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 获取套餐基础信息
+        $packageInfo = Db::name('package')
+            ->where('id', $id)
+            ->find();
+
+        if (!$packageInfo) {
+            $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)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        // 统计库存和销售情况
+        $totalStock = 0;
+        $totalSold = 0;
+        foreach ($specList as $spec) {
+            $totalStock += $spec['stock'];
+            $totalSold += $spec['sold_count'];
+        }
+
+        // 统计用户购买数量(如果user_package表存在)
+        $userPurchaseCount = 0;
+        try {
+            $userPurchaseCount = Db::name('user_package')
+                ->where('package_id', $id)
+                ->count();
+        } catch (\Exception $e) {
+            // 表不存在,设为0
+        }
+
+        // 统计企业购买数量(如果company_package表存在)
+        $companyPurchaseCount = 0;
+        try {
+            $companyPurchaseCount = Db::name('company_package')
+                ->where('package_id', $id)
+                ->count();
+        } catch (\Exception $e) {
+            // 表不存在,设为0
+        }
+
+        $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
+            ]
+        ];
+
+        $this->success('查询成功', $result);
+    }
+
+    /**
+     * 添加/编辑套餐规格
+     * @return void
+     */
+    public function editSpec()
+    {
+        $id = $this->request->post('id');
+        $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', '');
+        $description = $this->request->post('description', '');
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($packageId) || empty($specName)) {
+            $this->error(__('参数不完整'));
+        }
+
+        if ($price <= 0) {
+            $this->error(__('价格必须大于0'));
+        }
+
+        // 检查套餐是否存在
+        $packageInfo = Db::name('package')
+            ->where('id', $packageId)
+            ->find();
+
+        if (!$packageInfo) {
+            $this->error(__('套餐不存在'));
+        }
+
+        // 验证规格名称是否已存在(同一套餐下不能重复)
+        $checkSpec = Db::name('package_price')
+            ->where('package_id', $packageId)
+            ->where('spec_name', $specName);
+
+        if (!empty($id)) {
+            $checkSpec->where('id', '<>', $id);
+        }
+
+        if ($checkSpec->find()) {
+            $this->error(__('同一套餐下规格名称不能重复'));
+        }
+
+        $data = [
+            'package_id' => $packageId,
+            'spec_name' => $specName,
+            'price' => $price,
+            'original_price' => $originalPrice,
+            'stock' => $stock,
+            'unit_value' => $unitValue,
+            'description' => $description,
+            'sort' => $sort,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $result = Db::name('package_price')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('package_price')
+                    ->insert($data);
+                $id = Db::name('package_price')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除套餐规格
+     * @return void
+     */
+    public function deleteSpec()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有用户购买记录
+        try {
+            $hasPurchase = Db::name('user_package')
+                ->where('package_price_id', $id)
+                ->count();
+
+            if ($hasPurchase > 0) {
+                $this->error(__('该规格已有用户购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        // 检查是否有企业购买记录
+        try {
+            $hasPurchase = Db::name('company_package')
+                ->where('package_price_id', $id)
+                ->count();
+
+            if ($hasPurchase > 0) {
+                $this->error(__('该规格已有企业购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        try {
+            $result = Db::name('package_price')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取套餐规格列表
+     * @return void
+     */
+    public function specList()
+    {
+        $packageId = $this->request->get('package_id');
+
+        if (empty($packageId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $specList = Db::name('package_price')
+            ->where('package_id', $packageId)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('查询成功', $specList);
+    }
+
+    /**
+     * 批量更新套餐状态
+     * @return void
+     */
+    public function batchStatus()
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('package')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result) {
+                $statusText = $status == 1 ? '启用' : '禁用';
+                $this->success("成功{$statusText}{$result}个套餐");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 批量更新规格库存
+     * @return void
+     */
+    public function batchUpdateStock()
+    {
+        $ids = $this->request->post('ids');
+        $stock = $this->request->post('stock');
+
+        if (empty($ids) || $stock === null) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('package_price')
+                ->whereIn('id', $ids)
+                ->update([
+                    'stock' => $stock,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result) {
+                $this->success("成功更新{$result}个规格的库存");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取套餐类型选项
+     * @return void
+     */
+    public function typeOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '个人套餐'],
+            ['value' => 2, 'label' => '团队套餐'],
+            ['value' => 3, 'label' => '企业套餐']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取套餐状态选项
+     * @return void
+     */
+    public function statusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '启用'],
+            ['value' => 2, 'label' => '禁用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取规格状态选项
+     * @return void
+     */
+    public function specStatusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '正常'],
+            ['value' => 2, 'label' => '售罄']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 套餐统计信息
+     * @return void
+     */
+    public function statistics()
+    {
+        // 套餐总数
+        $totalPackage = Db::name('package')->count();
+
+        // 启用套餐数
+        $activePackage = Db::name('package')
+            ->where('status', 1)
+            ->count();
+
+        // 规格总数
+        $totalSpec = Db::name('package_price')->count();
+
+        // 总库存
+        $totalStock = Db::name('package_price')
+            ->sum('stock');
+
+        // 总已售
+        $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
+        ];
+
+        $this->success('获取成功', $stats);
+    }
+
+    /**
+     * 导出套餐数据
+     * @return void
+     */
+    public function export()
+    {
+        $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
+     */
+    public function selectOptions()
+    {
+        $packageList = Db::name('package')
+            ->where('status', 1)
+            ->field('id as value, name as label, type')
+            ->order('sort', 'asc')
+            ->order('id', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('获取成功', $packageList);
+    }
+
+    /**
+     * 获取套餐规格选项(用于购买时选择)
+     * @return void
+     */
+    public function specOptions()
+    {
+        $packageId = $this->request->get('package_id');
+
+        if (empty($packageId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $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')
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('获取成功', $specList);
+    }
+
+    /**
+     * 更新规格销售数量(模拟用户购买)
+     * @return void
+     */
+    public function updateSoldCount()
+    {
+        $id = $this->request->post('id');
+        $quantity = $this->request->post('quantity', 1);
+
+        if (empty($id) || $quantity <= 0) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取当前规格信息
+            $specInfo = Db::name('package_price')
+                ->where('id', $id)
+                ->lock(true)
+                ->find();
+
+            if (!$specInfo) {
+                throw new \Exception('规格不存在');
+            }
+
+            if ($specInfo['stock'] < $quantity) {
+                throw new \Exception('库存不足');
+            }
+
+            // 更新库存和销售数量
+            $result = Db::name('package_price')
+                ->where('id', $id)
+                ->update([
+                    'stock' => Db::raw('stock - ' . $quantity),
+                    'sold_count' => Db::raw('sold_count + ' . $quantity),
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            // 如果库存为0,自动标记为售罄
+            if ($specInfo['stock'] - $quantity <= 0) {
+                Db::name('package_price')
+                    ->where('id', $id)
+                    ->update([
+                        'status' => 2, // 售罄
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+            Db::commit();
+
+            if ($result) {
+                $this->success('更新成功');
+            } else {
+                $this->error(__('更新失败'));
+            }
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+}

+ 592 - 0
app/admin/controller/product/Product.php

@@ -0,0 +1,592 @@
+<?php
+/**
+ * 产品管理控制器
+ */
+
+namespace app\admin\controller\product;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+use think\facade\Validate;
+
+class Product extends Backend
+{
+
+    protected array $noNeedLogin = ['*'];
+    /**
+     * 产品列表
+     * @return void
+     */
+    public function index():void
+    {
+        $where = [];
+
+        // 产品名称搜索
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        // 产品编码搜索
+        if (!empty($this->request->get('code'))) {
+            $code = $this->request->get('code');
+            $where[] = ['p.code', 'like', '%' . $code . '%'];
+        }
+
+        // 分类筛选
+        if (!empty($this->request->get('category_id'))) {
+            $categoryId = $this->request->get('category_id');
+            $where['p.category_id'] = $categoryId;
+        }
+
+        // 库存状态筛选
+        if (!empty($this->request->get('stock_status'))) {
+            $stockStatus = $this->request->get('stock_status');
+            $where['p.stock_status'] = $stockStatus;
+        }
+
+        // 产品状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $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['p.sort'] = 'asc';
+            $order['p.id'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据
+        $productList = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->field('p.*, pc.name as category_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'] = [];
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $productList->total()
+        ]);
+    }
+
+    /**
+     * 新增/编辑产品
+     * @return void
+     */
+    public function edit():void
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $code = $this->request->post('code');
+        $categoryId = $this->request->post('category_id');
+        $description = $this->request->post('description', '');
+        $detail = $this->request->post('detail', '');
+        $keyAttributes = $this->request->post('key_attributes', []);
+        $image = $this->request->post('image');
+        $stockStatus = $this->request->post('stock_status', 1);
+        $status = $this->request->post('status', 1);
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($name)) {
+            $this->error(__('产品名称不能为空'));
+        }
+
+        if (empty($code)) {
+            $this->error(__('产品编码不能为空'));
+        }
+
+        // 验证产品编码是否已存在(编辑时排除自己)
+        $checkCode = Db::name('product')
+            ->where('code', $code);
+
+        if (!empty($id)) {
+            $checkCode->where('id', '<>', $id);
+        }
+
+        if ($checkCode->find()) {
+            $this->error(__('产品编码已存在'));
+        }
+
+        // 处理图片和属性字段
+        if (empty($image)) {
+            $image = "";
+        }
+
+        if (!is_array($keyAttributes)) {
+            $keyAttributes = [];
+        }
+
+        $data = [
+            'name' => $name,
+            'code' => $code,
+            'category_id' => $categoryId ?: null,
+            'description' => $description,
+            'detail' => $detail,
+            'key_attributes' => json_encode($keyAttributes, JSON_UNESCAPED_UNICODE),
+            'image' => $image,
+            'stock_status' => $stockStatus,
+            'status' => $status,
+            'sort' => $sort,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $productInfo = Db::name('product')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$productInfo) {
+                    $this->error(__('产品不存在'));
+                }
+
+                $result = Db::name('product')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product')
+                    ->insert($data);
+                $id = Db::name('product')->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(__('参数错误'));
+        }
+
+        // 检查是否有套餐关联
+        $hasPackage = Db::name('package')
+            ->where('product_id', $id)
+            ->count();
+
+        if ($hasPackage > 0) {
+            $this->error(__('该产品下有关联的套餐,请先删除套餐'));
+        }
+
+        // 检查是否有订单关联
+        $hasOrder = Db::name('order_item')
+            ->where('product_id', $id)
+            ->count();
+
+        if ($hasOrder > 0) {
+            $this->error(__('该产品已有订单记录,不能删除'));
+        }
+
+        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 {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 产品详情
+     * @return void
+     */
+    public function detail():void
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 获取产品基础信息
+        $productInfo = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->where('p.id', $id)
+            ->field('p.*, pc.name as category_name')
+            ->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)
+            ->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)
+            ->count();
+
+        $result = [
+            'product_info' => $productInfo,
+            'spec_list' => $specList,
+            'package_count' => $packageCount,
+            'order_count' => $orderCount
+        ];
+
+        $this->success('查询成功', $result);
+    }
+
+    /**
+     * 批量更新产品状态
+     * @return void
+     */
+    public function batchStatus():void
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status,
+                    '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}个产品");
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 批量更新库存状态
+     * @return void
+     */
+    public function batchStockStatus():void
+    {
+        $ids = $this->request->post('ids');
+        $stockStatus = $this->request->post('stock_status');
+
+        if (empty($ids) || empty($stockStatus)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product')
+                ->whereIn('id', $ids)
+                ->update([
+                    'stock_status' => $stockStatus,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+
+        if ($result) {
+            $statusText = [
+                1 => '充足',
+                2 => '缺货',
+                3 => '下架'
+            ][$stockStatus] ?? '未知';
+            $this->success("成功更新{$result}个产品的库存状态为{$statusText}");
+        } else {
+            $this->error(__('操作失败'));
+        }
+
+    }
+
+    /**
+     * 添加/编辑产品规格
+     * @return void
+     */
+    public function editSpec():void
+    {
+        $id = $this->request->post('id');
+        $productId = $this->request->post('product_id');
+        $name = $this->request->post('name');
+        $value = $this->request->post('value', []);
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($productId) || empty($name)) {
+            $this->error(__('参数不完整'));
+        }
+
+        // 检查产品是否存在
+        $productInfo = Db::name('product')
+            ->where('id', $productId)
+            ->find();
+
+        if (!$productInfo) {
+            $this->error(__('产品不存在'));
+        }
+
+        // 处理规格值
+        if (!is_array($value)) {
+            $value = [];
+        }
+
+        $data = [
+            'product_id' => $productId,
+            'name' => $name,
+            'value' => json_encode($value, JSON_UNESCAPED_UNICODE),
+            'sort' => $sort
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $result = Db::name('product_spec')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product_spec')
+                    ->insert($data);
+                $id = Db::name('product_spec')->getLastInsID();
+                $message = '新增成功';
+            }
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success($message, ['id' => $id]);
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 删除产品规格
+     * @return void
+     */
+    public function deleteSpec():void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            $result = Db::name('product_spec')
+                ->where('id', $id)
+                ->delete();
+
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+
+        if ($result) {
+            $this->success('删除成功');
+        } else {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 获取产品规格列表
+     * @return void
+     */
+    public function specList():void
+    {
+        $productId = $this->request->get('product_id');
+
+        if (empty($productId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $specList = Db::name('product_spec')
+            ->where('product_id', $productId)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        foreach ($specList as &$spec) {
+            if (!empty($spec['value'])) {
+                $spec['value'] = json_decode($spec['value'], true);
+            } else {
+                $spec['value'] = [];
+            }
+        }
+
+        $this->success('查询成功', $specList);
+    }
+
+    /**
+     * 获取产品状态选项
+     * @return void
+     */
+    public function statusOptions():void
+    {
+        $options = [
+            ['value' => 1, 'label' => '上架'],
+            ['value' => 2, 'label' => '下架']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取库存状态选项
+     * @return void
+     */
+    public function stockStatusOptions():void
+    {
+        $options = [
+            ['value' => 1, 'label' => '充足'],
+            ['value' => 2, 'label' => '缺货'],
+            ['value' => 3, 'label' => '下架']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 导出产品列表
+     * @return void
+     */
+    public function export():void
+    {
+        $where = [];
+
+        // 根据筛选条件构建查询条件
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $status;
+        }
+
+        // 查询数据
+        $productList = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->field('p.id, p.name, p.code, pc.name as category_name, 
+                     p.stock_status, p.status, p.sort, p.create_time')
+            ->where($where)
+            ->order('p.sort', 'asc')
+            ->order('p.id', 'desc')
+            ->select()
+            ->toArray();
+
+        // 状态映射
+        $stockStatusMap = [1 => '充足', 2 => '缺货', 3 => '下架'];
+        $statusMap = [1 => '上架', 2 => '下架'];
+
+        foreach ($productList as &$item) {
+            $item['stock_status'] = $stockStatusMap[$item['stock_status']] ?? '未知';
+            $item['status'] = $statusMap[$item['status']] ?? '未知';
+        }
+
+        $this->success('查询成功', $productList);
+    }
+}

+ 435 - 0
app/admin/controller/product/ProductCategory.php

@@ -0,0 +1,435 @@
+<?php
+/**
+ * 产品分类管理控制器
+ */
+
+namespace app\admin\controller\product;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class ProductCategory extends Backend
+{
+    /**
+     * 分类列表(树形结构)
+     * @return void
+     */
+    public function index()
+    {
+        $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;
+        }
+
+        // 排序
+        $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['sort'] = 'asc';
+            $order['id'] = 'asc';
+        }
+
+        // 查询所有分类
+        $categoryList = Db::name('product_category')
+            ->where($where)
+            ->order($order)
+            ->select()
+            ->toArray();
+
+        // 构建树形结构
+        $tree = $this->buildTree($categoryList);
+
+        $this->success('查询成功', $tree);
+    }
+
+    /**
+     * 构建树形结构
+     * @param array $items
+     * @param int $parentId
+     * @return array
+     */
+    private function buildTree(array $items, $parentId = 0)
+    {
+        $tree = [];
+        foreach ($items as $item) {
+            if ($item['parent_id'] == $parentId) {
+                $children = $this->buildTree($items, $item['id']);
+                if ($children) {
+                    $item['children'] = $children;
+                }
+                $tree[] = $item;
+            }
+        }
+        return $tree;
+    }
+
+    /**
+     * 新增/编辑分类
+     * @return void
+     */
+    public function edit()
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $parentId = $this->request->post('parent_id', 0);
+        $icon = $this->request->post('icon', '');
+        $sort = $this->request->post('sort', 0);
+        $status = $this->request->post('status', 1);
+
+        if (empty($name)) {
+            $this->error(__('分类名称不能为空'));
+        }
+
+        // 验证分类名称是否已存在(同一父级下不能重复)
+        $checkName = Db::name('product_category')
+            ->where('name', $name)
+            ->where('parent_id', $parentId);
+
+        if (!empty($id)) {
+            $checkName->where('id', '<>', $id);
+        }
+
+        if ($checkName->find()) {
+            $this->error(__('同级分类下已存在相同名称'));
+        }
+
+        // 检查父级分类是否存在
+        if ($parentId > 0) {
+            $parentCategory = Db::name('product_category')
+                ->where('id', $parentId)
+                ->find();
+
+            if (!$parentCategory) {
+                $this->error(__('父级分类不存在'));
+            }
+
+            // 防止循环引用(不能将分类设置为自己的子分类)
+            if (!empty($id) && $parentId == $id) {
+                $this->error(__('不能将分类设置为自己的父级'));
+            }
+        }
+
+        $data = [
+            'name' => $name,
+            'parent_id' => $parentId,
+            'icon' => $icon,
+            'sort' => $sort,
+            'status' => $status
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $categoryInfo = Db::name('product_category')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$categoryInfo) {
+                    $this->error(__('分类不存在'));
+                }
+
+                $result = Db::name('product_category')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product_category')
+                    ->insert($data);
+                $id = Db::name('product_category')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除分类
+     * @return void
+     */
+    public function delete()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有子分类
+        $hasChildren = Db::name('product_category')
+            ->where('parent_id', $id)
+            ->count();
+
+        if ($hasChildren > 0) {
+            $this->error(__('该分类下有子分类,请先删除子分类'));
+        }
+
+        // 检查是否有产品关联
+        $hasProduct = Db::name('product')
+            ->where('category_id', $id)
+            ->count();
+
+        if ($hasProduct > 0) {
+            $this->error(__('该分类下有关联的产品,请先删除或转移产品'));
+        }
+
+        try {
+            $result = Db::name('product_category')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 分类详情
+     * @return void
+     */
+    public function detail()
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $categoryInfo = Db::name('product_category')
+            ->where('id', $id)
+            ->find();
+
+        if (!$categoryInfo) {
+            $this->error(__('分类不存在'));
+        }
+
+        // 获取父级分类信息
+        if ($categoryInfo['parent_id'] > 0) {
+            $parentInfo = Db::name('product_category')
+                ->where('id', $categoryInfo['parent_id'])
+                ->field('id, name')
+                ->find();
+            $categoryInfo['parent_info'] = $parentInfo;
+        } else {
+            $categoryInfo['parent_info'] = null;
+        }
+
+        // 统计子分类数量
+        $childrenCount = Db::name('product_category')
+            ->where('parent_id', $id)
+            ->count();
+        $categoryInfo['children_count'] = $childrenCount;
+
+        // 统计产品数量
+        $productCount = Db::name('product')
+            ->where('category_id', $id)
+            ->count();
+        $categoryInfo['product_count'] = $productCount;
+
+        $this->success('查询成功', $categoryInfo);
+    }
+
+    /**
+     * 批量更新分类状态
+     * @return void
+     */
+    public function batchStatus()
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product_category')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status
+                ]);
+
+            if ($result) {
+                $statusText = $status == 1 ? '启用' : '禁用';
+                $this->success("成功{$statusText}{$result}个分类");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取分类下拉选项(用于选择父级分类)
+     * @return void
+     */
+    public function selectOptions()
+    {
+        // 获取所有启用的分类
+        $categoryList = Db::name('product_category')
+            ->where('status', 1)
+            ->order('sort', 'asc')
+            ->order('id', 'asc')
+            ->select()
+            ->toArray();
+
+        // 构建树形结构用于前端展示
+        $tree = $this->buildTreeForSelect($categoryList);
+
+        $this->success('查询成功', $tree);
+    }
+
+    /**
+     * 构建用于下拉选择的树形结构
+     * @param array $items
+     * @param int $parentId
+     * @param int $level
+     * @return array
+     */
+    private function buildTreeForSelect(array $items, $parentId = 0, $level = 0)
+    {
+        $tree = [];
+        $prefix = str_repeat(' ', $level * 2);
+
+        foreach ($items as $item) {
+            if ($item['parent_id'] == $parentId) {
+                $item['label'] = ($level > 0 ? $prefix . '├─ ' : '') . $item['name'];
+                $item['value'] = $item['id'];
+
+                $children = $this->buildTreeForSelect($items, $item['id'], $level + 1);
+                if ($children) {
+                    $item['children'] = $children;
+                }
+                $tree[] = $item;
+            }
+        }
+        return $tree;
+    }
+
+    /**
+     * 获取分类状态选项
+     * @return void
+     */
+    public function statusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '启用'],
+            ['value' => 2, 'label' => '禁用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 移动分类排序
+     * @return void
+     */
+    public function moveSort()
+    {
+        $id = $this->request->post('id');
+        $direction = $this->request->post('direction'); // up:上移, down:下移
+
+        if (empty($id) || empty($direction)) {
+            $this->error(__('参数错误'));
+        }
+
+        $categoryInfo = Db::name('product_category')
+            ->where('id', $id)
+            ->find();
+
+        if (!$categoryInfo) {
+            $this->error(__('分类不存在'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取同级分类
+            $siblings = Db::name('product_category')
+                ->where('parent_id', $categoryInfo['parent_id'])
+                ->order('sort', 'asc')
+                ->order('id', 'asc')
+                ->select()
+                ->toArray();
+
+            // 找到当前分类在列表中的位置
+            $currentIndex = -1;
+            foreach ($siblings as $index => $sibling) {
+                if ($sibling['id'] == $id) {
+                    $currentIndex = $index;
+                    break;
+                }
+            }
+
+            if ($currentIndex === -1) {
+                throw new \Exception('分类在同级中不存在');
+            }
+
+            if ($direction == 'up' && $currentIndex > 0) {
+                // 上移:与上一个交换排序值
+                $prevSibling = $siblings[$currentIndex - 1];
+
+                Db::name('product_category')
+                    ->where('id', $id)
+                    ->update(['sort' => $prevSibling['sort']]);
+
+                Db::name('product_category')
+                    ->where('id', $prevSibling['id'])
+                    ->update(['sort' => $categoryInfo['sort']]);
+
+            } elseif ($direction == 'down' && $currentIndex < count($siblings) - 1) {
+                // 下移:与下一个交换排序值
+                $nextSibling = $siblings[$currentIndex + 1];
+
+                Db::name('product_category')
+                    ->where('id', $id)
+                    ->update(['sort' => $nextSibling['sort']]);
+
+                Db::name('product_category')
+                    ->where('id', $nextSibling['id'])
+                    ->update(['sort' => $categoryInfo['sort']]);
+            } else {
+                // 已经在最前或最后,无需移动
+                Db::rollback();
+                $this->success('无需移动');
+            }
+
+            Db::commit();
+            $this->success('移动成功');
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('移动失败: ' . $e->getMessage());
+        }
+    }
+}

+ 1 - 1
app/admin/controller/user_center/User.php → app/admin/controller/userCenter/User.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace app\admin\controller\user_center;
+namespace app\admin\controller\userCenter;
 use app\common\controller\Backend;
 use think\facade\Db;
 

+ 29 - 0
web/src/api/backend/company/index.ts

@@ -0,0 +1,29 @@
+import createAxios from '/@/utils/axios'
+
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/company.index/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data:anyObj) {
+    return createAxios({
+        url: '/admin/company.index/delete',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 禁用/启用企业 传id
+export function toggleStatus(data:anyObj) {
+    return createAxios({
+        url: '/admin/company.index/toggleStatus',
+        method: 'post',
+        data: data,
+    })
+}

+ 40 - 0
web/src/api/backend/userCenter/user.ts

@@ -0,0 +1,40 @@
+import createAxios from '/@/utils/axios'
+
+
+// 添加、编辑
+export function edit(data: anyObj) {
+    return createAxios({
+        url: '/admin/userCenter.user/edit',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 删除
+export function del(data:anyObj) {
+    return createAxios({
+        url: '/admin/userCenter.user/delete',
+        method: 'post',
+        data: data,
+    })
+}
+
+// 解除上下级关系
+export function removeSubordinate(data:anyObj) {
+    return createAxios({
+        url: '/userCenter.user/removeSubordinate',
+        method: 'get',
+        params: data,
+    })
+}
+
+// 详情
+export function detail(id: number) {
+    return createAxios({
+        url: '/admin/userCenter.user/detail',
+        method: 'get',
+        params: {
+            id: id,
+        },
+    })
+}

+ 1 - 0
web/src/assets/icons/combo.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="1766050217682" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7560" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M869.4 263.7H698L597.9 158.4c-9.6-10.1-25.7-10.5-35.8-0.9-10.1 9.6-10.5 25.6-0.9 35.8l66.9 70.4H401l66.9-70.4c9.6-10.1 9.2-26.1-0.9-35.8-10.1-9.6-26.2-9.2-35.8 0.9l-100 105.3H159.8c-14 0-25.3 11.3-25.3 25.3v181.6c0 14 11.3 25.3 25.3 25.3h40V827c0 14 11.3 25.3 25.3 25.3H804c14 0 25.3-11.3 25.3-25.3V495.9h40c14 0 25.3-11.3 25.3-25.3V289c0.1-14-11.3-25.3-25.2-25.3z m-90.7 538H250.5V495.9h528.2v305.8z m65.4-356.4h-659v-131H844v131z" p-id="7561"></path><path d="M347.5 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3zM514.6 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3zM681.7 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3z" p-id="7562"></path></svg>

Plik diff jest za duży
+ 0 - 0
web/src/assets/icons/product.svg


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

@@ -0,0 +1,330 @@
+<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>
+
+

+ 154 - 9
web/src/views/backend/application/combo/index.vue

@@ -49,17 +49,26 @@
                 </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 } from 'vue'
+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'
 // 引入国际化
@@ -75,6 +84,9 @@ defineOptions({
 // 国际化方法
 const { t } = useI18n()
 
+const popupFormRef = ref<InstanceType<typeof PopupForm>>()
+const detailRef = ref<InstanceType<typeof Detail>>()
+
 // 筛选表单数据,包含套餐名称与库存状态
 const searchForm = reactive({
     name: '', // 套餐名称关键词
@@ -88,7 +100,8 @@ const inventoryOptions = [
     { label: '售罄', value: '3' },
 ]
 
-// 静态模拟数据,来源于原型图
+// 静态模拟数据,来源于原型图 + 规格示例数据
+// 规格字段与 popupForm 中的 SpecificationItem 一致,方便后续联动编辑
 const baseData = [
     {
         id: 1,
@@ -97,6 +110,26 @@ const baseData = [
         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,
@@ -105,6 +138,26 @@ const baseData = [
         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,
@@ -113,6 +166,17 @@ const baseData = [
         remaining_stock: 0,
         sold_quantity: 120,
         create_time: '2025-10-19',
+        specificationsList: [
+            {
+                specName: '体验版',
+                price: 49,
+                stock: 0,
+                packageType: '1',
+                chargeType: '1',
+                quantity: 3,
+                remark: '促销活动已售罄',
+            },
+        ],
     },
 ]
 
@@ -133,7 +197,12 @@ const baTable = new baTableClass(
                 effect: 'plain',
                 minWidth: 120,
             },
-            { label: '剩余库存', prop: 'remaining_stock', align: 'center', minWidth: 100 },
+            {
+                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 },
@@ -174,19 +243,94 @@ const del = (row:TableRow) => {
     ElMessage.success(`已删除套餐:${row.name}`)
 }
 
-// 编辑动作,展示提示
+// 编辑动作:打开弹框并带入当前行
 const onEdit = (row:TableRow ) => {
-    ElMessage.info(`编辑套餐:${row.name}`)
+    popupFormRef.value?.editDialog(row as any)
 }
 
 // 新增动作,展示提示
 const onAdd = () => {
-    ElMessage.info('点击添加新套餐')
+    popupFormRef.value?.openDialog()
 }
 
-// 套餐详情动作,展示提示
-const viewDetail = (row: Record<string, any>) => {
-    ElMessage.success(`查看套餐详情:${row.name}`)
+// 处理弹框提交成功(新增 / 编辑)
+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()
 }
 
 // 输入或选择变化时触发过滤
@@ -210,3 +354,4 @@ onMounted(() => {
     margin: 0 6px;
 }
 </style>
+

+ 327 - 19
web/src/views/backend/application/combo/popupForm.vue

@@ -1,10 +1,10 @@
 <template>
 	<!-- 对话框表单 -->
 	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
-		@close="handleClose" width="600px">
+		@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">
@@ -13,18 +13,102 @@
 					<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-icon>
-						<Check />
-					</el-icon>
-					确定
-				</el-button>
+				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading"
+					class="submit-btn">确定</el-button>
 			</div>
 		</template>
 	</el-dialog>
@@ -33,7 +117,6 @@
 <script setup lang="ts">
 import { ref, reactive, computed } from 'vue'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
-import { Check } from '@element-plus/icons-vue';
 import { useI18n } from 'vue-i18n'
 
 const { t } = useI18n()
@@ -41,7 +124,9 @@ const { t } = useI18n()
 // 显示隐藏
 const dialogVisible = ref(false)
 // 类型:add、edit
-const dialogType = ref('add')
+const dialogType = ref<'add' | 'edit'>('add')
+// 当前编辑的套餐 ID(新增时为 null)
+const currentEditId = ref<number | null>(null)
 
 const loading = ref(false)
 
@@ -52,7 +137,7 @@ const emits = defineEmits(['success'])
 
 const formData = reactive({
 	name: '',
-	specificationsList:[],
+	specificationsList: [] as SpecificationItem[],
 })
 
 
@@ -63,27 +148,68 @@ const rules: FormRules = {
 		{ required: true, message: '请输入套餐名称', trigger: 'change' }
 	],
 	specificationsList: [
-		{ required: true, message: '请选择规格', trigger: 'blur' }
+		{
+			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) => {
+// 编辑:接收列表行数据,回填到表单
+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()
 }
 
 // 提交表单
@@ -94,14 +220,15 @@ const onSubmit = async () => {
 		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')
+		emits('success', submitData)
 		handleClose()
 	} catch (error) {
 		ElMessage({
@@ -119,10 +246,191 @@ const handleClose = () => {
 	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 });
+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>

+ 214 - 0
web/src/views/backend/company/index.vue

@@ -0,0 +1,214 @@
+<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', 'columnDisplay']">
+            <el-input class="form-input" v-model="baTable.table.filter!.name" @input="onSearchInput"
+                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-select>
+            <template #quickSearchPrepend>
+                <el-button type="primary" @click="onAdd">添加</el-button>
+            </template>
+        </TableHeader>
+
+        <!-- 表格 -->
+        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+        <Table>
+            <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" />
+                    </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>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+
+        <!-- 表单 -->
+        <PopupForm />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide, } from 'vue'
+import { cloneDeep } from 'lodash-es'
+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'
+
+defineOptions({
+    name: 'company/index',
+})
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+    new baTableApi('/admin/company.index/'),
+    {
+        column: [
+            { label: '编号', prop: 'id', align: 'center', width: 70 },
+            {
+                label: '企业名称',
+                prop: 'name',
+                align: 'center',
+                minWidth: 120
+            },
+            {
+                label: '创建人',
+                prop: 'creator',
+                align: 'center',
+                minWidth: 150
+            },
+            {
+                label: '状态',
+                prop: 'status',
+                align: 'center',
+                render: 'slot',
+                slotName: 'status',
+                minWidth: 150
+            },
+            {
+                label: '创建时间',
+                prop: 'create_time',
+                align: 'center',
+                minWidth: 160
+            },
+            {
+                label: '操作',
+                align: 'center',
+                render: 'slot',
+                slotName: 'operate',
+                operator: false,
+            },
+        ],
+        dblClickNotEditColumn: [undefined,],
+    },
+    {
+        defaultItems: {
+            status: '1',
+        },
+    }
+)
+
+// 设置假数据
+baTable.getData = () => {
+    baTable.table.loading = true
+    // 模拟异步请求
+    return new Promise<void>((resolve) => {
+        setTimeout(() => {
+            // 根据筛选条件过滤数据
+            let filteredData = [
+                {
+                    id: 1,
+                    name: '聚德科技',
+                    creator: '张三',
+                    status: '1',
+                    create_time: '2025-01-10 10:00:00',
+                },
+                {
+                    id: 2,
+                    name: '棱镜科技',
+                    creator: '李四',
+                    status: '1',
+                    create_time: '2025-01-11 09:00:00',
+                },
+                {
+                    id: 3,
+                    name: '测试公司',
+                    creator: '王五',
+                    status: '0',
+                    create_time: '2025-01-12 11:00:00',
+                },
+                {
+                    id: 4,
+                    name: '示例企业',
+                    creator: '赵六',
+                    status: '1',
+                    create_time: '2025-01-13 14:00:00',
+                },
+                {
+                    id: 5,
+                    name: '腾达科技',
+                    creator: '刘七',
+                    status: '0',
+                    create_time: '2025-01-14 08:30:00',
+                },
+
+            ]
+
+            // 根据筛选条件过滤
+            const nameFilter = baTable.table.filter?.name
+            const statusFilter = baTable.table.filter?.status
+
+            if (nameFilter) {
+                filteredData = filteredData.filter(item => 
+                    item.name.includes(nameFilter.trim())
+                )
+            }
+
+            if (statusFilter) {
+                filteredData = filteredData.filter(item => 
+                    item.status === statusFilter
+                )
+            }
+
+            baTable.table.data = filteredData
+            baTable.table.total = filteredData.length
+            baTable.table.loading = false
+            resolve()
+        }, 300)
+    })
+}
+
+provide('baTable', baTable)
+
+// 搜索
+const onSearchInput = () => {
+    baTable.table.filter!.page = 1
+    baTable.getData()
+}
+
+// 删除
+const del = (row: TableRow) => {
+    baTable.onTableAction('delete', { row })
+}
+
+const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
+    if (type === 'Edit' && !row) return
+    baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
+    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)
+
+
+onMounted(() => {
+    baTable.mount()
+    baTable.getData()
+})
+</script>
+
+<style scoped lang="scss">
+.form-input {
+    width: 180px;
+    margin:0 5px;
+}
+</style>

+ 122 - 0
web/src/views/backend/company/popupForm.vue

@@ -0,0 +1,122 @@
+<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"
+        width="600px"
+        style="height: 300px;"
+    >
+        <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"
+                >
+                    <el-form-item label="企业名称" prop="name">
+                        <el-input
+                            v-model="baTable.form.items!.name"
+                            placeholder="请输入企业名称"
+                        />
+                    </el-form-item>
+                    
+                    <el-form-item label="所属人" prop="creator">
+                        <el-select
+                            v-model="baTable.form.items!.creator"
+                            placeholder="请选择所属人"
+                            style="width: 100%"
+                        >
+                            <el-option
+                                v-for="item in userOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            />
+                        </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>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">取消</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
+                    确认
+                </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 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'
+
+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: '姓名' })],
+    
+})
+
+const userOptions = [
+    { label: '张三', value: '张三' },
+    { label: '李四', value: '李四' },
+    { label: '王五', value: '王五' },
+    { label: '赵六', value: '赵六' },
+    { label: '刘七', value: '刘七' },
+]
+
+const statusOptions = {
+    '0': '禁用',
+    '1': '正常',
+}
+
+// 提交
+const btnSubmit = () => {
+	formRef.value?.validate((valid: boolean) => {
+		if (valid) {
+            baTable.table.loading = true
+			
+		}
+	})
+}
+
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 505 - 0
web/src/views/backend/userCenter/user/detailDialog.vue

@@ -0,0 +1,505 @@
+<template>
+    <el-dialog
+        v-model="visible"
+        class="ba-operate-dialog user-detail-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'">
+                详情
+            </div>
+        </template>
+
+        <el-scrollbar class="ba-table-form-scrollbar">
+            <div class="detail-wrapper">
+                <!-- 用户信息部分 -->
+                <div class="detail-section">
+                    <div class="section-title">用户信息</div>
+                    <el-descriptions :column="2" border size="small" class="user-info-descriptions">
+                        <el-descriptions-item label="用户名称">
+                            {{ userInfo.name || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="邮箱">
+                            {{ userInfo.email || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="企业">
+                            {{ userInfo.company || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="状态">
+                            <el-tag :type="userInfo.status === '正常' ? 'success' : 'danger'" size="small">
+                                {{ userInfo.status || '-' }}
+                            </el-tag>
+                        </el-descriptions-item>
+                        <el-descriptions-item label="获得总佣金金额">
+                            {{ userInfo.totalCommission || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="当前佣金金额">
+                            {{ userInfo.currentCommission || '-' }}
+                        </el-descriptions-item>
+                        <el-descriptions-item label="推广链接" :span="2">
+                            {{ userInfo.promotionLink || '-' }}
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </div>
+
+                <!-- 购买产品部分 -->
+                <div class="detail-section">
+                    <div class="section-title">购买产品</div>
+                    <el-table
+                        :data="purchasedProducts"
+                        border
+                        size="small"
+                        style="width: 100%"
+                        v-loading="productsLoading"
+                    >
+                        <el-table-column
+                            prop="productName"
+                            label="产品名称"
+                            align="center"
+                            min-width="120"
+                        />
+                        <el-table-column
+                            prop="packageType"
+                            label="套餐类型"
+                            align="center"
+                            min-width="140"
+                        />
+                        <el-table-column
+                            prop="remainingPoints"
+                            label="剩余积分"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                {{ row.remainingPoints || '--' }}
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="remainingTimes"
+                            label="剩余次数"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                {{ row.remainingTimes || '--' }}
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="expirationDate"
+                            label="到期时间"
+                            align="center"
+                            min-width="120"
+                        >
+                            <template #default="{ row }">
+                                {{ row.expirationDate || '--' }}
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="purchaseDate"
+                            label="购买时间"
+                            align="center"
+                            min-width="120"
+                        />
+                        <el-table-column
+                            prop="status"
+                            label="状态"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                <el-tag
+                                    :type="row.status === '正常' ? 'success' : 'danger'"
+                                    size="small"
+                                >
+                                    {{ row.status }}
+                                </el-tag>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                    <!-- 购买产品分页 -->
+                    <div class="pagination-wrapper">
+                        <el-pagination
+                            v-model:current-page="productsPage"
+                            :page-size="productsPageSize"
+                            :total="productsTotal"
+                            layout="prev, pager, next, ->, jumper"
+                            small
+                            @current-change="handleProductsPageChange"
+                        >
+                            <template #jumper>
+                                <span class="pagination-jumper">
+                                    前往
+                                    <el-input-number
+                                        v-model="productsPage"
+                                        :min="1"
+                                        :max="Math.ceil(productsTotal / productsPageSize)"
+                                        size="small"
+                                        controls-position="right"
+                                        style="width: 60px; margin: 0 4px"
+                                        @change="(val: number | undefined) => val && handleProductsPageChange(val)"
+                                    />
+                                    页
+                                </span>
+                            </template>
+                        </el-pagination>
+                    </div>
+                </div>
+
+                <!-- 下级人员部分 -->
+                <div class="detail-section">
+                    <div class="section-title">下级人员</div>
+                    <div class="search-wrapper">
+                        <el-input
+                            v-model="subordinateSearchName"
+                            placeholder="人员名称"
+                            clearable
+                            size="small"
+                            style="width: 200px"
+                            @input="handleSubordinateSearch"
+                        />
+                    </div>
+                    <el-table
+                        :data="subordinatePersonnel"
+                        border
+                        size="small"
+                        style="width: 100%; margin-top: 12px"
+                        v-loading="subordinatesLoading"
+                    >
+                        <el-table-column
+                            prop="personnelName"
+                            label="人员名称"
+                            align="center"
+                            min-width="120"
+                        />
+                        <el-table-column
+                            prop="commissionAmount"
+                            label="产生分佣金额"
+                            align="center"
+                            min-width="140"
+                        />
+                        <el-table-column
+                            prop="status"
+                            label="状态"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                <el-tag
+                                    :type="row.status === '正常' ? 'success' : 'danger'"
+                                    size="small"
+                                >
+                                    {{ row.status }}
+                                </el-tag>
+                            </template>
+                        </el-table-column>
+                        <el-table-column
+                            prop="bindingTime"
+                            label="绑定时间"
+                            align="center"
+                            min-width="120"
+                        />
+                        <el-table-column
+                            label="操作"
+                            align="center"
+                            min-width="100"
+                        >
+                            <template #default="{ row }">
+                                <el-button
+                                    type="danger"
+                                    link
+                                    size="small"
+                                    @click="handleUnbind(row)"
+                                >
+                                    解除
+                                </el-button>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                    <!-- 下级人员分页 -->
+                    <div class="pagination-wrapper">
+                        <el-pagination
+                            v-model:current-page="subordinatesPage"
+                            :page-size="subordinatesPageSize"
+                            :total="subordinatesTotal"
+                            layout="prev, pager, next, ->, jumper"
+                            small
+                            @current-change="handleSubordinatesPageChange"
+                        >
+                            <template #jumper>
+                                <span class="pagination-jumper">
+                                    前往
+                                    <el-input-number
+                                        v-model="subordinatesPage"
+                                        :min="1"
+                                        :max="Math.ceil(subordinatesTotal / subordinatesPageSize)"
+                                        size="small"
+                                        controls-position="right"
+                                        style="width: 60px; margin: 0 4px"
+                                        @change="(val: number | undefined) => val && handleSubordinatesPageChange(val)"
+                                    />
+                                    页
+                                </span>
+                            </template>
+                        </el-pagination>
+                    </div>
+                </div>
+            </div>
+        </el-scrollbar>
+
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button size="large" @click="handleClose">
+                    关闭
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+interface UserInfo {
+    name: string
+    email: string
+    company: string
+    status: string
+    totalCommission: number | string
+    currentCommission: number | string
+    promotionLink: string
+}
+
+interface PurchasedProduct {
+    productName: string
+    packageType: string
+    remainingPoints?: string
+    remainingTimes?: string | number
+    expirationDate?: string
+    purchaseDate: string
+    status: string
+}
+
+interface SubordinatePersonnel {
+    personnelName: string
+    commissionAmount: number
+    status: string
+    bindingTime: string
+}
+
+const visible = ref(false)
+const userInfo = reactive<UserInfo>({
+    name: '',
+    email: '',
+    company: '',
+    status: '',
+    totalCommission: '',
+    currentCommission: '',
+    promotionLink: '',
+})
+
+// 购买产品相关
+const purchasedProducts = ref<PurchasedProduct[]>([])
+const productsLoading = ref(false)
+const productsPage = ref(1)
+const productsPageSize = ref(10)
+const productsTotal = ref(0)
+
+// 下级人员相关
+const subordinatePersonnel = ref<SubordinatePersonnel[]>([])
+const subordinatesLoading = ref(false)
+const subordinateSearchName = ref('')
+const subordinatesPage = ref(1)
+const subordinatesPageSize = ref(10)
+const subordinatesTotal = ref(0)
+
+// 打开弹框
+const open = (row: any) => {
+    // 设置用户信息
+    userInfo.name = row.name || '张三'
+    userInfo.email = row.email || '9854112544@qq.com'
+    userInfo.company = row.company || '腾达'
+    userInfo.status = '正常'
+    userInfo.totalCommission = 500
+    userInfo.currentCommission = 100
+    userInfo.promotionLink = 'XXXX.com?from=用户ID'
+
+    // 加载购买产品数据
+    loadPurchasedProducts()
+
+    // 加载下级人员数据
+    loadSubordinatePersonnel()
+
+    visible.value = true
+}
+
+// 加载购买产品数据
+const loadPurchasedProducts = () => {
+    productsLoading.value = true
+    // 模拟异步请求
+    setTimeout(() => {
+        const mockData: PurchasedProduct[] = [
+            {
+                productName: '聚德神笔',
+                packageType: '日卡',
+                remainingPoints: '--',
+                remainingTimes: '--',
+                expirationDate: '2025-10-11',
+                purchaseDate: '2025-10-11',
+                status: '已过期',
+            },
+            {
+                productName: '棱镜',
+                packageType: '棱镜10次卡',
+                remainingPoints: '--',
+                remainingTimes: 10,
+                expirationDate: '--',
+                purchaseDate: '2025-10-11',
+                status: '正常',
+            },
+        ]
+        purchasedProducts.value = mockData
+        productsTotal.value = mockData.length
+        productsLoading.value = false
+    }, 300)
+}
+
+// 加载下级人员数据
+const loadSubordinatePersonnel = () => {
+    subordinatesLoading.value = true
+    // 模拟异步请求
+    setTimeout(() => {
+        let mockData: SubordinatePersonnel[] = [
+            {
+                personnelName: '王五',
+                commissionAmount: 0,
+                status: '正常',
+                bindingTime: '2025-10-11',
+            },
+            {
+                personnelName: '李四',
+                commissionAmount: 10,
+                status: '正常',
+                bindingTime: '2025-10-11',
+            },
+        ]
+
+        // 根据搜索条件过滤
+        if (subordinateSearchName.value) {
+            mockData = mockData.filter(item =>
+                item.personnelName.includes(subordinateSearchName.value)
+            )
+        }
+
+        subordinatePersonnel.value = mockData
+        subordinatesTotal.value = mockData.length
+        subordinatesLoading.value = false
+    }, 300)
+}
+
+// 购买产品分页变化
+const handleProductsPageChange = (page: number) => {
+    productsPage.value = page
+    loadPurchasedProducts()
+}
+
+// 下级人员分页变化
+const handleSubordinatesPageChange = (page: number) => {
+    subordinatesPage.value = page
+    loadSubordinatePersonnel()
+}
+
+// 下级人员搜索
+const handleSubordinateSearch = () => {
+    subordinatesPage.value = 1
+    loadSubordinatePersonnel()
+}
+
+// 解除绑定
+const handleUnbind = (row: SubordinatePersonnel) => {
+    ElMessageBox.confirm(
+        `确定要解除与"${row.personnelName}"的绑定关系吗?`,
+        '提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    )
+        .then(() => {
+            ElMessage.success('解除绑定成功')
+            loadSubordinatePersonnel()
+        })
+        .catch(() => {
+            // 取消操作
+        })
+}
+
+// 关闭弹框
+const handleClose = () => {
+    visible.value = false
+    // 重置数据
+    subordinateSearchName.value = ''
+    productsPage.value = 1
+    subordinatesPage.value = 1
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss">
+.user-detail-dialog {
+    .detail-wrapper {
+        padding: 12px 16px 24px;
+    }
+
+    .detail-section {
+        margin-bottom: 24px;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+    }
+
+    .section-title {
+        margin-bottom: 12px;
+        font-weight: 600;
+        font-size: 14px;
+        color: #333;
+    }
+
+    .user-info-descriptions {
+        :deep(.el-descriptions__label) {
+            font-weight: 500;
+        }
+    }
+
+    .search-wrapper {
+        margin-bottom: 12px;
+    }
+
+    .pagination-wrapper {
+        display: flex;
+        justify-content: flex-end;
+        margin-top: 12px;
+        padding-top: 12px;
+
+        .pagination-jumper {
+            display: inline-flex;
+            align-items: center;
+            font-size: 13px;
+            color: #606266;
+        }
+    }
+
+    .dialog-footer {
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+    }
+}
+</style>
+

+ 254 - 0
web/src/views/backend/userCenter/user/index.vue

@@ -0,0 +1,254 @@
+<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', 'columnDisplay']">
+            <el-input class="form-input" v-model="baTable.table.filter!.name" @input="onSearchInput"
+                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-select>
+            <template #quickSearchPrepend>
+                <el-button type="primary" @click="onAdd">添加</el-button>
+            </template>
+        </TableHeader>
+
+        <!-- 表格 -->
+        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+        <Table>
+            <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" plain size="small" @click="detail(scope.row)">详情</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </Table>
+
+        <!-- 表单 -->
+        <PopupForm />
+
+        <!-- 详情弹框 -->
+        <DetailDialog ref="detailDialogRef" />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide, useTemplateRef } from 'vue'
+import { cloneDeep } from 'lodash-es'
+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 DetailDialog from './detailDialog.vue'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+
+defineOptions({
+    name: 'userCenter/user',
+})
+
+const { t } = useI18n()
+
+const baTable = new baTableClass(
+    new baTableApi('/admin/userCenter.user/'),
+    {
+        column: [
+            { label: '编号', prop: 'id', align: 'center', width: 70 },
+            {
+                label: '用户名',
+                prop: 'name',
+                align: 'center',
+                minWidth: 120
+            },
+            {
+                label: '邮箱',
+                prop: 'email',
+                align: 'center',
+                minWidth: 150
+            },
+            {
+                label: '企业',
+                prop: 'company',
+                align: 'center',
+                minWidth: 160
+            },
+            {
+                label: '状态',
+                prop: 'status',
+                align: 'center',
+                render: 'tag',
+                custom: { '1': 'success', '0': 'danger' },
+                replaceValue: { '1': '启用', '0': '禁用' },
+                minWidth: 150
+            },
+            {
+                label: '邀请人',
+                prop: 'inviter',
+                align: 'center',
+                minWidth: 120
+            },
+            {
+                label: '创建时间',
+                prop: 'create_time',
+                align: 'center',
+                minWidth: 160
+            },
+            {
+                label: '最后登录时间',
+                prop: 'last_login_time',
+                align: 'center',
+                minWidth: 160
+            },
+            {
+                label: '操作',
+                align: 'center',
+                render: 'slot',
+                slotName: 'operate',
+                operator: false,
+            },
+        ],
+        dblClickNotEditColumn: [undefined,],
+    },
+    {
+        defaultItems: {
+            status: '1',
+        },
+    }
+)
+
+// 设置假数据
+baTable.getData = () => {
+    baTable.table.loading = true
+    // 模拟异步请求
+    return new Promise<void>((resolve) => {
+        setTimeout(() => {
+            // 根据筛选条件过滤数据
+            let filteredData = [
+                {
+                    id: 1,
+                    name: '张三',
+                    email: 'zhangsan@example.com',
+                    company: '聚德科技',
+                    status: '1',
+                    inviter: '李四',
+                    create_time: '2025-01-10 10:00:00',
+                    last_login_time: '2025-01-15 14:30:00',
+                },
+                {
+                    id: 2,
+                    name: '王五',
+                    email: 'wangwu@example.com',
+                    company: '棱镜科技',
+                    status: '1',
+                    inviter: '赵六',
+                    create_time: '2025-01-11 09:00:00',
+                    last_login_time: '2025-01-14 16:20:00',
+                },
+                {
+                    id: 3,
+                    name: '刘七',
+                    email: 'liuqi@example.com',
+                    company: '测试公司',
+                    status: '0',
+                    inviter: '张三',
+                    create_time: '2025-01-12 11:00:00',
+                    last_login_time: '2025-01-13 10:15:00',
+                },
+                {
+                    id: 4,
+                    name: '陈八',
+                    email: 'chenba@example.com',
+                    company: '示例企业',
+                    status: '1',
+                    inviter: '王五',
+                    create_time: '2025-01-13 14:00:00',
+                    last_login_time: '2025-01-15 09:45:00',
+                },
+                {
+                    id: 5,
+                    name: '周九',
+                    email: 'zhoujiu@example.com',
+                    company: '聚德科技',
+                    status: '1',
+                    inviter: '李四',
+                    create_time: '2025-01-14 08:30:00',
+                    last_login_time: '2025-01-15 11:20:00',
+                },
+            ]
+
+            // 根据筛选条件过滤
+            const nameFilter = baTable.table.filter?.name
+            const statusFilter = baTable.table.filter?.status
+
+            if (nameFilter) {
+                filteredData = filteredData.filter(item => 
+                    item.name.includes(nameFilter.trim())
+                )
+            }
+
+            if (statusFilter) {
+                filteredData = filteredData.filter(item => 
+                    item.status === statusFilter
+                )
+            }
+
+            baTable.table.data = filteredData
+            baTable.table.total = filteredData.length
+            baTable.table.loading = false
+            resolve()
+        }, 300)
+    })
+}
+
+provide('baTable', baTable)
+
+// 搜索
+const onSearchInput = () => {
+    baTable.table.filter!.page = 1
+    baTable.getData()
+}
+
+// 删除
+const del = (row: TableRow) => {
+    baTable.onTableAction('delete', { row })
+}
+
+const openForm = (type: 'Add' | 'Edit', row?: TableRow) => {
+    if (type === 'Edit' && !row) return
+    baTable.form.items = row ? cloneDeep(row) : cloneDeep(baTable.form.defaultItems)
+    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)
+
+// 详情弹框引用
+const detailDialogRef = useTemplateRef('detailDialogRef')
+
+// 详情
+const detail = (row: TableRow) => {
+    if (detailDialogRef.value) {
+        detailDialogRef.value.open(row)
+    }
+}
+
+onMounted(() => {
+    baTable.mount()
+    baTable.getData()
+})
+</script>
+
+<style scoped lang="scss">
+.form-input {
+    width: 160px;
+    margin:0 5px;
+}
+</style>

+ 151 - 0
web/src/views/backend/userCenter/user/popupForm.vue

@@ -0,0 +1,151 @@
+<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"
+                >
+                    <el-form-item label="姓名" prop="name">
+                        <el-input
+                            v-model="baTable.form.items!.name"
+                            placeholder="请输入姓名"
+                        />
+                    </el-form-item>
+                    <el-form-item label="邮箱" prop="email">
+                        <el-input
+                            v-model="baTable.form.items!.email"
+                            type="email"
+                            placeholder="请输入邮箱"
+                        />
+                    </el-form-item>
+                    <el-form-item label="密码" prop="password" v-if="baTable.form.operate === 'Add'">
+                        <el-input
+                            v-model="baTable.form.items!.password"
+                            type="password"
+                            placeholder="请输入密码"
+                            show-password
+                        />
+                    </el-form-item>
+                    <el-form-item label="企业" prop="company">
+                        <el-select
+                            v-model="baTable.form.items!.company"
+                            placeholder="请选择企业"
+                            style="width: 100%"
+                        >
+                            <el-option
+                                v-for="item in companyOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            />
+                        </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>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">取消</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="btnSubmit" type="primary">
+                    确认
+                </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/userCenter/user'
+
+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: '姓名' })],
+    email: [
+        buildValidatorData({ name: 'required', title: '邮箱' }),
+        buildValidatorData({ name: 'email', title: '邮箱' })
+    ],
+    password: [
+        {
+            required: true,
+            validator: (rule: any, val: string, callback: Function) => {
+                if (baTable.form.operate === 'Add') {
+                    if (!val) {
+                        return callback(new Error(t('Please input field', { field: '密码' })))
+                    }
+                }
+                return callback()
+            },
+            trigger: 'blur',
+        },
+    ],
+})
+
+const companyOptions = [
+    { label: '腾达', value: '腾达' },
+    { label: '聚德科技', value: '聚德科技' },
+    { label: '棱镜科技', value: '棱镜科技' },
+    { label: '测试公司', value: '测试公司' },
+    { label: '示例企业', value: '示例企业' },
+]
+
+const statusOptions = {
+    '0': '禁用',
+    '1': '启用',
+}
+
+// 提交
+const btnSubmit = () => {
+	formRef.value?.validate((valid: boolean) => {
+		if (valid) {
+            baTable.table.loading = true
+			
+		}
+	})
+}
+
+</script>
+
+<style scoped lang="scss">
+
+</style>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików