zhangqin 3 tygodni temu
rodzic
commit
1c6c0f49f3

+ 1 - 0
package.json

@@ -101,6 +101,7 @@
     "normalize.css": "7.0.0",
     "nprogress": "0.2.0",
     "path-to-regexp": "2.4.0",
+    "qrcode": "^1.5.4",
     "qrcodejs2": "0.0.2",
     "quill": "1.3.7",
     "screenfull": "4.2.0",

+ 29 - 0
src/api/adv/channel.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+// 查询分组列表
+export function pageProject(data) {
+  return request({
+    url: '/channel/page',
+    method: 'get',
+    params: data
+  })
+}
+
+
+// 新增或更新渠道
+export function addOrUpdateChannel(data) {
+  return request({
+    url: '/channel/addOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 批量复制渠道
+export function saveBatchChannel(data) {
+  return request({
+    url: '/channel/saveBatch',
+    method: 'post',
+    data: data
+  })
+}

+ 19 - 0
src/api/adv/project.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// 查询项目列表
+export function pageProject(data) {
+  return request({
+    url: '/project/page',
+    method: 'get',
+    params: data
+  })
+}
+
+// 新增项目
+export function addProject(data) {
+  return request({
+    url: '/project/add',
+    method: 'post',
+    data: data
+  })
+}

+ 9 - 0
src/api/qw/allocationRule.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 查询企业微信分配规则列表
+export function listAllocationRule() {
+  return request({
+    url: '/qw/allocationRule/list',
+    method: 'get'
+  })
+}

+ 9 - 0
src/api/qw/groupActive.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 查询群活码列表
+export function listGroupActive() {
+  return request({
+    url: '/qw/groupActive/list',
+    method: 'get'
+  })
+}

+ 622 - 0
src/views/adv/channel/index.vue

@@ -0,0 +1,622 @@
+<template>
+  <div class="app-container channel-container">
+    <el-row :gutter="20" class="channel-wrapper">
+      <!-- 分组区域 -->
+      <el-col :xs="24" :sm="8" :md="6" class="group-section">
+        <div class="section-header">
+          <h3>分组管理</h3>
+          <el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAddGroup">新增分组</el-button>
+        </div>
+        <div class="search-box">
+          <el-input 
+            v-model="queryParams.groupChannelName" 
+            placeholder="搜索分组"
+            clearable
+            size="small"
+            @input="getGroupList"
+          />
+        </div>
+        <div class="group-list">
+          <div 
+            :class="['group-item', 'all-group', { active: selectedGroupId === 'all' }]"
+            @click="selectAllGroup"
+          >
+            <span class="group-name">全部</span>
+          </div>
+          <div 
+            v-for="item in groupList" 
+            :key="item.id"
+            :class="['group-item', { active: selectedGroupId === item.id }]"
+            @click="selectGroup(item)"
+          >
+            <span class="group-name">{{ item.channelName }}</span>
+          </div>
+          <div v-if="groupList.length === 0" class="empty-tip">暂无分组</div>
+        </div>
+      </el-col>
+
+      <!-- 渠道区域 -->
+      <el-col :xs="24" :sm="16" :md="18" class="channel-section">
+        <div class="section-header">
+          <h3>{{ selectedGroupId ? '渠道管理' : '请选择分组' }}</h3>
+          <el-button 
+            v-if="selectedGroupId" 
+            type="primary" 
+            size="mini" 
+            icon="el-icon-plus" 
+            @click="handleAddChannel"
+          >新增渠道</el-button>
+        </div>
+        
+        <div v-if="selectedGroupId" class="search-box">
+          <el-input 
+            v-model="queryParams.channelChannelName" 
+            placeholder="搜索渠道"
+            clearable
+            size="small"
+            @input="getChannelList"
+          />
+        </div>
+        
+        <div v-if="!selectedGroupId" class="empty-state">
+          <p>请在左侧选择分组后查看渠道列表</p>
+        </div>
+
+        <el-table 
+          v-else
+          border 
+          v-loading="channelLoading" 
+          :data="channelList"
+          class="channel-table"
+        >
+          <el-table-column label="渠道名称" align="center" prop="channelName" min-width="150" show-overflow-tooltip />
+          <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
+            <template slot-scope="scope">
+              <span>{{ parseTime(scope.row.updateTime) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
+            <template slot-scope="scope">
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-edit"
+                @click="handleEditChannel(scope.row)"
+              >编辑</el-button>
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-document-copy"
+                @click="handleCopyChannel(scope.row)"
+              >复制</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        
+        <!-- 分页 -->
+        <el-pagination
+          v-if="channelList.length > 0"
+          :current-page="channelPageNum"
+          :page-size="channelPageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="channelTotal"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleChannelPageSizeChange"
+          @current-change="handleChannelPageChange"
+          style="text-align: right; margin-top: 10px;"
+        />
+      </el-col>
+    </el-row>
+
+    <!-- 分组对话框 -->
+    <el-dialog :title="groupDialogTitle" :visible.sync="groupDialogOpen" width="500px" append-to-body>
+      <el-form ref="groupForm" :model="groupForm" :rules="groupRules" label-width="100px">
+        <el-form-item label="分组名称" prop="channelName">
+          <el-input 
+            v-model="groupForm.channelName" 
+            placeholder="请输入分组名称"
+            clearable
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="groupDialogOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitGroup">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 渠道对话框 -->
+    <el-dialog :title="channelDialogTitle" :visible.sync="channelDialogOpen" width="500px" append-to-body>
+      <el-form ref="channelForm" :model="channelForm" :rules="channelRules" label-width="100px">
+        <el-form-item label="所属分组" prop="parentId">
+          <el-select 
+            v-model="channelForm.parentId" 
+            placeholder="请选择分组"
+            clearable
+          >
+            <el-option
+              v-for="item in groupList"
+              :key="item.id"
+              :label="item.channelName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="渠道名称" prop="channelName">
+          <el-input 
+            v-model="channelForm.channelName" 
+            placeholder="请输入渠道名称"
+            clearable
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="channelDialogOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitChannel">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 复制渠道对话框 -->
+    <el-dialog title="复制渠道" :visible.sync="batchCopyDialogOpen" width="500px" append-to-body>
+      <el-form ref="batchCopyForm" :model="batchCopyForm" :rules="batchCopyRules" label-width="100px">
+        <el-form-item label="渠道名称" prop="channelName">
+          <el-input 
+            v-model="batchCopyForm.channelName" 
+            placeholder="请输入渠道名称"
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="复制数量" prop="num">
+          <el-input-number 
+            v-model="batchCopyForm.num" 
+            :min="1" 
+            :max="1000"
+            placeholder="请输入复制数量"
+          />
+        </el-form-item>
+        <el-form-item label="起始编号" prop="start">
+          <el-input 
+            v-model="batchCopyForm.start" 
+            placeholder="请输入起始编号(仅整数)"
+            clearable
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="batchCopyDialogOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitBatchCopy">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageProject, addOrUpdateChannel, saveBatchChannel } from "@/api/adv/channel";
+
+export default {
+  name: "Channel",
+  data() {
+    return {
+      // 分组列表
+      groupList: [],
+      // 选中的分组ID
+      selectedGroupId: null,
+      // 渠道列表
+      channelList: [],
+      channelLoading: false,
+      // 渠道分页参数
+      channelPageNum: 1,
+      channelPageSize: 10,
+      channelTotal: 0,
+      // 查询参数
+      queryParams: {
+        groupChannelName: "",
+        channelChannelName: ""
+      },
+      // ... existing code ...
+      
+      // 分组对话框
+      groupDialogOpen: false,
+      groupDialogTitle: "新增分组",
+      groupForm: {
+        id: undefined,
+        channelName: ""
+      },
+      groupRules: {
+        channelName: [
+          { required: true, message: "分组名称不能为空", trigger: "blur" }
+        ]
+      },
+
+      // 渠道对话框
+      channelDialogOpen: false,
+      channelDialogTitle: "新增渠道",
+      channelForm: {
+        id: undefined,
+        channelName: "",
+        parentId: null
+      },
+      channelRules: {
+        parentId: [
+          { required: true, message: "请选择分组", trigger: "change" }
+        ],
+        channelName: [
+          { required: true, message: "渠道名称不能为空", trigger: "blur" }
+        ]
+      },
+
+      // 批量复制对话框
+      batchCopyDialogOpen: false,
+      batchCopyForm: {
+        channelName: "",
+        num: 1,
+        start: "",
+        parentId: null
+      },
+      batchCopyRules: {
+        channelName: [
+          { required: true, message: "渠道名称不能为空", trigger: "blur" }
+        ],
+        num: [
+          { required: true, message: "复制数量不能为空", trigger: "blur" },
+          { type: "number", message: "复制数量必须是整数", trigger: "blur" }
+        ],
+        start: [
+          { required: true, message: "起始编号不能为空", trigger: "blur" },
+          { pattern: /^\d+$/, message: "起始编号只能是整数", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getGroupList();
+    // 页面加载完成后默认选中“全部”
+    this.$nextTick(() => {
+      this.selectAllGroup();
+    });
+  },
+  methods: {
+    /** 获取分组列表 */
+    getGroupList() {
+      const params = { pageNum: 1, pageSize: 1000, parentId: 0 };
+      // 只有输入了名称才添加到参数中
+      if (this.queryParams.groupChannelName) {
+        params.channelName = this.queryParams.groupChannelName;
+      }
+      pageProject(params).then(response => {
+        this.groupList = response.data.records || [];
+      }).catch(error => {
+        console.error('加载分组列表失败:', error);
+        this.groupList = [];
+      });
+    },
+
+    /** 选择全部分组 */
+    selectAllGroup() {
+      this.selectedGroupId = 'all';
+      this.channelPageNum = 1;
+      this.getChannelList();
+    },
+
+    /** 选择分组 */
+    selectGroup(group) {
+      this.selectedGroupId = group.id;
+      this.channelPageNum = 1;
+      this.getChannelList();
+    },
+
+    /** 获取渠道列表 */
+    getChannelList() {
+      if (!this.selectedGroupId) return;
+      this.channelLoading = true;
+      const params = { pageNum: this.channelPageNum, pageSize: this.channelPageSize };
+      // 当不是选择"全部"时,添加 parentId 参数
+      if (this.selectedGroupId !== 'all') {
+        params.parentId = this.selectedGroupId;
+      }
+      // 只有输入了名称才添加到参数中
+      if (this.queryParams.channelChannelName) {
+        params.channelName = this.queryParams.channelChannelName;
+      }
+      pageProject(params).then(response => {
+        this.channelList = response.data.records || [];
+        this.channelTotal = response.data.total || 0;
+        this.channelLoading = false;
+      }).catch(error => {
+        console.error('加载渠道列表失败:', error);
+        this.channelList = [];
+        this.channelLoading = false;
+      });
+    },
+
+    /** 渠道分页变更 */
+    handleChannelPageChange(pageNum) {
+      this.channelPageNum = pageNum;
+      this.getChannelList();
+    },
+
+    /** 渠道与每页条数变更 */
+    handleChannelPageSizeChange(pageSize) {
+      this.channelPageSize = pageSize;
+      this.channelPageNum = 1;
+      this.getChannelList();
+    },
+
+    /** 新增分组 */
+    handleAddGroup() {
+      this.groupForm = { id: undefined, channelName: "" };
+      this.groupDialogTitle = "新增分组";
+      this.groupDialogOpen = true;
+    },
+
+    /** 提交分组 */
+    submitGroup() {
+      this.$refs["groupForm"].validate(valid => {
+        if (valid) {
+          const data = {
+            channelName: this.groupForm.channelName,
+            parentId: 0
+          };
+          addOrUpdateChannel(data).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess("新增成功");
+              this.groupDialogOpen = false;
+              this.getGroupList();
+            }
+          }).catch(error => {
+            console.error('保存分组失败:', error);
+            this.msgError("保存失败");
+          });
+        }
+      });
+    },
+
+    /** 新增渠道 */
+    handleAddChannel() {
+      this.channelForm = { id: undefined, channelName: "", parentId: this.selectedGroupId };
+      this.channelDialogTitle = "新增渠道";
+      this.channelDialogOpen = true;
+    },
+
+    /** 编辑渠道 */
+    handleEditChannel(channel) {
+      this.channelForm = {
+        id: channel.id,
+        channelName: channel.channelName,
+        parentId: channel.parentId
+      };
+      this.channelDialogTitle = "编辑渠道";
+      this.channelDialogOpen = true;
+    },
+
+    /** 提交渠道 */
+    submitChannel() {
+      this.$refs["channelForm"].validate(valid => {
+        if (valid) {
+          const data = {
+            channelName: this.channelForm.channelName,
+            parentId: this.channelForm.parentId
+          };
+          if (this.channelForm.id) {
+            data.id = this.channelForm.id;
+          }
+          addOrUpdateChannel(data).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess(this.channelForm.id ? "编辑成功" : "新增成功");
+              this.channelDialogOpen = false;
+              this.getChannelList();
+            }
+          }).catch(error => {
+            console.error('保存渠道失败:', error);
+            this.msgError("保存失败");
+          });
+        }
+      });
+    },
+
+    /** 复制渠道 */
+    handleCopyChannel(channel) {
+      this.batchCopyForm = { 
+        channelName: channel.channelName, 
+        num: 1, 
+        start: "",
+        parentId: channel.parentId
+      };
+      this.batchCopyDialogOpen = true;
+      this.$nextTick(() => {
+        this.$refs["batchCopyForm"].clearValidate();
+      });
+    },
+
+    /** 提交批量复制 */
+    submitBatchCopy() {
+      this.$refs["batchCopyForm"].validate(valid => {
+        if (valid) {
+          // 确保 start 是整数类型
+          const startNum = parseInt(this.batchCopyForm.start);
+          const data = {
+            channelName: this.batchCopyForm.channelName,
+            num: this.batchCopyForm.num,
+            start: startNum,
+            parentId: this.batchCopyForm.parentId
+          };
+          saveBatchChannel(data).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess("复制成功");
+              this.batchCopyDialogOpen = false;
+              this.getChannelList();
+            }
+          }).catch(error => {
+            console.error('批量复制失败:', error);
+            this.msgError("批量复制失败");
+          });
+        }
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.channel-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: calc(100vh - 100px);
+}
+
+.channel-wrapper {
+  height: 100%;
+}
+
+.group-section,
+.channel-section {
+  background-color: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 15px;
+  border-bottom: 2px solid #f0f0f0;
+
+  h3 {
+    margin: 0;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+  }
+}
+
+.group-list {
+  max-height: 600px;
+  overflow-y: auto;
+}
+
+.search-box {
+  margin-bottom: 15px;
+  
+  ::v-deep .el-input__inner {
+    border-radius: 6px;
+    border: 1px solid #dcdfe6;
+    transition: all 0.3s ease;
+    
+    &:focus {
+      border-color: #667eea;
+      box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+    }
+  }
+}
+
+.group-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 15px;
+  margin-bottom: 8px;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+
+  &:hover {
+    background-color: #f5f7fa;
+    border-color: #667eea;
+  }
+
+  &.active {
+    background-color: rgba(102, 126, 234, 0.1);
+    border-color: #667eea;
+    
+    .group-name {
+      color: #667eea;
+      font-weight: 600;
+    }
+  }
+
+  .group-name {
+    flex: 1;
+    font-size: 14px;
+    color: #606266;
+    word-break: break-all;
+  }
+
+  &.all-group {
+    margin-bottom: 15px;
+    padding: 14px 15px;
+    font-weight: 600;
+    border-bottom: 2px solid #e8e8e8;
+    
+    .group-name {
+      font-size: 15px;
+      font-weight: 600;
+    }
+  }
+
+  .group-actions {
+    display: flex;
+    gap: 5px;
+    margin-left: 10px;
+
+    .el-button--text {
+      color: #909399;
+
+      &:hover {
+        color: #667eea;
+      }
+    }
+  }
+}
+
+.empty-tip {
+  text-align: center;
+  padding: 40px 0;
+  color: #909399;
+  font-size: 14px;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 60px 0;
+  color: #909399;
+  font-size: 14px;
+}
+
+.channel-table {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+
+  ::v-deep .el-table__header {
+    th {
+      background-color: #f5f7fa;
+      color: #606266;
+      font-weight: 600;
+    }
+  }
+
+  ::v-deep .el-table__row {
+    transition: all 0.3s ease;
+
+    &:hover {
+      background-color: rgba(102, 126, 234, 0.05);
+    }
+  }
+}
+
+.dialog-footer {
+  text-align: right;
+}
+
+// 响应式调整
+@media (max-width: 992px) {
+  .group-section,
+  .channel-section {
+    margin-bottom: 20px;
+  }
+}
+</style>

+ 178 - 0
src/views/adv/project/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="项目名称" prop="projectName">
+        <el-input
+          v-model="queryParams.projectName"
+          placeholder="请输入项目名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 项目列表 -->
+    <el-table border v-loading="loading" :data="projectList">
+      <el-table-column label="项目名称" align="center" prop="projectName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 新增项目对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="项目名称" prop="projectName">
+          <el-input v-model="form.projectName" placeholder="请输入项目名称" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageProject, addProject } from "@/api/adv/project";
+
+export default {
+  name: "Project",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 项目表格数据
+      projectList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        projectName: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        projectName: [
+          { required: true, message: "项目名称不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询项目列表 */
+    getList() {
+      this.loading = true;
+      const params = {
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize
+      };
+      // 只有输入了名称才添加到参数中
+      if (this.queryParams.projectName) {
+        params.projectName = this.queryParams.projectName;
+      }
+      pageProject(params).then(response => {
+        this.projectList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      }).catch(error => {
+        console.error('加载项目列表失败:', error);
+        this.projectList = [];
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        projectName: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "新增项目";
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          addProject(this.form).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            }
+          }).catch(error => {
+            console.error('新增项目失败:', error);
+            this.msgError("新增失败");
+          });
+        }
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+</style>

+ 344 - 5
src/views/adv/site/index.vue

@@ -44,7 +44,7 @@
           <span>{{ parseTime(scope.row.createTime) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="360" fixed="right">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="420" fixed="right">
         <template slot-scope="scope">
           <el-button
             size="mini"
@@ -76,6 +76,12 @@
             icon="el-icon-delete"
             @click="handleDelete(scope.row)"
           >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-link"
+            @click="handleGenerateUrl(scope.row)"
+          >生成投放url</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -98,6 +104,40 @@
               :disabled="isDetail"
             />
           </el-form-item>
+          <el-form-item label="渠道" prop="channelId">
+            <el-select 
+              v-model="form.channelId" 
+              placeholder="请选择渠道" 
+              style="width: 100%"
+              filterable
+              @change="handleChannelChange"
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in channelList"
+                :key="item.id"
+                :label="item.channelName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="项目" prop="projectId">
+            <el-select 
+              v-model="form.projectId" 
+              placeholder="请选择项目" 
+              style="width: 100%"
+              filterable
+              @change="handleProjectChange"
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in projectList"
+                :key="item.id"
+                :label="item.projectName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
         </div>
 
         <!-- 投放配置 -->
@@ -138,6 +178,65 @@
               />
             </el-select>
           </el-form-item>
+          
+          <!-- 企微分配规则 -->
+          <el-form-item label="企微分配规则" prop="allocationRule">
+            <el-radio-group v-model="form.allocationRule" @change="handleAllocationRuleChange" :disabled="isDetail">
+              <el-radio 
+                label="1"
+                :border="true"
+              >个人码分配</el-radio>
+              <el-radio 
+                label="0"
+                :border="true"
+              >活码分配</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          
+          <!-- 根据选择的分配规则显示不同的下拉框 -->
+          <el-form-item 
+            v-if="form.allocationRule === '1'" 
+            label="企业微信" 
+            prop="allocationRuleId"
+            class="slide-fade"
+          >
+            <el-select 
+              v-model="form.allocationRuleId" 
+              placeholder="请选择企业微信" 
+              style="width: 100%"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in allocationRuleList"
+                :key="item.id"
+                :label="item.allocationRuleName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item 
+            v-if="form.allocationRule === '0'" 
+            label="群活码" 
+            prop="allocationRuleId"
+            class="slide-fade"
+          >
+            <el-select 
+              v-model="form.allocationRuleId" 
+              placeholder="请选择群活码" 
+              style="width: 100%"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in groupActiveList"
+                :key="item.id"
+                :label="item.groupActiveName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
         </div>
 
         <!-- 广告商信息 -->
@@ -347,6 +446,12 @@
           <span v-else-if="detail.distributeType === 2">自动分配</span>
           <span v-else>-</span>
         </el-descriptions-item>
+        <el-descriptions-item label="一级分配规则">
+          <span v-if="detail.allocationRule === '1'">选择员工"个人码"分配规则</span>
+          <span v-else-if="detail.allocationRule === '0'">选择员工"活码"分配规则</span>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="二级分配汇总">{{ detail.allocationRuleId || '-' }}</el-descriptions-item>
         <el-descriptions-item label="分配规则">{{ detail.distributeRule || '-' }}</el-descriptions-item>
         <el-descriptions-item label="来源名称">{{ detail.sourceName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="项目名称">{{ detail.projectName || '-' }}</el-descriptions-item>
@@ -390,6 +495,40 @@
         <el-button @click="statisticsOpen = false">关 闭</el-button>
       </div>
     </el-dialog>
+
+    <!-- 生成投放url对话框 -->
+    <el-dialog title="生成投放url" :visible.sync="urlDialogOpen" width="500px" append-to-body>
+      <el-form label-width="100px">
+        <el-form-item label="投放链接">
+          <div class="url-container">
+            <el-input 
+              v-model="launchUrl" 
+              readonly
+              class="url-input"
+            />
+            <el-button 
+              type="primary" 
+              size="small"
+              icon="el-icon-document-copy"
+              @click="copyUrl"
+            >复制</el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="二维码">
+          <div id="qrcode" class="qrcode-container"></div>
+          <el-button 
+            type="primary" 
+            size="small"
+            icon="el-icon-download"
+            @click="downloadQrcode"
+            style="margin-top: 10px;"
+          >下载</el-button>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="urlDialogOpen = false">关 闭</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -398,9 +537,14 @@ import { listSite, getSite, addSite, updateSite, delSite, getSiteStatistics, ena
 import { pageAdvertiser } from "@/api/adv/advertiser";
 import { pagePromotionAccount } from "@/api/adv/promotionAccount";
 import { pageTemplate } from "@/api/adv/landingPageTemplate";
+import QRCode from 'qrcode';
 
 import { pageCallbackAccount, getCallbackAccount, queryEventType, saveEventType } from "@/api/adv/callbackAccount";
 import { pageDomain } from "@/api/adv/domain";
+import { pageProject as pageChannel } from "@/api/adv/channel";
+import { pageProject } from "@/api/adv/project";
+import { listAllocationRule } from "@/api/qw/allocationRule";
+import { listGroupActive } from "@/api/qw/groupActive";
 
 export default {
   name: "Site",
@@ -446,17 +590,28 @@ export default {
       promotionAccountList: [],
       // 落地页模板列表
       landingPageTemplateList: [],
-
+      // 企业微信分配规则列表
+      allocationRuleList: [],
+      // 群活码列表
+      groupActiveList: [],
       // 回传账号列表
       callbackAccountList: [],
       // 投放域名列表
       launchDomainList: [],
+      // 渠道列表
+      channelList: [],
+      // 项目列表
+      projectList: [],
       // 转换事件列表
       conversionEvents: [],
       // 广告商事件选项(systemBuiltin='0')
       advertiserEventOptions: [],
       // 系统事件选项(systemBuiltin='1')
       systemEventOptions: [],
+      // 生成url对话框
+      urlDialogOpen: false,
+      // 投放链接
+      launchUrl: "",
       // 表单校验
       rules: {
         siteName: [
@@ -533,14 +688,21 @@ export default {
         promotionAccountName: undefined,
         launchPageId: undefined,
         launchPageName: undefined,
-
+        projectId: undefined,
+        projectName: undefined,
+        channelId: undefined,
+        channelName: undefined,
         launchDomain: undefined,
         configCallback: 0,
         callbackAccountId: undefined,
-        callbackAccountName: undefined
+        callbackAccountName: undefined,
+        allocationRule: undefined,
+        allocationRuleId: undefined
       };
       this.promotionAccountList = [];
       this.launchDomainList = [];
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
       this.conversionEvents = [];
       this.advertiserEventOptions = [];
       this.systemEventOptions = [];
@@ -580,6 +742,14 @@ export default {
             }
           }
         }
+        // 如果服务持了批釋规则,加载对应的新批釋数据
+        if (this.form.allocationRule) {
+          if (this.form.allocationRule === '1') {
+            this.loadAllocationRuleList();
+          } else if (this.form.allocationRule === '0') {
+            this.loadGroupActiveList();
+          }
+        }
         this.open = true;
         this.title = "修改站点";
       });
@@ -659,7 +829,7 @@ export default {
         this.msgSuccess("删除成功");
       }).catch(function() {});
     },
-    /** 启用/停用按操作 */
+    /** 启用/停用按操作 */
     handleEnable(row) {
       const statusText = row.status === 1 ? '停用' : '启用';
       this.$confirm(`是否确认${statusText}站点"${row.siteName}"?`, "提示", {
@@ -673,6 +843,67 @@ export default {
         this.msgSuccess(`${statusText}成功`);
       }).catch(function() {});
     },
+    /** 生成投放url */
+    handleGenerateUrl(row) {
+      this.launchUrl = row.siteUrl || '';
+      this.urlDialogOpen = true;
+      this.$nextTick(() => {
+        this.generateQrcode();
+      });
+    },
+    /** 生成二维码 */
+    generateQrcode() {
+      const qrcodeElement = document.getElementById('qrcode');
+      if (qrcodeElement) {
+        qrcodeElement.innerHTML = '';
+        // 创建 canvas 元素
+        const canvas = document.createElement('canvas');
+        QRCode.toCanvas(canvas, this.launchUrl, {
+          errorCorrectionLevel: 'H',
+          type: 'image/jpeg',
+          quality: 0.95,
+          margin: 1,
+          width: 200
+        }).then(() => {
+          qrcodeElement.appendChild(canvas);
+        }).catch(error => {
+          console.error('生成二维码失败:', error);
+          this.msgError('生成二维码失败');
+        });
+      }
+    },
+    /** 复制url */
+    copyUrl() {
+      if (!this.launchUrl) {
+        this.msgError('投放链接为空');
+        return;
+      }
+      navigator.clipboard.writeText(this.launchUrl).then(() => {
+        this.msgSuccess('复制成功');
+      }).catch(() => {
+        // 平台不支持新API,改成传统方法
+        const textarea = document.createElement('textarea');
+        textarea.value = this.launchUrl;
+        document.body.appendChild(textarea);
+        textarea.select();
+        document.execCommand('copy');
+        document.body.removeChild(textarea);
+        this.msgSuccess('复制成功');
+      });
+    },
+    /** 下载二维码 */
+    downloadQrcode() {
+      const qrcodeCanvas = document.querySelector('#qrcode canvas');
+      if (qrcodeCanvas) {
+        const link = document.createElement('a');
+        link.href = qrcodeCanvas.toDataURL();
+        link.download = `qrcode_${new Date().getTime()}.png`;
+        link.click();
+        this.msgSuccess('下载成功');
+      } else {
+        this.msgError('没有找到二维码');
+      }
+    },
     /** 加载下拉选项数据 */
     loadSelectOptions() {
       // 加载广告商列表(已废弃,改用loadAdvertiserList)
@@ -716,6 +947,22 @@ export default {
         console.error('加载域名失败:', error);
         this.launchDomainList = [];
       });
+      
+      // 加载渠道列表
+      pageChannel({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.channelList = response.data.records || [];
+      }).catch(error => {
+        console.error('加载渠道列表失败:', error);
+        this.channelList = [];
+      });
+      
+      // 加载项目列表
+      pageProject({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.projectList = response.data.records || [];
+      }).catch(error => {
+        console.error('加载项目列表失败:', error);
+        this.projectList = [];
+      });
     },
     /** 投放类型变化时 */
     handleLaunchTypeChange(launchType) {
@@ -735,6 +982,12 @@ export default {
       this.advertiserEventOptions = [];
       this.systemEventOptions = [];
       
+      // 重置企微分配规则相关数据
+      this.form.allocationRule = undefined;
+      this.form.allocationRuleId = undefined;
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      
       // 根据投放类型加载广告商列表
       if (launchType) {
         this.loadAdvertiserList(launchType);
@@ -815,6 +1068,30 @@ export default {
         }
       }
     },
+    
+    /** 渠道变化时 */
+    handleChannelChange(channelId) {
+      this.form.channelName = "";
+      
+      if (channelId) {
+        const channel = this.channelList.find(item => item.id === channelId);
+        if (channel) {
+          this.form.channelName = channel.channelName;
+        }
+      }
+    },
+
+    /** 项目变化时 */
+    handleProjectChange(projectId) {
+      this.form.projectName = "";
+      
+      if (projectId) {
+        const project = this.projectList.find(item => item.id === projectId);
+        if (project) {
+          this.form.projectName = project.projectName;
+        }
+      }
+    },
 
     /** 回传账号变化时 */
     handleCallbackAccountChange(callbackAccountId) {
@@ -842,6 +1119,12 @@ export default {
       this.advertiserEventOptions = [];
       this.systemEventOptions = [];
       
+      // 重置企微分配规则相关数据
+      this.form.allocationRule = undefined;
+      this.form.allocationRuleId = undefined;
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      
       // 如果选择配置回传,且已选择广告商,加载回传账号列表
       if (value === 1 && this.form.advertiserId) {
         this.loadCallbackAccountList(this.form.advertiserId);
@@ -920,6 +1203,40 @@ export default {
         this.conversionEvents[index].systemEventTypeName = event.eventName;
       }
     },
+    /** 企微分配规则变化 */
+    handleAllocationRuleChange(allocationRule) {
+      // 重置需要加载的数据列表
+      this.form.allocationRuleId = undefined;
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      
+      // 根据不同的分配规则加载对应数据
+      if (allocationRule === '1') {
+        // 加载企业微信分配规则列表
+        this.loadAllocationRuleList();
+      } else if (allocationRule === '0') {
+        // 加载群活码列表
+        this.loadGroupActiveList();
+      }
+    },
+    /** 加载企业微信分配规则列表 */
+    loadAllocationRuleList() {
+      listAllocationRule().then(response => {
+        this.allocationRuleList = response.data || [];
+      }).catch(error => {
+        console.error('加载企业微信分配规则列表失败:', error);
+        this.allocationRuleList = [];
+      });
+    },
+    /** 加载群活码列表 */
+    loadGroupActiveList() {
+      listGroupActive().then(response => {
+        this.groupActiveList = response.data || [];
+      }).catch(error => {
+        console.error('加载群活码列表失败:', error);
+        this.groupActiveList = [];
+      });
+    },
     /** 保存转换事件 */
     saveConversionEvents() {
       // 调用保存接口
@@ -1253,5 +1570,27 @@ export default {
     }
   }
 }
+
+// 投放链接对话框样式
+.url-container {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  
+  .url-input {
+    flex: 1;
+  }
+}
+
+.qrcode-container {
+  display: flex;
+  justify-content: center;
+  padding: 20px 0;
+  
+  canvas {
+    border: 1px solid #e0e0e0;
+    border-radius: 4px;
+  }
+}
 </style>