Browse Source

feat(physical): 添加商城货品管理和合作商家管理功能

- 新增商城货品管理API接口,包括查询、新增、修改、删除等操作
- 新增商城合作商家管理API接口,包含基本CRUD和启用商家查询功能
- 新增服务页面页签管理API接口,支持分类查询和启用状态筛选
- 实现商城货品管理页面,包含货品列表展示、搜索、分页等功能
- 添加货品表单验证和图片上传功能,支持主图、详情图和轮播图上传
- 实现货品上下架状态切换功能和批量操作功能
- 创建商城合作商家管理页面,提供商家信息维护界面
- 集成权限控制,确保操作符合用户权限要求
fugui001 2 tuần trước cách đây
mục cha
commit
3b8657d6b5

+ 63 - 0
src/api/system/physical/merchantItem/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { MerchantItemVO, MerchantItemForm, MerchantItemQuery } from '@/api/system/physical/merchantItem/types';
+
+/**
+ * 查询商城货品管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listMerchantItem = (query?: MerchantItemQuery): AxiosPromise<MerchantItemVO[]> => {
+  return request({
+    url: '/physical/merchantItem/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询商城货品管理详细
+ * @param id
+ */
+export const getMerchantItem = (id: string | number): AxiosPromise<MerchantItemVO> => {
+  return request({
+    url: '/physical/merchantItem/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增商城货品管理
+ * @param data
+ */
+export const addMerchantItem = (data: MerchantItemForm) => {
+  return request({
+    url: '/physical/merchantItem',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改商城货品管理
+ * @param data
+ */
+export const updateMerchantItem = (data: MerchantItemForm) => {
+  return request({
+    url: '/physical/merchantItem',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除商城货品管理
+ * @param id
+ */
+export const delMerchantItem = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/merchantItem/' + id,
+    method: 'delete'
+  });
+};

+ 225 - 0
src/api/system/physical/merchantItem/types.ts

@@ -0,0 +1,225 @@
+export interface MerchantItemVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 货品名称,如:【和成天下】尊享·和成
+   */
+  name: string;
+
+  /**
+   * 所属商家ID,外键关联 physical_merchant_partner.id
+   */
+  merchantId: string | number;
+
+  /**
+   * 价格(元)
+   */
+  price: number;
+
+  /**
+   * 【建议删除或重命名】
+   */
+  quantity: number;
+
+  /**
+   * 当前库存
+   */
+  stock: number;
+
+  /**
+   * 总销量(需事务保证一致性)
+   */
+  totalSales: number;
+
+  /**
+   * 展示排序值,越小越靠前
+   */
+  sortOrder: number;
+
+  /**
+   * 状态:上架/下架 on_sale off_sale
+   */
+  status: string;
+
+  /**
+   * 创建人
+   */
+  createdBy: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt: string;
+
+  productImage?: string;
+
+  detailImage?: string;
+
+  carouselImages?: string;
+
+  categoryTagId?: string | number;
+}
+
+export interface MerchantItemForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 货品名称,如:【和成天下】尊享·和成
+   */
+  name?: string;
+
+  /**
+   * 所属商家ID,外键关联 physical_merchant_partner.id
+   */
+  merchantId?: string | number;
+
+  /**
+   * 价格(元)
+   */
+  price?: number;
+
+  /**
+   * 【建议删除或重命名】
+   */
+  quantity?: number;
+
+  /**
+   * 当前库存
+   */
+  stock?: number;
+
+  /**
+   * 总销量(需事务保证一致性)
+   */
+  totalSales?: number;
+
+  /**
+   * 展示排序值,越小越靠前
+   */
+  sortOrder?: number;
+
+  /**
+   * 状态:上架/下架 on_sale off_sale
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  productImage?: string;
+
+  detailImage?: string;
+
+  carouselImages?: string;
+
+  categoryTagId?: string | number;
+}
+
+export interface MerchantItemQuery extends PageQuery {
+  /**
+   * 货品名称,如:【和成天下】尊享·和成
+   */
+  name?: string;
+
+  /**
+   * 所属商家ID,外键关联 physical_merchant_partner.id
+   */
+  merchantId?: string | number;
+
+  /**
+   * 价格(元)
+   */
+  price?: number;
+
+  /**
+   * 【建议删除或重命名】
+   */
+  quantity?: number;
+
+  /**
+   * 当前库存
+   */
+  stock?: number;
+
+  /**
+   * 总销量(需事务保证一致性)
+   */
+  totalSales?: number;
+
+  /**
+   * 展示排序值,越小越靠前
+   */
+  sortOrder?: number;
+
+  /**
+   * 状态:上架/下架 on_sale off_sale
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+
+  productImage?: string;
+
+  detailImage?: string;
+
+  carouselImages?: string;
+
+  categoryTagId?: string | number;
+
+}

+ 72 - 0
src/api/system/physical/merchantPartner/index.ts

@@ -0,0 +1,72 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { MerchantPartnerVO, MerchantPartnerForm, MerchantPartnerQuery } from '@/api/system/physical/merchantPartner/types';
+
+/**
+ * 查询商城合作商家管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listMerchantPartner = (query?: MerchantPartnerQuery): AxiosPromise<MerchantPartnerVO[]> => {
+  return request({
+    url: '/physical/merchantPartner/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询商城合作商家管理详细
+ * @param id
+ */
+export const getMerchantPartner = (id: string | number): AxiosPromise<MerchantPartnerVO> => {
+  return request({
+    url: '/physical/merchantPartner/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增商城合作商家管理
+ * @param data
+ */
+export const addMerchantPartner = (data: MerchantPartnerForm) => {
+  return request({
+    url: '/physical/merchantPartner',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改商城合作商家管理
+ * @param data
+ */
+export const updateMerchantPartner = (data: MerchantPartnerForm) => {
+  return request({
+    url: '/physical/merchantPartner',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除商城合作商家管理
+ * @param id
+ */
+export const delMerchantPartner = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/merchantPartner/' + id,
+    method: 'delete'
+  });
+};
+/**
+ * 查询所有商家
+ */
+export const selectPhysicalMerchantPartnerAllEnabled = (): AxiosPromise<MerchantPartnerVO> => {
+  return request({
+    url: '/physical/merchantPartner/selectPhysicalMerchantPartnerAllEnabled',
+    method: 'get'
+  });
+};

+ 141 - 0
src/api/system/physical/merchantPartner/types.ts

@@ -0,0 +1,141 @@
+export interface MerchantPartnerVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 商家名称,如:人人乐超市发货
+   */
+  name: string;
+
+  /**
+   * 联系人姓名
+   */
+  contactPerson: string;
+
+  /**
+   * 联系方式(手机号)
+   */
+  contactPhone: string;
+
+  /**
+   * 状态:启用/禁用 enabled/disabled
+   */
+  status: string;
+
+  /**
+   * 创建人
+   */
+  createdBy: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt: string;
+}
+
+export interface MerchantPartnerForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 商家名称,如:人人乐超市发货
+   */
+  name?: string;
+
+  /**
+   * 联系人姓名
+   */
+  contactPerson?: string;
+
+  /**
+   * 联系方式(手机号)
+   */
+  contactPhone?: string;
+
+  /**
+   * 状态:启用/禁用 enabled/disabled
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+}
+
+export interface MerchantPartnerQuery extends PageQuery {
+
+  /**
+   * 商家名称,如:人人乐超市发货
+   */
+  name?: string;
+
+  /**
+   * 联系人姓名
+   */
+  contactPerson?: string;
+
+  /**
+   * 联系方式(手机号)
+   */
+  contactPhone?: string;
+
+  /**
+   * 状态:启用/禁用 enabled/disabled
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 73 - 0
src/api/system/physical/serviceTab/index.ts

@@ -0,0 +1,73 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ServiceTabVO, ServiceTabForm, ServiceTabQuery } from '@/api/system/physical/serviceTab/types';
+
+/**
+ * 查询服务页面页签管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listServiceTab = (query?: ServiceTabQuery): AxiosPromise<ServiceTabVO[]> => {
+  return request({
+    url: '/physical/serviceTab/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询服务页面页签管理详细
+ * @param id
+ */
+export const getServiceTab = (id: string | number): AxiosPromise<ServiceTabVO> => {
+  return request({
+    url: '/physical/serviceTab/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增服务页面页签管理
+ * @param data
+ */
+export const addServiceTab = (data: ServiceTabForm) => {
+  return request({
+    url: '/physical/serviceTab',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改服务页面页签管理
+ * @param data
+ */
+export const updateServiceTab = (data: ServiceTabForm) => {
+  return request({
+    url: '/physical/serviceTab',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除服务页面页签管理
+ * @param id
+ */
+export const delServiceTab = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/physical/serviceTab/' + id,
+    method: 'delete'
+  });
+};
+/**
+ * 查询启用的类别
+ * @param category
+ */
+export const selectEnabledTabsByCategoryList = (category: string | number): AxiosPromise<ServiceTabVO> => {
+  return request({
+    url: '/physical/serviceTab/selectEnabledTabsByCategoryList/' + category,
+    method: 'get'
+  });
+};

+ 140 - 0
src/api/system/physical/serviceTab/types.ts

@@ -0,0 +1,140 @@
+export interface ServiceTabVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 页签名称,如:新手推荐、热门视频、本地特产
+   */
+  name: string;
+
+  /**
+   * 所属类别,如:视频video、商城shop
+   */
+  category: string;
+
+  /**
+   * 排序值,数字越小越靠前
+   */
+  sortOrder: number;
+
+  /**
+   * 状态:启用/禁用
+   */
+  status: string;
+
+  /**
+   * 创建人
+   */
+  createdBy: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt: string;
+}
+
+export interface ServiceTabForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 页签名称,如:新手推荐、热门视频、本地特产
+   */
+  name?: string;
+
+  /**
+   * 所属类别,如:视频video、商城shop
+   */
+  category?: string;
+
+  /**
+   * 排序值,数字越小越靠前
+   */
+  sortOrder?: number;
+
+  /**
+   * 状态:启用/禁用
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+}
+
+export interface ServiceTabQuery extends PageQuery {
+  /**
+   * 页签名称,如:新手推荐、热门视频、本地特产
+   */
+  name?: string;
+
+  /**
+   * 所属类别,如:视频video、商城shop
+   */
+  category?: string;
+
+  /**
+   * 排序值,数字越小越靠前
+   */
+  sortOrder?: number;
+
+  /**
+   * 状态:启用/禁用
+   */
+  status?: string;
+
+  /**
+   * 创建人
+   */
+  createdBy?: string;
+
+  /**
+   * 创建时间
+   */
+  createdAt?: string;
+
+  /**
+   * 最后更新人
+   */
+  updatedBy?: string;
+
+  /**
+   * 最后更新时间
+   */
+  updatedAt?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 815 - 0
src/views/system/physical/merchantItem/index.vue

@@ -0,0 +1,815 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="货品名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入货品名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+                <el-option label="上架" value="on_sale" />
+                <el-option label="下架" value="off_sale" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:merchantItem:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:merchantItem:edit']"
+              >修改</el-button
+            >
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:merchantItem:remove']"
+              >删除</el-button
+            >
+          </el-col>
+          <!--          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:merchantItem:export']">导出</el-button>
+          </el-col>-->
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="merchantItemList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="编号" align="center" prop="id" v-if="true" />
+        <el-table-column label="商品名称" align="center" prop="name" />
+        <el-table-column label="商品图片" align="center" width="90">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.productImage"
+              :src="scope.row.productImage"
+              style="width: 40px; height: 40px; border-radius: 4px; cursor: zoom-in"
+              :preview-src-list="[scope.row.productImage]"
+              :preview-teleported="true"
+              fit="cover"
+            />
+            <span v-else></span>
+          </template>
+        </el-table-column>
+        <el-table-column label="所属商家" align="center" prop="merchantId" />
+        <el-table-column label="价格 (元)" align="center" prop="price" />
+        <el-table-column label="数量" align="center" prop="quantity" />
+        <el-table-column label="库存" align="center" prop="stock" />
+        <el-table-column label="总销量" align="center" prop="totalSales" />
+        <el-table-column label="商品排序" align="center" prop="sortOrder" />
+        <el-table-column label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === 'on_sale' ? 'success' : 'danger'" style="cursor: pointer">
+              {{ scope.row.status === 'on_sale' ? '上架' : '下架' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建人" align="center" prop="createdBy" />
+        <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip :content="scope.row.status === 'on_sale' ? '下架' : '上架'" placement="top">
+              <el-button
+                link
+                :type="scope.row.status === 'on_sale' ? 'warning' : 'success'"
+                :icon="scope.row.status === 'on_sale' ? 'VideoPause' : 'VideoPlay'"
+                @click="handleToggleStatus(scope.row)"
+                v-hasPermi="['physical:merchantItem:edit']"
+              ></el-button>
+            </el-tooltip>
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:merchantItem:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:merchantItem:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改商城货品管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
+      <el-form ref="merchantItemFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="货品名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入货品名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="form.status" placeholder="请选择状态">
+                <el-option label="上架" value="on_sale" />
+                <el-option label="下架" value="off_sale" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="价格 (元)" prop="price">
+              <el-input v-model="form.price" placeholder="请输入价格" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="数量" prop="quantity">
+              <el-input v-model="form.quantity" placeholder="请输入数量" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="商品库存" prop="stock">
+              <el-input v-model="form.stock" placeholder="请输入商品库存" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="商品排序" prop="sortOrder">
+              <el-input v-model="form.sortOrder" placeholder="请输入商品排序" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="所属商家" prop="merchantId">
+              <el-select v-model="form.merchantId" placeholder="请选择所属商家" filterable clearable style="width: 100%">
+                <el-option v-for="item in merchantOptions" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属页签" prop="serviceTabId">
+              <el-select v-model="form.categoryTagId" placeholder="请选择所属页签" filterable clearable style="width: 100%">
+                <el-option v-for="item in serviceTabOptions" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="商品主图" prop="productImage">
+              <div class="upload-container">
+                <el-upload
+                  class="upload-icon"
+                  action="#"
+                  :on-change="handleProductImageChange"
+                  :on-remove="handleProductImageRemove"
+                  :file-list="productImageFileList"
+                  :auto-upload="false"
+                  :limit="1"
+                  accept="image/*"
+                >
+                  <template #trigger>
+                    <el-button type="primary">点击选择主图</el-button>
+                  </template>
+
+                  <template #default>
+                    <div class="preview-area" @click="handleProductImagePreviewClick">
+                      <img
+                        v-if="productImagePreviewUrl || form.productImage"
+                        :src="productImagePreviewUrl || form.productImage"
+                        alt="预览图"
+                        style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
+                      />
+                      <div v-else style="margin-top: 10px; color: #999">无图片</div>
+                    </div>
+                  </template>
+
+                  <template #tip>
+                    <div class="el-upload__tip">
+                      <span v-if="productImageFileList.length > 0">当前已选文件:{{ productImageFileList[0].name }}</span>
+                    </div>
+                  </template>
+                </el-upload>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="商品详情图片" prop="detailImage">
+              <div class="upload-container">
+                <el-upload
+                  class="upload-icon"
+                  action="#"
+                  :on-change="handleDetailImageChange"
+                  :on-remove="handleDetailImageRemove"
+                  :file-list="detailImageFileList"
+                  :auto-upload="false"
+                  :limit="1"
+                  accept="image/*"
+                >
+                  <template #trigger>
+                    <el-button type="primary">点击选择详情图</el-button>
+                  </template>
+
+                  <template #default>
+                    <div class="preview-area" @click="handleDetailImagePreviewClick">
+                      <img
+                        v-if="detailImagePreviewUrl || form.detailImage"
+                        :src="detailImagePreviewUrl || form.detailImage"
+                        alt="预览图"
+                        style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
+                      />
+                      <div v-else style="margin-top: 10px; color: #999">无图片</div>
+                    </div>
+                  </template>
+
+                  <template #tip>
+                    <div class="el-upload__tip">
+                      <span v-if="detailImageFileList.length > 0">当前已选文件:{{ detailImageFileList[0].name }}</span>
+                    </div>
+                  </template>
+                </el-upload>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="商品轮播图" prop="carouselImages">
+              <div class="upload-container">
+                <el-upload
+                  class="upload-icon"
+                  action="#"
+                  :on-change="handleCarouselImageChange"
+                  :on-remove="handleCarouselImageRemove"
+                  :file-list="carouselImageFileList"
+                  :auto-upload="false"
+                  :limit="9"
+                  multiple
+                  accept="image/*"
+                >
+                  <template #trigger>
+                    <el-button type="primary">点击选择轮播图</el-button>
+                  </template>
+                  <template #default>
+                    <div class="carousel-preview-area">
+                      <div v-for="(item, index) in carouselImageFileList" :key="index" class="carousel-item">
+                        <img
+                          :src="item.url || item.response"
+                          alt="轮播图"
+                          style="max-width: 100%; max-height: 100%"
+                          @click.stop="handleCarouselImagePreviewClick(index)"
+                        />
+                        <div class="carousel-item-mask" @click.stop="handleCarouselImageRemoveByIndex(index)">
+                          <el-icon><Delete /></el-icon>
+                        </div>
+                      </div>
+                      <div v-if="carouselImageFileList.length === 0" style="margin-top: 10px; color: #999">暂无图片,请点击左上角按钮添加</div>
+                    </div>
+                  </template>
+                  <template #tip>
+                    <div class="el-upload__tip">
+                      <span>最多可上传 9 张图片,支持 jpg/png/gif 格式</span>
+                    </div>
+                  </template>
+                </el-upload>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="MerchantItem" lang="ts">
+import { listMerchantItem, getMerchantItem, delMerchantItem, addMerchantItem, updateMerchantItem } from '@/api/system/physical/merchantItem';
+import { MerchantItemVO, MerchantItemQuery, MerchantItemForm } from '@/api/system/physical/merchantItem/types';
+import { uploadTournament } from '@/api/system/business/tournaments';
+import { selectPhysicalMerchantPartnerAllEnabled } from '@/api/system/physical/merchantPartner';
+import { selectEnabledTabsByCategoryList } from '@/api/system/physical/serviceTab';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { MerchantPartnerVO } from '@/api/system/physical/merchantPartner/types';
+import { ServiceTabVO } from '@/api/system/physical/serviceTab/types';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const merchantItemList = ref<MerchantItemVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const merchantItemFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: MerchantItemForm = {
+  id: undefined,
+  name: undefined,
+  merchantId: undefined,
+  price: undefined,
+  quantity: undefined,
+  stock: undefined,
+  totalSales: undefined,
+  sortOrder: undefined,
+  status: 'on_sale',
+  productImage: undefined,
+  detailImage: undefined,
+  createdBy: undefined,
+  createdAt: undefined,
+  updatedBy: undefined,
+  updatedAt: undefined
+};
+const data = reactive<PageData<MerchantItemForm, MerchantItemQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    merchantId: undefined,
+    price: undefined,
+    quantity: undefined,
+    stock: undefined,
+    totalSales: undefined,
+    sortOrder: undefined,
+    status: undefined,
+    createdBy: undefined,
+    createdAt: undefined,
+    updatedBy: undefined,
+    updatedAt: undefined,
+    params: {}
+  },
+  rules: {
+    id: [{ required: true, message: '主键 ID 不能为空', trigger: 'blur' }],
+    name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+    merchantId: [{ required: true, message: '所属商家不能为空', trigger: 'blur' }],
+    price: [{ required: true, message: '价格不能为空', trigger: 'blur' }],
+    stock: [{ required: true, message: '当前库存不能为空', trigger: 'blur' }],
+    status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
+    createdBy: [{ required: true, message: '创建人不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+// 商品主图上传相关
+const productImageFileList = ref<any[]>([]);
+const productImagePreviewUrl = ref('');
+
+// 商品详情图片上传相关
+const detailImageFileList = ref<any[]>([]);
+const detailImagePreviewUrl = ref('');
+
+// 商品轮播图上传相关
+const carouselImageFileList = ref<any[]>([]);
+const isUploading = ref(false);
+const uploadQueue = ref<any[]>([]);
+const merchantOptions = ref<MerchantPartnerVO[]>([]);
+const serviceTabOptions = ref<ServiceTabVO[]>([]);
+/** 查询商城货品管理列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listMerchantItem(queryParams.value);
+  merchantItemList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  merchantItemFormRef.value?.resetFields();
+  productImageFileList.value = [];
+  productImagePreviewUrl.value = '';
+  detailImageFileList.value = [];
+  detailImagePreviewUrl.value = '';
+  carouselImageFileList.value = [];
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: MerchantItemVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: MerchantItemVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getMerchantItem(_id);
+  Object.assign(form.value, res.data);
+
+  // 处理商品主图
+  if (res.data.productImage) {
+    productImageFileList.value = [
+      {
+        name: '已上传主图',
+        url: res.data.productImage,
+        status: 'success'
+      }
+    ];
+    productImagePreviewUrl.value = res.data.productImage;
+  }
+
+  // 处理商品详情图片
+  if (res.data.detailImage) {
+    detailImageFileList.value = [
+      {
+        name: '已上传详情图',
+        url: res.data.detailImage,
+        status: 'success'
+      }
+    ];
+    detailImagePreviewUrl.value = res.data.detailImage;
+  }
+  // 处理商品轮播图
+  if (res.data.carouselImages) {
+    const imageUrls = res.data.carouselImages.split(',').filter((url: string) => url.trim());
+    carouselImageFileList.value = imageUrls.map((url: string, index: number) => ({
+      name: `轮播图${index + 1}`,
+      url: url,
+      status: 'success'
+    }));
+  }
+  dialog.visible = true;
+  dialog.title = '修改';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  merchantItemFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        // 处理轮播图数据
+        if (carouselImageFileList.value.length > 0) {
+          form.value.carouselImages = carouselImageFileList.value.map((item) => item.url || item.response).join(',');
+        } else {
+          form.value.carouselImages = undefined;
+        }
+
+        if (form.value.id) {
+          await updateMerchantItem(form.value).finally(() => (buttonLoading.value = false));
+        } else {
+          await addMerchantItem(form.value).finally(() => (buttonLoading.value = false));
+        }
+        proxy?.$modal.msgSuccess('操作成功');
+        dialog.visible = false;
+        await getList();
+      } catch (error) {
+        console.error('提交失败:', error);
+        proxy?.$modal.msgError('提交失败,请重试');
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: MerchantItemVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delMerchantItem(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'physical/merchantItem/export',
+    {
+      ...queryParams.value
+    },
+    `merchantItem_${new Date().getTime()}.xlsx`
+  );
+};
+
+// 商品主图上传处理
+const handleProductImageChange = async (file: any) => {
+  const index = productImageFileList.value.findIndex((f) => f.uid === file.uid);
+  productImageFileList.value = [];
+
+  if (file.raw) {
+    productImagePreviewUrl.value = URL.createObjectURL(file.raw);
+  }
+
+  if (index === -1) {
+    productImageFileList.value.push(file);
+  }
+
+  try {
+    const rawFile = file.raw;
+    const res = await uploadTournament(rawFile);
+    if (res.code === 200) {
+      productImageFileList.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      form.value.productImage = productImageFileList.value[index].response;
+      productImagePreviewUrl.value = productImageFileList.value[index].response;
+      ElMessage.success('上传成功');
+    } else {
+      throw new Error(res.msg);
+    }
+  } catch (error) {
+    productImageFileList.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('上传失败,请重试');
+  }
+};
+
+// 商品主图移除处理
+const handleProductImageRemove = () => {
+  productImageFileList.value = [];
+  productImagePreviewUrl.value = '';
+  form.value.productImage = '';
+};
+
+// 商品主图预览点击
+const handleProductImagePreviewClick = () => {
+  if (productImagePreviewUrl.value || form.value.productImage) {
+    ElMessageBox.alert(`<img src="${productImagePreviewUrl.value || form.value.productImage}" style="max-width: 100%;" />`, '商品主图预览', {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '关闭'
+    });
+  }
+};
+
+// 商品详情图片上传处理
+const handleDetailImageChange = async (file: any) => {
+  const index = detailImageFileList.value.findIndex((f) => f.uid === file.uid);
+  detailImageFileList.value = [];
+
+  if (file.raw) {
+    detailImagePreviewUrl.value = URL.createObjectURL(file.raw);
+  }
+
+  if (index === -1) {
+    detailImageFileList.value.push(file);
+  }
+
+  try {
+    const rawFile = file.raw;
+    const res = await uploadTournament(rawFile);
+    if (res.code === 200) {
+      detailImageFileList.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      form.value.detailImage = detailImageFileList.value[index].response;
+      detailImagePreviewUrl.value = detailImageFileList.value[index].response;
+      ElMessage.success('上传成功');
+    } else {
+      throw new Error(res.msg);
+    }
+  } catch (error) {
+    detailImageFileList.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('上传失败,请重试');
+  }
+};
+
+// 商品详情图片移除处理
+const handleDetailImageRemove = () => {
+  detailImageFileList.value = [];
+  detailImagePreviewUrl.value = '';
+  form.value.detailImage = '';
+};
+
+// 商品详情图片预览点击
+const handleDetailImagePreviewClick = () => {
+  if (detailImagePreviewUrl.value || form.value.detailImage) {
+    ElMessageBox.alert(`<img src="${detailImagePreviewUrl.value || form.value.detailImage}" style="max-width: 100%;" />`, '商品详情图片预览', {
+      dangerouslyUseHTMLString: true,
+      confirmButtonText: '关闭'
+    });
+  }
+};
+// 商品轮播图上传处理
+const handleCarouselImageChange = async (file: any) => {
+  if (file.raw) {
+    // 创建临时预览 URL
+    const tempUrl = URL.createObjectURL(file.raw);
+    const tempFile = {
+      ...file,
+      url: tempUrl
+    };
+
+    // 添加到文件列表
+    carouselImageFileList.value.push(tempFile);
+    const currentIndex = carouselImageFileList.value.length - 1;
+
+    // 如果正在上传,加入队列
+    if (isUploading.value) {
+      uploadQueue.value.push({ file, index: currentIndex });
+      return;
+    }
+
+    // 开始上传
+    await uploadFile(file, currentIndex);
+  }
+};
+
+// 执行文件上传
+const uploadFile = async (file: any, index: number) => {
+  isUploading.value = true;
+
+  try {
+    const rawFile = file.raw;
+    const res = await uploadTournament(rawFile);
+
+    if (res.code === 200) {
+      carouselImageFileList.value[index] = {
+        ...file,
+        status: 'success',
+        response: res.data.url
+      };
+      ElMessage.success(`第${index + 1}张图片上传成功`);
+    } else {
+      throw new Error(res.msg || '上传失败');
+    }
+  } catch (error) {
+    console.error('上传失败:', error);
+    carouselImageFileList.value[index] = {
+      ...file,
+      status: 'fail',
+      error: '上传失败'
+    };
+    ElMessage.error('图片上传失败,请重试');
+  } finally {
+    isUploading.value = false;
+
+    // 检查队列中是否有待上传的文件
+    if (uploadQueue.value.length > 0) {
+      const next = uploadQueue.value.shift()!;
+      await uploadFile(next.file, next.index);
+    }
+  }
+};
+// 商品轮播图移除处理
+const handleCarouselImageRemove = (file: any) => {
+  const index = carouselImageFileList.value.findIndex((f) => f.uid === file.uid);
+  if (index > -1) {
+    carouselImageFileList.value.splice(index, 1);
+  }
+};
+// 商品轮播图移除处理(通过索引)
+const handleCarouselImageRemoveByIndex = (index: number) => {
+  carouselImageFileList.value.splice(index, 1);
+};
+// 商品轮播图预览点击
+const handleCarouselImagePreviewClick = (index: number) => {
+  if (carouselImageFileList.value.length > 0) {
+    const currentImageUrl = carouselImageFileList.value[index].url || carouselImageFileList.value[index].response;
+
+    ElMessageBox.alert(
+      `<img src="${currentImageUrl}" style="max-width: 100%;" />`,
+      `轮播图预览 (${index + 1}/${carouselImageFileList.value.length})`,
+      {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '关闭'
+      }
+    );
+  }
+};
+onMounted(() => {
+  getList();
+  loadMerchantOptions();
+  loadServiceTabOptions();
+});
+
+/** 加载商家选项列表 */
+const loadMerchantOptions = async () => {
+  try {
+    const res = await selectPhysicalMerchantPartnerAllEnabled();
+    if (res.code === 200 && Array.isArray(res.data)) {
+      merchantOptions.value = res.data;
+    } else if (Array.isArray(res)) {
+      merchantOptions.value = res;
+    }
+  } catch (error) {
+    console.error('加载商家列表失败:', error);
+    proxy?.$modal.msgError('加载商家列表失败');
+  }
+};
+/** 加载页签选项列表 */
+const loadServiceTabOptions = async () => {
+  try {
+    const res = await selectEnabledTabsByCategoryList('shop');
+    if (res.code === 200 && Array.isArray(res.data)) {
+      serviceTabOptions.value = res.data;
+    } else if (Array.isArray(res)) {
+      serviceTabOptions.value = res;
+    }
+  } catch (error) {
+    console.error('加载页签列表失败:', error);
+    proxy?.$modal.msgError('加载页签列表失败');
+  }
+};
+/** 切换商品上架/下架状态 */
+const handleToggleStatus = async (row: MerchantItemVO) => {
+  const newStatus = row.status === 'on_sale' ? 'off_sale' : 'on_sale';
+  const actionText = newStatus === 'on_sale' ? '上架' : '下架';
+  try {
+    await proxy?.$modal.confirm(`确认要${actionText}该商品吗?`);
+
+    // 创建更新数据的副本
+    const updateData = { ...row, status: newStatus };
+
+    buttonLoading.value = true;
+    await updateMerchantItem(updateData);
+    proxy?.$modal.msgSuccess(`${actionText}成功`);
+    await getList();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      console.error('切换状态失败:', error);
+      proxy?.$modal.msgError(`${actionText}失败,请重试`);
+    }
+  } finally {
+    buttonLoading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.upload-container {
+  width: 100%;
+}
+
+.upload-icon {
+  width: 100%;
+}
+
+.preview-area {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 100px;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &:hover {
+    border-color: #409eff;
+  }
+}
+</style>

+ 280 - 0
src/views/system/physical/merchantPartner/index.vue

@@ -0,0 +1,280 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="商家名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入商家名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="联系人" prop="contactPerson">
+              <el-input v-model="queryParams.contactPerson" placeholder="请输入联系人姓名" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="联系方式" prop="contactPhone">
+              <el-input v-model="queryParams.contactPhone" placeholder="请输入联系方式" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:merchantPartner:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:merchantPartner:edit']"
+              >修改</el-button
+            >
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:merchantPartner:remove']"
+              >删除</el-button
+            >
+          </el-col>
+          <!--          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:merchantPartner:export']">导出</el-button>
+          </el-col>-->
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="merchantPartnerList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="编号" align="center" prop="id" v-if="true" />
+        <el-table-column label="商家名称" align="center" prop="name" />
+        <el-table-column label="联系人" align="center" prop="contactPerson" />
+        <el-table-column label="联系方式" align="center" prop="contactPhone" />
+        <el-table-column label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === 'enabled' ? 'success' : 'danger'">
+              {{ scope.row.status === 'enabled' ? '启用' : '禁用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建人" align="center" prop="createdBy" />
+        <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:merchantPartner:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button
+                link
+                type="primary"
+                icon="Delete"
+                @click="handleDelete(scope.row)"
+                v-hasPermi="['physical:merchantPartner:remove']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改商城合作商家管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="merchantPartnerFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="商家名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入商家名称" />
+        </el-form-item>
+        <el-form-item label="联系人" prop="contactPerson">
+          <el-input v-model="form.contactPerson" placeholder="请输入联系人姓名" />
+        </el-form-item>
+        <el-form-item label="联系方式" prop="contactPhone">
+          <el-input v-model="form.contactPhone" placeholder="请输入联系方式" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.status" placeholder="请选择状态">
+            <el-option label="启用" value="enabled" />
+            <el-option label="禁用" value="disabled" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="MerchantPartner" lang="ts">
+import {
+  listMerchantPartner,
+  getMerchantPartner,
+  delMerchantPartner,
+  addMerchantPartner,
+  updateMerchantPartner
+} from '@/api/system/physical/merchantPartner';
+import { MerchantPartnerVO, MerchantPartnerQuery, MerchantPartnerForm } from '@/api/system/physical/merchantPartner/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const merchantPartnerList = ref<MerchantPartnerVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const merchantPartnerFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: MerchantPartnerForm = {
+  id: undefined,
+  name: undefined,
+  contactPerson: undefined,
+  contactPhone: undefined,
+  status: 'enabled',
+  createdBy: undefined,
+  createdAt: undefined,
+  updatedBy: undefined,
+  updatedAt: undefined
+};
+const data = reactive<PageData<MerchantPartnerForm, MerchantPartnerQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    contactPerson: undefined,
+    contactPhone: undefined,
+    status: undefined,
+    createdBy: undefined,
+    createdAt: undefined,
+    updatedBy: undefined,
+    updatedAt: undefined,
+    params: {}
+  },
+  rules: {
+    id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
+    name: [{ required: true, message: '商家名称,如:人人乐超市发货不能为空', trigger: 'blur' }],
+    contactPerson: [{ required: true, message: '联系人姓名不能为空', trigger: 'blur' }],
+    contactPhone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
+    status: [{ required: true, message: '状态:启用/禁用 enabled/disabled不能为空', trigger: 'change' }],
+    createdBy: [{ required: true, message: '创建人不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询商城合作商家管理列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listMerchantPartner(queryParams.value);
+  merchantPartnerList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  merchantPartnerFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: MerchantPartnerVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加商城合作商家管理';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: MerchantPartnerVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getMerchantPartner(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = '修改商城合作商家管理';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  merchantPartnerFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateMerchantPartner(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addMerchantPartner(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: MerchantPartnerVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除商城合作商家管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delMerchantPartner(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'system/merchantPartner/export',
+    {
+      ...queryParams.value
+    },
+    `merchantPartner_${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 298 - 0
src/views/system/physical/serviceTab/index.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="页签名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入页签名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-tabs v-model="activeCategory" @tab-click="handleTabClick" class="category-tabs">
+          <el-tab-pane label="商城" name="shop"></el-tab-pane>
+          <el-tab-pane label="视频" name="video"></el-tab-pane>
+        </el-tabs>
+      </template>
+
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:serviceTab:add']">新增</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:serviceTab:edit']"
+            >修改</el-button
+          >
+        </el-col>
+        <el-col :span="1.5">
+          <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:serviceTab:remove']"
+          >删除</el-button
+          >
+        </el-col>
+        <!--          <el-col :span="1.5">
+          <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:serviceTab:export']">导出</el-button>
+        </el-col>-->
+        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+      </el-row>
+
+      <el-table v-loading="loading" border :data="serviceTabList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="编号" align="center" prop="id" v-if="false" />
+        <el-table-column label="标签名称" align="center" prop="name" />
+        <el-table-column label="排序值" align="center" prop="sortOrder" />
+        <el-table-column label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === 'enabled' ? 'success' : 'danger'">
+              {{ scope.row.status === 'enabled' ? '启用' : '禁用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:serviceTab:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:serviceTab:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改服务页面页签管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="serviceTabFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="标签名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入标签名称" />
+        </el-form-item>
+        <el-form-item label="类目" prop="category">
+          <el-select v-model="form.category" placeholder="请选择类目">
+            <el-option label="视频" value="video" />
+            <el-option label="商城" value="shop" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排序值" prop="sortOrder">
+          <el-input v-model="form.sortOrder" placeholder="请输入排序值,数字越小越靠前" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="ServiceTab" lang="ts">
+import { listServiceTab, getServiceTab, delServiceTab, addServiceTab, updateServiceTab } from '@/api/system/physical/serviceTab';
+import { ServiceTabVO, ServiceTabQuery, ServiceTabForm } from '@/api/system/physical/serviceTab/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const serviceTabList = ref<ServiceTabVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const activeCategory = ref('shop');
+
+const queryFormRef = ref<ElFormInstance>();
+const serviceTabFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: ServiceTabForm = {
+  id: undefined,
+  name: undefined,
+  category: undefined,
+  sortOrder: undefined,
+  status: 'enabled',
+  createdBy: undefined,
+  createdAt: undefined,
+  updatedBy: undefined,
+  updatedAt: undefined
+};
+const data = reactive<PageData<ServiceTabForm, ServiceTabQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    category: undefined,
+    sortOrder: undefined,
+    status: undefined,
+    createdBy: undefined,
+    createdAt: undefined,
+    updatedBy: undefined,
+    updatedAt: undefined,
+    params: {}
+  },
+  rules: {
+    id: [{ required: true, message: '主键 ID 不能为空', trigger: 'blur' }],
+    name: [{ required: true, message: '页签名称不能为空', trigger: 'blur' }],
+    category: [{ required: true, message: '所属类别不能为空', trigger: 'blur' }],
+    status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
+    createdBy: [{ required: true, message: '创建人不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询服务页面页签管理列表 */
+const getList = async () => {
+  loading.value = true;
+  queryParams.value.category = activeCategory.value;
+  const res = await listServiceTab(queryParams.value);
+  serviceTabList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  serviceTabFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** Tab 切换操作 */
+const handleTabClick = (tab: any) => {
+  console.log('当前选中的 Tab:', tab, '类别:', tab.props.name, '标签:', tab.props.label);
+  activeCategory.value = tab.props.name;
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ServiceTabVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  form.value.category = activeCategory.value;
+  dialog.visible = true;
+  dialog.title = '添加服务页面页签管理';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ServiceTabVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getServiceTab(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = '修改服务页面页签管理';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  serviceTabFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateServiceTab(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addServiceTab(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ServiceTabVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除服务页面页签管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delServiceTab(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'system/serviceTab/export',
+    {
+      ...queryParams.value
+    },
+    `serviceTab_${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.category-tabs {
+  :deep(.el-tabs__item) {
+    min-width: 100px;
+    text-align: center;
+    background-color: #f0f2f5;
+    border-radius: 4px 4px 0 0;
+    margin-right: 5px;
+    transition: all 0.3s;
+
+    &:hover {
+      background-color: #e6e8eb;
+    }
+
+    &.is-active {
+      background-color: #1890ff;
+      color: #fff;
+    }
+  }
+
+  :deep(.el-tabs__nav) {
+    display: flex;
+  }
+}
+</style>

+ 0 - 1
src/views/system/physical/tournaments/index.vue

@@ -438,7 +438,6 @@
             v-model="form.qualifierType"
             placeholder="请选择"
             :disabled="dialog.mode === 'view'"
-            @change="handleQualifierTypeChange"
           >
             <el-option v-for="dict in qualifier_type" :key="dict.value" :label="dict.label" :value="dict.value" />
           </el-select>