Ver Fonte

Merge remote-tracking branch 'origin/feature/new_adv'

# Conflicts:
#	package.json
zhangqin há 4 dias atrás
pai
commit
39dcafe3c2
56 ficheiros alterados com 10965 adições e 291 exclusões
  1. 62 0
      src/api/adv/advertiser.js
  2. 70 0
      src/api/adv/callbackAccount.js
  3. 29 0
      src/api/adv/channel.js
  4. 18 0
      src/api/adv/configuration.js
  5. 10 0
      src/api/adv/conversionLog.js
  6. 63 0
      src/api/adv/domain.js
  7. 73 0
      src/api/adv/landingPageTemplate.js
  8. 19 0
      src/api/adv/project.js
  9. 54 0
      src/api/adv/promotionAccount.js
  10. 60 0
      src/api/adv/site.js
  11. 34 0
      src/api/adv/siteStatistics.js
  12. 36 0
      src/api/adv/trackingLink.js
  13. 52 0
      src/api/qw/assignRule.js
  14. 2 64
      src/api/qw/contactWay.js
  15. 61 0
      src/api/qw/customerLink.js
  16. 35 0
      src/api/qw/groupActual.js
  17. 44 0
      src/api/qw/groupLiveCode.js
  18. BIN
      src/assets/images/image_de.png
  19. BIN
      src/assets/images/link-button.gif
  20. BIN
      src/assets/images/qr_code.png
  21. 36 4
      src/components/H5/FormWrapper.vue
  22. 1 12
      src/components/H5/config-item/common-config.vue
  23. 150 0
      src/components/H5/config-item/h5-add-wechat-button-config.vue
  24. 137 0
      src/components/H5/config-item/h5-form-config.vue
  25. 29 0
      src/components/H5/config-item/h5-image-config.vue
  26. 127 0
      src/components/H5/config-item/h5-link-button-config.vue
  27. 188 0
      src/components/H5/config-item/h5-qrcode-config.vue
  28. 3 2
      src/components/H5/css/base.css
  29. 102 0
      src/components/H5/h5-add-wechat-button.vue
  30. 233 0
      src/components/H5/h5-form.vue
  31. 10 1
      src/components/H5/h5-image.vue
  32. 102 0
      src/components/H5/h5-link-button.vue
  33. 127 0
      src/components/H5/h5-qrcode.vue
  34. 5 2
      src/components/H5/h5-sep.vue
  35. 4 0
      src/components/H5/h5-text.vue
  36. 1024 202
      src/components/H5Editor/index.vue
  37. 0 2
      src/router/index.js
  38. 1 0
      src/utils/errorCode.js
  39. 2 2
      src/utils/request.js
  40. 271 0
      src/views/adv/advertiser/index.vue
  41. 618 0
      src/views/adv/callbackAccount/index.vue
  42. 622 0
      src/views/adv/channel/index.vue
  43. 269 0
      src/views/adv/configuration/index.vue
  44. 163 0
      src/views/adv/conversionLog/index.vue
  45. 310 0
      src/views/adv/customPromotionAccount/index.vue
  46. 297 0
      src/views/adv/domain/index.vue
  47. 431 0
      src/views/adv/landingPageTemplate/index.vue
  48. 178 0
      src/views/adv/project/index.vue
  49. 736 0
      src/views/adv/promotionAccount/index.vue
  50. 1637 0
      src/views/adv/site/index.vue
  51. 602 0
      src/views/adv/statistics/index.vue
  52. 211 0
      src/views/adv/trackingLink/index.vue
  53. 357 0
      src/views/qw/assignRule/index.vue
  54. 693 0
      src/views/qw/customerLink/index.vue
  55. 304 0
      src/views/qw/groupActual/index.vue
  56. 263 0
      src/views/qw/groupLiveCode/index.vue

+ 62 - 0
src/api/adv/advertiser.js

@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// 分页查询广告商列表
+export function pageAdvertiser(query) {
+  return request({
+    url: '/adv/advertiser/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据ID查询广告商详情
+export function getAdvertiser(id) {
+  return request({
+    url: '/adv/advertiser/' + id,
+    method: 'get'
+  })
+}
+
+// 创建广告商
+export function addAdvertiser(data) {
+  return request({
+    url: '/adv/advertiser',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新广告商
+export function updateAdvertiser(id, data) {
+  return request({
+    url: '/adv/advertiser/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除广告商
+export function delAdvertiser(id) {
+  return request({
+    url: '/adv/advertiser/' + id,
+    method: 'delete'
+  })
+}
+
+// 批量删除广告商
+export function batchDelAdvertiser(ids) {
+  return request({
+    url: '/adv/advertiser/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+
+// 启用/停用广告商
+export function enableAdvertiser(id) {
+  return request({
+    url: '/adv/advertiser/enable/' + id,
+    method: 'post'
+  })
+}
+

+ 70 - 0
src/api/adv/callbackAccount.js

@@ -0,0 +1,70 @@
+import request from '@/utils/request'
+
+// 分页查询回传账号列表
+export function pageCallbackAccount(query) {
+  return request({
+    url: '/adv/callback-account/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据ID查询回传账号详情
+export function getCallbackAccount(id) {
+  return request({
+    url: '/adv/callback-account/' + id,
+    method: 'get'
+  })
+}
+
+// 创建回传账号
+export function addCallbackAccount(data) {
+  return request({
+    url: '/adv/callback-account',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新回传账号
+export function updateCallbackAccount(id, data) {
+  return request({
+    url: '/adv/callback-account/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除回传账号
+export function delCallbackAccount(id) {
+  return request({
+    url: '/adv/callback-account/' + id,
+    method: 'delete'
+  })
+}
+
+// 批量删除回传账号
+export function batchDelCallbackAccount(ids) {
+  return request({
+    url: '/adv/callback-account/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+
+// 查询事件类型
+export function queryEventType(advertiserId) {
+  return request({
+    url: '/adv/callback-account/queryEventType/' + advertiserId,
+    method: 'post'
+  })
+}
+
+// 保存转换事件
+export function saveEventType(id, data) {
+  return request({
+    url: '/adv/callback-account/saveEventType/' + id,
+    method: 'post',
+    data: data
+  })
+}

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

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

+ 18 - 0
src/api/adv/configuration.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询配置详情
+export function getConfigDetail() {
+    return request({
+        url: '/adv/config/detail',
+        method: 'get'
+    })
+}
+
+// 新增或修改配置
+export function addOrUpdateConfig(data) {
+    return request({
+        url: '/adv/config/addOrUpdate',
+        method: 'post',
+        data: data
+    })
+}

+ 10 - 0
src/api/adv/conversionLog.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 分页查询回传事件列表
+export function pageConversionLog(query) {
+  return request({
+    url: '/adv/conversion-log/page',
+    method: 'get',
+    params: query
+  })
+}

+ 63 - 0
src/api/adv/domain.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 分页查询域名列表
+export function pageDomain(query) {
+  return request({
+    url: '/adv/domains/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据ID查询域名详情
+export function getDomain(id) {
+  return request({
+    url: '/adv/domains/' + id,
+    method: 'get'
+  })
+}
+
+// 新增域名
+export function addDomain(data) {
+  return request({
+    url: '/adv/domains',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新域名
+export function updateDomain(id, data) {
+  return request({
+    url: '/adv/domains/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除域名
+export function delDomain(id) {
+  return request({
+    url: '/adv/domains/' + id,
+    method: 'delete'
+  })
+}
+
+// 批量删除域名
+export function batchDelDomain(ids) {
+  return request({
+    url: '/adv/domains/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+
+// 启用/禁用域名
+export function updateDomainStatus(id, status) {
+  return request({
+    url: '/adv/domains/' + id + '/status',
+    method: 'put',
+    params: { status }
+  })
+}
+

+ 73 - 0
src/api/adv/landingPageTemplate.js

@@ -0,0 +1,73 @@
+import request from '@/utils/request'
+
+// 分页查询模板列表
+export function pageTemplate(query) {
+  return request({
+    url: '/adv/landing-page-templates/page',
+    method: 'get',
+    params: query
+  })
+}
+
+
+
+// 根据ID查询模板详情
+export function getTemplate(id) {
+  return request({
+    url: '/adv/landing-page-templates/' + id,
+    method: 'get'
+  })
+}
+
+// 新增模板
+export function addTemplate(data) {
+  return request({
+    url: '/adv/landing-page-templates',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新模板
+export function updateTemplate(id, data) {
+  return request({
+    url: '/adv/landing-page-templates/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除模板
+export function delTemplate(id) {
+  return request({
+    url: '/adv/landing-page-templates/' + id,
+    method: 'delete'
+  })
+}
+
+// 批量删除模板
+export function batchDelTemplate(ids) {
+  return request({
+    url: '/adv/landing-page-templates/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+
+// 启用/禁用模板
+export function updateTemplateStatus(id, status) {
+  return request({
+    url: '/adv/landing-page-templates/enable/' + id,
+    method: 'post',
+    params: { status }
+  })
+}
+
+// 复制模板
+export function copyTemplate(id) {
+  return request({
+    url: '/adv/landing-page-templates/' + id + '/copy',
+    method: 'post'
+  })
+}
+

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

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

+ 54 - 0
src/api/adv/promotionAccount.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 分页查询推广账号列表
+export function pagePromotionAccount(query) {
+  return request({
+    url: '/adv/promotion-account/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据ID查询推广账号详情
+export function getPromotionAccount(id) {
+  return request({
+    url: '/adv/promotion-account/' + id,
+    method: 'get'
+  })
+}
+
+// 创建推广账号
+export function addPromotionAccount(data) {
+  return request({
+    url: '/adv/promotion-account',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新推广账号
+export function updatePromotionAccount(id, data) {
+  return request({
+    url: '/adv/promotion-account/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除推广账号
+export function delPromotionAccount(id) {
+  return request({
+    url: '/adv/promotion-account/' + id,
+    method: 'delete'
+  })
+}
+
+// 批量删除推广账号
+export function batchDelPromotionAccount(ids) {
+  return request({
+    url: '/adv/promotion-account/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+

+ 60 - 0
src/api/adv/site.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询站点列表
+export function listSite() {
+  return request({
+    url: '/adv/site/list',
+    method: 'get'
+  })
+}
+
+// 查询站点详情
+export function getSite(id) {
+  return request({
+    url: '/adv/site/' + id,
+    method: 'get'
+  })
+}
+
+// 创建站点
+export function addSite(data) {
+  return request({
+    url: '/adv/site',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新站点
+export function updateSite(id, data) {
+  return request({
+    url: '/adv/site/' + id,
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除站点
+export function delSite(id) {
+  return request({
+    url: '/adv/site/' + id,
+    method: 'delete'
+  })
+}
+
+// 查询站点统计数据
+export function getSiteStatistics(id) {
+  return request({
+    url: '/adv/site/' + id + '/statistics',
+    method: 'get'
+  })
+}
+
+// 启用/停用站点
+export function enableSite(id) {
+  return request({
+    url: '/adv/site/enable/' + id,
+    method: 'post'
+  })
+}
+

+ 34 - 0
src/api/adv/siteStatistics.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 查询站点统计列表
+export function pageSiteStatistics(query) {
+  return request({
+    url: '/adv/site-statistics/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询站点统计详情
+export function getSiteStatistics(id) {
+  return request({
+    url: '/adv/site-statistics/' + id,
+    method: 'get'
+  })
+}
+
+// 根据站点ID查询统计
+export function getSiteStatisticsBySiteId(siteId) {
+  return request({
+    url: '/adv/site-statistics/site/' + siteId,
+    method: 'get'
+  })
+}
+
+// 刷新站点统计数据
+export function refreshSiteStatistics(siteId) {
+  return request({
+    url: '/adv/site-statistics/refresh/' + siteId,
+    method: 'post'
+  })
+}

+ 36 - 0
src/api/adv/trackingLink.js

@@ -0,0 +1,36 @@
+import request from '@/utils/request'
+
+// 分页查询监测链接列表
+export function pageTrackingLink(query) {
+  return request({
+    url: '/adv/tracking-link/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询所有监测链接列表(不分页)
+export function listTrackingLink(query) {
+  return request({
+    url: '/adv/tracking-link/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据ID查询监测链接详情
+export function getTrackingLink(id) {
+  return request({
+    url: '/adv/tracking-link/' + id,
+    method: 'get'
+  })
+}
+
+// 根据广告商ID查询监测链接列表
+export function getTrackingLinkByAdvertiser(advertiserId) {
+  return request({
+    url: '/adv/tracking-link/advertiser/' + advertiserId,
+    method: 'get'
+  })
+}
+

+ 52 - 0
src/api/qw/assignRule.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询分配规则列表
+export function listAssignRule(query) {
+  return request({
+    url: '/qwAssignRule/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询分配规则列表(指定参数版本)
+export function pageAssignRule(params) {
+  return request({
+    url: '/qwAssignRule/page',
+    method: 'get',
+    params: params
+  })
+}
+
+// 获取分配规则详细
+export function getAssignRule(id) {
+  return request({
+    url: '/qwAssignRule/' + id,
+    method: 'get'
+  })
+}
+
+// 新增或编辑分配规则
+export function addOrUpdateAssignRule(data) {
+  return request({
+    url: '/qwAssignRule/addOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除分配规则
+export function delAssignRule(id) {
+  return request({
+    url: '/qwAssignRule/' + id,
+    method: 'delete'
+  })
+}
+
+// 启用/停用分配规则
+export function enableAssignRule(id, status) {
+  return request({
+    url: '/qwAssignRule/enable/' + id + '/' + status,
+    method: 'post'
+  })
+}

+ 2 - 64
src/api/qw/contactWay.js

@@ -1,71 +1,9 @@
 import request from '@/utils/request'
 
-// 查询企微活码列表
-export function listContactWay(query) {
+// 查询企活码列表
+export function listContactWay() {
   return request({
     url: '/qw/contactWay/list',
-    method: 'get',
-    params: query
-  })
-}
-export function sync(id) {
-  return request({
-    url: '/qw/contactWay/sync/' + id,
-    method: 'get'
-  })
-}
-export function informationList() {
-  return request({
-    url: '/qw/contactWay/informationList',
     method: 'get'
   })
 }
-export function statistics(query) {
-  return request({
-    url: '/qw/contactWay/statistics',
-    method: 'get',
-    params: query
-  })
-}
-// 查询企微活码详细
-export function getContactWay(id) {
-  return request({
-    url: '/qw/contactWay/' + id,
-    method: 'get'
-  })
-}
-
-// 新增企微活码
-export function addContactWay(data) {
-  return request({
-    url: '/qw/contactWay',
-    method: 'post',
-    data: data
-  })
-}
-
-// 修改企微活码
-export function updateContactWay(data) {
-  return request({
-    url: '/qw/contactWay',
-    method: 'put',
-    data: data
-  })
-}
-
-// 删除企微活码
-export function delContactWay(id) {
-  return request({
-    url: '/qw/contactWay/' + id,
-    method: 'delete'
-  })
-}
-
-// 导出企微活码
-export function exportContactWay(query) {
-  return request({
-    url: '/qw/contactWay/export',
-    method: 'get',
-    params: query
-  })
-}

+ 61 - 0
src/api/qw/customerLink.js

@@ -0,0 +1,61 @@
+import request from '@/utils/request'
+
+// 查询渠道链接列表
+export function pageCustomerLink(params) {
+  return request({
+    url: '/qwCustomerLink/page',
+    method: 'get',
+    params: params
+  })
+}
+
+// 获取渠道链接详情
+export function getCustomerLink(id) {
+  return request({
+    url: '/qwCustomerLink/' + id,
+    method: 'get'
+  })
+}
+
+// 新增或编辑渠道链接
+export function createOrUpdateCustomerLink(data) {
+  return request({
+    url: '/qwCustomerLink/createOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除渠道链接
+export function deleteCustomerLink(id) {
+  return request({
+    url: '/qwCustomerLink/delete/' + id,
+    method: 'post'
+  })
+}
+
+// 查询渠道链接的渠道列表
+export function pageCustomerLinkChannel(params) {
+  return request({
+    url: '/qwCustomerLink/channel/page',
+    method: 'get',
+    params: params
+  })
+}
+
+// 新增渠道链接的渠道
+export function createCustomerLinkChannel(data) {
+  return request({
+    url: '/qwCustomerLink/channel/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除渠道链接的渠道
+export function deleteCustomerLinkChannel(id) {
+  return request({
+    url: '/qwCustomerLink/channel/delete/' + id,
+    method: 'post'
+  })
+}

+ 35 - 0
src/api/qw/groupActual.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// 查询群实际码列表
+export function listGroupActual(query) {
+  return request({
+    url: '/qwGroupActual/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询群实际码详细
+export function getGroupActual(id) {
+  return request({
+    url: '/qwGroupActual/' + id,
+    method: 'get'
+  })
+}
+
+// 新增或修改群实际码
+export function addOrUpdateGroupActual(data) {
+  return request({
+    url: '/qwGroupActual/addOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除群实际码
+export function delGroupActual(id) {
+  return request({
+    url: '/qwGroupActual/' + id,
+    method: 'delete'
+  })
+}

+ 44 - 0
src/api/qw/groupLiveCode.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询活码列表
+export function listGroupLiveCode(query) {
+  return request({
+    url: '/qwGroupLiveCode/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询活码列表(指定参数版本)
+export function pageGroupLiveCode(params) {
+  return request({
+    url: '/qwGroupLiveCode/page',
+    method: 'get',
+    params: params
+  })
+}
+
+// 查询活码详细
+export function getGroupLiveCode(id) {
+  return request({
+    url: '/qwGroupLiveCode/' + id,
+    method: 'get'
+  })
+}
+
+// 新增或修改活码
+export function addOrUpdateGroupLiveCode(data) {
+  return request({
+    url: '/qwGroupLiveCode/addOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除活码
+export function delGroupLiveCode(id) {
+  return request({
+    url: '/qwGroupLiveCode/' + id,
+    method: 'delete'
+  })
+}

BIN
src/assets/images/image_de.png


BIN
src/assets/images/link-button.gif


BIN
src/assets/images/qr_code.png


+ 36 - 4
src/components/H5/FormWrapper.vue

@@ -26,6 +26,34 @@
       @update:config="updateFormConfig"
     />
 
+    <!-- Form component form items -->
+    <form-form-items
+      v-if="form.type === 'h5-form'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
+    <!-- Link Button component form items -->
+    <link-button-form-items
+      v-if="form.type === 'h5-link-button'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
+    <!-- Add Wechat Button component form items -->
+    <add-wechat-button-form-items
+      v-if="form.type === 'h5-add-wechat-button'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
+    <!-- Qrcode component form items -->
+    <qrcode-form-items
+      v-if="form.type === 'h5-qrcode'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
     <chat-form-items
       v-if="form.type === 'h5-chat'"
       :config="form"
@@ -38,7 +66,6 @@
       :index="index"
       :list="list"
       @bottom-change="handleBottomChange"
-      @delete="handleDelete"
       @update:config="updateFormConfig"
     />
   </el-form>
@@ -50,6 +77,10 @@ import ImageFormItems from './config-item/h5-image-config.vue';
 import SepFormItems from './config-item/h5-sep-config.vue';
 import CommonFormItems from './config-item/common-config.vue';
 import CountdownFormItems from './config-item/h5-countdown-config.vue';
+import FormFormItems from './config-item/h5-form-config.vue';
+import LinkButtonFormItems from './config-item/h5-link-button-config.vue';
+import AddWechatButtonFormItems from './config-item/h5-add-wechat-button-config.vue';
+import QrcodeFormItems from './config-item/h5-qrcode-config.vue';
 import ChatFormItems from './config-item/h5-chat-config.vue'
 export default {
   name: 'FormWrapper',
@@ -59,6 +90,10 @@ export default {
     SepFormItems,
     CommonFormItems,
     CountdownFormItems,
+    FormFormItems,
+    LinkButtonFormItems,
+    AddWechatButtonFormItems,
+    QrcodeFormItems,
     ChatFormItems
   },
   props: {
@@ -108,9 +143,6 @@ export default {
     },
     removeClassFromItem(className) {
       this.form.classText = this.form.classText.filter(cls => cls !== className);
-    },
-    handleDelete() {
-      this.$emit('delete', this.index);
     }
   }
 }

+ 1 - 12
src/components/H5/config-item/common-config.vue

@@ -1,12 +1,7 @@
 // CommonFormItems.vue - Common form items shared across all components
 <template>
   <div id="h5-config-common" ref="commonForm">
-    <el-form-item label="是否底部">
-      <el-switch v-model="config.fixe" @change="handleBottom"></el-switch>
-    </el-form-item>
-    <el-form-item label="点击是否加微">
-      <el-switch v-model="config.addWxFun"/>
-    </el-form-item>
+
     <el-form-item label="获客链接" v-if="config.addWxFun">
       <el-select v-model="config.workUrl" filterable @change="val => updateConfig('workUrl', val)" placeholder="请选择">
         <el-option
@@ -18,9 +13,6 @@
       </el-select>
       <el-button type="primary" @click="copyLink">复制链接</el-button>
     </el-form-item>
-    <el-form-item>
-      <el-button type="danger" @click="handleDelete">删 除</el-button>
-    </el-form-item>
   </div>
 </template>
 
@@ -78,9 +70,6 @@ export default {
     },
     handleBottom() {
       this.$emit('bottom-change');
-    },
-    handleDelete() {
-      this.$emit('delete');
     }
   }
 }

+ 150 - 0
src/components/H5/config-item/h5-add-wechat-button-config.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="add-wechat-button-config">
+    <div class="config-block">
+      <h3>加微按钮配置</h3>
+
+      <!-- 按钮样式选择 -->
+      <el-form-item label="按钮样式">
+        <el-radio-group v-model="localConfig.buttonStyle" @change="handleStyleChange">
+          <el-radio label="text">文字</el-radio>
+          <el-radio label="image">图片</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- 文字样式配置 -->
+      <template v-if="localConfig.buttonStyle === 'text'">
+        <el-form-item label="按钮文字">
+          <el-input 
+            v-model="localConfig.buttonText" 
+            placeholder="请输入按钮文字"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="按钮背景色">
+          <el-color-picker
+            v-model="localConfig.buttonColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="按钮文字色">
+          <el-color-picker
+            v-model="localConfig.buttonTextColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- 图片样式配置 -->
+      <template v-if="localConfig.buttonStyle === 'image'">
+        <el-form-item label="按钮图片">
+          <image-upload 
+            v-model="localConfig.buttonImage" 
+            :file-type='["png", "jpg", "jpeg", "gif"]' 
+            :limit="1"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- 其他设置 -->
+      <el-divider></el-divider>
+      <h4>其他设置</h4>
+      <el-form-item label="获客助手跳转">
+        <el-switch v-model="localConfig.enableGetCustomerAssistant" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item label="跟随屏幕">
+        <el-switch v-model="localConfig.followScreen" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item label="默认尝试拉起微信">
+        <el-switch v-model="localConfig.enableDefaultWechat" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item label="启用小程序授权">
+        <el-switch v-model="localConfig.enableMiniProgram" @change="handleConfigChange" />
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AddWechatButtonConfigComponent',
+
+  props: {
+    config: {
+      type: Object,
+      required: false,
+      default: () => ({
+        buttonStyle: 'text',
+        buttonText: '提交',
+        buttonColor: '#FF5A5A',
+        buttonTextColor: '#ffffff',
+        buttonImage: '',
+        enableGetCustomerAssistant: true,
+        followScreen: true,
+        enableDefaultWechat: false,
+        enableMiniProgram: false
+      })
+    }
+  },
+
+  data() {
+    return {
+      localConfig: JSON.parse(JSON.stringify(this.config))
+    }
+  },
+
+  methods: {
+    handleStyleChange() {
+      // 切换样式时不清空图片,保持默认值
+      this.handleConfigChange()
+    },
+    handleConfigChange() {
+      this.$emit('update:config', JSON.parse(JSON.stringify(this.localConfig)))
+    }
+  },
+
+  watch: {
+    config: {
+      handler(newVal) {
+        this.localConfig = JSON.parse(JSON.stringify(newVal))
+      },
+      deep: true
+    },
+    'localConfig.buttonImage'(newVal) {
+      // 监听图片变化,实时更新父组件
+      this.handleConfigChange()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.add-wechat-button-config {
+  width: 100%;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+}
+
+.config-block {
+  padding: 0px 15px;
+}
+
+h3 {
+  font-weight: 500;
+  margin: 15px 0 10px 0;
+  color: #303133;
+  font-size: 16px;
+  border-bottom: 1px solid #EBEEF5;
+  padding-bottom: 10px;
+}
+
+.el-form-item {
+  margin-bottom: 16px;
+}
+</style>

+ 137 - 0
src/components/H5/config-item/h5-form-config.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="form-config">
+    <div class="config-block">
+      <h3>表单配置</h3>
+
+      <!-- 第一行图片配置 -->
+      <el-form-item label="图片显示">
+        <el-switch v-model="localConfig.showImage" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item v-if="localConfig.showImage" label="上传图片">
+        <image-upload 
+          v-model="localConfig.formImage" 
+          :file-type='["png", "jpg", "jpeg", "gif"]' 
+          :limit="1"
+          @change="handleConfigChange"
+        />
+      </el-form-item>
+
+      <!-- 提交按钮配置 -->
+      <el-form-item label="提交按钮显示">
+        <el-switch v-model="localConfig.showSubmitBtn" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item v-if="localConfig.showSubmitBtn" label="按钮样式">
+        <el-radio-group v-model="localConfig.submitBtnStyle" @change="handleConfigChange">
+          <el-radio label="text">文字</el-radio>
+          <el-radio label="image">图片</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- 文字+背景色样式配置 -->
+      <template v-if="localConfig.showSubmitBtn && localConfig.submitBtnStyle === 'text'">
+        <el-form-item label="按钮文字">
+          <el-input 
+            v-model="localConfig.submitBtnText" 
+            placeholder="请输入按钮文字"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="按钮颜色">
+          <el-color-picker
+            v-model="localConfig.submitBtnColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- 图片样式配置 -->
+      <el-form-item v-if="localConfig.showSubmitBtn && localConfig.submitBtnStyle === 'image'" label="按钮图片">
+        <image-upload 
+          v-model="localConfig.submitBtnImage" 
+          :file-type='["png", "jpg", "jpeg", "gif"]' 
+          :limit="1"
+          @change="handleConfigChange"
+        />
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FormConfigComponent',
+
+  props: {
+    config: {
+      type: Object,
+      required: false,
+      default: () => ({
+        showImage: true,
+        formImage: '',
+        showSubmitBtn: true,
+        submitBtnStyle: 'text',
+        submitBtnColor: '#409EFF',
+        submitBtnText: '提交',
+        submitBtnImage: ''
+      })
+    }
+  },
+
+  data() {
+    return {
+      localConfig: JSON.parse(JSON.stringify(this.config))
+    }
+  },
+
+  methods: {
+    handleConfigChange() {
+      this.$emit('update:config', JSON.parse(JSON.stringify(this.localConfig)))
+    }
+  },
+
+  watch: {
+    config: {
+      handler(newVal) {
+        this.localConfig = JSON.parse(JSON.stringify(newVal))
+      },
+      deep: true
+    },
+    'localConfig.formImage'(newVal) {
+      // 监听第一行图片变化,实时更新
+      this.handleConfigChange()
+    },
+    'localConfig.submitBtnImage'(newVal) {
+      // 监听提交按钮图片变化,实时更新
+      this.handleConfigChange()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.form-config {
+  width: 100%;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+}
+
+.config-block {
+  padding: 0px 15px;
+}
+
+h3 {
+  font-weight: 500;
+  margin: 15px 0 10px 0;
+  color: #303133;
+  font-size: 16px;
+  border-bottom: 1px solid #EBEEF5;
+  padding-bottom: 10px;
+}
+
+.el-form-item {
+  margin-bottom: 16px;
+}
+</style>

+ 29 - 0
src/components/H5/config-item/h5-image-config.vue

@@ -4,6 +4,10 @@
     <el-form-item label="图片">
       <image-upload v-model="config.url" :file-type='["png", "jpg", "jpeg", "gif"]' :limit="1"/>
     </el-form-item>
+    <div class="image-tips">
+      <p>建议尺寸:宽750px,高不限;</p>
+      <p>支持格式:png/jpeg/gif,不大于1M尺寸。</p>
+    </div>
   </div>
 </template>
 
@@ -18,3 +22,28 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.image-tips {
+  margin-top: 12px;
+  padding: 12px;
+  background-color: #fef0e6;
+  border-left: 3px solid #ff9800;
+  border-radius: 4px;
+}
+
+.image-tips p {
+  margin: 4px 0;
+  color: #ff9800;
+  font-size: 12px;
+  line-height: 1.6;
+}
+
+.image-tips p:first-child {
+  margin-top: 0;
+}
+
+.image-tips p:last-child {
+  margin-bottom: 0;
+}
+</style>

+ 127 - 0
src/components/H5/config-item/h5-link-button-config.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="link-button-config">
+    <div class="config-block">
+      <h3>跳转按钮配置</h3>
+
+      <!-- 按钮样式选择 -->
+      <el-form-item label="按钮样式">
+        <el-radio-group v-model="localConfig.buttonStyle" @change="handleStyleChange">
+          <el-radio label="text">文字</el-radio>
+          <el-radio label="image">图片</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- 文字样式配置 -->
+      <template v-if="localConfig.buttonStyle === 'text'">
+        <el-form-item label="按钮文字">
+          <el-input 
+            v-model="localConfig.buttonText" 
+            placeholder="请输入按钮文字"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="按钮背景色">
+          <el-color-picker
+            v-model="localConfig.buttonColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="按钮文字色">
+          <el-color-picker
+            v-model="localConfig.buttonTextColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- 图片样式配置 -->
+      <template v-if="localConfig.buttonStyle === 'image'">
+        <el-form-item label="按钮图片">
+          <image-upload 
+            v-model="localConfig.buttonImage" 
+            :file-type='["png", "jpg", "jpeg", "gif"]' 
+            :limit="1"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LinkButtonConfigComponent',
+
+  props: {
+    config: {
+      type: Object,
+      required: false,
+      default: () => ({
+        buttonStyle: 'text',
+        buttonText: '提交',
+        buttonColor: '#FF5A5A',
+        buttonTextColor: '#ffffff',
+        buttonImage: ''
+      })
+    }
+  },
+
+  data() {
+    return {
+      localConfig: JSON.parse(JSON.stringify(this.config))
+    }
+  },
+
+  methods: {
+    handleStyleChange() {
+      // 切换样式时不清空图片,保持默认值
+      this.handleConfigChange()
+    },
+    handleConfigChange() {
+      this.$emit('update:config', JSON.parse(JSON.stringify(this.localConfig)))
+    }
+  },
+
+  watch: {
+    config: {
+      handler(newVal) {
+        this.localConfig = JSON.parse(JSON.stringify(newVal))
+      },
+      deep: true
+    },
+    'localConfig.buttonImage'(newVal) {
+      // 监听图片变化,实时更新父组件
+      this.handleConfigChange()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.link-button-config {
+  width: 100%;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+}
+
+.config-block {
+  padding: 0px 15px;
+}
+
+h3 {
+  font-weight: 500;
+  margin: 15px 0 10px 0;
+  color: #303133;
+  font-size: 16px;
+  border-bottom: 1px solid #EBEEF5;
+  padding-bottom: 10px;
+}
+
+.el-form-item {
+  margin-bottom: 16px;
+}
+</style>

+ 188 - 0
src/components/H5/config-item/h5-qrcode-config.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="qrcode-config">
+    <div class="config-block">
+      <h3>二维码配置</h3>
+
+      <!-- 获客助手跳转 -->
+      <el-form-item label="获客助手跳转">
+        <el-switch v-model="localConfig.enableGetCustomerAssistant" @change="handleConfigChange" />
+      </el-form-item>
+
+      <!-- 二维码尺寸 -->
+      <el-form-item label="二维码尺寸(px)">
+        <el-input-number 
+          v-model="localConfig.qrcodeSize" 
+          :min="80"
+          :max="300"
+          @change="handleConfigChange"
+        />
+      </el-form-item>
+
+      <!-- 复制微信号区域 -->
+      <el-divider></el-divider>
+      <h4>复制微信号按钮</h4>
+      <el-form-item label="是否启用">
+        <el-switch v-model="localConfig.showCopyBtn" @change="handleConfigChange" />
+      </el-form-item>
+
+      <template v-if="localConfig.showCopyBtn">
+        <el-form-item label="按钮文字">
+          <el-input 
+            v-model="localConfig.copyBtnText" 
+            placeholder="请输入按钮文字"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="文字颜色">
+          <el-color-picker
+            v-model="localConfig.copyBtnTextColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="填充颜色">
+          <el-color-picker
+            v-model="localConfig.copyBtnColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- 点击添加老师区域 -->
+      <el-divider></el-divider>
+      <h4>点击添加老师按钮</h4>
+      <el-form-item label="是否启用">
+        <el-switch v-model="localConfig.showClickBtn" @change="handleConfigChange" />
+      </el-form-item>
+
+      <template v-if="localConfig.showClickBtn">
+        <el-form-item label="按钮文字">
+          <el-input 
+            v-model="localConfig.clickBtnText" 
+            placeholder="请输入按钮文字"
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="文字颜色">
+          <el-color-picker
+            v-model="localConfig.clickBtnTextColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="填充颜色">
+          <el-color-picker
+            v-model="localConfig.clickBtnColor"
+            show-alpha
+            @change="handleConfigChange"
+          />
+        </el-form-item>
+
+        <el-form-item label="微信内跳转">
+          <el-switch v-model="localConfig.clickBtnWechatJump" @change="handleConfigChange" />
+        </el-form-item>
+      </template>
+
+      <!-- 其他选项 -->
+      <el-divider></el-divider>
+      <h4>其他设置</h4>
+      <el-form-item label="默认尝试拉起微信">
+        <el-switch v-model="localConfig.enableDefaultWechat" @change="handleConfigChange" />
+      </el-form-item>
+
+      <el-form-item label="启用小程序授权">
+        <el-switch v-model="localConfig.enableMiniProgram" @change="handleConfigChange" />
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'QrcodeConfigComponent',
+
+  props: {
+    config: {
+      type: Object,
+      required: false,
+      default: () => ({
+        qrcodeImage: '',
+        qrcodeSize: 140,
+        showCopyBtn: true,
+        copyBtnText: '复制并添加老师',
+        copyBtnColor: '#FF9500',
+        copyBtnTextColor: '#ffffff',
+        showClickBtn: true,
+        clickBtnText: '点击添加老师',
+        clickBtnColor: '#FF9500',
+        clickBtnTextColor: '#ffffff',
+        clickBtnWechatJump: false,
+        enableGetCustomerAssistant: false,
+        enableDefaultWechat: false,
+        enableMiniProgram: false
+      })
+    }
+  },
+
+  data() {
+    return {
+      localConfig: JSON.parse(JSON.stringify(this.config))
+    }
+  },
+
+  methods: {
+    handleConfigChange() {
+      this.$emit('update:config', JSON.parse(JSON.stringify(this.localConfig)))
+    }
+  },
+
+  watch: {
+    config: {
+      handler(newVal) {
+        this.localConfig = JSON.parse(JSON.stringify(newVal))
+      },
+      deep: true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.qrcode-config {
+  width: 100%;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+}
+
+.config-block {
+  padding: 0px 15px;
+}
+
+h3 {
+  font-weight: 500;
+  margin: 15px 0 10px 0;
+  color: #303133;
+  font-size: 16px;
+  border-bottom: 1px solid #EBEEF5;
+  padding-bottom: 10px;
+}
+
+h4 {
+  font-weight: 500;
+  margin: 10px 0 8px 0;
+  color: #606266;
+  font-size: 14px;
+}
+
+.el-form-item {
+  margin-bottom: 16px;
+}
+
+.el-divider {
+  margin: 15px 0;
+}
+</style>

+ 3 - 2
src/components/H5/css/base.css

@@ -1,9 +1,10 @@
 .parent-div{
-  border: 2px dashed transparent;
+  border: none;
   /*padding: 5px 0;*/
 }
 .parent-div.active{
-  border-color: #02ff9b;
+  outline: 2px dashed #02ff9b;
+  outline-offset: -2px;
 }
 .footer{
   position: absolute;

+ 102 - 0
src/components/H5/h5-add-wechat-button.vue

@@ -0,0 +1,102 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <div class="add-wechat-button-wrapper">
+      <!-- 文字样式按钮 -->
+      <button 
+        v-if="config.buttonStyle === 'text'"
+        class="add-wechat-button text-style"
+        :style="{ 
+          backgroundColor: config.buttonColor,
+          color: config.buttonTextColor
+        }"
+        @click="handleClick"
+      >
+        {{ config.buttonText }}
+      </button>
+
+      <!-- 图片样式按钮 -->
+      <img 
+        v-else-if="config.buttonStyle === 'image'"
+        :src="config.buttonImage || require('@/assets/images/default.jpg')"
+        class="add-wechat-button image-style"
+        @click="handleClick"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'h5-add-wechat-button',
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          classText: ['parent-div'],
+          buttonStyle: 'text',
+          buttonText: '提交',
+          buttonColor: '#FF5A5A',
+          buttonTextColor: '#ffffff',
+          buttonImage: ''
+        }
+      }
+    }
+  },
+  methods: {
+    handleClick() {
+      // 点击事件可在此处理
+    }
+  }
+}
+</script>
+
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+.add-wechat-button-wrapper {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.add-wechat-button {
+  border: none;
+  border-radius: 4px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  
+  &.text-style {
+    width: 100%;
+    padding: 12px 16px;
+    font-weight: 600;
+    
+    &:hover {
+      opacity: 0.9;
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+    
+    &:active {
+      opacity: 0.8;
+      transform: translateY(0);
+    }
+  }
+  
+  &.image-style {
+    max-width: 100%;
+    height: auto;
+    object-fit: contain;
+    
+    &:hover {
+      opacity: 0.9;
+      transform: scale(1.02);
+    }
+    
+    &:active {
+      opacity: 0.8;
+      transform: scale(1);
+    }
+  }
+}
+</style>

+ 233 - 0
src/components/H5/h5-form.vue

@@ -0,0 +1,233 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <div class="form-container">
+      <!-- 第一行:图片 -->
+      <div v-if="config.showImage" class="form-image-row">
+        <img v-if="config.formImage" :src="config.formImage" class="form-image" />
+      </div>
+
+      <!-- 第二行:手机号输入框 -->
+      <div class="form-input-row">
+        <input 
+          type="tel" 
+          class="form-input phone-input" 
+          placeholder="请输入手机号"
+          v-model="phoneNumber"
+        />
+      </div>
+
+      <!-- 第三行:验证码输入框 + 获取验证码按钮 -->
+      <div class="form-code-row">
+        <input 
+          type="text" 
+          class="form-input code-input" 
+          placeholder="请输入验证码"
+          v-model="verifyCode"
+        />
+        <button class="get-code-btn" @click="getVerifyCode">
+          {{ verifyCodeBtnText }}
+        </button>
+      </div>
+
+      <!-- 第四行:提交按钮 -->
+      <div v-if="config.showSubmitBtn" class="form-submit-row">
+        <!-- 文字+背景色样式 -->
+        <button 
+          v-if="config.submitBtnStyle === 'text'"
+          class="submit-btn text-style"
+          :style="{ backgroundColor: config.submitBtnColor }"
+        >
+          {{ config.submitBtnText }}
+        </button>
+        <!-- 图片样式 -->
+        <img 
+          v-else-if="config.submitBtnStyle === 'image'"
+          :src="config.submitBtnImage || require('@/assets/images/link-button.gif')"
+          class="submit-btn image-style"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'h5-form',
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          classText: ['parent-div'],
+          showImage: true,
+          formImage: '',
+          showSubmitBtn: true,
+          submitBtnStyle: 'text', // 'text' 或 'image'
+          submitBtnColor: '#409EFF',
+          submitBtnText: '提交',
+          submitBtnImage: ''
+        }
+      }
+    }
+  },
+  data() {
+    return {
+      phoneNumber: '',
+      verifyCode: '',
+      countdownTime: 0,
+      verifyCodeBtnText: '获取验证码'
+    }
+  },
+  methods: {
+    getVerifyCode() {
+      if (!this.phoneNumber) {
+        this.$message.warning('请先输入手机号');
+        return;
+      }
+      // 启动倒计时
+      this.countdownTime = 60;
+      this.updateButtonText();
+    },
+    updateButtonText() {
+      if (this.countdownTime > 0) {
+        this.verifyCodeBtnText = `${this.countdownTime}s`;
+        this.countdownTime--;
+        setTimeout(() => {
+          this.updateButtonText();
+        }, 1000);
+      } else {
+        this.verifyCodeBtnText = '获取验证码';
+      }
+    }
+  }
+}
+</script>
+
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+.form-container {
+  width: 100%;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.active {
+  border-color: #02ff9b;
+}
+
+.form-image-row {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 8px;
+}
+
+.form-image {
+  max-width: 100%;
+  height: auto;
+  max-height: 150px;
+  border-radius: 4px;
+}
+
+.form-input-row,
+.form-code-row {
+  width: 100%;
+  display: flex;
+  gap: 8px;
+}
+
+.form-code-row {
+  justify-content: space-between;
+}
+
+.form-input {
+  border: 1px solid #DCDFE6;
+  border-radius: 20px;
+  padding: 10px 16px;
+  font-size: 14px;
+  color: #303133;
+  
+  &::placeholder {
+    color: #909399;
+  }
+
+  &:focus {
+    outline: none;
+    border-color: #409EFF;
+    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+  }
+}
+
+.phone-input {
+  width: 100%;
+}
+
+.code-input {
+  flex: 1;
+  min-width: 150px;
+}
+
+.get-code-btn {
+  padding: 10px 16px;
+  border: 1px solid #409EFF;
+  background-color: #fff;
+  color: #409EFF;
+  border-radius: 20px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  white-space: nowrap;
+
+  &:hover {
+    background-color: #F0F9FF;
+  }
+
+  &:active {
+    background-color: #E6F2FF;
+  }
+}
+
+.form-submit-row {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-top: 8px;
+}
+
+.submit-btn {
+  border: none;
+  border-radius: 20px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  width: calc(100% - 32px);
+
+  &.text-style {
+    padding: 12px 48px;
+    color: #fff;
+    font-weight: 600;
+
+    &:hover {
+      opacity: 0.9;
+    }
+
+    &:active {
+      opacity: 0.8;
+    }
+  }
+
+  &.image-style {
+    max-width: 100%;
+    height: auto;
+    max-height: 60px;
+    object-fit: contain;
+    width: auto;
+
+    &:hover {
+      opacity: 0.9;
+    }
+  }
+}
+</style>

+ 10 - 1
src/components/H5/h5-image.vue

@@ -1,6 +1,6 @@
 <template>
   <div :class="config.classText.join(' ')">
-    <img :src="config.url || require('@/assets/images/default.jpg')" :style="config.style" />
+    <img :src="config.url || require('@/assets/images/image_de.png')" :style="config.style" />
   </div>
 <!--  <img  src="@/assets/images/default.jpg" width="200" height="200" />-->
 </template>
@@ -18,8 +18,17 @@ export default {
 </script>
 <style src="./css/base.css"></style>
 <style lang="scss" scoped>
+div {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
 img{
   width: 100% !important;
+  height: auto;
+  display: block;
 }
 .active{
   border-color: #02ff9b;

+ 102 - 0
src/components/H5/h5-link-button.vue

@@ -0,0 +1,102 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <div class="link-button-wrapper">
+      <!-- 文字样式按钮 -->
+      <button 
+        v-if="config.buttonStyle === 'text'"
+        class="link-button text-style"
+        :style="{ 
+          backgroundColor: config.buttonColor,
+          color: config.buttonTextColor
+        }"
+        @click="handleClick"
+      >
+        {{ config.buttonText }}
+      </button>
+
+      <!-- 图片样式按钮 -->
+      <img 
+        v-else-if="config.buttonStyle === 'image'"
+        :src="config.buttonImage || require('@/assets/images/link-button.gif')"
+        class="link-button image-style"
+        @click="handleClick"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'h5-link-button',
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          classText: ['parent-div'],
+          buttonStyle: 'text',
+          buttonText: '提交',
+          buttonColor: '#FF5A5A',
+          buttonTextColor: '#ffffff',
+          buttonImage: ''
+        }
+      }
+    }
+  },
+  methods: {
+    handleClick() {
+      // 点击事件可在此处理
+    }
+  }
+}
+</script>
+
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+.link-button-wrapper {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.link-button {
+  border: none;
+  border-radius: 4px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  
+  &.text-style {
+    width: 100%;
+    padding: 12px 16px;
+    font-weight: 600;
+    
+    &:hover {
+      opacity: 0.9;
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+    
+    &:active {
+      opacity: 0.8;
+      transform: translateY(0);
+    }
+  }
+  
+  &.image-style {
+    max-width: 100%;
+    height: auto;
+    object-fit: contain;
+    
+    &:hover {
+      opacity: 0.9;
+      transform: scale(1.02);
+    }
+    
+    &:active {
+      opacity: 0.8;
+      transform: scale(1);
+    }
+  }
+}
+</style>

+ 127 - 0
src/components/H5/h5-qrcode.vue

@@ -0,0 +1,127 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <div class="qrcode-container">
+      <div class="qrcode-image-row">
+        <img 
+          :src="config.qrcodeImage || require('@/assets/images/qr_code.png')"
+          class="qrcode-image"
+          :style="{ width: config.qrcodeSize + 'px', height: config.qrcodeSize + 'px' }"
+        />
+      </div>
+
+
+      <div v-if="config.showCopyBtn" class="copy-btn-row">
+        <button class="circle-btn copy-btn" :style="{ backgroundColor: config.copyBtnColor, color: config.copyBtnTextColor }">
+          {{ config.copyBtnText }}
+        </button>
+      </div>
+
+      <div v-if="config.showClickBtn" class="click-btn-row">
+        <button class="circle-btn click-btn" :style="{ backgroundColor: config.clickBtnColor, color: config.clickBtnTextColor }">
+          {{ config.clickBtnText }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'h5-qrcode',
+  props: {
+    config: {
+      type: Object,
+      default: () => ({
+        classText: ['parent-div'],
+        qrcodeImage: '',
+        qrcodeSize: 140,
+        showCopyBtn: true,
+        copyBtnText: '复制并添加老师',
+        copyBtnColor: '#FF9500',
+        copyBtnTextColor: '#ffffff',
+        showClickBtn: true,
+        clickBtnText: '点击添加老师',
+        clickBtnColor: '#FF9500',
+        clickBtnTextColor: '#ffffff'
+      })
+    }
+  }
+}
+</script>
+
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+.qrcode-container {
+  width: 100%;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  align-items: center;
+}
+
+.qrcode-image-row {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 8px;
+}
+
+.qrcode-image {
+  max-width: 100%;
+  height: auto;
+  border-radius: 4px;
+}
+
+.wechat-number-row {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 8px;
+}
+
+.wechat-number-text {
+  font-size: 14px;
+  color: #303133;
+  text-align: center;
+}
+
+.copy-btn-row,
+.click-btn-row {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-bottom: 8px;
+}
+
+.circle-btn {
+  border: none;
+  border-radius: 20px;
+  padding: 10px 20px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  white-space: nowrap;
+  font-weight: 500;
+
+  &:hover {
+    opacity: 0.9;
+  }
+
+  &:active {
+    opacity: 0.8;
+  }
+}
+
+.copy-btn {
+  min-width: 160px;
+}
+
+.click-btn {
+  min-width: 160px;
+}
+
+.active {
+  border-color: #02ff9b;
+}
+</style>

+ 5 - 2
src/components/H5/h5-sep.vue

@@ -19,8 +19,11 @@ export default {
 </script>
 <style src="./css/base.css"></style>
 <style lang="scss" scoped>
-p{
-  white-space: pre-line;
+div {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  box-sizing: border-box;
 }
 .active{
   border-color: #02ff9b;

+ 4 - 0
src/components/H5/h5-text.vue

@@ -20,6 +20,10 @@ export default {
 <style lang="scss" scoped>
 p{
   white-space: pre-line;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  box-sizing: border-box;
 }
 .active{
   border-color: #02ff9b;

+ 1024 - 202
src/components/H5Editor/index.vue

@@ -1,40 +1,122 @@
 <template>
   <div class="h5-editor-container">
-    <div class="h5-editor">
+    <!-- 标题编辑栏 -->
+    <div class="title-bar">
+      <el-input v-model="pageTitle" placeholder="输入落地页标题" class="title-input" />
     </div>
-    <el-row :gutter="24">
-      <el-col :span="2" class="button-body">
-        <div v-for="item in componentList">
-          <p/>
-          <div>
-            {{ item.groupName }}
+    <el-row :gutter="16" class="editor-wrapper">
+      <!-- 左上:页面选择面板 -->
+      <el-col :span="3" class="page-panel">
+        <div class="panel-header">
+          <h3>页面选择</h3>
+        </div>
+        <div class="page-list">
+          <div 
+            v-for="page in pageList" 
+            :key="page.pageStep"
+            class="page-card" 
+            :class="{active: currentPageStep === page.pageStep}"
+            @click="switchPage(page.pageStep)"
+          >
+            <div class="page-icon">
+              <i :class="page.icon"></i>
+            </div>
+            <div class="page-name">{{ page.name }}</div>
+          </div>
+        </div>
+      </el-col>
+
+      <!-- 左下:组件面板 -->
+      <el-col :span="3" class="left-panel">
+        <div class="panel-header">
+          <h3>组件库</h3>
+        </div>
+        <div class="components-list">
+          <div v-for="(group, idx) in componentList" :key="idx" class="component-group">
+            <div class="group-title">
+              <i class="el-icon-box"></i>
+              {{ group.groupName }}
+            </div>
+            <div class="buttons-wrapper">
+              <div 
+                v-for="comp in group.comps" 
+                :key="comp.type"
+                class="comp-button" 
+                draggable="true"
+                @dragstart="handleDragStart($event, comp)"
+                @dragend="handleDragEnd"
+              >
+                <i :class="comp.icon"></i>
+                <span>{{ comp.label }}</span>
+              </div>
+            </div>
           </div>
-          <el-button class="button-tag" @click="add(item)" v-for="item in item.comps">
-            <!-- 添加图标和文字 -->
-            <i :class="item.icon" style="margin-right: 5px"></i>
-            {{ item.label }}
-          </el-button>
         </div>
       </el-col>
-      <el-col :span="12">
+      
+      <!-- 中间:预览区域 -->
+      <el-col :span="10" class="center-panel">
+        <div class="panel-header">
+          <h3>页面预览</h3>
+          <span class="device-info">手机预览 (375px)</span>
+        </div>
         <div class="parent-container">
-          <div class="view-body">
-            <draggable v-model="list" @end="end">
-              <div v-for="(item, index) in list" :key="index" @click="select(index)" class="view-item">
-                <component :is="item.type" :config="item"/>
+          <!-- 模拟手机导航栏 -->
+          <div class="mobile-nav-bar">
+            <div class="nav-left">
+              <i class="el-icon-arrow-left"></i>
+            </div>
+            <div class="nav-title">{{ pageTitle || '落地页标题' }}</div>
+            <div class="nav-right">
+              <i class="el-icon-more"></i>
+            </div>
+          </div>
+          <div class="view-body" @dragover.prevent="handleDragOver" @drop="handleDrop" @dragleave="handleDragLeave" @mousemove="handleMouseMove">
+            <draggable 
+              v-model="list" 
+              @end="end" 
+              class="draggable-area"
+              animation="200"
+              group="components"
+              :disabled="false"
+              ghost-class="ghost"
+              drag-class="drag"
+            >
+              <div v-for="(item, index) in list" :key="'item-' + index" 
+                   class="draggable-wrapper"
+                   @dragenter="handleItemDragEnter($event, index)"
+                   @dragover="handleItemDragOver($event, index)"
+                   @dragleave="handleItemDragLeave($event, index)">
+                <div @click="select(index)" class="view-item" :class="{active: item.active}">
+                  <component :is="item.type" :config="item"/>
+                  <!-- 删除按钮 -->
+                  <div v-if="item.active" class="delete-button-wrapper" @click.stop="handleDelete(index)">
+                    <i class="el-icon-close"></i>
+                  </div>
+                </div>
               </div>
             </draggable>
+            <div v-if="list.length === 0" class="empty-state">
+              <i class="el-icon-picture"></i>
+              <p>从左侧拖拽或点击添加组件</p>
+            </div>
           </div>
         </div>
       </el-col>
-      <el-col :span="10" style="overflow-y: auto;height:80vh;">
-        <form-wrapper
-          :form="form"
-          :index="index"
-          :list="list"
-          @update:form="updateForm"
-          @delete="del"
-        />
+      
+      <!-- 右侧:属性面板 -->
+      <el-col :span="8" class="right-panel">
+        <div class="panel-header">
+          <h3>属性设置</h3>
+        </div>
+        <div class="properties-wrapper">
+          <form-wrapper
+            :form="form"
+            :index="index"
+            :list="list"
+            @update:form="updateForm"
+          />
+        </div>
       </el-col>
     </el-row>
   </div>
@@ -47,6 +129,10 @@ import H5Image from '@/components/H5/h5-image.vue'
 import H5Button from '@/components/H5/h5-button.vue'
 import H5Sep from '@/components/H5/h5-sep.vue'
 import H5Countdown from '@/components/H5/h5-countdown.vue'
+import H5Form from '@/components/H5/h5-form.vue'
+import H5LinkButton from '@/components/H5/h5-link-button.vue'
+import H5AddWechatButton from '@/components/H5/h5-add-wechat-button.vue'
+import H5Qrcode from '@/components/H5/h5-qrcode.vue'
 import FormWrapper from '@/components/H5/FormWrapper.vue'
 import H5Chat from '@/components/H5/h5-chat.vue'
 import AgentAvatar from '@/assets/images/customer.png?inline'
@@ -59,6 +145,10 @@ export default {
     H5Image,// 对应模板中的<h5-image>
     H5Sep,// 对应模板中的<h5-sep>
     H5Countdown,// 对应模板中的<h5-countdown>
+    H5Form,// 对应模板中的<h5-form>
+    H5LinkButton,// 对应模板中的<h5-link-button>
+    H5AddWechatButton,// 对应模板中的<h5-add-wechat-button>
+    H5Qrcode,// 对应模板中的<h5-qrcode>
     FormWrapper,
     H5Chat
   },
@@ -73,6 +163,14 @@ export default {
   },
   data() {
     return {
+      pageTitle: '',
+      currentPageStep: 'home',
+      pageList: [
+        { pageStep: 'home', name: '首页', icon: 'el-icon-house' },
+        { pageStep: 'appletLand', name: '小程序授权页', icon: 'el-icon-document' },
+        { pageStep: 'purchaseSuccess', name: '小程序出码页', icon: 'el-icon-success' }
+      ],
+      configList: [],
       componentList: [{
         groupName: '基础控件',
         comps: [
@@ -101,20 +199,159 @@ export default {
             icon: 'el-icon-timer'            // 计时器图标表示倒计时
           },
           {
-            type: 'h5-chat',
-            label: '互动问答',
-            icon: 'el-icon-chat-square'     // 购物袋表示购买相关
+            type: 'h5-form',
+            label: '表单',
+            icon: 'el-icon-document-copy'    // 表单图标
+          },
+          {
+            type: 'h5-link-button',
+            label: '跳转按钮',
+            icon: 'el-icon-connection'       // 跳转图标
+          },
+          {
+            type: 'h5-add-wechat-button',
+            label: '加微按钮',
+            icon: 'el-icon-connection'       // 加微图标
+          },
+          {
+            type: 'h5-qrcode',
+            label: '二维码',
+            icon: 'el-icon-picture'          // 二维码图标
           }
         ]
       }],
       list: [],
-      index: 0,
-      form: {}
+      index: -1,
+      form: {},
+      draggedComponent: null, // 存储被拖拽的组件信息
+      insertIndex: -1 // 存储插入位置
     }
   },
   methods: {
-    initData(json) {
-      this.list = JSON.parse(json)
+    initData(json, templateType) {
+      // 根据模板类型设置页面列表
+      if (templateType === '小程序表单类') {
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' },
+          { pageStep: 'appletLand', name: '小程序授权页', icon: 'el-icon-document' },
+          { pageStep: 'purchaseSuccess', name: '小程序出码页', icon: 'el-icon-success' }
+        ]
+      } else if (templateType === '小程序加粉类') {
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' },
+          { pageStep: 'purchaseSuccess', name: '小程序出码页', icon: 'el-icon-success' }
+        ]
+      } else if (templateType === '免费加粉类') {
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' }
+        ]
+      } else if (templateType === '免费表单类') {
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' },
+          { pageStep: 'receiveSuccess', name: '领取成功页', icon: 'el-icon-circle-check' }
+        ]
+      } else if (templateType === '一站式小程序表单类') {
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' },
+          { pageStep: 'purchaseSuccess', name: '小程序出码页', icon: 'el-icon-success' }
+        ]
+      } else {
+        // 默认显示首页
+        this.pageList = [
+          { pageStep: 'home', name: '首页', icon: 'el-icon-house' }
+        ]
+      }
+
+      // 防止switchPage将当前的空list保存到新加载的configList中
+      this.currentPageStep = ''
+      try {
+        const data = JSON.parse(json)
+        // 支持新格式
+        if (data.pageTitle !== undefined && Array.isArray(data.configList)) {
+          this.pageTitle = data.pageTitle
+          // 深拷贝接收到的configList
+          this.configList = JSON.parse(JSON.stringify(data.configList))
+          // 初始化configList,确保所有页面都存在
+          this.initializeConfigList()
+          // 加载首页数据
+          this.switchPage('home')
+        } else if (Array.isArray(data)) {
+          // 兼容旧格式(直接是数组)
+          this.configList = [{
+            pageStep: 'home',
+            moduleList: data
+          }, {
+            pageStep: 'appletLand',
+            moduleList: []
+          }, {
+            pageStep: 'purchaseSuccess',
+            moduleList: []
+          }]
+          this.switchPage('home')
+        } else {
+          // 其他格式
+          this.configList = [{
+            pageStep: 'home',
+            moduleList: Array.isArray(data) ? data : []
+          }, {
+            pageStep: 'appletLand',
+            moduleList: []
+          }, {
+            pageStep: 'purchaseSuccess',
+            moduleList: []
+          }]
+          this.switchPage('home')
+        }
+      } catch (e) {
+        // 初始化为新格式
+        this.pageTitle = ''
+        this.configList = this.pageList.map(page => ({
+          pageStep: page.pageStep,
+          moduleList: []
+        }))
+        this.switchPage('home')
+      }
+      // 重置选中状态和属性表单
+      this.form = {}
+      this.index = -1
+    },
+    initializeConfigList() {
+      // 根据当前的pageList初始化配置
+      this.pageList.forEach(page => {
+        if (!this.configList.find(config => config.pageStep === page.pageStep)) {
+          this.configList.push({
+            pageStep: page.pageStep,
+            moduleList: []
+          })
+        }
+      })
+    },
+    switchPage(pageStep) {
+      // 保存当前页面的数据
+      const currentConfig = this.configList.find(config => config.pageStep === this.currentPageStep)
+      if (currentConfig) {
+        currentConfig.moduleList = this.list
+      }
+      
+      // 切换到新页面
+      this.currentPageStep = pageStep
+      let newConfig = this.configList.find(config => config.pageStep === pageStep)
+      
+      // 如果找不到对应的配置,创建一个新的
+      if (!newConfig) {
+        newConfig = {
+          pageStep: pageStep,
+          moduleList: []
+        }
+        this.configList.push(newConfig)
+      }
+      
+      // 使用$set确保列表更新响应式追踪
+      this.$set(this, 'list', newConfig.moduleList || [])
+      
+      // 重置选中和表单
+      this.form = {}
+      this.index = -1
     },
     end() {
 
@@ -126,7 +363,6 @@ export default {
         name: item.label,
         fixe: false,
         classText: ['parent-div'],
-        addWxFun: false,
         workUrl: ''
       }
       if (item.type === 'h5-text') {
@@ -142,8 +378,6 @@ export default {
         }
       } else if (item.type === 'h5-image') {
         data.url = null
-      } else if (item.type === 'h5-button') {
-        data.content = '默认文本'
       } else if (item.type === 'h5-sep') {
         data.classText = [...data.classText, 'divider']
         data.style = {
@@ -156,101 +390,46 @@ export default {
         data.minutes = 0
         data.seconds = 0
         data.countdownMode = 1
-      } else if (item.type === 'h5-chat') {
-        data = {...data,
-          ...{
-            messages: [
-              {
-                id: 1,
-                sender: "agent",
-                text: "您好!欢迎报名【中老年健康养生大讲堂】,请仔细答一下问题,便于帮您分配专业的老师进行指导。",
-                welcome: true
-              },
-              {
-                id: 2,
-                sender: "agent",
-                text: "您的年龄?",
-                options: [
-                  { id: 1, text: "45-55岁" },
-                  { id: 2, text: "55-60岁" },
-                  { id: 3, text: "60-65岁" },
-                  { id: 4, text: "65岁以上" }
-                ],
-                userSelection: null
-              },
-              {
-                id: 3,
-                sender: "user",
-                text: '45-55岁',
-                userSelection: { id: 1, text: "45-55岁" }
-              },
-              {
-                id: 4,
-                sender: "agent",
-                text: "更想通过课程学习到哪些知识?",
-                options: [
-                  { id: 1, text: "食疗食补" },
-                  { id: 2, text: "经络疏通" },
-                  { id: 3, text: "脏腑调养" },
-                  { id: 4, text: "启蒙养生" },
-                  { id: 5, text: "以上所有" }
-                ],
-                userSelection: null
-              },
-              {
-                id: 5,
-                sender: "user",
-                text: '食疗食补',
-                userSelection: { id: 1, text: "食疗食补" }
-              }
-            ],
-            agentMsg:[{
-              id: 1,
-              sender: "agent",
-              text: "您好!欢迎报名【中老年健康养生大讲堂】,请仔细答一下问题,便于帮您分配专业的老师进行指导。",
-              welcome: true
-            },
-              {
-                id: 2,
-                sender: "agent",
-                text: "您的年龄?",
-                options: [
-                  { id: 1, text: "45-55岁" },
-                  { id: 2, text: "55-60岁" },
-                  { id: 3, text: "60-65岁" },
-                  { id: 4, text: "65岁以上" }
-                ],
-                userSelection: null
-              },
-              {
-                id: 4,
-                sender: "agent",
-                text: "更想通过课程学习到哪些知识?",
-                options: [
-                  { id: 1, text: "食疗食补" },
-                  { id: 2, text: "经络疏通" },
-                  { id: 3, text: "脏腑调养" },
-                  { id: 4, text: "启蒙养生" },
-                  { id: 5, text: "以上所有" }
-                ],
-                userSelection: null
-              }
-              ],
-            style: {
-              avatar: window.location.origin+AgentAvatar,
-              buttonColor: '#409EFF',
-              textColor: ''
-            }
-          }
-        }
-        console.log(data)
-        let h5chat = this.list.some(item => item.type && item.type.includes('h5-chat'))
-        if (h5chat) {
-          alert('全局只能允许有一个互动问答!')
-          return
-        }
+      } else if (item.type === 'h5-form') {
+        data.showImage = true
+        data.formImage = ''
+        data.showSubmitBtn = true
+        data.submitBtnStyle = 'text'
+        data.submitBtnColor = '#409EFF'
+        data.submitBtnText = '提交'
+        data.submitBtnImage = ''
+      } else if (item.type === 'h5-link-button') {
+        data.buttonStyle = 'text'
+        data.buttonText = '提交'
+        data.buttonColor = '#FF5A5A'
+        data.buttonTextColor = '#ffffff'
+        data.buttonImage = ''
+      } else if (item.type === 'h5-add-wechat-button') {
+        data.buttonStyle = 'text'
+        data.buttonText = '提交'
+        data.buttonColor = '#FF5A5A'
+        data.buttonTextColor = '#ffffff'
+        data.buttonImage = ''
+        data.enableGetCustomerAssistant = true
+        data.followScreen = true
+        data.enableDefaultWechat = false
+        data.enableMiniProgram = false
+      } else if (item.type === 'h5-qrcode') {
+        data.qrcodeImage = ''
+        data.qrcodeSize = 140
+        data.showCopyBtn = true
+        data.copyBtnText = '复制并添加老师'
+        data.copyBtnColor = '#FF9500'
+        data.copyBtnTextColor = '#ffffff'
+        data.showClickBtn = true
+        data.clickBtnText = '点击添加老师'
+        data.clickBtnColor = '#FF9500'
+        data.clickBtnTextColor = '#ffffff'
+        data.clickBtnWechatJump = false
+        data.enableGetCustomerAssistant = false
+        data.enableDefaultWechat = false
+        data.enableMiniProgram = false
       }
-      console.log(this.list)
 
       if (this.index !== null && this.index >= 0 && this.index < this.list.length) {
         this.list.splice(this.index + 1, 0, data)
@@ -260,35 +439,34 @@ export default {
     },
     select(index) {
       this.index = index
-      let className = 'active'
-      this.clearClass(className)
-      this.addClass(className)
-      this.$set(this.list[index], 'active', true)  // 确保响应式更新
-      // 直接绑定list中的对象(关键修改)
-      this.form = this.list[index]
-    },
-    clearClass(classText) {
-      this.list.forEach(item => {
-        // 推荐使用 $set
-        this.$set(item, 'classText', item.classText.filter(c => c !== classText))
+      // 更新所有元素的active状态
+      this.list.forEach((item, i) => {
+        this.$set(item, 'active', i === index)
       })
-    },
-    addClass(classText) {
-      this.list[this.index].classText.push(classText)
+      // 直接绑定list中的对象
+      this.form = this.list[index]
     },
 
     del() {
-      this.list.splice(this.index, 1)
+      if (this.index >= 0 && this.index < this.list.length) {
+        this.list.splice(this.index, 1)
+        this.form = {}
+        this.index = -1
+      }
+    },
+    /** 处理删除操作 */
+    handleDelete(index) {
+      this.index = index
+      this.list.splice(index, 1)
       this.form = {}
+      this.index = -1
     },
     updateForm(newForm) {
       // 更新表单数据
-      if (this.index === undefined || !this.list[this.index]) {
-        console.error('Invalid index or list item')
+      if (this.index < 0 || this.index >= this.list.length) {
         return
       }
 
-      console.log('newForm:', newForm) // 检查 newForm 数据
       Object.assign(this.form, newForm)
 
       // 更新列表中的数据
@@ -304,102 +482,746 @@ export default {
           }
         }
       }
+    },
+    /** 处理左侧组件拖拽开始 */
+    handleDragStart(event, comp) {
+      this.draggedComponent = comp
+      this.insertIndex = -1
+      event.dataTransfer.effectAllowed = 'copy'
+      event.dataTransfer.setData('text/html', event.target.innerHTML)
+    },
+    /** 处理拖拽结束 */
+    handleDragEnd() {
+      this.draggedComponent = null
+      this.insertIndex = -1
+    },
+    /** 处理控件池drag-enter */
+    handleItemDragEnter(event, index) {
+      // 仅根据是否来自左侧组件池
+      if (!this.draggedComponent) return
+      event.preventDefault()
+    },
+    /** 处理控件池drag-over */
+    handleItemDragOver(event, index) {
+      // 只有来自于左侧组件池的拖拽有效
+      if (!this.draggedComponent) return
+      event.preventDefault()
+      event.dataTransfer.dropEffect = 'copy'
+      
+      const wrapper = event.currentTarget
+      const rect = wrapper.getBoundingClientRect()
+      const midPoint = rect.top + rect.height / 2
+      
+      // 根据鼠标的纵位置判断是开始位置还是结数位置
+      wrapper.classList.remove('drag-before', 'drag-after')
+      if (event.clientY < midPoint) {
+        wrapper.classList.add('drag-before')
+        this.insertIndex = index
+      } else {
+        wrapper.classList.add('drag-after')
+        this.insertIndex = index + 1
+      }
+    },
+    /** 处理控件池drag-leave */
+    handleItemDragLeave(event, index) {
+      // 仅在来自左侧拖拽时事
+      if (!this.draggedComponent) return
+      const wrapper = event.currentTarget
+      if (event.target === wrapper) {
+        wrapper.classList.remove('drag-before', 'drag-after')
+      }
+    },
+    /** 根据鼠标位置计算插入索引 */
+    handleMouseMove(event) {
+      // 此方法已经不使用,位置计算变为了handleItemDragOver
+    },
+    /** 处理拖拽经过预览区域 */
+    handleDragOver(event) {
+      event.preventDefault()
+      event.dataTransfer.dropEffect = 'copy'
+      // 添加视觉反馈
+      this.$el.querySelector('.view-body').classList.add('drag-over')
 
-      console.log('Updated list:', this.list[this.index])
+      // 自动滚动逻辑
+      const container = this.$el.querySelector('.parent-container')
+      if (container) {
+        const rect = container.getBoundingClientRect()
+        const threshold = 60 // 边缘阈值
+        const scrollSpeed = 15 // 滚动速度
+
+        // 向下滚动
+        if (event.clientY > rect.bottom - threshold) {
+          container.scrollTop += scrollSpeed
+        }
+        // 向上滚动
+        else if (event.clientY < rect.top + threshold) {
+          container.scrollTop -= scrollSpeed
+        }
+      }
+    },
+    /** 处理拖拽离开预览区域 */
+    handleDragLeave(event) {
+      if (event.target === this.$el.querySelector('.view-body')) {
+        this.$el.querySelector('.view-body').classList.remove('drag-over')
+      }
+    },
+    /** 处理拖拽放下 */
+    handleDrop(event) {
+      event.preventDefault()
+      this.$el.querySelector('.view-body').classList.remove('drag-over')
+      
+      // 清除所有拖拽样式
+      document.querySelectorAll('.draggable-wrapper').forEach(el => {
+        el.classList.remove('drag-before', 'drag-after')
+      })
+      
+      // 处理新添加的组件(来自于左侧组件池)
+      if (this.draggedComponent) {
+        const newItem = this.createNewComponent(this.draggedComponent)
+        // 在插入位置插入新控件
+        if (this.insertIndex >= 0 && this.insertIndex <= this.list.length) {
+          this.list.splice(this.insertIndex, 0, newItem)
+        } else {
+          this.list.push(newItem)
+        }
+        this.draggedComponent = null
+        this.insertIndex = -1
+      }
+    },
+    /** 根据组件贫模对象创建新控件 */
+    createNewComponent(item) {
+      let data = {
+        type: item.type,
+        active: false,
+        name: item.label,
+        fixe: false,
+        classText: ['parent-div'],
+        workUrl: ''
+      }
+      if (item.type === 'h5-text') {
+        data.content = '默认文本'
+        data.style = {
+          textAlign: 'left',
+          fontSize: 20,
+          background: '#FFF',
+          color: '#000',
+          fontWeight: 'none',
+          fontStyle: 'none',
+          textDecoration: 'none'
+        }
+      } else if (item.type === 'h5-image') {
+        data.url = null
+      } else if (item.type === 'h5-sep') {
+        data.classText = [...data.classText, 'divider']
+        data.style = {
+          height: '10px',
+          background: 'rgb(228, 231, 237)'
+        }
+      } else if (item.type === 'h5-countdown') {
+        data.days = 0
+        data.hours = 0
+        data.minutes = 0
+        data.seconds = 0
+        data.countdownMode = 1
+      } else if (item.type === 'h5-form') {
+        data.showImage = true
+        data.formImage = ''
+        data.showSubmitBtn = true
+        data.submitBtnStyle = 'text'
+        data.submitBtnColor = '#409EFF'
+        data.submitBtnText = '提交'
+        data.submitBtnImage = ''
+      } else if (item.type === 'h5-link-button') {
+        data.buttonStyle = 'text'
+        data.buttonText = '提交'
+        data.buttonColor = '#FF5A5A'
+        data.buttonTextColor = '#ffffff'
+        data.buttonImage = ''
+      } else if (item.type === 'h5-add-wechat-button') {
+        data.buttonStyle = 'text'
+        data.buttonText = '提交'
+        data.buttonColor = '#FF5A5A'
+        data.buttonTextColor = '#ffffff'
+        data.buttonImage = ''
+        data.enableGetCustomerAssistant = true
+        data.followScreen = true
+        data.enableDefaultWechat = false
+        data.enableMiniProgram = false
+      } else if (item.type === 'h5-qrcode') {
+        data.qrcodeImage = ''
+        data.qrcodeSize = 140
+        data.showCopyBtn = true
+        data.copyBtnText = '复制并添加老师'
+        data.copyBtnColor = '#FF9500'
+        data.copyBtnTextColor = '#ffffff'
+        data.showClickBtn = true
+        data.clickBtnText = '点击添加老师'
+        data.clickBtnColor = '#FF9500'
+        data.clickBtnTextColor = '#ffffff'
+        data.clickBtnWechatJump = false
+        data.enableGetCustomerAssistant = false
+        data.enableDefaultWechat = false
+        data.enableMiniProgram = false
+      }
+      return data
+    },
+    /** 获取完整的页面数据(新格式JSON) */
+    getPageData() {
+      // 保存当前页面数据
+      const currentConfig = this.configList.find(config => config.pageStep === this.currentPageStep)
+      if (currentConfig) {
+        currentConfig.moduleList = this.list
+      }
+      
+      return {
+        pageTitle: this.pageTitle,
+        configList: this.configList
+      }
+    },
+    /** 获取JSON字符串 */
+    getJsonString() {
+      return JSON.stringify(this.getPageData())
     }
   }
 }
 </script>
 <style lang="scss" scoped>
-.button-body {
-  text-align: center;
-  margin: 0 auto;
+.h5-editor-container {
+  width: 100%;
+  height: 100%;
+  background: #f0f2f5;
+  display: flex;
+  flex-direction: column;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+// 标题编辑栏
+.title-bar {
+  padding: 10px 20px;
+  background: #fff;
+  border-bottom: 1px solid #ebeef5;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.04);
+  z-index: 100;
 
-  .button-tag:first-child {
-    margin-top: 0 !important;
+  .title-input {
+    width: 300px;
+    
+    ::v-deep .el-input__inner {
+      border: 1px solid #dcdfe6;
+      border-radius: 4px;
+      &:focus {
+        border-color: #409eff;
+      }
+    }
   }
+}
 
-  .button-tag {
-    margin-left: 0;
-    margin-top: 10px;
+.editor-wrapper {
+  flex: 1;
+  padding: 0;
+  margin: 0 !important;
+  display: flex !important;
+  overflow: hidden;
+}
+
+// 面板公共样式
+.panel-header {
+  padding: 15px 20px;
+  border-bottom: 1px solid #f0f0f0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-shrink: 0;
+  background: #fff;
+
+  h3 {
+    margin: 0;
+    font-size: 15px;
+    font-weight: 600;
+    color: #1f2d3d;
+  }
+
+  .device-info {
+    font-size: 12px;
+    color: #909399;
+    background: #f4f4f5;
+    padding: 2px 8px;
+    border-radius: 10px;
   }
 }
 
-/* 外层容器 (父级div) */
-.parent-container {
-  background: #eff3f5;
-  padding: 40px 0;
-  width: 100%;
+// 左侧通用面板样式
+.page-panel,
+.left-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #fff;
+  border-right: 1px solid #ebeef5;
+  box-shadow: none;
+  z-index: 10;
+  border-radius: 0;
+}
+
+// 页面选择面板
+.page-panel {
+  .page-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 15px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+    &::-webkit-scrollbar-thumb {
+      background: #e0e0e0;
+      border-radius: 2px;
+    }
+  }
+
+  .page-card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    padding: 15px 10px;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    background: #fff;
+    cursor: pointer;
+    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+    position: relative;
+    overflow: hidden;
+
+    .page-icon {
+      font-size: 24px;
+      color: #606266;
+      transition: color 0.3s;
+    }
 
-  /* 关键设置 */
-  height: 80vh;
-  overflow-y: auto; /* 滚动条出在这里 */
-  border: 1px solid #DCDEE2;
+    .page-name {
+      font-size: 12px;
+      color: #606266;
+      font-weight: 500;
+      transition: color 0.3s;
+    }
+
+    &:hover {
+      border-color: #b3d8ff;
+      background: #ecf5ff;
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+
+      .page-icon, .page-name {
+        color: #409eff;
+      }
+    }
+
+    &.active {
+      background: #409eff;
+      border-color: #409eff;
+      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+
+      .page-icon, .page-name {
+        color: #fff;
+      }
+    }
+  }
 }
-/* 定制滚动条样式 */
-.parent-container::-webkit-scrollbar {
-  width: 6px; /* 滚动条宽度 */
+
+// 左下组件面板
+.left-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #fff;
+  border-radius: 4px 0 0 4px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+
+  .components-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 12px;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #d0d0d0;
+      border-radius: 3px;
+
+      &:hover {
+        background: #aaa;
+      }
+    }
+  }
 }
 
-.parent-container::-webkit-scrollbar-track {
-  background: #f1f1f1; /* 滚动条轨道背景色 */
-  border-radius: 3px; /* 轨道圆角 */
+.component-group {
+  margin-bottom: 16px;
+
+  .group-title {
+    font-size: 12px;
+    font-weight: 600;
+    color: #606266;
+    padding: 8px 0;
+    margin-bottom: 12px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    flex-shrink: 0;
+
+    i {
+      font-size: 14px;
+      color: #409eff;
+    }
+  }
+
+  .buttons-wrapper {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+  }
 }
 
-.parent-container::-webkit-scrollbar-thumb {
-  background: #888; /* 滚动条滑块颜色 */
-  border-radius: 3px; /* 滑块圆角 */
+.comp-button {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 15px 10px;
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  cursor: grab;
+  transition: all 0.2s ease;
+  min-height: 80px;
+
+  i {
+    font-size: 24px;
+    color: #606266;
+    margin-bottom: 8px;
+    transition: transform 0.2s;
+  }
+
+  span {
+    text-align: center;
+    word-break: break-word;
+    white-space: normal;
+  }
+
+  &:hover {
+    border-color: #409eff;
+    color: #409eff;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+    transform: translateY(-2px);
+
+    i {
+      color: #409eff;
+      transform: scale(1.1);
+    }
+    
+    span {
+      color: #409eff;
+    }
+  }
+
+  &:active {
+    background: #f0f9ff;
+    transform: scale(0.98);
+    cursor: grabbing;
+  }
 }
 
-.parent-container::-webkit-scrollbar-thumb:hover {
-  background: #555; /* 滑块悬停颜色 */
+// 中间预览面板
+.center-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #eaedf1;
+  border-left: 1px solid #ebeef5;
+  border-right: 1px solid #ebeef5;
+  box-shadow: inset 0 0 20px rgba(0,0,0,0.02);
+  
+  .panel-header {
+    background: #fff;
+    border-bottom: 1px solid #ebeef5;
+  }
 }
-.view-body {
-  width: 375px; /* 明确具体值(会覆盖下面的width:100%) */
-  height: auto; /* 改掉height:100%,否则会根据父级高度计算*/
 
-  /* 清除非必须设置 */
-  overflow-y: visible; /* 保持默认 */
+// 模拟手机外壳
+.mobile-nav-bar {
+  width: 375px;
+  background: #fff;
+  border-bottom: 1px solid #f2f2f2;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 12px;
+  flex-shrink: 0;
+  z-index: 20;
+  
+  border-top-left-radius: 30px; 
+  border-top-right-radius: 30px; 
+  padding-top: 10px; 
+  height: 54px; 
+  box-sizing: border-box;
+
+  box-shadow: 0 0 0 12px #333; 
+  margin: 0 auto;
+  
+  .nav-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #000;
+  }
+  
+  i {
+    font-size: 18px;
+    color: #333;
+  }
+    
+    .nav-left, .nav-right {
+      width: 40px;
+      display: flex;
+      align-items: center;
+      
+      i {
+        font-size: 18px;
+        color: #333;
+        cursor: pointer;
+      }
+    }
+
+    .nav-right {
+      justify-content: flex-end;
+    }
+}
 
-  /* 其他属性 */
-  margin: 0 auto; /* 代替外层flex的justify-content */
+.view-body {
+  width: 375px;
+  min-height: 667px;
   background: #fff;
+  padding: 0;
   position: relative;
+  
+  border-bottom-left-radius: 30px; 
+  border-bottom-right-radius: 30px; 
+  padding-bottom: 30px; 
+  
+  box-shadow: 0 0 0 12px #333, 0 20px 40px -10px rgba(0,0,0,0.4); 
+  margin: 0 auto;
+  
+  &.drag-over {
+    background-color: #f0f9ff;
+    
+    &::after {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      border: 2px dashed #409eff;
+      border-radius: 0 0 30px 30px;
+      pointer-events: none;
+      z-index: 100;
+    }
+  }
+  
+  &:hover {
+    box-shadow: 0 0 0 12px #333, 0 25px 50px -12px rgba(0,0,0,0.5);
+  }
+}
+
+.parent-container {
+  flex: 1;
+  padding: 40px 0;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  
+  &::-webkit-scrollbar {
+    width: 0; 
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #d0d0d0;
+    border-radius: 3px;
+
+    &:hover {
+      background: #aaa;
+    }
+  }
+}
+
+.draggable-area {
   padding: 0;
+  width: 100%;
+}
+
+.draggable-wrapper {
+  position: relative;
+  transition: border-color 0.2s ease, background-color 0.2s ease;
+  cursor: move;
+}
+
+.draggable-wrapper.drag-before {
+  border-top: 3px solid #409eff;
 }
 
-.icon.svg {
+.draggable-wrapper.drag-after {
+  border-bottom: 3px solid #409eff;
+}
+
+.view-item {
   cursor: pointer;
-  margin-left: 20px;
-  width: 20px;
-  height: 20px;
+  transition: background-color 0.2s ease;
+  border: none;
+  position: relative;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  box-sizing: border-box;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+
+  &:hover {
+    background-color: #f0f2f5;
+  }
+
+  &.active {
+    outline: 2px solid #409eff;
+    outline-offset: -2px;
+    z-index: 2;
+    background-color: #ecf5ff;
+  }
 }
 
-.icon.svg.active {
-  color: #02ff9b;
+.ghost {
+  opacity: 0.5;
+  background: #f0f9ff;
 }
 
-.icon-span {
+.drag {
+  opacity: 0;
 }
 
-.divider {
+.insert-placeholder {
   width: 100%;
-  background: #DCDEE2;
-  height: 10px;
+  height: 3px;
+  background: linear-gradient(to right, #409eff 0%, #409eff 100%);
+  position: relative;
+  margin: 4px 0;
+  border-radius: 2px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  span {
+    position: absolute;
+    background: #fff;
+    padding: 0 8px;
+    font-size: 10px;
+    color: #409eff;
+    font-weight: 600;
+  }
 }
 
-.button-tag {
-  /* 新增图标样式 */
-  .el-icon {
-    margin-right: 6px; /* 图标与文本间距 */
-    font-size: 16px; /* 统一图标大小 */
-    vertical-align: -2px; /* 垂直居中补偿 */
+.delete-button-wrapper {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  width: 28px;
+  height: 28px;
+  background: #ff4444;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+
+  i {
+    color: #fff;
+    font-size: 16px;
+  }
+
+  &:hover {
+    background: #ff2222;
+    transform: scale(1.1);
+    box-shadow: 0 2px 8px rgba(255, 68, 68, 0.3);
+  }
+
+  &:active {
+    transform: scale(0.95);
   }
+}
 
-  &.is-plain .el-icon {
-    color: #666; /* 浅色主题下保持可视性 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+  color: #909399;
+  font-size: 12px;
+
+  i {
+    font-size: 48px;
+    margin-bottom: 12px;
+    color: #d0d0d0;
   }
 }
 
+// 右侧属性面板
+.right-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #fff;
+  border-radius: 0 4px 4px 0;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.properties-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 12px;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #d0d0d0;
+    border-radius: 3px;
+
+    &:hover {
+      background: #aaa;
+    }
+  }
+}
+
+// 分割线样式
+.divider {
+  width: 100%;
+  background: #DCDEE2;
+  height: 10px;
+}
 
-</style>
+</style>

+ 0 - 2
src/router/index.js

@@ -272,8 +272,6 @@ export const constantRoutes = [
     }
   },
 
-
-
 ]
 
 

+ 1 - 0
src/utils/errorCode.js

@@ -2,5 +2,6 @@ export default {
   '401': '认证失败,无法访问系统资源',
   '403': '当前操作没有权限',
   '404': '访问资源不存在',
+  '500': '服务器内部错误',
   'default': '系统未知错误,请反馈给管理员'
 }

+ 2 - 2
src/utils/request.js

@@ -59,8 +59,8 @@ service.interceptors.request.use(config => {
 service.interceptors.response.use(res => {
     // 未设置状态码则默认成功状态
     const code = res.data.code || 200;
-    // 获取错误信息
-    const msg = errorCode[code] || res.data.msg || errorCode['default']
+    // 获取错误信息 - 优先使用后端返回的 message 或 msg,兼容两种字段名
+    const msg = res.data.message || res.data.msg || errorCode[code] || errorCode['default']
     if (code === 401) {
       MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
           confirmButtonText: '重新登录',

+ 271 - 0
src/views/adv/advertiser/index.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="广告商名称" prop="advertiserName">
+        <el-input
+          v-model="queryParams.advertiserName"
+          placeholder="请输入广告商名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="类型" prop="custom">
+        <el-select v-model="queryParams.custom" placeholder="请选择类型" clearable size="small">
+          <el-option label="线上广告商" :value="1" />
+          <el-option label="自定义广告商" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="enabled">
+        <el-select v-model="queryParams.enabled" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="禁用" :value="0" />
+        </el-select>
+      </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="advertiserList">
+      <el-table-column label="广告商名称" align="left" prop="advertiserName" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.enabled"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleStatusChange(scope.row)"
+            style="margin-right: 10px; vertical-align: middle;"
+          >
+          </el-switch>
+          <span>{{ scope.row.advertiserName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="类型" align="center" prop="custom">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.custom === 1" type="success">线上广告商</el-tag>
+          <el-tag v-else type="danger">自定义广告商</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否支持API" align="center" prop="supportApi">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.supportApi === 1" type="success">是</el-tag>
+          <el-tag v-else type="info">否</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否支持回传" align="center" prop="supportCallback">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.supportCallback === 1" type="success">是</el-tag>
+          <el-tag v-else type="info">否</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="enabled">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.enabled === 1" type="success">启用</el-tag>
+          <el-tag v-else type="danger">禁用</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.current"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改广告商对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="广告商名称" prop="advertiserName">
+          <el-input v-model="form.advertiserName" placeholder="请输入广告商名称" />
+        </el-form-item>
+        <el-form-item label="类型" prop="custom">
+          <el-select v-model="form.custom" placeholder="请选择类型" style="width: 100%" disabled>
+            <el-option label="自定义广告商" :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="enabled">
+          <el-radio-group v-model="form.enabled">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </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 { pageAdvertiser, getAdvertiser, addAdvertiser, updateAdvertiser, delAdvertiser, batchDelAdvertiser, enableAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "Advertiser",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 广告商表格数据
+      advertiserList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        current: 1,
+        size: 10,
+        advertiserName: undefined,
+        custom: undefined,
+        enabled: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        advertiserName: [
+          { required: true, message: "广告商名称不能为空", trigger: "blur" }
+        ],
+        custom: [
+          { required: true, message: "类型不能为空", trigger: "change" }
+        ],
+        enabled: [
+          { required: true, message: "状态不能为空", trigger: "change" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询广告商列表 */
+    getList() {
+      this.loading = true;
+      pageAdvertiser(this.queryParams).then(response => {
+        this.advertiserList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        advertiserName: undefined,
+        custom: 2,
+        enabled: 1
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.current = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加广告商";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids[0];
+      getAdvertiser(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改广告商";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != undefined) {
+            updateAdvertiser(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addAdvertiser(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 启用/停用状态切换 */
+    handleStatusChange(row) {
+      const statusText = row.enabled === 1 ? '启用' : '停用';
+      this.$confirm(`确认要${statusText}广告商“${row.advertiserName}”吗?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return enableAdvertiser(row.id);
+      }).then(() => {
+        this.msgSuccess(`${statusText}成功`);
+        this.getList();
+      }).catch(() => {
+        // 取消操作,恢复状态
+        row.enabled = row.enabled === 1 ? 0 : 1;
+      });
+    }
+  }
+};
+</script>
+

+ 618 - 0
src/views/adv/callbackAccount/index.vue

@@ -0,0 +1,618 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="账号名称" prop="accountName">
+        <el-input
+          v-model="queryParams.accountName"
+          placeholder="请输入账号名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select v-model="queryParams.advertiserId" placeholder="请选择广告商" clearable size="small" filterable>
+          <el-option
+            v-for="item in advertiserOptions"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </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>
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="accountList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="id" width="80" />
+      <el-table-column label="回传账号" align="center" prop="accountName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="广告商" align="center" prop="advertiserName" 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-column label="操作" align="center" class-name="small-padding fixed-width" width="300">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-setting"
+            @click="handleConversionType(scope.row)"
+          >添加转换类型</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-s-data"
+            @click="handleConversionLog(scope.row)"
+          >回传结果</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.current"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改回传账号对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="广告商" prop="advertiserId">
+          <el-select 
+            v-model="form.advertiserId" 
+            placeholder="请选择广告商" 
+            style="width: 100%"
+            filterable
+            @change="handleAdvertiserChange"
+          >
+            <el-option
+              v-for="item in advertiserOptions"
+              :key="item.id"
+              :label="item.advertiserName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <el-form-item label="账号名称" prop="accountName">
+          <el-input v-model="form.accountName" placeholder="请输入账号名称" />
+        </el-form-item>
+        
+        <!-- advertiserId = 1 的字段 -->
+        <template v-if="form.advertiserId === 10001">
+          <el-form-item label="ocpcUid" prop="adAccountId">
+            <el-input v-model="form.adAccountId" placeholder="请输入ocpcUid" />
+          </el-form-item>
+          <el-form-item label="ocpcToken" prop="accessToken">
+            <el-input v-model="form.accessToken" placeholder="请输入ocpcToken" type="password" show-password />
+          </el-form-item>
+        </template>
+        
+        <!-- advertiserId = 3 的字段 -->
+        <template v-if="form.advertiserId === 10003">
+          <el-form-item label="广告账户id" prop="adAccountId">
+            <el-input v-model="form.adAccountId" placeholder="请输入广告账户id" />
+          </el-form-item>
+          <el-form-item label="数据源id" prop="scrId">
+            <el-input v-model="form.scrId" placeholder="请输入数据源id" />
+          </el-form-item>
+          <el-form-item label="数据源token" prop="accessToken">
+            <el-input v-model="form.accessToken" placeholder="请输入数据源token" type="password" show-password />
+          </el-form-item>
+        </template>
+        
+        <!-- advertiserId = 4 的字段 -->
+        <template v-if="form.advertiserId === 10004">
+          <el-form-item label="ownerId" prop="adAccountId">
+            <el-input v-model="form.adAccountId" placeholder="请输入ownerId" />
+          </el-form-item>
+          <el-form-item label="apiId" prop="appId">
+            <el-input v-model="form.appId" placeholder="请输入apiId" />
+          </el-form-item>
+          <el-form-item label="apiKey" prop="appSecret">
+            <el-input v-model="form.appSecret" placeholder="请输入apiKey" type="password" show-password />
+          </el-form-item>
+        </template>
+        
+        <!-- advertiserId = 5 的字段 -->
+        <template v-if="form.advertiserId === 10005">
+          <el-form-item label="账户id" prop="adAccountId">
+            <el-input v-model="form.adAccountId" placeholder="请输入账户id" />
+          </el-form-item>
+          <el-form-item label="应用id" prop="appId">
+            <el-input v-model="form.appId" placeholder="请输入应用id" />
+          </el-form-item>
+          <el-form-item label="应用秘钥" prop="appSecret">
+            <el-input v-model="form.appSecret" placeholder="请输入应用秘钥" type="password" show-password />
+          </el-form-item>
+          <el-form-item label="scr_id" prop="scrId">
+            <el-input v-model="form.scrId" placeholder="请输入scr_id" />
+          </el-form-item>
+        </template>
+      </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>
+
+    <!-- 转换类型配置对话框 -->
+    <el-dialog title="添加转换类型" :visible.sync="conversionOpen" width="700px" append-to-body>
+      <div style="margin-bottom: 15px;">
+        <el-button type="primary" size="small" icon="el-icon-plus" @click="addConversionEvent">添加事件</el-button>
+      </div>
+      
+      <div v-if="conversionEvents.length === 0" style="text-align: center; padding: 40px 0; color: #909399;">
+        暂无转换事件,请点击“添加事件”按钮添加
+      </div>
+      
+      <div v-for="(event, index) in conversionEvents" :key="index" class="conversion-event-item">
+        <div class="event-header">
+          <span class="event-title">事件{{ index + 1 }}</span>
+          <el-button 
+            type="danger" 
+            size="mini" 
+            icon="el-icon-delete" 
+            circle
+            @click="removeConversionEvent(index)"
+          ></el-button>
+        </div>
+        
+        <el-form label-width="140px" style="margin-top: 10px;">
+          <el-form-item label="广告商转化类型">
+            <el-select 
+              v-model="event.advertiserEventType" 
+              placeholder="请选择广告商转化类型" 
+              style="width: 100%"
+              @change="handleAdvertiserEventChange(index, $event)"
+            >
+              <el-option
+                v-for="item in advertiserEventOptions"
+                :key="item.eventType"
+                :label="item.eventName"
+                :value="item.eventType"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item label="回传数据类型">
+            <el-select 
+              v-model="event.systemEventType" 
+              placeholder="请选择回传数据类型" 
+              style="width: 100%"
+              @change="handleSystemEventChange(index, $event)"
+            >
+              <el-option
+                v-for="item in systemEventOptions"
+                :key="item.eventType"
+                :label="item.eventName"
+                :value="item.eventType"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+      
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="saveConversionEvents">保 存</el-button>
+        <el-button @click="conversionOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageCallbackAccount, getCallbackAccount, addCallbackAccount, updateCallbackAccount, delCallbackAccount, batchDelCallbackAccount, queryEventType, saveEventType } from "@/api/adv/callbackAccount";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "CallbackAccount",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 回传账号表格数据
+      accountList: [],
+      // 广告商选项
+      advertiserOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否显示转换类型弹窗
+      conversionOpen: false,
+      // 当前操作的回传账号
+      currentAccount: null,
+      // 转换事件列表
+      conversionEvents: [],
+      // systemBuiltIn=0)
+      advertiserEventOptions: [],
+      // systemBuiltIn=1)
+      systemEventOptions: [],
+      // 查询参数
+      queryParams: {
+        current: 1,
+        size: 10,
+        accountName: undefined,
+        advertiserId: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        advertiserId: [
+          { required: true, message: "请选择广告商", trigger: "change" }
+        ],
+        accountName: [
+          { required: true, message: "账号名称不能为空", trigger: "blur" }
+        ],
+        adAccountId: [
+          { 
+            validator: (rule, value, callback) => {
+              if ([1, 3, 4, 5].includes(this.form.advertiserId) && !value) {
+                callback(new Error('此字段不能为空'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        accessToken: [
+          { 
+            validator: (rule, value, callback) => {
+              if ([1, 3].includes(this.form.advertiserId) && !value) {
+                callback(new Error('此字段不能为空'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        scrId: [
+          { 
+            validator: (rule, value, callback) => {
+              if ([3, 5].includes(this.form.advertiserId) && !value) {
+                callback(new Error('此字段不能为空'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        appId: [
+          { 
+            validator: (rule, value, callback) => {
+              if ([4, 5].includes(this.form.advertiserId) && !value) {
+                callback(new Error('此字段不能为空'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        appSecret: [
+          { 
+            validator: (rule, value, callback) => {
+              if ([4, 5].includes(this.form.advertiserId) && !value) {
+                callback(new Error('此字段不能为空'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询广告商选项 */
+    getAdvertiserOptions() {
+      pageAdvertiser({ current: 1, size: 1000 }).then(response => {
+        this.advertiserOptions = response.data.records;
+      });
+    },
+    /** 查询回传账号列表 */
+    getList() {
+      this.loading = true;
+      pageCallbackAccount(this.queryParams).then(response => {
+        this.accountList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        advertiserId: undefined,
+        advertiserName: undefined,
+        accountName: undefined,
+        adAccountId: undefined,
+        accessToken: undefined,
+        scrId: undefined,
+        appId: undefined,
+        appSecret: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.current = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.getAdvertiserOptions();
+      this.open = true;
+      this.title = "添加回传账号";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.getAdvertiserOptions();
+      const id = row.id || this.ids[0];
+      getCallbackAccount(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改回传账号";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != undefined) {
+            updateCallbackAccount(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addCallbackAccount(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 广告商变化时 */
+    handleAdvertiserChange(advertiserId) {
+      this.form.advertiserName = "";
+      if (advertiserId) {
+        const advertiser = this.advertiserOptions.find(item => item.id === advertiserId);
+        if (advertiser) {
+          this.form.advertiserName = advertiser.advertiserName;
+        }
+      }
+      // 清空其他字段
+      this.form.adAccountId = undefined;
+      this.form.accessToken = undefined;
+      this.form.scrId = undefined;
+      this.form.appId = undefined;
+      this.form.appSecret = undefined;
+    },
+    /** 添加转换类型按钮操作 */
+    handleConversionType(row) {
+      this.currentAccount = row;
+      this.conversionOpen = true;
+      
+      // 加载事件类型选项
+      this.loadEventTypes(row.advertiserId);
+      
+      // 解析已有的转换事件
+      this.parseConversionEvents(row.conversionEvent);
+    },
+    /** 回传结果按钮操作 */
+    handleConversionLog(row) {
+      this.$router.push({
+        path: '/adv/new-adv/conversionLog',
+        query: { callbackAccountId: row.id }
+      });
+    },
+    /** 加载事件类型选项 */
+    loadEventTypes(advertiserId) {
+      queryEventType(advertiserId).then(response => {
+        const eventTypes = response.data || [];
+        // systemBuiltin='0' 为广告商事件
+        this.advertiserEventOptions = eventTypes.filter(item => item.systemBuiltin === '0');
+        // systemBuiltin='1' 为系统事件
+        this.systemEventOptions = eventTypes.filter(item => item.systemBuiltin === '1');
+      }).catch(error => {
+        console.error('加载事件类型失败:', error);
+        this.msgError('加载事件类型失败');
+      });
+    },
+    /** 解析转换事件 */
+    parseConversionEvents(conversionEvent) {
+      try {
+        if (conversionEvent && typeof conversionEvent === 'string') {
+          this.conversionEvents = JSON.parse(conversionEvent);
+        } else if (Array.isArray(conversionEvent)) {
+          this.conversionEvents = conversionEvent;
+        } else {
+          this.conversionEvents = [];
+        }
+      } catch (error) {
+        console.error('解析转换事件失败:', error);
+        this.conversionEvents = [];
+      }
+    },
+    /** 添加转换事件 */
+    addConversionEvent() {
+      this.conversionEvents.push({
+        systemEventType: '',
+        systemEventTypeName: '',
+        advertiserEventType: '',
+        advertiserEventName: ''
+      });
+    },
+    /** 删除转换事件 */
+    removeConversionEvent(index) {
+      this.conversionEvents.splice(index, 1);
+    },
+    /** 广告商事件变化 */
+    handleAdvertiserEventChange(index, eventType) {
+      const event = this.advertiserEventOptions.find(item => item.eventType === eventType);
+      if (event) {
+        this.conversionEvents[index].advertiserEventName = event.eventName;
+      }
+    },
+    /** 系统事件变化 */
+    handleSystemEventChange(index, eventType) {
+      const event = this.systemEventOptions.find(item => item.eventType === eventType);
+      if (event) {
+        this.conversionEvents[index].systemEventTypeName = event.eventName;
+      }
+    },
+    /** 保存转换事件 */
+    saveConversionEvents() {
+      // 校验数据
+      for (let i = 0; i < this.conversionEvents.length; i++) {
+        const event = this.conversionEvents[i];
+        if (!event.systemEventType || !event.advertiserEventType) {
+          this.msgError(`请完善事件${i + 1}的配置`);
+          return;
+        }
+      }
+      
+      // 调用保存接口
+      saveEventType(this.currentAccount.id, this.conversionEvents).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('保存成功');
+          this.conversionOpen = false;
+          this.getList();
+        }
+      }).catch(error => {
+        console.error('保存失败:', error);
+        this.msgError('保存失败');
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids;
+      this.$confirm('是否确认删除选中的回传账号?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        // 单条删除使用 delCallbackAccount,批量删除使用 batchDelCallbackAccount
+        if (row.id) {
+          return delCallbackAccount(row.id);
+        } else {
+          return batchDelCallbackAccount(ids);
+        }
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.conversion-event-item {
+  margin-bottom: 20px;
+  padding: 15px;
+  border: 1px solid #e8e8e8;
+  border-radius: 4px;
+  background-color: #fafafa;
+}
+
+.event-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.event-title {
+  font-weight: 600;
+  font-size: 15px;
+  color: #303133;
+}
+</style>

+ 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>

+ 269 - 0
src/views/adv/configuration/index.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="20">
+      <!-- 站点重复报名设置 -->
+      <el-col :span="8">
+        <el-card shadow="hover" class="box-card" @click.native="openDialog('duplicate')">
+          <div slot="header" class="clearfix">
+            <span>站点重复报名设置</span>
+            <el-button style="float: right; padding: 3px 0" type="text">设置</el-button>
+          </div>
+          <div class="card-content">
+            <i class="el-icon-document-copy card-icon"></i>
+            <p class="card-desc">配置学员报名规则及重复报名限制天数</p>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!-- 全局熟客设置 -->
+      <el-col :span="8">
+        <el-card shadow="hover" class="box-card" @click.native="openDialog('regular')">
+          <div slot="header" class="clearfix">
+            <span>全局熟客设置</span>
+            <el-button style="float: right; padding: 3px 0" type="text">设置</el-button>
+          </div>
+          <div class="card-content">
+            <i class="el-icon-user-solid card-icon"></i>
+            <p class="card-desc">配置全局熟客判断及小程序授权熟客逻辑</p>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!-- 重复回传设置 -->
+      <el-col :span="8">
+        <el-card shadow="hover" class="box-card" @click.native="openDialog('repeat')">
+          <div slot="header" class="clearfix">
+            <span>重复回传设置</span>
+            <el-button style="float: right; padding: 3px 0" type="text">设置</el-button>
+          </div>
+          <div class="card-content">
+            <i class="el-icon-refresh card-icon"></i>
+            <p class="card-desc">配置线索去重策略及回传过滤规则</p>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 弹窗1: 站点重复报名设置 -->
+    <el-dialog title="站点重复报名设置" :visible.sync="dialogs.duplicate" width="600px" append-to-body>
+      <el-form ref="duplicateForm" :model="form.duplicateRegistrationSettings" label-width="120px">
+        <el-form-item label="报名逻辑">
+          <el-radio-group v-model="form.duplicateRegistrationSettings.signUpLogic">
+            <el-radio :label="1">学员可正常报名</el-radio>
+            <el-radio :label="2">学员不可重复报名</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="处理逻辑" v-if="form.duplicateRegistrationSettings.signUpLogic === 2">
+          <div>
+            客户站点报名后的
+            <el-input-number v-model="form.duplicateRegistrationSettings.day" :min="1" :max="9999" size="small" style="width: 100px; margin: 0 5px;" />
+            天内,不可重复报名相同项目站点
+          </div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitConfig">确 定</el-button>
+        <el-button @click="dialogs.duplicate = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 弹窗2: 全局熟客设置 -->
+    <el-dialog title="全局熟客设置" :visible.sync="dialogs.regular" width="650px" append-to-body>
+      <el-form ref="regularForm" :model="form.regularCustomerSettings" label-width="200px">
+        <el-form-item label="全局熟客开关">
+          <el-switch
+            v-model="form.regularCustomerSettings.repeatSwitch"
+            :active-value="1"
+            :inactive-value="2"
+            active-text="开启"
+            inactive-text="关闭">
+          </el-switch>
+          <div class="form-tip">开启后,客户“站点投放报名”或“在线客服留言”时,将分配当前名片跟进人的二维码。特殊的,站点投放配置个微群活码或客服码,则在此模式下无法实现全局熟客,将按照分配规则进行分配出码</div>
+        </el-form-item>
+        <el-form-item label="小程序/加粉站点熟客判断">
+          <el-switch
+            v-model="form.regularCustomerSettings.authSwitch"
+            :active-value="1"
+            :inactive-value="2"
+            active-text="开启"
+            inactive-text="关闭">
+          </el-switch>
+          <div class="form-tip">小程序表单站点/加粉类站点可支持在微信授权后,根据微信身份判断熟客</div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitConfig">确 定</el-button>
+        <el-button @click="dialogs.regular = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 弹窗3: 重复回传设置 -->
+    <el-dialog title="重复回传设置" :visible.sync="dialogs.repeat" width="600px" append-to-body>
+      <el-form ref="repeatForm" :model="form.repeatPostbackSettings" label-width="140px">
+        <el-form-item label="重复回传过滤">
+          <el-switch
+            v-model="form.repeatPostbackSettings.filter"
+            :active-value="1"
+            :inactive-value="2"
+            active-text="开启"
+            inactive-text="关闭">
+          </el-switch>
+          <div class="form-tip">开启后,若投放站点获取的线索与系统中已有客户重复,系统将不针对该线索进行回传。</div>
+        </el-form-item>
+        <el-form-item label="过滤方式" v-if="form.repeatPostbackSettings.filter === 1">
+          <el-radio-group v-model="form.repeatPostbackSettings.method">
+            <el-radio :label="1">按行为过滤</el-radio>
+            <el-radio :label="2">按手机号过滤</el-radio>
+          </el-radio-group>
+          <div class="form-tip" v-if="form.repeatPostbackSettings.method === 1">若同一个客户在不同站点,重复触发相同行为事件,则不会向广告商回传</div>
+          <div class="form-tip" v-if="form.repeatPostbackSettings.method === 2">若同一个客户在不同站点,重复提交相同手机号,则不会向广告商回传</div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitConfig">确 定</el-button>
+        <el-button @click="dialogs.repeat = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { getConfigDetail, addOrUpdateConfig } from "@/api/adv/configuration";
+
+export default {
+  name: "AdvConfiguration",
+  data() {
+    return {
+      // 弹窗控制
+      dialogs: {
+        duplicate: false,
+        regular: false,
+        repeat: false
+      },
+      // 完整表单数据
+      form: {
+        duplicateRegistrationSettings: {
+          signUpLogic: 1,
+          day: undefined
+        },
+        regularCustomerSettings: {
+          repeatSwitch: 2,
+          authSwitch: 2
+        },
+        repeatPostbackSettings: {
+          filter: 2,
+          method: 1
+        }
+      }
+    };
+  },
+  created() {
+    this.getDetail();
+  },
+  methods: {
+    /** 获取配置详情 */
+    getDetail() {
+      getConfigDetail().then(response => {
+        if (response.data) {
+          // 合并数据,防止后端返回null导致结构丢失
+          const data = response.data;
+          if (data.duplicateRegistrationSettings) {
+            this.form.duplicateRegistrationSettings = { ...this.form.duplicateRegistrationSettings, ...data.duplicateRegistrationSettings };
+          }
+          if (data.regularCustomerSettings) {
+            this.form.regularCustomerSettings = { ...this.form.regularCustomerSettings, ...data.regularCustomerSettings };
+          }
+          if (data.repeatPostbackSettings) {
+            this.form.repeatPostbackSettings = { ...this.form.repeatPostbackSettings, ...data.repeatPostbackSettings };
+          }
+        }
+      });
+    },
+    /** 打开弹窗 */
+    openDialog(type) {
+      if (this.dialogs.hasOwnProperty(type)) {
+        this.dialogs[type] = true;
+      }
+    },
+    /** 提交保存 */
+    submitConfig() {
+      const loading = this.$loading({
+        lock: true,
+        text: '保存中...',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+      
+      addOrUpdateConfig(this.form).then(response => {
+        loading.close();
+        // 根据要求:返回1成功,0失败
+        // 通常 axios拦截器已处理code!=200的情况,但这里根据描述 data=1 成功
+        if (response.code === 200) {
+          if (response.data === 1 || response.data === '1' || response.data === true) {
+            this.$message.success("保存成功");
+            this.dialogs.duplicate = false;
+            this.dialogs.regular = false;
+            this.dialogs.repeat = false;
+            this.getDetail(); // 刷新数据
+          } else {
+            this.$message.error("保存失败");
+          }
+        } else {
+            this.$message.error(response.msg || "保存失败");
+        }
+      }).catch(() => {
+        loading.close();
+      });
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.app-container {
+  padding: 20px;
+  background-color: #f0f2f5;
+  min-height: calc(100vh - 84px);
+}
+
+.box-card {
+  cursor: pointer;
+  height: 220px;
+  transition: all 0.3s;
+  
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 12px 20px rgba(0, 0, 0, 0.1);
+  }
+
+  .card-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 120px;
+    
+    .card-icon {
+      font-size: 48px;
+      color: #409EFF;
+      margin-bottom: 20px;
+    }
+    
+    .card-desc {
+      color: #909399;
+      font-size: 14px;
+      text-align: center;
+      line-height: 1.5;
+      padding: 0 10px;
+    }
+  }
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+  margin-top: 5px;
+}
+</style>

+ 163 - 0
src/views/adv/conversionLog/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="业务ID" prop="traceId">
+        <el-input
+          v-model="queryParams.traceId"
+          placeholder="请输入业务ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="账号ID" prop="callbackAccountId">
+        <el-input
+          v-model="queryParams.callbackAccountId"
+          placeholder="请输入账号ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="回传事件" prop="conversionType">
+        <el-input
+          v-model="queryParams.conversionType"
+          placeholder="请输入回传事件"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="回传状态" prop="callbackStatus">
+        <el-select v-model="queryParams.callbackStatus" placeholder="请选择回传状态" clearable size="small">
+          <el-option label="待回传" :value="0" />
+          <el-option label="成功" :value="1" />
+          <el-option label="失败" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select v-model="queryParams.advertiserId" placeholder="请选择广告商" clearable size="small" filterable>
+          <el-option
+            v-for="item in advertiserOptions"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </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">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="logList">
+      <el-table-column label="账号ID" align="center" prop="callbackAccountId" width="120" show-overflow-tooltip />
+      <el-table-column label="业务ID" align="center" prop="traceId" min-width="150" show-overflow-tooltip />
+      <el-table-column label="系统回传事件" align="center" prop="sysConversionEvent" min-width="150" show-overflow-tooltip />
+      <el-table-column label="广告商回传类型" align="center" prop="advConversionEvent" min-width="150" show-overflow-tooltip />
+      <el-table-column label="站点ID" align="center" prop="siteId" width="100" />
+      <el-table-column label="着陆页" align="center" prop="landingUrl" min-width="200" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <el-link v-if="scope.row.landingUrl" :href="scope.row.landingUrl" target="_blank" type="primary">
+            {{ scope.row.landingUrl }}
+          </el-link>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <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" prop="callbackStatus" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.callbackStatus === 0" type="info">待回传</el-tag>
+          <el-tag v-else-if="scope.row.callbackStatus === 1" type="success">成功</el-tag>
+          <el-tag v-else-if="scope.row.callbackStatus === 2" type="danger">失败</el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { pageConversionLog } from "@/api/adv/conversionLog";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "ConversionLog",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 回传事件表格数据
+      logList: [],
+      // 广告商选项
+      advertiserOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        traceId: undefined,
+        callbackAccountId: undefined,
+        conversionType: undefined,
+        callbackStatus: undefined,
+        advertiserId: undefined
+      }
+    };
+  },
+  created() {
+    // 从 URL 参数中获取 callbackAccountId
+    if (this.$route.query.callbackAccountId) {
+      this.queryParams.callbackAccountId = this.$route.query.callbackAccountId;
+    }
+    this.getList();
+    this.getAdvertiserOptions();
+  },
+  methods: {
+    /** 查询广告商选项 */
+    getAdvertiserOptions() {
+      pageAdvertiser({ current: 1, size: 1000 }).then(response => {
+        this.advertiserOptions = response.data.records;
+      });
+    },
+    /** 查询回传事件列表 */
+    getList() {
+      this.loading = true;
+      pageConversionLog(this.queryParams).then(response => {
+        this.logList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    }
+  }
+};
+</script>

+ 310 - 0
src/views/adv/customPromotionAccount/index.vue

@@ -0,0 +1,310 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="账号名称" prop="accountName">
+        <el-input
+          v-model="queryParams.accountName"
+          placeholder="请输入账号名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select v-model="queryParams.advertiserId" placeholder="请选择广告商" clearable size="small" filterable>
+          <el-option
+            v-for="item in advertiserOptions"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </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>
+      <!-- <el-col :span="1.5">
+        <el-button
+          plain
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col> -->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="accountList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="推广账户名称" align="center" prop="accountName" show-overflow-tooltip />
+      <el-table-column label="广告商名称" align="center" prop="advertiserName" 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-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column> -->
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.current"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改自定义推广账号对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="广告商" prop="advertiserId">
+          <el-select 
+            v-model="form.advertiserId" 
+            placeholder="请选择广告商" 
+            style="width: 100%" 
+            filterable
+            @change="handleAdvertiserChange"
+            :disabled="isEdit"
+          >
+            <el-option
+              v-for="item in advertiserOptions"
+              :key="item.id"
+              :label="item.advertiserName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="推广账号名称" prop="accountName">
+          <el-input 
+            v-model="form.accountName" 
+            placeholder="请输入推广账号名称"
+            clearable
+            :disabled="isEdit"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">取 消</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pagePromotionAccount, getPromotionAccount, addPromotionAccount, updatePromotionAccount, delPromotionAccount, batchDelPromotionAccount } from "@/api/adv/promotionAccount";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "CustomPromotionAccount",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 推广账号表格数据
+      accountList: [],
+      // 广告商选项
+      advertiserOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否为编辑模式
+      isEdit: false,
+      // 查询参数
+      queryParams: {
+        current: 1,
+        size: 10,
+        custom: 2,
+        accountName: undefined,
+        advertiserId: undefined
+      },
+      // 表单参数
+      form: {
+
+      },
+      // 表单校验
+      rules: {
+        advertiserId: [
+          { required: true, message: "请选择广告商", trigger: "change" }
+        ],
+        accountName: [
+          { required: true, message: "推广账号名称不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询广告商选项(只加载自定义广告商custom=2) */
+    getAdvertiserOptions() {
+      pageAdvertiser({ pageNum: 1, pageSize: 1000, enabled: 1, custom: 2 }).then(response => {
+        this.advertiserOptions = response.data.records;
+      });
+    },
+    /** 查询推广账号列表 */
+    getList() {
+      this.loading = true;
+      pagePromotionAccount(this.queryParams).then(response => {
+        this.accountList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        promotionType: "展示类",
+        advertiserId: undefined,
+        advertiserName: undefined,
+        configMode: 1,
+        accountName: undefined,
+        accountShortName: undefined,
+        apiSwitch: 2,
+        custom: 2,
+        appId: undefined,
+        appSecret: undefined,
+        adAccountId: undefined,
+        callbackUrl: undefined,
+        authUrl: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.current = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.isEdit = false;
+      // 新增时加载广告商列表(只加载自定义广告商custom=2)
+      this.getAdvertiserOptions();
+      this.open = true;
+      this.title = "添加自定义推广账号";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.isEdit = true;
+      const id = row.id || this.ids[0];
+      getPromotionAccount(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改自定义推广账号";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != undefined) {
+            updatePromotionAccount(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addPromotionAccount(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids;
+      this.$confirm('是否确认删除选中的推广账号?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return batchDelPromotionAccount(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    },
+    /** 广告商变化时 */
+    handleAdvertiserChange(advertiserId) {
+      this.form.advertiserName = "";
+      if (advertiserId) {
+        const advertiser = this.advertiserOptions.find(item => item.id === advertiserId);
+        if (advertiser) {
+          this.form.advertiserName = advertiser.advertiserName;
+        }
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 297 - 0
src/views/adv/domain/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="域名名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入域名名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="域名地址" prop="domain">
+        <el-input
+          v-model="queryParams.domain"
+          placeholder="请输入域名地址"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="禁用" :value="0" />
+        </el-select>
+      </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>
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="domainList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="id" width="80" />
+      <el-table-column label="域名名称" align="center" prop="name" />
+      <el-table-column label="域名地址" align="center" prop="domain" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleStatusChange(scope.row)"
+          ></el-switch>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" 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-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.page"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改域名对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="域名名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入域名名称" />
+        </el-form-item>
+        <el-form-item label="域名地址" prop="domain">
+          <el-input v-model="form.domain" placeholder="请输入域名地址,如:www.example.com" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" 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 { pageDomain, getDomain, addDomain, updateDomain, delDomain, batchDelDomain, updateDomainStatus } from "@/api/adv/domain";
+
+export default {
+  name: "Domain",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 域名表格数据
+      domainList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        page: 1,
+        size: 10,
+        name: undefined,
+        domain: undefined,
+        status: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: "域名名称不能为空", trigger: "blur" }
+        ],
+        domain: [
+          { required: true, message: "域名地址不能为空", trigger: "blur" }
+        ],
+        status: [
+          { required: true, message: "状态不能为空", trigger: "change" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询域名列表 */
+    getList() {
+      this.loading = true;
+      pageDomain(this.queryParams).then(response => {
+        this.domainList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 域名状态修改
+    handleStatusChange(row) {
+      let text = row.status === 1 ? "启用" : "禁用";
+      this.$confirm('确认要"' + text + '""' + row.name + '"域名吗?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return updateDomainStatus(row.id, row.status);
+      }).then(() => {
+        this.msgSuccess(text + "成功");
+      }).catch(() => {
+        row.status = row.status === 0 ? 1 : 0;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        name: undefined,
+        domain: undefined,
+        status: 1,
+        remark: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.page = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加域名";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids[0];
+      getDomain(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改域名";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != undefined) {
+            updateDomain(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addDomain(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids;
+      this.$confirm('是否确认删除选中的域名?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return batchDelDomain(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    }
+  }
+};
+</script>
+

+ 431 - 0
src/views/adv/landingPageTemplate/index.vue

@@ -0,0 +1,431 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="模板名称" prop="templateName">
+        <el-input
+          v-model="queryParams.templateName"
+          placeholder="请输入模板名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="模板类型" prop="templateType">
+        <el-input
+          v-model="queryParams.templateType"
+          placeholder="请输入模板类型"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="禁用" :value="0" />
+        </el-select>
+      </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>
+      <!-- <el-col :span="1.5">
+        <el-button
+          plain
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col> -->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="templateList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="id" width="200" show-overflow-tooltip />
+      <el-table-column label="模板名称" align="center" prop="templateName" />
+      <el-table-column label="模板类型" align="center" prop="templateType" width="120" />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleStatusChange(scope.row)"
+          ></el-switch>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" 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-column label="操作" align="center" class-name="small-padding fixed-width" width="210">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document-copy"
+            @click="handleCopy(scope.row)"
+          >复制</el-button>
+          <!-- <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button> -->
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.page"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改模板对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="模板名称" prop="templateName">
+          <el-input v-model="form.templateName" placeholder="请输入模板名称" />
+        </el-form-item>
+        <el-form-item label="模板类型" prop="templateType">
+          <el-select v-model="form.templateType" placeholder="请选择模板类型" style="width: 100%" @change="handleTemplateTypeChange" :disabled="form.id != undefined">
+            <el-option label="免费表单类" value="免费表单类" />
+            <el-option label="小程序表单类" value="小程序表单类" />
+            <el-option label="一站式小程序表单类" value="一站式小程序表单类" />
+            <el-option label="免费加粉类" value="免费加粉类" />
+            <el-option label="小程序加粉类" value="小程序加粉类" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="编辑页面">
+          <el-button type="primary" @click="openH5(form.templateData)" :disabled="!form.templateType">
+            编辑页面
+          </el-button>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" 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>
+
+    <!-- H5编辑对话框 -->
+    <el-dialog title="编辑页面" :visible.sync="h5Open" append-to-body fullscreen class="h5-editor-dialog">
+      <div class="h5-editor-wrapper">
+        <H5Editor ref="h5Editor" />
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="h5Ok">确 定</el-button>
+        <el-button @click="h5Cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageTemplate, getTemplate, addTemplate, updateTemplate, batchDelTemplate, updateTemplateStatus, copyTemplate } from "@/api/adv/landingPageTemplate";
+import H5Editor from "@/components/H5Editor/index.vue";
+
+export default {
+  name: "LandingPageTemplate",
+  components: {
+    H5Editor
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 模板表格数据
+      templateList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // H5编辑器对话框
+      h5Open: false,
+      queryParams: {
+        page: 1,
+        size: 10,
+        templateName: undefined,
+        templateType: undefined,
+        domainId: undefined,
+        status: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        templateName: [
+          { required: true, message: "模板名称不能为空", trigger: "blur" }
+        ],
+        status: [
+          { required: true, message: "状态不能为空", trigger: "change" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询模板列表 */
+    getList() {
+      this.loading = true;
+      pageTemplate(this.queryParams).then(response => {
+        this.templateList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 模板状态修改
+    handleStatusChange(row) {
+      let text = row.status === 1 ? "启用" : "禁用";
+      this.$confirm('确认要"' + text + '""' + row.templateName + '"模板吗?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return updateTemplateStatus(row.id, row.status);
+      }).then(() => {
+        this.msgSuccess(text + "成功");
+      }).catch(() => {
+        row.status = row.status === 0 ? 1 : 0;
+      });
+    },
+    // 处理模板类型改变
+    handleTemplateTypeChange(val) {
+      let id = null;
+      switch (val) {
+        case '免费表单类':
+          id = 1;
+          break;
+        case '小程序表单类':
+          id = 2;
+          break;
+        case '一站式小程序表单类':
+          id = 3;
+          break;
+        case '免费加粉类':
+          id = 4;
+          break;
+        case '小程序加粉类':
+          id = 5;
+          break;
+      }
+      
+      if (id) {
+        getTemplate(id).then(response => {
+          if (response.data && response.data.templateData) {
+            this.form.templateData = response.data.templateData;
+            // 如果是在新增模式下且没有填写标题,可以考虑是否要同步其他信息,暂时只同步内容
+          }
+        });
+      }
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        templateName: undefined,
+        templateData: undefined,
+        templateType: undefined,
+        domainId: undefined,
+        status: 0,
+        remark: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.page = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      // 清空H5编辑器的缓存数据
+      if (this.$refs.h5Editor) {
+        this.$refs.h5Editor.initData('{"pageTitle":"","configList":[]}')
+      }
+      this.open = true;
+      this.title = "添加模板";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids[0];
+      getTemplate(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改模板";
+      });
+    },
+    /** 复制按钮操作 */
+    /** 复制按钮操作 */
+    handleCopy(row) {
+      this.reset();
+      const id = row.id;
+      getTemplate(id).then(response => {
+        this.form = response.data;
+        this.form.id = undefined;
+        this.form.templateName = this.form.templateName + "_副本";
+        this.open = true;
+        this.title = "复制并添加模板";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          // 验证JSON格式
+          if (this.form.templateData) {
+            try {
+              JSON.parse(this.form.templateData);
+            } catch (e) {
+              this.msgError("模板数据格式不正确,必须是有效的JSON");
+              return;
+            }
+          }
+          
+          if (this.form.id != undefined) {
+            updateTemplate(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addTemplate(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids;
+      this.$confirm('是否确认删除选中的模板?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return batchDelTemplate(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    },
+    /** 打开H5编辑器 */
+    openH5(templateData) {
+      this.h5Open = true
+      this.$nextTick(() => {
+        // 支持新旧数据格式
+        this.$refs.h5Editor.initData(templateData || '{"pageTitle":"","configList":[]}', this.form.templateType)
+      })
+    },
+    /** H5编辑器确定 */
+    h5Ok() {
+      // 通过ref获取子组件的完整数据(新格式JSON)
+      const pageData = this.$refs.h5Editor.getPageData()
+      this.form.templateData = JSON.stringify(pageData)
+      this.h5Open = false // 关闭对话框
+    },
+    /** H5编辑器取消 */
+    h5Cancel() {
+      this.h5Open = false;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.h5-editor-dialog /deep/ .el-dialog {
+  display: flex;
+  flex-direction: column;
+}
+
+.h5-editor-dialog /deep/ .el-dialog__body {
+  flex: 1;
+  overflow: hidden;
+  padding: 0;
+}
+
+.h5-editor-wrapper {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.h5-editor-wrapper /deep/ .h5-editor-container {
+  width: 100%;
+  height: 100%;
+}
+</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>

+ 736 - 0
src/views/adv/promotionAccount/index.vue

@@ -0,0 +1,736 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="账号名称" prop="accountName">
+        <el-input
+          v-model="queryParams.accountName"
+          placeholder="请输入账号名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select v-model="queryParams.advertiserId" placeholder="请选择广告商" clearable size="small" filterable>
+          <el-option
+            v-for="item in advertiserOptions"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </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>
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="accountList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="账户简称" align="center" prop="accountShortName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="广告商名称" align="center" prop="advertiserName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="读取方式" align="center" prop="apiSwitch" width="120">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.apiSwitch === 1 ? 'success' : 'info'" size="small">
+            {{ scope.row.apiSwitch === 1 ? 'API' : '手动录入' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="广告主ID" align="center" prop="adAccountId" min-width="180" 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-column label="操作" align="center" class-name="small-padding fixed-width" width="240" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-check"
+            @click="handleVerify(scope.row)"
+            :disabled="!scope.row.authUrl"
+          >验证</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.current"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改推广账号对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body class="promotion-dialog">
+      <el-form ref="form" :model="form" :rules="rules" label-width="140px" class="elegant-form">
+        <!-- 基础信息 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-info"></i>
+            <span>基础信息</span>
+          </div>
+          <el-form-item label="推广方式" prop="promotionType">
+            <el-input v-model="form.promotionType" disabled placeholder="展示类" />
+          </el-form-item>
+          <el-form-item label="广告商" prop="advertiserId">
+            <el-select 
+              v-model="form.advertiserId" 
+              placeholder="请选择广告商" 
+              style="width: 100%" 
+              filterable
+              @change="handleAdvertiserChange"
+              :disabled="isEdit"
+            >
+              <el-option
+                v-for="item in advertiserOptions"
+                :key="item.id"
+                :label="item.advertiserName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <!-- 如何申请API按钮 -->
+          <el-form-item v-if="form.advertiserId" label=" " class="api-help-button">
+            <el-button 
+              type="text" 
+              @click="openApiHelp"
+              icon="el-icon-question"
+            >如何申请API</el-button>
+          </el-form-item>
+          <!-- <el-form-item label="配置方式" prop="configMode">
+            <el-radio-group v-model="form.configMode">
+              <el-radio :label="1" :border="true">服务模式</el-radio>
+              <el-radio :label="2" :border="true">广告主模式</el-radio>
+            </el-radio-group>
+          </el-form-item> -->
+        </div>
+
+        <!-- 账号信息 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-user"></i>
+            <span>账号信息</span>
+          </div>
+          <el-form-item label="推广账号名称" prop="accountName">
+            <el-input 
+              v-model="form.accountName" 
+              placeholder="请输入推广账号名称"
+              prefix-icon="el-icon-user-solid"
+              clearable
+              :disabled="isEdit"
+            />
+          </el-form-item>
+          <el-form-item label="推广账户简称" prop="accountShortName">
+            <el-input 
+              v-model="form.accountShortName" 
+              placeholder="请输入推广账户简称"
+              prefix-icon="el-icon-tickets"
+              clearable
+            />
+          </el-form-item>
+        </div>
+
+        <!-- API配置 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-setting"></i>
+            <span>API配置</span>
+          </div>
+          <el-form-item label="API读取账号信息" prop="apiSwitch">
+            <el-radio-group v-model="form.apiSwitch">
+              <el-radio :label="1" :border="true">开</el-radio>
+              <el-radio :label="2" :border="true">关</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          
+          <template v-if="form.apiSwitch === 1">
+            <el-form-item label="AppID" prop="appId" class="slide-fade">
+              <el-input 
+                v-model="form.appId" 
+                placeholder="请输入AppID"
+                prefix-icon="el-icon-key"
+                clearable
+              />
+            </el-form-item>
+            <el-form-item label="Secret Key" prop="appSecret" class="slide-fade">
+              <el-input 
+                v-model="form.appSecret" 
+                placeholder="请输入Secret Key"
+                prefix-icon="el-icon-lock"
+                type="password"
+                show-password
+                clearable
+              />
+            </el-form-item>
+            <el-form-item label="广告主id" prop="adAccountId" class="slide-fade">
+              <el-input 
+                v-model="form.adAccountId" 
+                placeholder="请输入广告主id"
+                prefix-icon="el-icon-postcard"
+                clearable
+              />
+            </el-form-item>
+            <el-form-item label="回调地址" prop="callbackUrl" class="slide-fade">
+              <el-input 
+                v-model="form.callbackUrl" 
+                placeholder="请输入回调地址"
+                prefix-icon="el-icon-link"
+                clearable
+              />
+            </el-form-item>
+            <el-form-item v-if="form.advertiserId !== 10002" label="应用授权链接" prop="authUrl" class="slide-fade">
+              <el-input 
+                v-model="form.authUrl" 
+                placeholder="请输入应用授权链接"
+                prefix-icon="el-icon-connection"
+                clearable
+              />
+            </el-form-item>
+          </template>
+        </div>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">取 消</el-button>
+        <el-button type="primary" @click="submitForm" icon="el-icon-check">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pagePromotionAccount, getPromotionAccount, addPromotionAccount, updatePromotionAccount, delPromotionAccount, batchDelPromotionAccount } from "@/api/adv/promotionAccount";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "PromotionAccount",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 推广账号表格数据
+      accountList: [],
+      // 广告商选项
+      advertiserOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否为编辑模式
+      isEdit: false,
+      // 查询参数
+      queryParams: {
+        current: 1,
+        size: 10,
+        custom:1,
+        accountName: undefined,
+        advertiserId: undefined
+      },
+      // 表单参数
+      form: {
+      },
+      // API文档URL映射
+      apiDocumentUrls: {
+        10001: 'https://probe.bjmantis.net/msp/prmt/help/api/BAIDU.pdf',
+        10002: 'https://probe.bjmantis.net/msp/prmt/help/api/TOUTIAO.pdf',
+        10003: 'https://probe.bjmantis.net/msp/prmt/help/api/GDT.pdf',
+        10004: 'https://probe.bjmantis.net/msp/prmt/help/api/OPPO.pdf',
+        10005: 'https://probe.bjmantis.net/msp/prmt/help/api/VIVO.pdf',
+        10006: 'https://probe.bjmantis.net/msp/prmt/help/api/AIQIYI.pdf'
+      },
+      // 表单校验
+      rules: {
+        promotionType: [
+          { required: true, message: "推广方式不能为空", trigger: "blur" }
+        ],
+        advertiserId: [
+          { required: true, message: "请选择广告商", trigger: "change" }
+        ],
+        configMode: [
+          { required: true, message: "请选择配置方式", trigger: "change" }
+        ],
+        accountName: [
+          { required: true, message: "推广账号名称不能为空", trigger: "blur" }
+        ],
+        accountShortName: [
+          { required: true, message: "推广账户简称不能为空", trigger: "blur" }
+        ],
+        apiSwitch: [
+          { required: true, message: "请选择API读取账号信息", trigger: "change" }
+        ],
+        appId: [
+          { 
+            validator: (rule, value, callback) => {
+              if (this.form.apiSwitch === 1 && !value) {
+                callback(new Error('请输入AppID'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        appSecret: [
+          { 
+            validator: (rule, value, callback) => {
+              if (this.form.apiSwitch === 1 && !value) {
+                callback(new Error('请输入Secret Key'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ],
+        adAccountId: [
+          { 
+            validator: (rule, value, callback) => {
+              if (this.form.apiSwitch === 1 && !value) {
+                callback(new Error('请输入广告主id'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "blur" 
+          }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+    // 移除自动加载广告商列表,改为新增时按需加载
+  },
+  methods: {
+    /** 查询广告商选项 */
+    getAdvertiserOptions() {
+      pageAdvertiser({ pageNum: 1, pageSize: 1000, enabled: 1, custom: 1 }).then(response => {
+        this.advertiserOptions = response.data.records;
+      });
+    },
+    /** 查询推广账号列表 */
+    getList() {
+      this.loading = true;
+      pagePromotionAccount(this.queryParams).then(response => {
+        this.accountList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        promotionType: "展示类",
+        advertiserId: undefined,
+        advertiserName: undefined,
+        configMode: 1,
+        accountName: undefined,
+        accountShortName: undefined,
+        apiSwitch: 2,
+        custom: 1,
+        appId: undefined,
+        appSecret: undefined,
+        adAccountId: undefined,
+        callbackUrl: undefined,
+        authUrl: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.current = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.isEdit = false;
+      // 新增时加载广告商列表(只加载线上广告商custom=1)
+      this.getAdvertiserOptions();
+      this.open = true;
+      this.title = "添加推广账号";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.isEdit = true;
+      const id = row.id || this.ids[0];
+      getPromotionAccount(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改推广账号";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != undefined) {
+            updatePromotionAccount(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addPromotionAccount(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id ? [row.id] : this.ids;
+      this.$confirm('是否确认删除选中的推广账号?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return batchDelPromotionAccount(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    },
+    /** 广告商变化时 */
+    handleAdvertiserChange(advertiserId) {
+      this.form.advertiserName = "";
+      if (advertiserId) {
+        const advertiser = this.advertiserOptions.find(item => item.id === advertiserId);
+        if (advertiser) {
+          this.form.advertiserName = advertiser.advertiserName;
+        }
+      }
+    },
+    /** 打开API帮助文档 */
+    openApiHelp() {
+      const url = this.apiDocumentUrls[this.form.advertiserId];
+      if (url) {
+        window.open(url, '_blank');
+      } else {
+        this.$message.warning('此广告商暂无法理文档');
+      }
+    },
+    /** 验证按钮操作 */
+    handleVerify(row) {
+      if (!row.authUrl) {
+        this.$message.warning('此账户暂未配置应用授权链接');
+        return;
+      }
+      window.open(row.authUrl, '_blank');
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+// 对话框样式
+::v-deep .promotion-dialog {
+  .el-dialog__header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    padding: 20px;
+    margin: 0;
+    border-radius: 4px 4px 0 0;
+    
+    .el-dialog__title {
+      color: #fff;
+      font-size: 18px;
+      font-weight: 500;
+    }
+    
+    .el-dialog__headerbtn .el-dialog__close {
+      color: #fff;
+      font-size: 20px;
+      
+      &:hover {
+        color: #f0f0f0;
+      }
+    }
+  }
+  
+  .el-dialog__body {
+    padding: 25px 30px;
+    background-color: #f8f9fa;
+    max-height: 70vh;
+    overflow-y: auto;
+  }
+  
+  .el-dialog__footer {
+    padding: 15px 30px;
+    border-top: 1px solid #e8e8e8;
+    background-color: #fff;
+  }
+}
+
+// 表单样式
+.elegant-form {
+  .form-section {
+    background: #fff;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+    
+    &:hover {
+      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+      transform: translateY(-2px);
+    }
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  
+  .section-title {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 2px solid #e8e8e8;
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
+    
+    i {
+      font-size: 18px;
+      margin-right: 8px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      background-clip: text;
+    }
+    
+    span {
+      position: relative;
+      
+      &::after {
+        content: '';
+        position: absolute;
+        left: 0;
+        bottom: -12px;
+        width: 0;
+        height: 2px;
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        transition: width 0.3s ease;
+      }
+    }
+  }
+  
+  .form-section:hover .section-title span::after {
+    width: 100%;
+  }
+  
+  .el-form-item {
+    margin-bottom: 20px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  
+  ::v-deep .el-form-item__label {
+    color: #606266;
+    font-weight: 500;
+    font-size: 14px;
+  }
+  
+  ::v-deep .el-input__inner,
+  ::v-deep .el-textarea__inner {
+    border-radius: 6px;
+    border: 1px solid #dcdfe6;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      border-color: #c0c4cc;
+    }
+    
+    &:focus {
+      border-color: #667eea;
+      box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+    }
+    
+    &:disabled {
+      background-color: #f5f7fa;
+      color: #909399;
+    }
+  }
+  
+  ::v-deep .el-radio {
+    margin-right: 20px;
+    
+    &.is-bordered {
+      border-radius: 6px;
+      padding: 10px 20px;
+      transition: all 0.3s ease;
+      
+      &:hover {
+        border-color: #667eea;
+      }
+      
+      &.is-checked {
+        border-color: #667eea;
+        background-color: rgba(102, 126, 234, 0.05);
+      }
+    }
+  }
+  
+  // API字段显示动画
+  .slide-fade {
+    animation: slideDown 0.3s ease;
+  }
+}
+
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// API帮助按钮样式
+.api-help-button {
+  ::v-deep .el-form-item__content {
+    padding-top: 5px;
+  }
+  
+  .el-button {
+    color: #667eea;
+    font-size: 13px;
+    transition: all 0.3s ease;
+    padding: 0;
+    
+    &:hover {
+      color: #764ba2;
+      
+      .el-icon-question {
+        animation: bounce 0.5s ease;
+      }
+    }
+    
+    .el-icon-question {
+      margin-right: 6px;
+    }
+  }
+}
+
+@keyframes bounce {
+  0%, 100% {
+    transform: translateY(0);
+  }
+  50% {
+    transform: translateY(-3px);
+  }
+}
+
+// 按钮样式
+.dialog-footer {
+  text-align: right;
+  
+  .el-button {
+    padding: 10px 24px;
+    border-radius: 6px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    
+    &.el-button--primary {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border: none;
+      
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+    }
+    
+    &.el-button--default {
+      &:hover {
+        color: #667eea;
+        border-color: #667eea;
+        background-color: rgba(102, 126, 234, 0.05);
+      }
+    }
+  }
+}
+</style>
+

+ 1637 - 0
src/views/adv/site/index.vue

@@ -0,0 +1,1637 @@
+<template>
+  <div class="app-container">
+    <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>
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="success"
+          icon="el-icon-refresh"
+          size="mini"
+          @click="getList"
+        >刷新</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="siteList">
+      <el-table-column label="ID" align="center" prop="id" width="80" />
+      <el-table-column label="站点名称" align="center" prop="siteName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="投放类型" align="center" prop="launchType" width="100" />
+      <el-table-column label="配置回传" align="center" prop="configCallback" width="100">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.configCallback === 1 ? 'success' : 'info'" size="small">
+            {{ scope.row.configCallback === 1 ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="站点状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
+            {{ scope.row.status === 1 ? '启用' : '停用' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <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-column label="操作" align="center" class-name="small-padding fixed-width" width="420" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-data-line"
+            @click="handleStatistics(scope.row)"
+          >统计</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            :icon="scope.row.status === 1 ? 'el-icon-video-pause' : 'el-icon-video-play'"
+            @click="handleEnable(scope.row)"
+          >{{ scope.row.status === 1 ? '停用' : '启用' }}</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            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>
+
+    <!-- 添加或修改站点对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body class="site-dialog">
+      <el-form ref="form" :model="form" :rules="rules" label-width="130px" class="elegant-form">
+        <!-- 基础信息 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-info"></i>
+            <span>基础信息</span>
+          </div>
+          <el-form-item label="站点名称" prop="siteName">
+            <el-input 
+              v-model="form.siteName" 
+              placeholder="请输入站点名称"
+              prefix-icon="el-icon-notebook-2"
+              clearable
+              :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>
+
+        <!-- 投放配置 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-setting"></i>
+            <span>投放配置</span>
+          </div>
+          <el-form-item label="投放类型" prop="launchType">
+            <el-select 
+              v-model="form.launchType" 
+              placeholder="请选择投放类型" 
+              style="width: 100%"
+              prefix-icon="el-icon-s-flag"
+              @change="handleLaunchTypeChange"
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in launchTypeOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="广告类型" prop="adType">
+            <el-select 
+              v-model="form.adType" 
+              placeholder="请选择广告类型" 
+              style="width: 100%"
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in adTypeOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <!-- 企微分配规则 -->
+          <el-form-item label="企微分配规则" prop="allocationRule">
+            <el-radio-group v-model="form.allocationRule" @change="handleAllocationRuleChange" :disabled="!form.launchType || isDetail">
+              <el-radio 
+                label="1"
+                :border="true"
+              >个人码分配</el-radio>
+              <el-radio 
+                label="2"
+                :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.ruleName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item 
+            v-if="form.allocationRule === '2'" 
+            :label="form.launchType === 1 ? '群活码' : '企业微信活码'" 
+            prop="allocationRuleId"
+            class="slide-fade"
+          >
+            <el-select 
+              v-model="form.allocationRuleId" 
+              :placeholder="form.launchType === 1 ? '请选择群活码' : '请选择企业微信活码'" 
+              style="width: 100%"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in form.launchType === 1 ? groupActiveList : contactWayList"
+                :key="item.id"
+                :label="form.launchType === 1 ? item.groupName : item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+
+        <!-- 广告商信息 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-office-building"></i>
+            <span>广告商信息</span>
+          </div>
+          <el-form-item label="投放广告商" prop="advertiserId">
+            <el-select 
+              v-model="form.advertiserId" 
+              placeholder="请先选择投放类型" 
+              style="width: 100%"
+              @change="handleAdvertiserChange"
+              filterable
+              :disabled="!form.launchType || isDetail"
+            >
+              <el-option
+                v-for="item in advertiserList"
+                :key="item.id"
+                :label="item.advertiserName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="投放广告商账号" prop="promotionAccountId">
+            <el-select 
+              v-model="form.promotionAccountId" 
+              placeholder="请选择投放广告商账号" 
+              style="width: 100%"
+              @change="handlePromotionAccountChange"
+              filterable
+              :disabled="!form.advertiserId || isDetail"
+            >
+              <el-option
+                v-for="item in promotionAccountList"
+                :key="item.id"
+                :label="item.accountName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+
+        <!-- 页面和来源 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-monitor"></i>
+            <span>页面和来源</span>
+          </div>
+          <el-form-item label="投放页面" prop="launchPageId">
+            <el-select 
+              v-model="form.launchPageId" 
+              placeholder="请选择投放页面" 
+              style="width: 100%"
+              @change="handleLaunchPageChange"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in landingPageTemplateList"
+                :key="item.id"
+                :label="item.templateName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="投放域名" prop="launchDomain">
+            <el-select 
+              v-model="form.launchDomain" 
+              placeholder="请选择投放域名" 
+              style="width: 100%"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in launchDomainList"
+                :key="item.id"
+                :label="item.domain"
+                :value="item.domain"
+              />
+            </el-select>
+          </el-form-item>
+
+        </div>
+
+        <!-- 回传配置 -->
+        <div class="form-section">
+          <div class="section-title">
+            <i class="el-icon-share"></i>
+            <span>回传配置</span>
+          </div>
+          <el-form-item label="是否配置回传" prop="configCallback">
+            <el-radio-group v-model="form.configCallback" @change="handleConfigCallbackChange" :disabled="isDetail">
+              <el-radio 
+                v-for="item in configCallbackOptions"
+                :key="item.value"
+                :label="item.value"
+                :border="true"
+              >{{ item.label }}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item 
+            v-if="form.configCallback === 1" 
+            label="回传账号" 
+            prop="callbackAccountId"
+            class="slide-fade"
+          >
+            <el-select 
+              v-model="form.callbackAccountId" 
+              placeholder="请选择回传账号" 
+              style="width: 100%"
+              @change="handleCallbackAccountChange"
+              filterable
+              :disabled="isDetail"
+            >
+              <el-option
+                v-for="item in callbackAccountList"
+                :key="item.id"
+                :label="item.accountName"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <!-- 转换类型配置区域 -->
+          <div v-if="form.configCallback === 1 && form.callbackAccountId" class="conversion-config-section">
+            <div class="config-header">
+              <span class="config-title">转换类型配置</span>
+              <el-button type="primary" size="small" icon="el-icon-plus" @click="addConversionEvent">添加事件</el-button>
+            </div>
+            
+            <div v-if="conversionEvents.length === 0" class="empty-tip">
+              暂无转换事件,请点击“添加事件”按钮添加
+            </div>
+            
+            <div v-for="(event, index) in conversionEvents" :key="index" class="conversion-event-item">
+              <div class="event-header">
+                <span class="event-title">事件{{ index + 1 }}</span>
+                <el-button 
+                  type="danger" 
+                  size="mini" 
+                  icon="el-icon-delete" 
+                  circle
+                  @click="removeConversionEvent(index)"
+                ></el-button>
+              </div>
+              
+              <el-form label-width="130px" style="margin-top: 10px;">
+                <el-form-item label="广告商转化类型">
+                  <el-select 
+                    v-model="event.advertiserEventType" 
+                    placeholder="请选择广告商转化类型" 
+                    style="width: 100%"
+                    @change="handleAdvertiserEventChange(index, $event)"
+                  >
+                    <el-option
+                      v-for="item in advertiserEventOptions"
+                      :key="item.eventType"
+                      :label="item.eventName"
+                      :value="item.eventType"
+                    />
+                  </el-select>
+                </el-form-item>
+                
+                <el-form-item label="回传数据类型">
+                  <el-select 
+                    v-model="event.systemEventType" 
+                    placeholder="请选择回传数据类型" 
+                    style="width: 100%"
+                    @change="handleSystemEventChange(index, $event)"
+                  >
+                    <el-option
+                      v-for="item in systemEventOptions"
+                      :key="item.eventType"
+                      :label="item.eventName"
+                      :value="item.eventType"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-form>
+            </div>
+          </div>
+        </div>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">取 消</el-button>
+        <el-button v-if="!isDetail" type="primary" @click="submitForm" icon="el-icon-check">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 站点详情对话框 -->
+    <el-dialog title="站点详情" :visible.sync="detailOpen" width="900px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="站点名称">{{ detail.siteName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="站点URL" :span="2">{{ detail.siteUrl || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="投放类型">{{ detail.launchType || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="广告类型">{{ detail.adType || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="广告商名称" :span="2">{{ detail.advertiserName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="推广账户名称" :span="2">{{ detail.promotionAccountName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="投放页面名称" :span="2">{{ detail.launchPageName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="投放域名" :span="2">{{ detail.launchDomain || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="一级分配规则">
+          <span v-if="detail.allocationRule === '1'">选择员工"个人码"分配规则</span>
+          <span v-else-if="detail.allocationRule === '2'">选择员工"活码"分配规则</span>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="二级分配汇总">{{ detail.allocationRuleId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="项目名称">{{ detail.projectName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="配置回传">
+          <el-tag :type="detail.configCallback === 1 ? 'success' : 'info'" size="small">
+            {{ detail.configCallback === 1 ? '是' : '否' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="回传账号名称">{{ detail.callbackAccountName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="站点状态">
+          <el-tag :type="detail.status === 1 ? 'success' : 'info'" size="small">
+            {{ detail.status === 1 ? '启用' : '停用' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="创建人">{{ detail.creator || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ parseTime(detail.createTime) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="更新人">{{ detail.updater || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间" :span="2">{{ parseTime(detail.updateTime) || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="detailOpen = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 站点统计对话框 -->
+    <el-dialog title="站点统计" :visible.sync="statisticsOpen" width="800px" append-to-body>
+      <el-descriptions :column="2" border v-if="statistics">
+        <el-descriptions-item label="站点ID">{{ statistics.siteId }}</el-descriptions-item>
+        <el-descriptions-item label="访问量">{{ statistics.visitCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="访客数">{{ statistics.visitorCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="转化数">{{ statistics.conversionCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="转化率">
+          {{ statistics.conversionRate ? (statistics.conversionRate * 100).toFixed(2) + '%' : '0%' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="统计时间">{{ parseTime(statistics.updateTime) }}</el-descriptions-item>
+      </el-descriptions>
+      <div v-else style="text-align: center; padding: 40px 0;">
+        <el-empty description="暂无统计数据"></el-empty>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <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>
+
+<script>
+import { listSite, getSite, addSite, updateSite, delSite, getSiteStatistics, enableSite } from "@/api/adv/site";
+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 { pageAssignRule } from "@/api/qw/assignRule";
+import { pageGroupLiveCode } from "@/api/qw/groupLiveCode";
+import { listContactWay } from "@/api/qw/contactWay";
+
+export default {
+  name: "Site",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 站点表格数据
+      siteList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否为详情模式(只读)
+      isDetail: false,
+      // 是否显示详情对话框
+      detailOpen: false,
+      // 详情数据
+      detail: {},
+      // 是否显示统计弹出层
+      statisticsOpen: false,
+      // 统计数据
+      statistics: null,
+      // 表单参数
+      form: {},
+      // 投放类型选项
+      launchTypeOptions: [
+        { label: "线上投放", value: 1 },
+        { label: "线下投放", value: 2 }
+      ],
+      // 广告类型选项
+      adTypeOptions: [
+        { label: "信息流广告", value: "信息流广告" }
+      ],
+      // 是否配置回传选项
+      configCallbackOptions: [
+        { label: "否", value: 0 },
+        { label: "是", value: 1 }
+      ],
+      // 广告商列表
+      advertiserList: [],
+      // 广告商账号列表
+      promotionAccountList: [],
+      // 落地页模板列表
+      landingPageTemplateList: [],
+      // 企业微信分配规则列表
+      allocationRuleList: [],
+      // 群活码列表
+      groupActiveList: [],
+      // 企业微信活码列表
+      contactWayList: [],
+      // 回传账号列表
+      callbackAccountList: [],
+      // 投放域名列表
+      launchDomainList: [],
+      // 渠道列表
+      channelList: [],
+      // 项目列表
+      projectList: [],
+      // 转换事件列表
+      conversionEvents: [],
+      // 广告商事件选项(systemBuiltin='0')
+      advertiserEventOptions: [],
+      // 系统事件选项(systemBuiltin='1')
+      systemEventOptions: [],
+      // 生成url对话框
+      urlDialogOpen: false,
+      // 投放链接
+      launchUrl: "",
+      // 表单校验
+      rules: {
+        siteName: [
+          { required: true, message: "站点名称不能为空", trigger: "blur" }
+        ],
+        siteUrl: [
+          { required: true, message: "站点地址不能为空", trigger: "blur" }
+        ],
+        launchType: [
+          { required: true, message: "请选择投放类型", trigger: "change" }
+        ],
+        adType: [
+          { required: true, message: "请选择广告类型", trigger: "change" }
+        ],
+        advertiserId: [
+          { required: true, message: "请选择投放广告商", trigger: "change" }
+        ],
+        promotionAccountId: [
+          { required: true, message: "请选择投放广告商账号", trigger: "change" }
+        ],
+        launchPageId: [
+          { required: true, message: "请选择投放页面", trigger: "change" }
+        ],
+
+        launchDomain: [
+          { required: true, message: "请选择投放域名", trigger: "change" }
+        ],
+        configCallback: [
+          { required: true, message: "请选择是否配置回传", trigger: "change" }
+        ],
+        callbackAccountId: [
+          { 
+            validator: (rule, value, callback) => {
+              if (this.form.configCallback === 1 && !value) {
+                callback(new Error('请选择回传账号'));
+              } else {
+                callback();
+              }
+            }, 
+            trigger: "change" 
+          }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询站点列表 */
+    getList() {
+      this.loading = true;
+      listSite().then(response => {
+        this.siteList = response.data;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        siteName: undefined,
+        siteUrl: undefined,
+        launchType: undefined,
+        adType: undefined,
+        advertiserId: undefined,
+        advertiserName: undefined,
+        promotionAccountId: undefined,
+        promotionAccountName: undefined,
+        launchPageId: undefined,
+        launchPageName: undefined,
+        projectId: undefined,
+        projectName: undefined,
+        channelId: undefined,
+        channelName: undefined,
+        launchDomain: undefined,
+        configCallback: 0,
+        callbackAccountId: undefined,
+        callbackAccountName: undefined,
+        allocationRule: undefined,
+        allocationRuleId: undefined
+      };
+      this.promotionAccountList = [];
+      this.launchDomainList = [];
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      this.contactWayList = [];
+      this.conversionEvents = [];
+      this.advertiserEventOptions = [];
+      this.systemEventOptions = [];
+      this.resetForm("form");
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      // 不再预加载广告商列表,等待选择投放类型后再加载
+      this.loadCommonSelectOptions();
+      this.isDetail = false;
+      this.open = true;
+      this.title = "添加站点";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.loadCommonSelectOptions();
+      this.isDetail = false;
+      getSite(row.id).then(response => {
+        this.form = response.data;
+        // 确保 allocationRule 是字符串类型
+        if (this.form.allocationRule !== undefined && this.form.allocationRule !== null) {
+          this.form.allocationRule = String(this.form.allocationRule);
+        }
+        // 根据投放类型加载广告商列表
+        if (this.form.launchType) {
+          this.loadAdvertiserList(this.form.launchType);
+        }
+        // 如果有广告商ID,加载对应的广告商账号列表
+        if (this.form.advertiserId) {
+          this.loadPromotionAccountList(this.form.advertiserId);
+          
+          // 如果配置了回传,加载回传账号列表
+          if (this.form.configCallback === 1) {
+            this.loadCallbackAccountList(this.form.advertiserId);
+            
+            // 如果有回传账号,加载事件类型和转换事件
+            if (this.form.callbackAccountId) {
+              this.loadEventTypesAndConversionEvents(this.form.advertiserId, this.form.callbackAccountId);
+            }
+          }
+        }
+        // 如果设置了企微分配规则,加载对应的数据
+        if (this.form.allocationRule) {
+          if (this.form.allocationRule === '1') {
+            // 加载企业微信分配规则列表
+            this.loadAllocationRuleList();
+          } else if (this.form.allocationRule === '2') {
+            // 加载活码列表(根据投放类型)
+            if (this.form.launchType === 1) {
+              this.loadGroupActiveList();
+            } else if (this.form.launchType === 2) {
+              this.loadContactWayList();
+            }
+          }
+        }
+        this.$nextTick(() => {
+          this.open = true;
+          this.title = "修改站点";
+        });
+      });
+    },
+    /** 查看统计按钮操作 */
+    handleStatistics(row) {
+      this.$router.push({
+        path: '/adv/new-adv/statistics',
+        query: { siteId: row.id }
+      });
+    },
+    /** 详情按钮操作 */
+    handleDetail(row) {
+      this.detail = {};
+      this.detailOpen = true;
+      getSite(row.id).then(response => {
+        this.detail = response.data;
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          // 如果配置了回传且有转换事件,校验转换事件
+          if (this.form.configCallback === 1 && this.form.callbackAccountId && this.conversionEvents.length > 0) {
+            for (let i = 0; i < this.conversionEvents.length; i++) {
+              const event = this.conversionEvents[i];
+              if (!event.systemEventType || !event.advertiserEventType) {
+                this.msgError(`请完善事件${i + 1}的配置`);
+                return;
+              }
+            }
+          }
+          
+          if (this.form.id != undefined) {
+            // 修改
+            updateSite(this.form.id, this.form).then(response => {
+              if (response.code === 200) {
+                // 如果配置了回传,保存转换事件
+                if (this.form.configCallback === 1 && this.form.callbackAccountId) {
+                  this.saveConversionEvents();
+                } else {
+                  this.msgSuccess("修改成功");
+                  this.open = false;
+                  this.getList();
+                }
+              }
+            });
+          } else {
+            // 新增
+            addSite(this.form).then(response => {
+              if (response.code === 200) {
+                // 如果配置了回传,保存转换事件
+                if (this.form.configCallback === 1 && this.form.callbackAccountId) {
+                  this.saveConversionEvents();
+                } else {
+                  this.msgSuccess("新增成功");
+                  this.open = false;
+                  this.getList();
+                }
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      this.$confirm('是否确认删除站点"' + row.siteName + '"?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return delSite(row.id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function() {});
+    },
+    /** 启用/停用按鑵操作 */
+    handleEnable(row) {
+      const statusText = row.status === 1 ? '停用' : '启用';
+      this.$confirm(`是否确认${statusText}站点"${row.siteName}"?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return enableSite(row.id);
+      }).then(() => {
+        this.getList();
+        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)
+      pageAdvertiser({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.advertiserList = response.data.records;
+      }).catch(error => {
+        console.error('加载广告商列表失败:', error);
+        this.advertiserList = [];
+      });
+      
+      // 加载落地页模板列表
+      pageTemplate({ pageNum: 1, pageSize: 1000, status: 1 }).then(response => {
+        this.landingPageTemplateList = response.data.records;
+      }).catch(error => {
+        console.error('加载落地页模板失败:', error);
+        this.landingPageTemplateList = [];
+      });
+      
+      // 加载域名列表
+      pageDomain({ pageNum: 1, pageSize: 1000, status: 1 }).then(response => {
+        this.launchDomainList = response.data.records;
+      }).catch(error => {
+        console.error('加载域名失败:', error);
+        this.launchDomainList = [];
+      });
+    },
+    /** 加载公共下拉选项数据(不包含广告商) */
+    loadCommonSelectOptions() {
+      // 加载落地页模板列表
+      pageTemplate({ pageNum: 1, pageSize: 1000, status: 1 }).then(response => {
+        this.landingPageTemplateList = response.data.records;
+      }).catch(error => {
+        console.error('加载落地页模板失败:', error);
+        this.landingPageTemplateList = [];
+      });
+      
+      // 加载域名列表
+      pageDomain({ pageNum: 1, pageSize: 1000, status: 1 }).then(response => {
+        this.launchDomainList = response.data.records;
+      }).catch(error => {
+        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) {
+      // 重置广告商相关数据
+      this.form.advertiserId = undefined;
+      this.form.advertiserName = "";
+      this.form.promotionAccountId = undefined;
+      this.form.promotionAccountName = "";
+      this.advertiserList = [];
+      this.promotionAccountList = [];
+      
+      // 重置回传账号相关数据
+      this.form.callbackAccountId = undefined;
+      this.form.callbackAccountName = "";
+      this.callbackAccountList = [];
+      this.conversionEvents = [];
+      this.advertiserEventOptions = [];
+      this.systemEventOptions = [];
+      
+      // 重置企业微信分配规则相关数据
+      this.form.allocationRule = undefined;
+      this.form.allocationRuleId = undefined;
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      this.contactWayList = [];
+      
+      // 根据投放类型加载广告商列表
+      if (launchType) {
+        this.loadAdvertiserList(launchType);
+      }
+      
+      // 根据投放类型重新加载活码数据(如果已选择活码分配)
+      if (this.form.allocationRule === '2') {
+        if (launchType === 1) {
+          this.loadGroupActiveList();
+        } else if (launchType === 2) {
+          this.loadContactWayList();
+        }
+      }
+    },
+    /** 加载广告商列表 */
+    loadAdvertiserList(launchType) {
+      if (!launchType) return;
+      // launchType: 1=线上投放, 2=线下投放
+      // custom: 1=线上广告商, 2=自定义广告商
+      const customValue = launchType; // launchType值直接对应custom值
+      pageAdvertiser({ 
+        pageNum: 1, 
+        pageSize: 1000, 
+        enabled: 1,
+        custom: customValue 
+      }).then(response => {
+        this.advertiserList = response.data.records;
+      }).catch(error => {
+        console.error('加载广告商列表失败:', error);
+        this.advertiserList = [];
+      });
+    },
+    /** 广告商变化时 */
+    handleAdvertiserChange(advertiserId) {
+      this.form.advertiserName = "";
+      this.form.promotionAccountId = undefined;
+      this.form.promotionAccountName = "";
+      this.promotionAccountList = [];
+      
+      // 重置回传账号相关数据
+      this.form.callbackAccountId = undefined;
+      this.form.callbackAccountName = "";
+      this.callbackAccountList = [];
+      
+      if (advertiserId) {
+        const advertiser = this.advertiserList.find(item => item.id === advertiserId);
+        if (advertiser) {
+          this.form.advertiserName = advertiser.advertiserName;
+        }
+        // 加载对应的广告商账号列表
+        this.loadPromotionAccountList(advertiserId);
+        
+        // 如果已选择配置回传,加载回传账号列表
+        if (this.form.configCallback === 1) {
+          this.loadCallbackAccountList(advertiserId);
+        }
+      }
+    },
+    /** 加载广告商账号列表 */
+    loadPromotionAccountList(advertiserId) {
+      if (!advertiserId) return;
+      pagePromotionAccount({ pageNum: 1, pageSize: 1000, advertiserId: advertiserId }).then(response => {
+        this.promotionAccountList = response.data.records;
+      }).catch(error => {
+        console.error('加载广告商账号失败:', error);
+        this.promotionAccountList = [];
+      });
+    },
+    /** 广告商账号变化时 */
+    handlePromotionAccountChange(promotionAccountId) {
+      this.form.promotionAccountName = "";
+      if (promotionAccountId) {
+        const account = this.promotionAccountList.find(item => item.id === promotionAccountId);
+        if (account) {
+          this.form.promotionAccountName = account.accountName;
+        }
+      }
+    },
+    /** 投放页面变化时 */
+    handleLaunchPageChange(launchPageId) {
+      this.form.launchPageName = "";
+      
+      if (launchPageId) {
+        const template = this.landingPageTemplateList.find(item => item.id === launchPageId);
+        if (template) {
+          this.form.launchPageName = template.templateName;
+        }
+      }
+    },
+    
+    /** 渠道变化时 */
+    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) {
+      this.form.callbackAccountName = "";
+      this.conversionEvents = [];
+      this.advertiserEventOptions = [];
+      this.systemEventOptions = [];
+      
+      if (callbackAccountId) {
+        const account = this.callbackAccountList.find(item => item.id === callbackAccountId);
+        if (account) {
+          this.form.callbackAccountName = account.accountName;
+          // 加载事件类型和转换事件
+          this.loadEventTypesAndConversionEvents(this.form.advertiserId, callbackAccountId);
+        }
+      }
+    },
+    /** 配置回传变化时 */
+    handleConfigCallbackChange(value) {
+      // 重置回传账号
+      this.form.callbackAccountId = undefined;
+      this.form.callbackAccountName = "";
+      this.callbackAccountList = [];
+      this.conversionEvents = [];
+      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);
+      }
+    },
+    /** 加载回传账号列表 */
+    loadCallbackAccountList(advertiserId) {
+      if (!advertiserId) return;
+      pageCallbackAccount({ pageNum: 1, pageSize: 1000, advertiserId: advertiserId }).then(response => {
+        this.callbackAccountList = response.data.records;
+      }).catch(error => {
+        console.error('加载回传账号列表失败:', error);
+        this.callbackAccountList = [];
+      });
+    },
+    /** 加载事件类型和转换事件 */
+    loadEventTypesAndConversionEvents(advertiserId, callbackAccountId) {
+      // 加载事件类型选项
+      queryEventType(advertiserId).then(response => {
+        const eventTypes = response.data || [];
+        // systemBuiltin='0' 为广告商事件
+        this.advertiserEventOptions = eventTypes.filter(item => item.systemBuiltin === '0');
+        // systemBuiltin='1' 为系统事件
+        this.systemEventOptions = eventTypes.filter(item => item.systemBuiltin === '1');
+      }).catch(error => {
+        console.error('加载事件类型失败:', error);
+      });
+      
+      // 加载已有的转换事件
+      getCallbackAccount(callbackAccountId).then(response => {
+        const account = response.data;
+        this.parseConversionEvents(account.conversionEvent);
+      }).catch(error => {
+        console.error('加载回传账号详情失败:', error);
+      });
+    },
+    /** 解析转换事件 */
+    parseConversionEvents(conversionEvent) {
+      try {
+        if (conversionEvent && typeof conversionEvent === 'string') {
+          this.conversionEvents = JSON.parse(conversionEvent);
+        } else if (Array.isArray(conversionEvent)) {
+          this.conversionEvents = conversionEvent;
+        } else {
+          this.conversionEvents = [];
+        }
+      } catch (error) {
+        console.error('解析转换事件失败:', error);
+        this.conversionEvents = [];
+      }
+    },
+    /** 添加转换事件 */
+    addConversionEvent() {
+      this.conversionEvents.push({
+        systemEventType: '',
+        systemEventTypeName: '',
+        advertiserEventType: '',
+        advertiserEventName: ''
+      });
+    },
+    /** 删除转换事件 */
+    removeConversionEvent(index) {
+      this.conversionEvents.splice(index, 1);
+    },
+    /** 广告商事件变化 */
+    handleAdvertiserEventChange(index, eventType) {
+      const event = this.advertiserEventOptions.find(item => item.eventType === eventType);
+      if (event) {
+        this.conversionEvents[index].advertiserEventName = event.eventName;
+      }
+    },
+    /** 系统事件变化 */
+    handleSystemEventChange(index, eventType) {
+      const event = this.systemEventOptions.find(item => item.eventType === eventType);
+      if (event) {
+        this.conversionEvents[index].systemEventTypeName = event.eventName;
+      }
+    },
+    /** 企微分配规则变化 */
+    handleAllocationRuleChange(allocationRule) {
+      // 重置需要加载的数据列表
+      this.form.allocationRuleId = undefined;
+      this.allocationRuleList = [];
+      this.groupActiveList = [];
+      this.contactWayList = [];
+      
+      // 根据不同的分配规则加载对应数据
+      if (allocationRule === '1') {
+        // 加载企业微信分配规则列表
+        this.loadAllocationRuleList();
+      } else if (allocationRule === '2') {
+        // 根据投放类型加载群活码或企业微信活码
+        if (this.form.launchType === 1) {
+          this.loadGroupActiveList();
+        } else if (this.form.launchType === 2) {
+          this.loadContactWayList();
+        }
+      }
+    },
+    /** 加载企业微信分配规则列表 */
+    loadAllocationRuleList() {
+      pageAssignRule({
+        pageNum: 1,
+        pageSize: 1000,
+        status: 1
+      }).then(response => {
+        this.allocationRuleList = response.data.records || [];
+      }).catch(error => {
+        console.error('加载企业微信分配规则列表失败:', error);
+        this.allocationRuleList = [];
+      });
+    },
+    /** 加载群活码列表 */
+    loadGroupActiveList() {
+      pageGroupLiveCode({
+        pageNum: 1,
+        pageSize: 1000,
+        status: 1
+      }).then(response => {
+        this.groupActiveList = response.data.records || [];
+      }).catch(error => {
+        console.error('加载群活码列表失败:', error);
+        this.groupActiveList = [];
+      });
+    },
+    /** 加载企业微信活码列表 */
+    loadContactWayList() {
+      // 调用/qw/contactWay/list接口获取企业微信活码数据
+      // 注意:此接口返回rows字段,不是data.records
+      listContactWay().then(response => {
+        this.contactWayList = response.rows || [];
+      }).catch(error => {
+        console.error('加载企业微信活码列表失败:', error);
+        this.contactWayList = [];
+      });
+    },
+    /** 保存转换事件 */
+    saveConversionEvents() {
+      // 调用保存接口
+      saveEventType(this.form.callbackAccountId, this.conversionEvents).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('保存成功');
+          this.open = false;
+          this.getList();
+        }
+      }).catch(error => {
+        console.error('保存失败:', error);
+        this.msgError('保存失败');
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+// 对话框样式
+::v-deep .site-dialog {
+  .el-dialog__header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    padding: 20px;
+    margin: 0;
+    border-radius: 4px 4px 0 0;
+    
+    .el-dialog__title {
+      color: #fff;
+      font-size: 18px;
+      font-weight: 500;
+    }
+    
+    .el-dialog__headerbtn .el-dialog__close {
+      color: #fff;
+      font-size: 20px;
+      
+      &:hover {
+        color: #f0f0f0;
+      }
+    }
+  }
+  
+  .el-dialog__body {
+    padding: 25px 30px;
+    background-color: #f8f9fa;
+  }
+  
+  .el-dialog__footer {
+    padding: 15px 30px;
+    border-top: 1px solid #e8e8e8;
+    background-color: #fff;
+  }
+}
+
+// 表单样式
+.elegant-form {
+  .form-section {
+    background: #fff;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+    
+    &:hover {
+      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+      transform: translateY(-2px);
+    }
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  
+  .section-title {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 2px solid #e8e8e8;
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
+    
+    i {
+      font-size: 18px;
+      margin-right: 8px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      background-clip: text;
+    }
+    
+    span {
+      position: relative;
+      
+      &::after {
+        content: '';
+        position: absolute;
+        left: 0;
+        bottom: -12px;
+        width: 0;
+        height: 2px;
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        transition: width 0.3s ease;
+      }
+    }
+  }
+  
+  .form-section:hover .section-title span::after {
+    width: 100%;
+  }
+  
+  .el-form-item {
+    margin-bottom: 20px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  
+  ::v-deep .el-form-item__label {
+    color: #606266;
+    font-weight: 500;
+    font-size: 14px;
+  }
+  
+  ::v-deep .el-input__inner,
+  ::v-deep .el-textarea__inner {
+    border-radius: 6px;
+    border: 1px solid #dcdfe6;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      border-color: #c0c4cc;
+    }
+    
+    &:focus {
+      border-color: #667eea;
+      box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+    }
+  }
+  
+  ::v-deep .el-select {
+    .el-input__inner {
+      padding-left: 15px;
+    }
+  }
+  
+  ::v-deep .el-radio {
+    margin-right: 20px;
+    
+    &.is-bordered {
+      border-radius: 6px;
+      padding: 10px 20px;
+      transition: all 0.3s ease;
+      
+      &:hover {
+        border-color: #667eea;
+      }
+      
+      &.is-checked {
+        border-color: #667eea;
+        background-color: rgba(102, 126, 234, 0.05);
+      }
+    }
+  }
+  
+  // 回传账号显示动画
+  .slide-fade {
+    animation: slideDown 0.3s ease;
+  }
+}
+
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 按钮样式优化
+.dialog-footer {
+  text-align: right;
+  
+  .el-button {
+    padding: 10px 24px;
+    border-radius: 6px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    
+    &.el-button--primary {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border: none;
+      
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+    }
+    
+    &.el-button--default {
+      &:hover {
+        color: #667eea;
+        border-color: #667eea;
+        background-color: rgba(102, 126, 234, 0.05);
+      }
+    }
+  }
+}
+
+// 表格样式优化
+.el-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;
+      font-size: 14px;
+    }
+  }
+  
+  ::v-deep .el-table__row {
+    transition: all 0.3s ease;
+    
+    &:hover {
+      background-color: rgba(102, 126, 234, 0.05);
+    }
+  }
+}
+
+// 顶部按钮组样式
+.mb8 {
+  margin-bottom: 15px;
+  
+  .el-button {
+    border-radius: 6px;
+    padding: 10px 20px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    
+    &.el-button--primary {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border: none;
+      
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+      }
+    }
+    
+    &.el-button--success {
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(103, 194, 58, 0.4);
+      }
+    }
+  }
+}
+
+// 转换类型配置区域样式
+.conversion-config-section {
+  margin-top: 20px;
+  padding: 20px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  border: 1px solid #e8e8e8;
+  
+  .config-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    
+    .config-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+  
+  .empty-tip {
+    text-align: center;
+    padding: 30px 0;
+    color: #909399;
+    font-size: 14px;
+  }
+}
+
+.conversion-event-item {
+  margin-bottom: 15px;
+  padding: 15px;
+  border: 1px solid #e8e8e8;
+  border-radius: 6px;
+  background-color: #fff;
+  transition: all 0.3s ease;
+  
+  &:hover {
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  }
+  
+  .event-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #f0f0f0;
+    
+    .event-title {
+      font-weight: 600;
+      font-size: 14px;
+      color: #303133;
+    }
+  }
+}
+
+// 投放链接对话框样式
+.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>
+

+ 602 - 0
src/views/adv/statistics/index.vue

@@ -0,0 +1,602 @@
+<template>
+  <div class="app-container statistics-container">
+    <!-- 搜索筛选 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" class="search-form">
+      <el-form-item label="站点" prop="siteId">
+        <el-select
+          v-model="queryParams.siteId"
+          placeholder="请选择站点"
+          clearable
+          size="small"
+          filterable
+        >
+          <el-option
+            v-for="item in siteList"
+            :key="item.id"
+            :label="item.siteName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select
+          v-model="queryParams.advertiserId"
+          placeholder="请选择广告商"
+          clearable
+          size="small"
+          filterable
+        >
+          <el-option
+            v-for="item in advertiserList"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="日期范围" prop="dateRange">
+        <el-date-picker
+          v-model="queryParams.dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          format="yyyy-MM-dd"
+          value-format="yyyy-MM-dd"
+          size="small"
+          style="width: 280px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <div class="date-quick-buttons">
+          <el-button type="primary" plain size="small" @click="setDateRange('today')">今日</el-button>
+          <el-button type="primary" plain size="small" @click="setDateRange('yesterday')">昨日</el-button>
+          <el-button type="primary" plain size="small" @click="setDateRange('week')">本周</el-button>
+          <el-button type="primary" plain size="small" @click="setDateRange('month')">本月</el-button>
+          <el-button type="primary" plain size="small" @click="setDateRange('30days')">30天</el-button>
+        </div>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" 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="success"
+          icon="el-icon-refresh"
+          size="mini"
+          @click="getList"
+        >刷新</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table border v-loading="loading" :data="statisticsList" class="stats-table">
+      <el-table-column label="站点ID" align="center" prop="siteId" width="80" />
+      <el-table-column label="站点名称" align="center" prop="siteName" min-width="150" show-overflow-tooltip />
+      
+      <!-- 流量数据 -->
+      <el-table-column label="流量数据" align="center">
+        <el-table-column label="PV" align="center" prop="pv" width="100">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.pv || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="UV" align="center" prop="uv" width="100">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.uv || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 广告数据 -->
+      <el-table-column label="广告数据" align="center">
+        <el-table-column label="展示数" align="center" prop="impressionCount" width="100">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.impressionCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="点击数" align="center" prop="clickCount" width="100">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.clickCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="点击率(%)" align="center" prop="clickRate" width="100">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.clickRate || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="平均点击单价" align="center" prop="avgClickPrice" width="120">
+          <template slot-scope="scope">
+            <span class="money-text">¥{{ scope.row.avgClickPrice || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 成本数据 -->
+      <el-table-column label="成本数据" align="center">
+        <el-table-column label="账面花费" align="center" prop="accountCost" width="120">
+          <template slot-scope="scope">
+            <span class="money-text">¥{{ scope.row.accountCost || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="实际花费" align="center" prop="actualCost" width="120">
+          <template slot-scope="scope">
+            <span class="money-text primary">¥{{ scope.row.actualCost || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 转化数据 -->
+      <el-table-column label="转化数据" align="center">
+        <el-table-column label="名片数" align="center" prop="cardCount" width="100">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.cardCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="名片获取率(%)" align="center" prop="cardAcquireRate" width="120">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.cardAcquireRate || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="名片获取成本" align="center" prop="cardAcquireCost" width="120">
+          <template slot-scope="scope">
+            <span class="money-text">¥{{ scope.row.cardAcquireCost || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 企微数据 -->
+      <el-table-column label="企微数据" align="center">
+        <el-table-column label="企微添加人数" align="center" prop="wechatAddCount" width="120">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.wechatAddCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微添加率(%)" align="center" prop="wechatAddRate" width="120">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.wechatAddRate || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微添加成本" align="center" prop="wechatAddCost" width="120">
+          <template slot-scope="scope">
+            <span class="money-text">¥{{ scope.row.wechatAddCost || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微加群人数" align="center" prop="wechatGroupCount" width="120">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.wechatGroupCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微加群率(%)" align="center" prop="wechatGroupRate" width="120">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.wechatGroupRate || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微删除人数" align="center" prop="wechatDeleteCount" width="120">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.wechatDeleteCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="企微删除率" align="center" prop="wechatDeleteCountRate" width="120">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.wechatDeleteCountRate || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 小程序数据 -->
+      <el-table-column label="小程序数据" align="center">
+        <el-table-column label="发起进入小程序人数" align="center" prop="miniLaunchIndexCount" width="140">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.miniLaunchIndexCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="进入小程序落地页" align="center" prop="miniAuthIndexCount" width="140">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.miniAuthIndexCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="小程序授权人数" align="center" prop="miniAuthCount" width="140">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.miniAuthCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="进入出码页人数" align="center" prop="miniQrCodeIndexCount" width="140">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.miniQrCodeIndexCount || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <!-- 下次转化数据 -->
+      <el-table-column label="下次转化数据" align="center">
+        <el-table-column label="报名成功人数" align="center" prop="registerSuccessCount" width="120">
+          <template slot-scope="scope">
+            <span class="number-text">{{ scope.row.registerSuccessCount || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="报名成功加微率(%)" align="center" prop="registerWechatRate" width="140">
+          <template slot-scope="scope">
+            <span class="rate-text">{{ scope.row.registerWechatRate || 0 }}</span>
+          </template>
+        </el-table-column>
+      </el-table-column>
+
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180" fixed="right">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="操作" align="center" width="120" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+        </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="统计详情" :visible.sync="detailOpen" width="900px" append-to-body class="detail-dialog">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="站点ID">{{ detail.siteId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="站点名称">{{ detail.siteName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="PV">{{ detail.pv || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="UV">{{ detail.uv || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="展示数">{{ detail.impressionCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="点击数">{{ detail.clickCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="点击率">{{ detail.clickRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="平均点击单价">¥{{ detail.avgClickPrice || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="账面花费">¥{{ detail.accountCost || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="实际花费">¥{{ detail.actualCost || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="名片数">{{ detail.cardCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="名片获取率">{{ detail.cardAcquireRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="名片获取成本">¥{{ detail.cardAcquireCost || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微添加人数">{{ detail.wechatAddCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微添加率">{{ detail.wechatAddRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微添加成本">¥{{ detail.wechatAddCost || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微加群人数">{{ detail.wechatGroupCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微加群率">{{ detail.wechatGroupRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="报名成功人数">{{ detail.registerSuccessCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="报名成功加微率">{{ detail.registerWechatRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="企微删除率">{{ detail.wechatDeleteCountRate || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ parseTime(detail.createTime) }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间">{{ parseTime(detail.updateTime) }}</el-descriptions-item>
+      </el-descriptions>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="detailOpen = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageSiteStatistics, getSiteStatistics } from "@/api/adv/siteStatistics";
+import { listSite } from "@/api/adv/site";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "SiteStatistics",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 统计列表
+      statisticsList: [],
+      // 站点列表
+      siteList: [],
+      // 详情对话框
+      detailOpen: false,
+      // 详情数据
+      detail: {},
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        siteId: undefined,
+        advertiserId: undefined,
+        startDate: undefined,
+        endDate: undefined,
+        dateRange: []
+      },
+      // 广告商列表
+      advertiserList: []
+    };
+  },
+  created() {
+    // 从 URL参数获取站点ID
+    const siteId = this.$route.query.siteId;
+    if (siteId) {
+      this.queryParams.siteId = parseInt(siteId);
+    }
+    // 默认设置今天的日期范围
+    const today = new Date();
+    const dateStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
+    this.queryParams.startDate = dateStr;
+    this.queryParams.endDate = dateStr;
+    this.queryParams.dateRange = [dateStr, dateStr];
+    this.getSiteList();
+    this.getAdvertiserList();
+    this.getList();
+  },
+  methods: {
+    /** 查询站点列表 */
+    getSiteList() {
+      listSite().then(response => {
+        this.siteList = response.data || [];
+      });
+    },
+    /** 查询广告商列表 */
+    getAdvertiserList() {
+      pageAdvertiser({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.advertiserList = response.data.records || [];
+      }).catch(error => {
+        console.error('查询广告商列表失败:', error);
+        this.advertiserList = [];
+      });
+    },
+    /** 设置日期范围 */
+    setDateRange(rangeType) {
+      const today = new Date();
+      let startDate, endDate;
+      
+      if (rangeType === 'today') {
+        startDate = new Date(today);
+        endDate = new Date(today);
+      } else if (rangeType === 'yesterday') {
+        startDate = new Date(today.getTime() - 24 * 60 * 60 * 1000);
+        endDate = new Date(today.getTime() - 24 * 60 * 60 * 1000);
+      } else if (rangeType === 'week') {
+        const dayOfWeek = today.getDay();
+        const diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
+        startDate = new Date(today.getFullYear(), today.getMonth(), diff);
+        endDate = new Date(today);
+      } else if (rangeType === 'month') {
+        startDate = new Date(today.getFullYear(), today.getMonth(), 1);
+        endDate = new Date(today);
+      } else if (rangeType === '30days') {
+        startDate = new Date(today.getTime() - 29 * 24 * 60 * 60 * 1000);
+        endDate = new Date(today);
+      }
+      
+      const formatDate = (date) => {
+        return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
+      };
+      
+      this.queryParams.startDate = formatDate(startDate);
+      this.queryParams.endDate = formatDate(endDate);
+      this.queryParams.dateRange = [this.queryParams.startDate, this.queryParams.endDate];
+    },
+    /** 查询统计列表 */
+    getList() {
+      this.loading = true;
+      const params = {
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize,
+        siteId: this.queryParams.siteId,
+        advertiserId: this.queryParams.advertiserId,
+        startDate: this.queryParams.startDate,
+        endDate: this.queryParams.endDate
+      };
+      pageSiteStatistics(params).then(response => {
+        this.statisticsList = response.data.records || [];
+        this.total = response.data.total || 0;
+        this.loading = false;
+      }).catch((error) => {
+        console.error('查询统计列表失败:', error);
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      // 不会会改版面pageNum,维持已有的pageNum
+      if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
+        this.queryParams.startDate = this.queryParams.dateRange[0];
+        this.queryParams.endDate = this.queryParams.dateRange[1];
+      }
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      // 重新设置为today是不是更好?这里应该是按照简易逻辑重新设置日期
+      const today = new Date();
+      const dateStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
+      this.queryParams.startDate = dateStr;
+      this.queryParams.endDate = dateStr;
+      this.queryParams.dateRange = [dateStr, dateStr];
+      this.handleQuery();
+    },
+    /** 详情按钮操作 */
+    handleDetail(row) {
+      getSiteStatistics(row.id).then(response => {
+        this.detail = response.data;
+        this.detailOpen = true;
+      });
+    },
+    /** 获取ROI标签类型 */
+    getRoiType(roi) {
+      const value = parseFloat(roi || 0);
+      if (value >= 3) return 'success';
+      if (value >= 2) return 'warning';
+      if (value >= 1) return 'info';
+      return 'danger';
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.statistics-container {
+  padding: 20px;
+}
+
+// 搜索表单样式
+.search-form {
+  background: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  margin-bottom: 20px;
+}
+
+// 表格样式
+.stats-table {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  
+  .number-text {
+    color: #409eff;
+    font-weight: 500;
+  }
+  
+  .rate-text {
+    color: #e6a23c;
+    font-weight: 500;
+  }
+  
+  .money-text {
+    color: #f56c6c;
+    font-weight: 500;
+    
+    &.primary {
+      color: #409eff;
+    }
+    
+    &.success {
+      color: #67c23a;
+    }
+  }
+  
+  ::v-deep .el-table__header {
+    th {
+      background-color: #f5f7fa;
+      color: #606266;
+      font-weight: 600;
+      font-size: 13px;
+    }
+  }
+  
+  ::v-deep .el-table__row {
+    transition: all 0.3s ease;
+    
+    &:hover {
+      background-color: rgba(102, 126, 234, 0.05);
+    }
+  }
+}
+
+// 工具栏样式
+.mb8 {
+  margin-bottom: 15px;
+  
+  .el-button {
+    border-radius: 6px;
+    padding: 10px 20px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    
+    &.el-button--success {
+      &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(103, 194, 58, 0.4);
+      }
+    }
+  }
+}
+
+// 日期快速设置按钮样式
+.date-quick-buttons {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+  
+  .el-button {
+    padding: 8px 18px;
+    font-size: 13px;
+    font-weight: 500;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+    }
+    
+    &:active {
+      transform: translateY(0);
+    }
+  }
+}
+
+// 详情对话框样式
+::v-deep .detail-dialog {
+  .el-dialog__header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    padding: 20px;
+    margin: 0;
+    border-radius: 4px 4px 0 0;
+    
+    .el-dialog__title {
+      color: #fff;
+      font-size: 18px;
+      font-weight: 500;
+    }
+    
+    .el-dialog__headerbtn .el-dialog__close {
+      color: #fff;
+      font-size: 20px;
+      
+      &:hover {
+        color: #f0f0f0;
+      }
+    }
+  }
+  
+  .el-dialog__body {
+    padding: 25px 30px;
+  }
+  
+  .el-descriptions {
+    ::v-deep .el-descriptions-item__label {
+      font-weight: 600;
+      color: #606266;
+      background-color: #fafafa;
+    }
+  }
+}
+
+.dialog-footer {
+  text-align: right;
+  
+  .el-button {
+    padding: 10px 24px;
+    border-radius: 6px;
+    font-weight: 500;
+  }
+}
+</style>

+ 211 - 0
src/views/adv/trackingLink/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="链接名称" prop="trackingName">
+        <el-input
+          v-model="queryParams.trackingName"
+          placeholder="请输入链接名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="广告商" prop="advertiserId">
+        <el-select v-model="queryParams.advertiserId" placeholder="请选择广告商" clearable size="small" filterable>
+          <el-option
+            v-for="item in advertiserOptions"
+            :key="item.id"
+            :label="item.advertiserName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="推广类型" prop="promotionType">
+        <el-input
+          v-model="queryParams.promotionType"
+          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="success"
+          icon="el-icon-refresh"
+          size="mini"
+          @click="getList"
+        >刷新</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="linkList">
+      <el-table-column label="ID" align="center" prop="id" width="80" />
+      <el-table-column label="链接名称" align="center" prop="trackingName" show-overflow-tooltip />
+      <el-table-column label="广告商" align="center" prop="advertiserName" width="200" show-overflow-tooltip />
+      <el-table-column label="推广类型" align="center" prop="promotionType" width="120" />
+      <el-table-column label="监测链接" align="center" prop="trackingUrl" show-overflow-tooltip min-width="200" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleView(scope.row)"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document-copy"
+            @click="handleCopy(scope.row)"
+          >复制链接</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.current"
+      :limit.sync="queryParams.size"
+      @pagination="getList"
+    />
+
+    <!-- 详情对话框 -->
+    <el-dialog title="监测链接详情" :visible.sync="open" width="700px" append-to-body>
+      <el-descriptions :column="1" border v-if="detail">
+        <el-descriptions-item label="链接ID">{{ detail.id }}</el-descriptions-item>
+        <el-descriptions-item label="链接名称">{{ detail.trackingName }}</el-descriptions-item>
+        <el-descriptions-item label="广告商">{{ detail.advertiserName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="推广类型">{{ detail.promotionType }}</el-descriptions-item>
+        <el-descriptions-item label="监测链接">
+          <el-link :href="detail.trackingUrl" target="_blank" type="primary">{{ detail.trackingUrl }}</el-link>
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ detail.createTime }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间">{{ detail.updateTime }}</el-descriptions-item>
+      </el-descriptions>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="handleCopy(detail)">复制链接</el-button>
+        <el-button @click="open = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { pageTrackingLink, getTrackingLink } from "@/api/adv/trackingLink";
+import { pageAdvertiser } from "@/api/adv/advertiser";
+
+export default {
+  name: "TrackingLink",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 监测链接表格数据
+      linkList: [],
+      // 广告商选项
+      advertiserOptions: [],
+      // 是否显示弹出层
+      open: false,
+      // 详情数据
+      detail: null,
+      // 查询参数
+      queryParams: {
+        current: 1,
+        size: 10,
+        trackingName: undefined,
+        advertiserId: undefined,
+        promotionType: undefined
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getAdvertiserOptions();
+  },
+  methods: {
+    /** 查询广告商选项 */
+    getAdvertiserOptions() {
+      pageAdvertiser({ current: 1, size: 1000, enabled: 1 }).then(response => {
+        this.advertiserOptions = response.data.records;
+      });
+    },
+    /** 获取广告商名称 */
+    getAdvertiserName(advertiserId) {
+      const advertiser = this.advertiserOptions.find(item => item.id === advertiserId);
+      return advertiser ? advertiser.advertiserName : advertiserId;
+    },
+    /** 查询监测链接列表 */
+    getList() {
+      this.loading = true;
+      pageTrackingLink(this.queryParams).then(response => {
+        this.linkList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.current = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 查看详情按钮操作 */
+    handleView(row) {
+      this.detail = null;
+      this.open = true;
+      getTrackingLink(row.id).then(response => {
+        this.detail = response.data;
+      });
+    },
+    /** 复制链接按钮操作 */
+    handleCopy(row) {
+      const url = row.trackingUrl;
+      if (!url) {
+        this.msgError("链接地址为空");
+        return;
+      }
+      
+      // 创建临时input元素
+      const input = document.createElement('input');
+      input.value = url;
+      document.body.appendChild(input);
+      input.select();
+      
+      try {
+        // 执行复制
+        const successful = document.execCommand('copy');
+        if (successful) {
+          this.msgSuccess("链接已复制到剪贴板");
+        } else {
+          this.msgError("复制失败,请手动复制");
+        }
+      } catch (err) {
+        this.msgError("复制失败,请手动复制");
+      }
+      
+      // 移除临时元素
+      document.body.removeChild(input);
+    }
+  }
+};
+</script>
+

+ 357 - 0
src/views/qw/assignRule/index.vue

@@ -0,0 +1,357 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="规则名称" prop="ruleName">
+        <el-input
+          v-model="queryParams.ruleName"
+          placeholder="请输入规则名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="停用" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" 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
+          type="primary"
+          plain
+          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 v-loading="loading" :data="assignRuleList" border>
+      <el-table-column label="规则名称" align="center" prop="ruleName" />
+      <el-table-column label="分配类型" align="center" prop="assignType">
+        <template slot-scope="scope">
+          <span v-if="scope.row.assignType === 1">轮询</span>
+          <span v-else-if="scope.row.assignType === 2">依次</span>
+          <span v-else-if="scope.row.assignType === 3">按权重</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="员工数" align="center" prop="qwAssignRuleUsers">
+        <template slot-scope="scope">
+          {{ scope.row.qwAssignRuleUsers && Array.isArray(scope.row.qwAssignRuleUsers) ? scope.row.qwAssignRuleUsers.length : 0 }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+            {{ scope.row.status === 1 ? '启用' : '停用' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280px">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >编辑</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleStatusChange(scope.row)"
+          >{{ scope.row.status === 1 ? '停用' : '启用' }}</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleDetails(scope.row)"
+          >分配详情</el-button>
+        </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="700px" append-to-body @close="cancel">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="规则名称" prop="ruleName">
+          <el-input v-model="form.ruleName" placeholder="请输入规则名称" />
+        </el-form-item>
+        <el-form-item label="分配类型" prop="assignType">
+          <el-radio-group v-model="form.assignType">
+            <el-radio :label="3">按权重</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="选择员工" prop="qwAssignRuleUsers" style="margin-top: 2%">
+          <div>
+            <el-button
+              size="medium"
+              icon="el-icon-circle-plus-outline"
+              plain
+              @click="handleSelectUser">请选择使用员工</el-button>
+          </div>
+          <div style="margin-top: 10px;">
+            <el-table :data="personnelList" border size="small" style="margin-top: 10px;">
+              <el-table-column label="企微号" align="center" prop="qwUserId" width="120px" />
+              <el-table-column label="企微姓名" align="center" prop="qwUserName" width="120px" />
+              <el-table-column label="权重数" align="center" width="100px">
+                <template slot-scope="scope">
+                  <el-input v-model.number="scope.row.weight" type="number" size="small" />
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" align="center" width="80px">
+                <template slot-scope="scope">
+                  <el-button type="danger" size="mini" icon="el-icon-delete" @click="handleRemovePersonnel(scope.row)"></el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </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>
+
+    <!-- 分配详情对话框 -->
+    <el-dialog title="分配详情" :visible.sync="detailsOpen" width="900px" append-to-body>
+      <el-table :data="detailsPersonnelList" border style="margin-bottom: 20px;">
+        <el-table-column label="企微号" align="center" prop="qwUserId" />
+        <el-table-column label="企微姓名" align="center" prop="qwUserName" />
+        <el-table-column label="权重数" align="center" prop="weight" />
+        <el-table-column label="今日分配数" align="center" prop="assignNumToDay" />
+        <el-table-column label="累积分配数" align="center" prop="assignNumCount" />
+        <el-table-column label="今日添加数" align="center" prop="addNumToDay" />
+        <el-table-column label="累积添加数" align="center" prop="addNumCount" />
+      </el-table>
+    </el-dialog>
+
+    <!-- 选择员工对话框 -->
+    <el-dialog :title="selectUserDialog.title" :visible.sync="selectUserDialog.open" width="1300px" append-to-body>
+      <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listAssignRule, getAssignRule, addOrUpdateAssignRule, enableAssignRule } from "@/api/qw/assignRule";
+import qwUserList from '@/views/qw/user/qwUserList.vue'
+
+export default {
+  name: "AssignRule",
+  components: { qwUserList },
+  data() {
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      assignRuleList: [],
+      title: "",
+      open: false,
+      detailsOpen: false,
+      detailsPersonnelList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        ruleName: null,
+        status: null,
+      },
+      form: {
+        assignType: 3,
+        qwAssignRuleUsers: null
+      },
+      personnelList: [],
+      selectUserDialog: {
+        title: "选择员工",
+        open: false
+      },
+      rules: {
+        ruleName: [
+          { required: true, message: "规则名称不能为空", trigger: "blur" }
+        ]
+      },
+
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询分配规则列表 */
+    getList() {
+      this.loading = true;
+      listAssignRule(this.queryParams).then(response => {
+        this.assignRuleList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    // ... existing code ...
+
+    /** 新增按钮 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "新增分配规则";
+    },
+
+    /** 修改按钮 */
+    handleUpdate(row) {
+      this.reset();
+      this.open = true;
+      this.title = "修改分配规则";
+      getAssignRule(row.id).then(response => {
+        this.form = response.data;
+        if (this.form.qwAssignRuleUsers && Array.isArray(this.form.qwAssignRuleUsers)) {
+          this.personnelList = this.form.qwAssignRuleUsers;
+        }
+      });
+    },
+
+    /** 启用/停用 */
+    handleStatusChange(row) {
+      const status = row.status === 1 ? 0 : 1;
+      const statusText = status === 1 ? "启用" : "停用";
+      this.$confirm(`确认要${statusText}该分配规则吗?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        enableAssignRule(row.id, status).then(response => {
+          this.msgSuccess(`${statusText}成功`);
+          this.getList();
+        });
+      }).catch(() => {});
+    },
+
+    /** 分配详情 */
+    handleDetails(row) {
+      if (row.qwAssignRuleUsers && Array.isArray(row.qwAssignRuleUsers)) {
+        this.detailsPersonnelList = row.qwAssignRuleUsers;
+      } else {
+        this.detailsPersonnelList = [];
+      }
+      this.detailsOpen = true;
+    },
+
+    /** 选择员工 */
+    handleSelectUser() {
+      this.selectUserDialog.open = true;
+      this.$nextTick(() => {
+        this.$refs.QwUserList.getDetails(null, null, null, null);
+      });
+    },
+
+    /** 接收选中的员工 */
+    selectUserList(selectUsers) {
+      this.selectUserDialog.open = false;
+      if (selectUsers && selectUsers.length > 0) {
+        selectUsers.forEach(user => {
+          const exists = this.personnelList.find(p => p.sysQwUserId === user.id);
+          if (!exists) {
+            this.personnelList.push({
+              assignId: this.form.id || null,
+              sysQwUserId: user.id,
+              qwUserName: user.qwUserName,
+              qwUserId: user.qwUserId,
+              weight: 1,
+              assignNumToDay: 0,
+              assignNumCount: 0,
+              addNumToDay: 0,
+              addNumCount: 0
+            });
+          }
+        });
+      }
+    },
+
+    /** 移除员工 */
+    handleRemovePersonnel(item) {
+      const index = this.personnelList.findIndex(p => p.qwUserId === item.qwUserId);
+      if (index > -1) {
+        this.personnelList.splice(index, 1);
+      }
+    },
+
+    /** 表单提交 */
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          if (this.personnelList.length === 0) {
+            this.$message.error("请选择至少一个员工");
+            return;
+          }
+          // 准备发送的数据,包含 weight
+          const submitPersonnelList = this.personnelList.map(p => ({
+            assignId: this.form.id || null,
+            sysQwUserId: p.sysQwUserId,
+            qwUserName: p.qwUserName,
+            qwUserId: p.qwUserId,
+            weight: p.weight
+          }));
+          const data = {
+            ...this.form,
+            qwAssignRuleUsers: submitPersonnelList
+          };
+          addOrUpdateAssignRule(data).then(response => {
+            this.msgSuccess(this.form.id ? "修改成功" : "新增成功");
+            this.open = false;
+            this.getList();
+          });
+        }
+      });
+    },
+
+    /** 取消 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: null,
+        ruleName: null,
+        assignType: 3,
+        qwAssignRuleUsers: null,
+        status: 1
+      };
+      this.personnelList = [];
+      this.resetForm("form");
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    }
+  }
+};
+</script>

+ 693 - 0
src/views/qw/customerLink/index.vue

@@ -0,0 +1,693 @@
+<template>
+  <div class="app-container">
+    <!-- 查询表单 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="链接名称" prop="linkName">
+        <el-input
+          v-model="queryParams.linkName"
+          placeholder="请输入链接名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="企业微信" prop="corpName">
+        <el-input
+          v-model="queryParams.corpName"
+          placeholder="请输入企业微信"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" 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
+          type="primary"
+          plain
+          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 v-loading="loading" :data="list" border>
+      <el-table-column label="链接名称" align="center" prop="linkName" />
+      <el-table-column label="企业微信" align="center" prop="corpName" />
+      <el-table-column label="渠道链接" align="center" prop="url" show-overflow-tooltip />
+      <el-table-column label="创建人" align="center" prop="createBy" />
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="最后修改人" align="center" prop="updateBy" />
+      <el-table-column label="修改时间" align="center" prop="updateTime" />
+      <el-table-column label="今日添加数" align="center" prop="toDayAddNum" />
+      <el-table-column label="累积添加数" align="center" prop="countAddNum" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">编辑</el-button>
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleViewChannel(scope.row)">查看渠道</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
+        </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 @close="cancel">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="链接名称" prop="linkName">
+          <el-input v-model="form.linkName" placeholder="请输入链接名称" />
+        </el-form-item>
+        <el-form-item label="企业微信" prop="corpId">
+          <el-select v-model="form.corpId" placeholder="请选择企业微信" @change="handleCorpChange">
+            <el-option v-for="item in qwCompanyList" :key="item.corpId" :label="item.corpName" :value="item.corpId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="自动通过好友" prop="skipVerify">
+          <div style="display: flex; align-items: center; height: 32px;">
+            <el-switch v-model="form.skipVerify" :active-value="1" :inactive-value="0" />
+          </div>
+        </el-form-item>
+        <el-form-item label="员工选择" prop="linkUser">
+          <div>
+            <el-button
+              size="medium"
+              icon="el-icon-circle-plus-outline"
+              plain
+              @click="handleSelectUser">请选择员工</el-button>
+          </div>
+          <div style="margin-top: 10px;">
+            <el-table :data="personnelList" border size="small" style="margin-top: 10px;">
+              <el-table-column label="企微号" align="center" prop="qwUserId" width="120px" />
+              <el-table-column label="企微姓名" align="center" prop="qwUserName" width="120px" />
+              <el-table-column label="操作" align="center" width="80px">
+                <template slot-scope="scope">
+                  <el-button type="danger" size="mini" icon="el-icon-delete" @click="handleRemovePersonnel(scope.row)"></el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </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>
+
+    <!-- 查看渠道链接对话框 -->
+    <el-dialog title="查看渠道链接" :visible.sync="channelDialogOpen" width="900px" append-to-body @close="cancelChannel">
+      <!-- 查询表单 -->
+      <el-form :model="channelQueryParams" ref="channelQueryForm" :inline="true" v-show="channelShowSearch" label-width="68px">
+        <el-form-item label="渠道名称" prop="linkName">
+          <el-input
+            v-model="channelQueryParams.linkName"
+            placeholder="请输入渠道名称"
+            clearable
+            size="small"
+            @keyup.enter.native="getChannelList"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="getChannelList">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetChannelQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            icon="el-icon-plus"
+            size="mini"
+            @click="handleAddChannel"
+          >新增</el-button>
+        </el-col>
+        <right-toolbar :showSearch.sync="channelShowSearch" @queryTable="getChannelList"></right-toolbar>
+      </el-row>
+
+      <!-- 渠道列表 -->
+      <el-table v-loading="channelLoading" :data="channelList" border>
+        <el-table-column label="渠道名称" align="center" prop="linkName" />
+        <el-table-column label="渠道链接" align="center" prop="url" show-overflow-tooltip />
+        <el-table-column label="今日添加数" align="center" prop="toDayAddNum" />
+        <el-table-column label="累积添加数" align="center" prop="countAddNum" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteChannel(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination
+        v-show="channelTotal>0"
+        :total="channelTotal"
+        :page.sync="channelQueryParams.pageNum"
+        :limit.sync="channelQueryParams.pageSize"
+        @pagination="getChannelList"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancelChannel">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增渠道对话框 -->
+    <el-dialog title="选择渠道" :visible.sync="selectChannelDialogOpen" width="900px" append-to-body>
+      <el-row :gutter="20">
+        <!-- 左侧:分组和渠道选择 -->
+        <el-col :xs="24" :sm="12">
+          <div style="border-right: 1px solid #dcdfe6; padding-right: 20px;">
+            <h4 style="margin-top: 0; margin-bottom: 15px;">分组管理</h4>
+            <div style="margin-bottom: 15px;">
+              <el-input 
+                v-model="selectChannelQueryParams.channelName" 
+                placeholder="搜索分组"
+                clearable
+                size="small"
+                @input="loadGroupList"
+              />
+            </div>
+            <div style="border: 1px solid #dcdfe6; border-radius: 4px; max-height: 400px; overflow-y: auto;">
+              <div 
+                v-for="item in groupList" 
+                :key="item.id"
+                :class="['group-item', { active: selectedGroupId === item.id }]"
+                @click="selectGroup(item)"
+                style="padding: 10px 15px; cursor: pointer; border-bottom: 1px solid #f0f0f0; user-select: none;"
+              >
+                <span>{{ item.channelName }}</span>
+              </div>
+              <div v-if="groupList.length === 0" style="padding: 20px; text-align: center; color: #909399;">暂无分组</div>
+            </div>
+          </div>
+        </el-col>
+    
+        <!-- 中间:渠道列表 -->
+        <el-col :xs="24" :sm="12">
+          <div style="padding: 0 20px;">
+            <h4 style="margin-top: 0; margin-bottom: 15px;">渠道模板</h4>
+            <div v-if="!selectedGroupId" style="padding: 40px 0; text-align: center; color: #909399;">
+              请上方选择分组
+            </div>
+            <div v-else>
+              <div style="border: 1px solid #dcdfe6; border-radius: 4px; max-height: 400px; overflow-y: auto;">
+                <div 
+                  v-for="item in selectChannelList" 
+                  :key="item.id"
+                  style="padding: 10px 15px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; user-select: none; cursor: pointer; background-color: #fff; transition: all 0.3s;"
+                  :style="{ backgroundColor: isChannelSelected(item) ? '#e6f7ff' : '#fff' }"
+                  @click="toggleChannelSelection(item)"
+                >
+                  <el-checkbox 
+                    :value="isChannelSelected(item)"
+                    style="margin-right: 8px;"
+                  />
+                  <span>{{ item.channelName }}</span>
+                </div>
+                <div v-if="selectChannelList.length === 0" style="padding: 20px; text-align: center; color: #909399;">暂无渠道</div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    
+      <!-- 右侧:已选渠道 -->
+      <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #dcdfe6;">
+        <h4 style="margin-top: 0; margin-bottom: 10px;">已选渠道({{ selectedChannels.length }})</h4>
+        <div style="border: 1px solid #dcdfe6; border-radius: 4px; padding: 10px; min-height: 80px; max-height: 150px; overflow-y: auto;">
+          <el-tag 
+            v-for="(item, index) in selectedChannels" 
+            :key="item.channelId || index"
+            closable
+            @close="removeSelectedChannel(item)"
+            style="margin: 5px; cursor: pointer;"
+          >
+            {{ item.channelName }}
+          </el-tag>
+          <div v-if="selectedChannels.length === 0" style="color: #909399; text-align: center; padding: 20px 0;">未选择任何渠道</div>
+        </div>
+      </div>
+    
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="selectChannelDialogOpen = false">取 消</el-button>
+        <el-button type="primary" @click="confirmAddChannel">确 定</el-button>
+      </div>
+    </el-dialog>
+    <!-- 选择员工对话框 -->
+    <el-dialog :title="selectUserDialog.title" :visible.sync="selectUserDialog.open" width="1300px" append-to-body>
+      <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  pageCustomerLink,
+  getCustomerLink,
+  createOrUpdateCustomerLink,
+  deleteCustomerLink,
+  pageCustomerLinkChannel,
+  createCustomerLinkChannel,
+  deleteCustomerLinkChannel
+} from '@/api/qw/customerLink'
+import { getMyQwCompanyList, listUser } from '@/api/qw/user'
+import { pageProject } from '@/api/adv/channel'
+import qwUserList from '@/views/qw/user/qwUserList.vue'
+import Pagination from '@/components/Pagination'
+import RightToolbar from '@/components/RightToolbar'
+
+export default {
+  name: 'CustomerLink',
+  components: {
+    qwUserList,
+    Pagination,
+    RightToolbar
+  },
+  data() {
+    return {
+      // 加载状态
+      loading: false,
+      channelLoading: false,
+      // 是否显示查询表单
+      showSearch: true,
+      channelShowSearch: true,
+      // 列表数据
+      list: [],
+      total: 0,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        linkName: undefined,
+        corpName: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单验证规则
+      rules: {
+        linkName: [
+          { required: true, message: '请输入链接名称', trigger: 'blur' }
+        ],
+        corpId: [
+          { required: true, message: '请选择企业微信', trigger: 'change' }
+        ],
+        linkUser: [
+          { required: true, message: '请选择员工', trigger: 'change' }
+        ]
+      },
+      // 对话框
+      open: false,
+      title: '',
+      // 企业微信列表
+      qwCompanyList: [],
+      // 企业微信员工列表
+      qwUserList: [],
+      // 分配规则人员列表
+      personnelList: [],
+      // 选择员工对话框
+      selectUserDialog: {
+        title: "选择员工",
+        open: false
+      },
+      
+      // 渠道链接对话框
+      channelDialogOpen: false,
+      channelList: [],
+      channelTotal: 0,
+      channelQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        linkName: undefined,
+        sysLinkId: undefined
+      },
+      // 新增渠道对话框
+      selectChannelDialogOpen: false,
+      selectChannelParams: {
+        channelId: undefined
+      },
+      channelOptions: [],
+      currentSysLinkId: undefined,
+      
+      // 渠道选择相关数据
+      groupList: [],  // 分组列表
+      selectedGroupId: null,  // 选中的分组ID
+      selectChannelList: [],  // 当前分组的渠道列表
+      selectChannelLoading: false,  // 渠道列表加载中
+      selectedChannels: [],  // 已选择的渠道列表(右侧)
+      selectChannelPageNum: 1,
+      selectChannelPageSize: 10,
+      selectChannelTotal: 0,
+      selectChannelQueryParams: {
+        channelName: ''
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.loadQwCompanyList()
+  },
+  methods: {
+    // 查询列表
+    getList() {
+      this.loading = true
+      pageCustomerLink(this.queryParams).then(response => {
+        this.list = response.data.records || []
+        this.total = response.data.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    // 搜索
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    // 重置查询
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    // 新增
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '新增渠道管理'
+    },
+    // 编辑
+    handleUpdate(row) {
+      this.reset()
+      this.open = true
+      this.title = '编辑渠道管理'
+      // 调用详情接口获取按键数据
+      getCustomerLink(row.id).then(response => {
+        this.form = response.data
+        // 从 qwCustomerLinkUsers 映射到 personnelList
+        if (this.form.qwCustomerLinkUsers && Array.isArray(this.form.qwCustomerLinkUsers)) {
+          this.personnelList = this.form.qwCustomerLinkUsers.map(user => ({
+            sysQwUserId: user.sysQwUserId,
+            qwUserName: user.qwUserName,
+            qwUserId: user.qwUserId
+          }))
+          // 同步 linkUser
+          this.form.linkUser = this.personnelList
+        }
+        this.loadQwUserList(this.form.corpId)
+      })
+    },
+    // 删除
+    handleDelete(row) {
+      this.$confirm('确定删除该渠道吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        deleteCustomerLink(row.id).then(response => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    },
+    // 查看渠道链接
+    handleViewChannel(row) {
+      this.channelDialogOpen = true
+      this.currentSysLinkId = row.id
+      this.channelQueryParams.sysLinkId = row.id
+      this.getChannelList()
+    },
+    // 查询渠道链接列表
+    getChannelList() {
+      this.channelLoading = true
+      pageCustomerLinkChannel(this.channelQueryParams).then(response => {
+        this.channelList = response.data.records || []
+        this.channelTotal = response.data.total || 0
+        this.channelLoading = false
+      }).catch(() => {
+        this.channelLoading = false
+      })
+    },
+    // 重置渠道查询
+    resetChannelQuery() {
+      this.channelQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        linkName: undefined,
+        sysLinkId: this.currentSysLinkId
+      }
+      this.getChannelList()
+    },
+    // 新增渠道链接
+    handleAddChannel() {
+      this.selectChannelDialogOpen = true
+      this.selectedChannels = []
+      this.selectedGroupId = null
+      this.selectChannelList = []
+      this.selectChannelQueryParams.channelName = ''
+      this.loadGroupList()
+    },
+    // 加载分组列表
+    loadGroupList() {
+      const params = { pageNum: 1, pageSize: 1000, parentId: 0 }
+      pageProject(params).then(response => {
+        this.groupList = response.data.records || []
+      }).catch(() => {
+        this.groupList = []
+      })
+    },
+    // 选择分组
+    selectGroup(group) {
+      this.selectedGroupId = group.id
+      this.selectChannelPageNum = 1
+      this.getSelectChannelList()
+    },
+    // 获取渠道列表
+    getSelectChannelList() {
+      if (!this.selectedGroupId) return
+      this.selectChannelLoading = true
+      const params = { pageNum: this.selectChannelPageNum, pageSize: this.selectChannelPageSize, parentId: this.selectedGroupId }
+      if (this.selectChannelQueryParams.channelName) {
+        params.channelName = this.selectChannelQueryParams.channelName
+      }
+      pageProject(params).then(response => {
+        this.selectChannelList = response.data.records || []
+        this.selectChannelTotal = response.data.total || 0
+        this.selectChannelLoading = false
+      }).catch(() => {
+        this.selectChannelLoading = false
+      })
+    },
+    // 勾选渠道
+    toggleChannelSelection(channel) {
+      const index = this.selectedChannels.findIndex(item => item.channelId === channel.id)
+      if (index > -1) {
+        // 已选中,取消选择
+        this.selectedChannels.splice(index, 1)
+      } else {
+        // 未选中,添加选择
+        this.selectedChannels.push({ channelId: channel.id, channelName: channel.channelName })
+      }
+    },
+    // 判断渠道是否已选择
+    isChannelSelected(channel) {
+      return this.selectedChannels.some(item => item.channelId === channel.id)
+    },
+    // 删除已选渠道
+    removeSelectedChannel(channel) {
+      const index = this.selectedChannels.findIndex(item => item.channelId === channel.channelId)
+      if (index > -1) {
+        this.selectedChannels.splice(index, 1)
+      }
+    },
+    // 加载渠道选项
+    loadChannelOptions() {
+      // 这里需要加载渠道列表,根据实际接口调整
+      // 暂时使用空数组,需要后续实现
+      this.channelOptions = []
+    },
+    // 确认新增渠道
+    confirmAddChannel() {
+      if (this.selectedChannels.length === 0) {
+        this.$message.error('请选择渠道')
+        return
+      }
+      const data = {
+        sysLinkId: this.currentSysLinkId,
+        channelList: this.selectedChannels
+      }
+      createCustomerLinkChannel(data).then(response => {
+        this.$message.success('新增成功')
+        this.selectChannelDialogOpen = false
+        this.getChannelList()
+      })
+    },
+    // 删除渠道链接
+    handleDeleteChannel(row) {
+      this.$confirm('确定删除该渠道链接吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        deleteCustomerLinkChannel(row.id).then(response => {
+          this.$message.success('删除成功')
+          this.getChannelList()
+        })
+      }).catch(() => {})
+    },
+    // 关闭渠道链接对话框
+    cancelChannel() {
+      this.channelDialogOpen = false
+      this.currentSysLinkId = undefined
+    },
+    // 表单提交
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          // 将 personnelList 赋值给 form.linkUser
+          this.form.linkUser = this.personnelList.map(p => ({
+            sysQwUserId: p.sysQwUserId,
+            qwUserName: p.qwUserName,
+            qwUserId: p.qwUserId
+          }))
+          const data = {
+            linkName: this.form.linkName,
+            corpId: this.form.corpId,
+            corpName: this.form.corpName,
+            skipVerify: this.form.skipVerify,
+            linkUser: this.form.linkUser
+          }
+          if (this.form.id) {
+            data.id = this.form.id
+          }
+          createOrUpdateCustomerLink(data).then(response => {
+            this.$message.success(this.form.id ? '编辑成功' : '新增成功')
+            this.open = false
+            this.getList()
+          })
+        }
+      })
+    },
+    // 重置表单
+    reset() {
+      this.form = {
+        linkName: undefined,
+        corpId: undefined,
+        corpName: undefined,
+        skipVerify: 0,
+        linkUser: []
+      }
+      this.personnelList = []
+      this.qwUserList = []
+      if (this.$refs.form) {
+        this.$refs.form.clearValidate()
+      }
+    },
+    // 取消
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 加载企业微信列表
+    loadQwCompanyList() {
+      getMyQwCompanyList().then(response => {
+        this.qwCompanyList = response.data || []
+      }).catch(() => {
+        this.qwCompanyList = []
+      })
+    },
+    // 企业微信改变
+    handleCorpChange(corpId) {
+      this.personnelList = []
+      this.form.linkUser = []
+      this.qwUserList = []
+      const selectedCompany = this.qwCompanyList.find(item => item.corpId === corpId)
+      if (selectedCompany) {
+        this.form.corpName = selectedCompany.corpName
+      }
+      // 不加载员工列表,等待用户点击"请选择员工"按钮时再加载
+    },
+    // 加载企业微信员工列表
+    loadQwUserList(corpId) {
+      if (!corpId) {
+        this.qwUserList = []
+        return
+      }
+      listUser({ corpId: corpId }).then(response => {
+        this.qwUserList = response.rows || []
+      }).catch(() => {
+        this.qwUserList = []
+      })
+    },
+    // 选择员工
+    handleSelectUser() {
+      if (!this.form.corpId) {
+        this.$message.warning('请先选择企业微信')
+        return
+      }
+      this.selectUserDialog.open = true
+      this.$nextTick(() => {
+        // 传递 corpId 给 qwUserList 组件,确保加载正确的企业微信员工
+        this.$refs.QwUserList.getDetails(this.form.corpId, null, null, null)
+      })
+    },
+
+    /** 接收选中的员工 */
+    selectUserList(selectUsers) {
+      this.selectUserDialog.open = false
+      if (selectUsers && selectUsers.length > 0) {
+        selectUsers.forEach(user => {
+          const exists = this.personnelList.find(p => p.sysQwUserId === user.id)
+          if (!exists) {
+            this.personnelList.push({
+              sysQwUserId: user.id,
+              qwUserName: user.qwUserName,
+              qwUserId: user.qwUserId
+            })
+          }
+        })
+        // 更新 form.linkUser
+        this.form.linkUser = this.personnelList.map(p => ({
+          sysQwUserId: p.sysQwUserId,
+          qwUserName: p.qwUserName,
+          qwUserId: p.qwUserId
+        }))
+      }
+    },
+
+    /** 移除员工 */
+    handleRemovePersonnel(item) {
+      const index = this.personnelList.findIndex(p => p.qwUserId === item.qwUserId)
+      if (index > -1) {
+        this.personnelList.splice(index, 1)
+      }
+    },
+    // 表单重置
+    resetForm(refName) {
+      if (this.$refs[refName]) {
+        this.$refs[refName].resetFields()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 304 - 0
src/views/qw/groupActual/index.vue

@@ -0,0 +1,304 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="群活码" prop="liveCodeId">
+        <el-select v-model="queryParams.liveCodeId" placeholder="请选择群活码" clearable size="small" @change="handleQuery">
+          <el-option
+            v-for="item in groupLiveCodeList"
+            :key="item.id"
+            :label="item.groupName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="群名称" prop="groupName">
+        <el-input
+          v-model="queryParams.groupName"
+          placeholder="请输入群名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="停用" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" 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
+          type="primary"
+          plain
+          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 v-loading="loading" :data="groupActualList" border>
+      <el-table-column label="群名称" align="center" prop="groupName" />
+      <el-table-column label="实际群二维码" align="center" prop="groupUrl">
+        <template slot-scope="scope">
+          <el-image
+            v-if="scope.row.groupUrl"
+            :src="scope.row.groupUrl"
+            :preview-src-list="[scope.row.groupUrl]"
+            style="width: 80px; height: 80px;"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="分配数" align="center" prop="assignNum" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+            {{ scope.row.status === 1 ? '启用' : '停用' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >编辑</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleStatusChange(scope.row)"
+          >{{ scope.row.status === 1 ? '停用' : '启用' }}</el-button>
+        </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 @close="cancel">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="群活码" prop="liveCodeId">
+          <el-select v-model="form.liveCodeId" placeholder="请选择群活码">
+            <el-option
+              v-for="item in groupLiveCodeList"
+              :key="item.id"
+              :label="item.groupName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="群名称" prop="groupName">
+          <el-input v-model="form.groupName" placeholder="请输入群名称" />
+        </el-form-item>
+        <el-form-item label="群二维码" prop="groupUrl">
+          <image-upload v-model="form.groupUrl" :limit="1" />
+        </el-form-item>
+        <el-form-item label="有效期" prop="efficientTime">
+          <el-date-picker v-model="form.efficientTime" type="date" placeholder="请选择有效期" value-format="yyyy-MM-dd" />
+        </el-form-item>
+        <el-form-item label="分配数" prop="assignNum">
+          <el-input-number v-model="form.assignNum" :min="0" />
+        </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 { listGroupActual, getGroupActual, addOrUpdateGroupActual, delGroupActual } from "@/api/qw/groupActual";
+import { listGroupLiveCode } from "@/api/qw/groupLiveCode";
+import ImageUpload from "@/components/ImageUpload";
+
+export default {
+  name: "GroupActual",
+  components: {
+    ImageUpload
+  },
+  data() {
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      groupActualList: [],
+      groupLiveCodeList: [],
+      title: "",
+      open: false,
+      liveCodeId: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveCodeId: null,
+        groupName: null,
+        status: null,
+      },
+      form: {},
+      rules: {
+        groupName: [
+          { required: true, message: "群名称不能为空", trigger: "blur" }
+        ],
+        groupUrl: [
+          { required: true, message: "群二维码不能为空", trigger: "change" }
+        ],
+        liveCodeId: [
+          { required: true, message: "群活码不能为空", trigger: "change" }
+        ]
+      }
+    };
+  },
+  created() {
+    // 从路由参数中获取liveCodeId
+    this.liveCodeId = this.$route.query.liveCodeId;
+    // 如果从群活码页面跳转过来,自动设置筛选条件
+    if (this.liveCodeId) {
+      this.queryParams.liveCodeId = this.liveCodeId;
+    }
+    this.loadGroupLiveCodeList();
+    this.getList();
+  },
+  methods: {
+    /** 查询群实际码列表 */
+    getList() {
+      this.loading = true;
+      const params = { ...this.queryParams };
+      if (this.liveCodeId) {
+        params.liveCodeId = this.liveCodeId;
+      }
+      listGroupActual(params).then(response => {
+        this.groupActualList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 新增按钮 */
+    handleAdd() {
+      this.reset();
+      // 如果是从群活码页面跳转过来的,新增时自动带入liveCodeId
+      if (this.liveCodeId) {
+        this.form.liveCodeId = parseInt(this.liveCodeId);
+      }
+      this.loadGroupLiveCodeList();
+      this.open = true;
+      this.title = "新增群实际码";
+    },
+
+    /** 加载群活码列表 */
+    loadGroupLiveCodeList() {
+      listGroupLiveCode({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.groupLiveCodeList = response.data.records;
+      }).catch(() => {
+        this.groupLiveCodeList = [];
+      });
+    },
+
+    // ... existing code ...
+    handleUpdate(row) {
+      this.reset();
+      this.loadGroupLiveCodeList();
+      this.open = true;
+      this.title = "修改群实际码";
+      getGroupActual(row.id).then(response => {
+        this.form = response.data;
+      });
+    },
+
+    /** 启用/停用 */
+    handleStatusChange(row) {
+      const status = row.status === 1 ? 0 : 1;
+      const statusText = status === 1 ? "启用" : "停用";
+      this.$confirm(`确认要${statusText}该群实际码吗?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        const data = {
+          id: row.id,
+          status: status
+        };
+        addOrUpdateGroupActual(data).then(response => {
+          this.msgSuccess(`${statusText}成功`);
+          this.getList();
+        });
+      }).catch(() => {});
+    },
+
+    /** 表单提交 */
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          const data = { ...this.form };
+          // 有效期处理:剆除时間秘,添加默认时間秘 00:00:00
+          if (data.efficientTime && typeof data.efficientTime === 'string') {
+            data.efficientTime = data.efficientTime + 'T00:00:00';
+          }
+          addOrUpdateGroupActual(data).then(response => {
+            this.msgSuccess(this.form.id ? "修改成功" : "新增成功");
+            this.open = false;
+            this.getList();
+          });
+        }
+      });
+    },
+
+    /** 取消 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: null,
+        liveCodeId: null,
+        groupName: null,
+        groupUrl: null,
+        efficientTime: null,
+        assignNum: 0,
+        status: 1
+      };
+      this.resetForm("form");
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      // 重置时,如果是从群活码页面跳转过来的,保留liveCodeId筛选
+      if (this.liveCodeId) {
+        this.queryParams.liveCodeId = this.liveCodeId;
+      }
+      this.handleQuery();
+    }
+  }
+};
+</script>

+ 263 - 0
src/views/qw/groupLiveCode/index.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="活码名称" prop="groupName">
+        <el-input
+          v-model="queryParams.groupName"
+          placeholder="请输入活码名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="启用" :value="1" />
+          <el-option label="停用" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" 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
+          type="primary"
+          plain
+          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 v-loading="loading" :data="groupLiveCodeList" border>
+      <el-table-column label="活码名称" align="center" prop="groupName" />
+      <el-table-column label="实际二维码数量" align="center" prop="qrcodeNum" />
+      <el-table-column label="今日加群数" align="center" prop="toDayNum" />
+      <el-table-column label="累计加群数" align="center" prop="countNum" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+            {{ scope.row.status === 1 ? '启用' : '停用' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >编辑</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleStatusChange(scope.row)"
+          >{{ scope.row.status === 1 ? '停用' : '启用' }}</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-arrow-right"
+            @click="handleGotoActual(scope.row)"
+          >群实际码</el-button>
+        </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 @close="cancel">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="活码名称" prop="groupName">
+          <el-input v-model="form.groupName" placeholder="请输入活码名称" />
+        </el-form-item>
+        <el-form-item label="项目" prop="projectId">
+          <el-select v-model="form.projectId" placeholder="请选择项目" @change="handleProjectChange">
+            <el-option
+              v-for="item in projectList"
+              :key="item.id"
+              :label="item.projectName"
+              :value="item.id"
+            />
+          </el-select>
+        </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 { listGroupLiveCode, getGroupLiveCode, addOrUpdateGroupLiveCode, delGroupLiveCode } from "@/api/qw/groupLiveCode";
+import { pageProject } from "@/api/adv/project";
+
+export default {
+  name: "GroupLiveCode",
+  data() {
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      groupLiveCodeList: [],
+      projectList: [],
+      title: "",
+      open: false,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        groupName: null,
+        status: null,
+      },
+      form: {},
+      rules: {
+        groupName: [
+          { required: true, message: "活码名称不能为空", trigger: "blur" }
+        ],
+        projectId: [
+          { required: true, message: "项目不能为空", trigger: "change" }
+        ],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询活码列表 */
+    getList() {
+      this.loading = true;
+      listGroupLiveCode(this.queryParams).then(response => {
+        this.groupLiveCodeList = response.data.records;
+        this.total = response.data.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 加载项目列表 */
+    loadProjectList() {
+      pageProject({ pageNum: 1, pageSize: 1000 }).then(response => {
+        this.projectList = response.data.records;
+      }).catch(() => {
+        this.projectList = [];
+      });
+    },
+
+    /** 处理项目变更 */
+    handleProjectChange() {
+      const project = this.projectList.find(p => p.id === this.form.projectId);
+      if (project) {
+        this.form.projectName = project.projectName;
+      }
+    },
+
+    /** 新增按钮 */
+    handleAdd() {
+      this.reset();
+      this.loadProjectList();
+      this.open = true;
+      this.title = "新增活码";
+    },
+
+    /** 修改按钮 */
+    handleUpdate(row) {
+      this.reset();
+      this.loadProjectList();
+      this.open = true;
+      this.title = "修改活码";
+      getGroupLiveCode(row.id).then(response => {
+        this.form = response.data;
+      });
+    },
+
+    /** 启用/停用 */
+    handleStatusChange(row) {
+      const status = row.status === 1 ? 0 : 1;
+      const statusText = status === 1 ? "启用" : "停用";
+      this.$confirm(`确认要${statusText}该活码吗?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        const data = {
+          id: row.id,
+          status: status
+        };
+        addOrUpdateGroupLiveCode(data).then(response => {
+          this.msgSuccess(`${statusText}成功`);
+          this.getList();
+        });
+      }).catch(() => {});
+    },
+
+    /** 进入群实际码页面 */
+    handleGotoActual(row) {
+      this.$router.push({
+        path: '/qw/contactWayC/groupActual',
+        query: { liveCodeId: row.id }
+      });
+    },
+
+    /** 表单提交 */
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          addOrUpdateGroupLiveCode(this.form).then(response => {
+            this.msgSuccess(this.form.id ? "修改成功" : "新增成功");
+            this.open = false;
+            this.getList();
+          });
+        }
+      });
+    },
+
+    /** 取消 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: null,
+        groupName: null,
+        projectId: null,
+        projectName: null,
+        status: 1
+      };
+      this.resetForm("form");
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    }
+  }
+};
+</script>