143 Commits 9e0bc4c917 ... 7f243627e6

Tác giả SHA1 Thông báo Ngày
  WanGxC 7f243627e6 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 30a68e889d ✨ feat: 修改logo; 相关bug修复 1 năm trước cách đây
  WanGxC 6a51a4d86d Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 556bb1c947 ✨ feat: SD新建广告活动: 内容相关投放已完成 1 năm trước cách đây
  WanGxC 7805d19038 ✨ feat: 1 năm trước cách đây
  WanGxC 2bab1f2d8f ✨ feat: 1 năm trước cách đây
  WanGxC c64be9d675 ✨ feat: 自定义定向--浏览再营销已完成 1 năm trước cách đây
  WanGxC 9c2940fafc ✨ feat: 自定义定向--浏览再营销细化功能完成 1 năm trước cách đây
  WanGxC c1c1d07f4f 🎈 perf: 1 năm trước cách đây
  WanGxC 52d2632b18 过年前的git 1 năm trước cách đây
  WanGxC 0769532cdc Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 7deae2dd2b ✨ feat: 新增sb关键词定向; 修复重复调用上传错误; 1 năm trước cách đây
  WanGxC 8e05690587 ✨ feat: 新增sb关键词定向 1 năm trước cách đây
  WanGxC ed368aedec Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 061c1424fe ✨ feat: 新增sp新建广告活动-关键词定向 1 năm trước cách đây
  guojing_wu d47b959496 Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu 798bd4ffef 修改广告活动头部信息 1 năm trước cách đây
  WanGxC 70c1347c57 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 92cbe02319 ✨ feat: 新增SD部分功能, 修复SP创建bug 1 năm trước cách đây
  WanGxC 3129e75d9a Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 7d4ed3f101 ✨ feat: sd新增: 创建广告活动,广告组... 1 năm trước cách đây
  guojing_wu 3eaa9601a3 Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu 2835edc8d4 升级element-plus 1 năm trước cách đây
  WanGxC 12f10ef3d6 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC d2d32edce1 ✨ feat: sb新建广告活动基本完成 1 năm trước cách đây
  guojing_wu 7d0f9aab61 ✨ feat: 完善自动化条件参数 1 năm trước cách đây
  WanGxC 94e68a9ef1 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 4486df1f90 test 1 năm trước cách đây
  WanGxC 5113447d07 ✨ feat: 新增上传图片和视频接口 1 năm trước cách đây
  guojing_wu 1bfc5f55c7 enum.js改为enum.ts 1 năm trước cách đây
  guojing_wu f512a4dfe0 fix bug 1 năm trước cách đây
  guojing_wu 416da85aeb 修复自动化竞价和预算 1 năm trước cách đây
  guojing_wu c6bfa3a0e2 移除baidu埋点 1 năm trước cách đây
  WanGxC 82971f1f9b ✨ feat: 新增SD新建广告活动页面 1 năm trước cách đây
  WanGxC e1ebed88ec ✨ feat: 1 năm trước cách đây
  guojing_wu 5a0d0cd97f 修复sd否定词组件导入缺失错误 1 năm trước cách đây
  WanGxC 894713b945 ✨ feat: 新增加载动画以及按钮禁用 1 năm trước cách đây
  guojing_wu 0f00bc667d finish auto 1 năm trước cách đây
  WanGxC 7bdda08b7a ✨ feat: 新增添加商品,更换商品功能 1 năm trước cách đây
  WanGxC ec52cf7785 ✨ feat: 新增商品集下着陆页创意模块从素材库中选择 1 năm trước cách đây
  WanGxC a7e391cb73 ✨ feat: 新增视频下的创意板块 1 năm trước cách đây
  WanGxC d76074049c ✨ feat: SB新建活动页面布局 1 năm trước cách đây
  WanGxC 3f6b8edbea ✨ feat: 新增SB新建广告活动-广告组,广告格式 1 năm trước cách đây
  guojing_wu cae8f36263 Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu 49bd75dc10 add auto-templates 1 năm trước cách đây
  WanGxC 4c28d65a29 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 4a1d1fccef ✨ feat: SP新建广告活动大部分完成 1 năm trước cách đây
  WanGxC 4644e8f6ae ✨ feat: 大部分SP新建广告活动,新增SB新建广告活动 1 năm trước cách đây
  WanGxC 4165998f46 ✨ feat: 更改UI布局,接口调用 1 năm trước cách đây
  WanGxC db55116be3 feat: ✨ SP新建广告活动-细化分类dialog 1 năm trước cách đây
  WanGxC 0a44562fa9 新建广告活动界面 1 năm trước cách đây
  guojing_wu 35e51b0c1a Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu be5298ddf5 完成分时预算和分时调价组件 1 năm trước cách đây
  WanGxC 03bc6aa469 商品定向 1 năm trước cách đây
  WanGxC ba118fcb3d 否定商品输入添加商品 1 năm trước cách đây
  WanGxC d995b41396 否定词,否定商品 1 năm trước cách đây
  WanGxC 56e5e7f502 完善表单校验 1 năm trước cách đây
  WanGxC 86476bd371 表单校验 1 năm trước cách đây
  WanGxC 27fb356b5d SP新建广告活动--商品搜索,添加,删除功能已完成 1 năm trước cách đây
  WanGxC f853584ec7 新增: 新建广告活动-广告组商品表格添加已选中商品,删除已选中商品功能; 按ASIN和SKU搜索功能; 1 năm trước cách đây
  WanGxC 4c148ada5b 新增: 新建广告活动-广告组内容 1 năm trước cách đây
  WanGxC 83afc93919 新增: SD广告活动详情页; SD广告组详情页 1 năm trước cách đây
  WanGxC 3778737406 新增: SP新建广告活动-广告活动; SD表格数据展示; 1 năm trước cách đây
  WanGxC 1ea8f81418 新增: 展示型推广SD-受众页面 1 năm trước cách đây
  WanGxC ca46f943a3 新增: 展示型推广SD-商品投放页面 1 năm trước cách đây
  WanGxC 3f00ddcf9a test2.0 1 năm trước cách đây
  WanGxC 38e03c131f 测试 1 năm trước cách đây
  WanGxC 2836b71699 部分样式修改 1 năm trước cách đây
  WanGxC 217dca0dad 新增展示型推广SD-广告活动;修改表格样式以及边框弧度 1 năm trước cách đây
  WanGxC bf63047117 新增sbgroupdetail/targets页面 1 năm trước cách đây
  WanGxC 8b74157da8 新增sb/groupdetail/ads,keywords,negativekeywords,searchwords页面 1 năm trước cách đây
  WanGxC 57a1313be9 新增品牌推广SB-detail-预算,广告位 1 năm trước cách đây
  WanGxC a4c7df3fc5 新增品牌推广SB-Detail页面; 修改Detail-head页面 1 năm trước cách đây
  WanGxC 29e9f4ef29 新增广告位页面;页面优化; 1 năm trước cách đây
  WanGxC 6626503528 修改表格数据为0或null时显示'--' 1 năm trước cách đây
  WanGxC 3c9c037b05 新增推广商品SB-商品投放页面;修改布局样式 1 năm trước cách đây
  WanGxC 36879b9968 新增SB数据趋势和广告结构页面;更改部分样式 1 năm trước cách đây
  WanGxC da9a326fbb 修改表格样式以及新增表头样式 1 năm trước cách đây
  WanGxC b0f99188d9 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC ee3de8049f 新增广告总览页面数据展示 1 năm trước cách đây
  WanGxC 0a9aea5b55 新增广告总览:总览页面 1 năm trước cách đây
  guojing_wu 08be276546 SP补全日周月曲线图 1 năm trước cách đây
  guojing_wu 26b8a9b1f3 demo 1 năm trước cách đây
  guojing_wu 01423356c4 完成广告组详情的定向、否定版块 1 năm trước cách đây
  WanGxC 8765f52888 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 29e271636b 修改tab样式 1 năm trước cách đây
  guojing_wu 2f0cf7cf10 完成广告组详情的推广商品和搜索词 1 năm trước cách đây
  WanGxC c37e742576 Merge branch 'test' into wang 1 năm trước cách đây
  WanGxC c0430c5ecb 新增广告总览页面 1 năm trước cách đây
  guojing_wu 00528b6786 完善单个广告活动信息展示 1 năm trước cách đây
  guojing_wu d0e099622d 数据对比组件进一步封装 1 năm trước cách đây
  guojing_wu 8ab710e2ba 调整广告组和广告版块的路径 1 năm trước cách đây
  WanGxC 55ef4a114e 同步修改 1 năm trước cách đây
  WanGxC 336b10531f 同步数据趋势图分离查询参数 1 năm trước cách đây
  WanGxC 8a2881a10d Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC a7994f8530 请求参数修改 1 năm trước cách đây
  guojing_wu eb78d61056 数据趋势图分离查询参数 1 năm trước cách đây
  guojing_wu c4eaba619b 修复日期选择后返回天数据问题;设置本地化时间格式 1 năm trước cách đây
  guojing_wu 69a0c35fa8 修复日月周切换问题 1 năm trước cách đây
  guojing_wu b119e88dfc 广告活动、购买的其它商品、搜索词i添加周和月统计数据 1 năm trước cách đây
  guojing_wu 752a35b820 Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu 66065af443 数据趋势添加周月筛选 1 năm trước cách đây
  WanGxC 88581ef4be 合并代码 1 năm trước cách đây
  WanGxC 3660b9cd60 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC d5776a2430 新增广告位,优化样式 1 năm trước cách đây
  guojing_wu c49d006eb5 完成搜索词和购买其它商品 1 năm trước cách đây
  WanGxC 437ba6a66f 新增推广商品页面;修改封装点击下拉框方法;添加下拉框以及柱状图x轴映射关系; 1 năm trước cách đây
  guojing_wu 9a6f154729 广告活动添加数据对比;组件化数据趋势图 1 năm trước cách đây
  WanGxC bc18d65bc1 Merge branch 'test' into wang 1 năm trước cách đây
  WanGxC fb37f99c54 修改图表部分样式; 1 năm trước cách đây
  guojing_wu 8852c57ffe sp版块命名 1 năm trước cách đây
  guojing_wu b8d4dd56ef tabs改为动态渲染 1 năm trước cách đây
  WanGxC a2cf16b646 Merge branch 'wang' into test 1 năm trước cách đây
  guojing_wu d43f0a8c24 广告活动添加时间联动功能 1 năm trước cách đây
  WanGxC e1738cab1d 修改图表部分样式;封装禁用下拉框方法; 1 năm trước cách đây
  guojing_wu d0342c92b0 修复图标resize报错问题 1 năm trước cách đây
  guojing_wu 95562f1081 调换change-metric的值顺序 1 năm trước cách đây
  WanGxC fef39e915e Merge remote-tracking branch 'origin/test' into test 1 năm trước cách đây
  WanGxC f3a49866f7 完成关键词-数据趋势的数据展示和广告结构的数据展示; 1 năm trước cách đây
  guojing_wu 5e281c97b5 修复路由参数类型 1 năm trước cách đây
  guojing_wu 5158f8eb4a Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  guojing_wu 6c6afdba04 完成sp单个广告活动的广告位 1 năm trước cách đây
  WanGxC c68843b3f7 完成广告活动-广告结构页面开发;修改mCard组件和TextSelector的@change暴露的newVal和oldVal顺序 1 năm trước cách đây
  WanGxC 9d316a894d Merge remote-tracking branch 'origin/test' into test 1 năm trước cách đây
  WanGxC 6723faf96f 完成广告活动-广告结构页面开发;修改mCard组件和TextSelector的@change暴露的newVal和oldVal顺序 1 năm trước cách đây
  WanGxC ff9f018fa7 完成广告活动-广告结构页面开发;修改mCard组件和TextSelector的@change暴露的newVal和oldVal顺序 1 năm trước cách đây
  guojing_wu 9e5e11963c 完成指标曲线图 1 năm trước cách đây
  WanGxC f38e2dd964 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 8582df7929 Merge remote-tracking branch 'origin/test' into test 1 năm trước cách đây
  guojing_wu 1dad2dae82 完成SP广告活动指标曲线展示 1 năm trước cách đây
  WanGxC c1922fe863 test git 1 năm trước cách đây
  WanGxC 1b7e86f907 修改SB样式 1 năm trước cách đây
  guojing_wu 3609e5d250 添加全局样式chart-tabs 1 năm trước cách đây
  WanGxC 76a11d5679 Merge branch 'test' into wang 1 năm trước cách đây
  WanGxC 3c2002d6c5 更新部分组件样式 1 năm trước cách đây
  guojing_wu 961bd00a66 统一样式 1 năm trước cách đây
  WanGxC f18725ab77 更新部分组件和页面 1 năm trước cách đây
  WanGxC 3e3311e217 更新部分组件和页面 1 năm trước cách đây
  WanGxC b2eb6964c0 Merge branch 'wang' into test 1 năm trước cách đây
  WanGxC 324125ccfa 更新部分组件和页面 1 năm trước cách đây
  guojing_wu a45ba27f3c Merge branch 'test' of http://34.206.75.59:10880/ASJ_ADS/ads_web into test 1 năm trước cách đây
  WanGxC b6e89472d1 Merge branch 'wang' of ASJ_ADS/ads_web into test 1 năm trước cách đây
  WanGxC 18bdf83793 Merge branch 'wang' of ASJ_ADS/ads_web into test 1 năm trước cách đây
100 tập tin đã thay đổi với 16060 bổ sung1368 xóa
  1. 1 1
      .env
  2. 5 2
      .prettierrc.js
  3. 3 3
      index.html
  4. 214 106
      package-lock.json
  5. 2 1
      package.json
  6. BIN
      src/assets/ansjer_image.png
  7. 9 2
      src/components/DateRangePicker/index.vue
  8. 1 1
      src/components/DynamicTime/index.vue
  9. 59 24
      src/components/MetricsCards/index.vue
  10. 10 6
      src/components/MetricsCards/mCard.vue
  11. 7 3
      src/components/TextSelector/index.vue
  12. 290 0
      src/components/TimerBidTable/index.vue
  13. 407 0
      src/components/TimerBudgetTable/index.vue
  14. 26 25
      src/components/cardComponents/ShowCard.vue
  15. 201 0
      src/components/conditionBuilder/condition-group.vue
  16. 265 0
      src/components/conditionBuilder/condition-group2.vue
  17. 100 0
      src/components/conditionBuilder/index.vue
  18. 24 0
      src/components/conditionBuilder/type.d.ts
  19. 30 0
      src/components/conditionBuilder/utils.ts
  20. 56 0
      src/components/dataCompare/index.vue
  21. 164 0
      src/components/echartsComponents/BarChart.vue
  22. 181 155
      src/components/echartsComponents/BarLineChart.vue
  23. 214 0
      src/components/echartsComponents/PieBarChart.vue
  24. 56 0
      src/components/echartsComponents/ScatterChart.vue
  25. 268 0
      src/components/echartsComponents/dataTendency.vue
  26. 69 0
      src/components/input-float/index.vue
  27. 30 0
      src/components/range-float/index.vue
  28. 1 1
      src/components/searchInput/index.vue
  29. 53 0
      src/components/select-button/index.vue
  30. 3 17
      src/components/shopSelector/index.vue
  31. 1 0
      src/components/shopSelector/types.ts
  32. 78 0
      src/components/tags-input/index.vue
  33. 3 3
      src/layout/logo/index.vue
  34. 8 6
      src/layout/navBars/breadcrumb/user.vue
  35. 2 0
      src/main.ts
  36. 12 0
      src/router/backEnd.ts
  37. 89 89
      src/router/index.ts
  38. 8 8
      src/router/route.ts
  39. 7 0
      src/settings.ts
  40. 26 0
      src/stores/publicData.ts
  41. 13 9
      src/stores/shopInfo.ts
  42. 5 5
      src/stores/themeConfig.ts
  43. 521 243
      src/theme/app.scss
  44. 3 3
      src/theme/element.scss
  45. 5 0
      src/types/pinia.d.ts
  46. 314 244
      src/types/views.d.ts
  47. 5 0
      src/utils/emitter.ts
  48. 3 3
      src/utils/service.ts
  49. 312 0
      src/views/adManage/ad-overview/chartComponents/dataTendency.vue
  50. 80 0
      src/views/adManage/ad-overview/daily/api.ts
  51. 400 0
      src/views/adManage/ad-overview/daily/crud.tsx
  52. 131 0
      src/views/adManage/ad-overview/daily/index.vue
  53. 88 0
      src/views/adManage/ad-overview/hourly/api.ts
  54. 329 0
      src/views/adManage/ad-overview/hourly/crud.tsx
  55. 135 0
      src/views/adManage/ad-overview/hourly/index.vue
  56. 57 0
      src/views/adManage/ad-overview/index.vue
  57. 80 0
      src/views/adManage/ad-overview/monthly/api.ts
  58. 397 0
      src/views/adManage/ad-overview/monthly/crud.tsx
  59. 131 0
      src/views/adManage/ad-overview/monthly/index.vue
  60. 74 0
      src/views/adManage/ad-overview/total/api.ts
  61. 0 0
      src/views/adManage/ad-overview/total/crud.tsx
  62. 914 0
      src/views/adManage/ad-overview/total/index.vue
  63. 80 0
      src/views/adManage/ad-overview/weekly/api.ts
  64. 397 0
      src/views/adManage/ad-overview/weekly/crud.tsx
  65. 131 0
      src/views/adManage/ad-overview/weekly/index.vue
  66. 0 24
      src/views/adManage/portfolios/PortfoliosSelector.vue
  67. 66 27
      src/views/adManage/portfolios/api.ts
  68. 88 72
      src/views/adManage/portfolios/crud.tsx
  69. 30 5
      src/views/adManage/portfolios/index.vue
  70. 0 25
      src/views/adManage/sb/campaigns/CreateCampaigns/adFormat/CommoditySet.vue
  71. 160 0
      src/views/adManage/sb/campaigns/CreateCampaigns/api/index.ts
  72. 335 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdCampaign.vue
  73. 366 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue
  74. 111 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdGroup.vue
  75. 60 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/DeliveryType.vue
  76. 986 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/FocusCreativity.vue
  77. 296 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/KeywordTarget.vue
  78. 354 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeGood.vue
  79. 247 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeWord.vue
  80. 810 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductOrientation.vue
  81. 444 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCommodity.vue
  82. 896 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity1.vue
  83. 461 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity2.vue
  84. 452 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCommodity.vue
  85. 908 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity1.vue
  86. 511 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity2.vue
  87. BIN
      src/views/adManage/sb/campaigns/CreateCampaigns/img/img_1.jpg
  88. 90 248
      src/views/adManage/sb/campaigns/CreateCampaigns/index.vue
  89. 49 7
      src/views/adManage/sb/campaigns/api.ts
  90. 59 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/api.ts
  91. 130 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/crud.tsx
  92. 73 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/index.vue
  93. 12 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/api.ts
  94. 11 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/api.ts
  95. 95 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/crud.tsx
  96. 67 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/index.vue
  97. 84 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/index.vue
  98. 28 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api.ts
  99. 122 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/crud.tsx
  100. 71 0
      src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/index.vue

+ 1 - 1
.env

@@ -3,6 +3,6 @@ VITE_PORT = 8080
 
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN = false
-
+BROWSER = Google Chrome.app
 # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
 VITE_PUBLIC_PATH = /web

+ 5 - 2
.prettierrc.js

@@ -1,12 +1,14 @@
 module.exports = {
+	singleAttributePerLine: false,
+	htmlWhitespaceSensitivity: 'ignore',
 	// 一行最多多少个字符
 	printWidth: 150,
 	// 指定每个缩进级别的空格数
 	tabWidth: 2,
 	// 使用制表符而不是空格缩进行
-	useTabs: true,
+	useTabs: false,
 	// 在语句末尾打印分号
-	semi: true,
+	semi: false,
 	// 使用单引号而不是双引号
 	singleQuote: true,
 	// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
@@ -19,6 +21,7 @@ module.exports = {
 	bracketSpacing: true,
 	// jsx 标签的反尖括号需要换行
 	jsxBracketSameLine: false,
+	bracketSameLine: true,
 	// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
 	arrowParens: 'always',
 	// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码

+ 3 - 3
index.html

@@ -17,7 +17,7 @@
 	</head>
 	<body>
 		<div id="app"></div>
-		<script type="text/javascript">
+		<!-- <script type="text/javascript">
 			// let _hmt = _hmt || [];
 			(function () {
                 let hm = document.createElement('script');
@@ -25,8 +25,8 @@
                 let s = document.getElementsByTagName('script')[0];
 				s.parentNode.insertBefore(hm, s);
 			})();
-		</script>
+		</script> -->
 		<script type="module" src="/src/main.ts"></script>
-		<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script>
+		<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
 	</body>
 </html>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 214 - 106
package-lock.json


+ 2 - 1
package.json

@@ -26,7 +26,7 @@
 		"echarts": "^5.4.1",
 		"echarts-gl": "^2.0.9",
 		"echarts-wordcloud": "^2.1.0",
-		"element-plus": "^2.3.9",
+		"element-plus": "^2.4.4",
 		"element-tree-line": "^0.2.1",
 		"font-awesome": "^4.7.0",
 		"js-cookie": "^3.0.1",
@@ -38,6 +38,7 @@
 		"pinia-plugin-persist": "^1.0.0",
 		"postcss": "^8.4.21",
 		"print-js": "^1.6.0",
+		"process": "^0.11.10",
 		"qrcodejs2-fixes": "^0.0.2",
 		"qs": "^6.11.0",
 		"screenfull": "^6.0.2",

BIN
src/assets/ansjer_image.png


+ 9 - 2
src/components/DateRangePicker/index.vue

@@ -14,10 +14,17 @@
       :clearable="false"
       :popper-options="{placement: props.popperPlacement}"
       @change="changedValue"
+
     />
   </div>
 </template>
 
+<script lang="ts">
+import { useShopInfo } from '/@/stores/shopInfo'
+const timezoneDefault = useShopInfo().profile.time_zone
+export default {}
+</script>
+
 <script setup lang="ts">
 import { ref, onMounted } from 'vue'
 import dayjs, { Dayjs } from 'dayjs'
@@ -28,7 +35,7 @@ defineOptions({
 
 const props = defineProps({
   modelValue: {type: Array, required: true },
-  timezone: { type: String, required: true },
+  timezone: { type: String, default: timezoneDefault },
   popperPlacement: { type: String, default: 'bottom-start' }
 })
 
@@ -116,4 +123,4 @@ const shortcuts = [
 
 <style scoped>
 
-</style>
+</style>

+ 1 - 1
src/components/DynamicTime/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <p>时区:{{ curTime }}</p>
+  <p style="white-space: nowrap">时区:{{ curTime }}</p>
 </template>
 
 <script lang="ts" setup>

+ 59 - 24
src/components/MetricsCards/index.vue

@@ -2,7 +2,7 @@
   <div class="metrics-cards">
     <MCard
      v-model="info.metric"
-     :metric-items="allMetricItems"
+     :metric-items="props.metricItems"
      :color="info.color"
      v-for="info in displayMetrics"
      @change-metric="changedMetric"
@@ -11,47 +11,54 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, withDefaults, Ref, onBeforeMount, watch, computed,ComputedRef } from 'vue'
+import { ref, Ref, onBeforeMount, watch, onMounted, computed } from 'vue'
 import MCard from './mCard.vue'
+import XEUtils from 'xe-utils';
 
-interface ModelData {
-  metric: string,
-  color: string
-}
 interface Props {
-  modelValue: ModelData[],
+  modelValue: ShowMetric[],
   metricItems: MetricData[],
-  colors?: string[]
 }
 const colorsMap: { [key: string]: boolean } = {}
-const props = withDefaults(defineProps<Props>(), { colors: () => ["aqua", "orange", "blue"] })
+const props = defineProps<Props>()
 const emits = defineEmits(['change', 'update:modelValue'])
-const allMetricItems = ref(props.metricItems)
+// const allMetricItems = ref(props.metricItems)
 const selectedMetric = ref(props.modelValue)
 const displayMetrics: Ref<{metric:string, color?: string}[]> = ref([])
 
-onBeforeMount(()=> {
-  for (const color of props.colors) {
-    colorsMap[color] = false
+const metricMap = computed(():{[key: string]: string} => {
+  const tmp:{[key: string]: string} = {}
+  for (const info of props.metricItems) {
+    tmp[info.value] = info.label
   }
-  const tmp:{[key: string]: boolean} = {}
+  return tmp
+})
+onBeforeMount(()=> {
+  const dup:{[key: string]: boolean} = {}
+  // 初始显示图线的三个维度
   for (const info of selectedMetric.value) {
     displayMetrics.value.push({ metric: info.metric, color: info.color })
-    tmp[info.metric] = true
-  }
-  for (const info of allMetricItems.value) {
-    if (info.disabled && !tmp[info.value]) { displayMetrics.value.push({ metric: info.value }) }
+    dup[info.metric] = true
+    if (info.color) { colorsMap[info.color] = true }
   }
 })
 
 const getColor = () => {
   for (const [k,v] of Object.entries(colorsMap)) {
-    if (!v) return k
+    if (!v) {
+      colorsMap[k] = true
+      return k
+    }
   }
   return ""
 }
-const changedMetric = (oldVal: string, newVal: string) => {
-  for (const info of allMetricItems.value) {
+const unsetColor = (color: string ) => {
+  if (XEUtils.has(colorsMap, color)) {
+    colorsMap[color] = false
+  }
+}
+const changedMetric = (newVal: string, oldVal: string) => {
+  for (const info of props.metricItems) {
     if (info.value === newVal) {
       info.disabled = true 
     } else if (info.value === oldVal) {
@@ -61,6 +68,7 @@ const changedMetric = (oldVal: string, newVal: string) => {
   const index = selectedMetric.value.findIndex( info => info.metric === oldVal)
   if (index > -1) {
     selectedMetric.value[index].metric = newVal
+    selectedMetric.value[index].label = metricMap.value[newVal]
     emits('update:modelValue', selectedMetric.value)
     emits('change', selectedMetric.value)
   }
@@ -71,16 +79,16 @@ const clickCard = (metric: string) => {
     if (selectedMetric.value.length <= 1 ) return
     const tmp = selectedMetric.value[index]
     selectedMetric.value.splice(index, 1)
-    colorsMap[tmp.color] = false
+    unsetColor(tmp.color)
     emits('update:modelValue', selectedMetric.value)
     emits('change', selectedMetric.value)
   } else {  // 不存在则添加
     if (selectedMetric.value.length === 3) { 
       selectedMetric.value[2].metric = metric
+      selectedMetric.value[2].label = metricMap.value[metric]
     } else {
       const color = getColor()
-      colorsMap[color] = true
-      selectedMetric.value.push({ metric: metric, color: color})
+      selectedMetric.value.push({ metric: metric, color: color, label: metricMap.value[metric]})
     }
     emits('update:modelValue', selectedMetric.value)
     emits('change', selectedMetric.value)
@@ -101,6 +109,33 @@ watch(selectedMetric.value, () => {
   }
 })
 
+watch(
+  props.metricItems,
+  () => {
+    const dup:{[key: string]: boolean} = {}
+    for (const info of displayMetrics.value) { dup[info.metric] = true }
+    let needNum = 6 - displayMetrics.value.length
+    if (needNum > 0) {  
+      // 从所有维度中选择剩余
+      for (const info of props.metricItems) {
+        if (!dup[info.value]) {
+          displayMetrics.value.push({ metric: info.value })
+          dup[info.value] = true
+          needNum --
+          if (needNum === 0) break
+        }
+      }
+    }
+    for (const info of props.metricItems) {
+      if (dup[info.value]) {
+        info.disabled = true
+      } else {
+        info.disabled = false
+      }  
+    }
+  }
+)
+
 </script>
 
 <style scoped>

+ 10 - 6
src/components/MetricsCards/mCard.vue

@@ -5,11 +5,11 @@
     <div class="metric-value">{{ selectedData?.metricVal }}</div>
     <div class="metric-pre">
       <span>{{ selectedData?.preVal }}&nbsp;&nbsp;</span>
-      <el-icon>
-        <!-- <Bottom class="green"/> -->
-        <Top class="green"/>
+      <el-icon v-show="selectedData?.gapVal" style="display: inline-block; padding-top: 2px">
+        <Top :class="colorClass" v-if="isBoost"/>
+        <Bottom :class="colorClass" v-else/>
       </el-icon>
-      <span class="green">{{ selectedData?.gapVal }}</span>
+      <span :class="colorClass">{{ selectedData?.gapVal ? selectedData?.gapVal + '%' : '' }}</span>
     </div>
   </el-card>
 </template>
@@ -30,9 +30,9 @@ interface Props {
 const props = defineProps<Props>()
 const emits = defineEmits(["update:modelValue", "change-metric"])
 const metric = ref(props.modelValue)
-const changeMetric = (oldVal: string, newVal: string) => {
+const changeMetric = ( newVal: string, oldVal: string) => {
   emits('update:modelValue', newVal)
-  emits('change-metric', oldVal, newVal)
+  emits('change-metric', newVal, oldVal)
 }
 const selectedData = computed(():MetricData|null => {
   const info = props.metricItems.find(item => item.value === metric.value)
@@ -44,6 +44,10 @@ const boardTopStyle = computed(() => {
   if (props.color) { style_["border-top-color"] = props.color }
   return style_
 })
+const isBoost = computed(():boolean => {
+  return (selectedData.value?.gapVal ?? -1) > 0
+})
+const colorClass = computed((): "green"|"red" => isBoost.value ? "green": "red")
 
 </script>
 

+ 7 - 3
src/components/TextSelector/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dropdown class="el-dropdown-link" @command="handleCommand" trigger="click">
+  <el-dropdown class="el-dropdown-link" @command="handleCommand" trigger="click" placement="bottom-start">
     <span @click.stop>
       {{ displayLabel }}
       <el-icon>
@@ -7,7 +7,7 @@
       </el-icon>
     </span>
     <template #dropdown>
-      <el-dropdown-menu>
+      <el-dropdown-menu class="dropdown-menu-scroll">
         <el-dropdown-item v-for="info in options" :command="info.value" :disabled="info.disabled"> {{ info.label }}</el-dropdown-item>
       </el-dropdown-menu>
     </template>
@@ -34,7 +34,7 @@ const handleCommand = (command: string) => {
   const oldVal = data.value
   data.value = command
   emits('update:modelValue', command)
-  emits('change', oldVal, command)
+  emits('change', command, oldVal)
 }
 
 </script>
@@ -46,4 +46,8 @@ const handleCommand = (command: string) => {
   /* display: flex;
   align-items: center; */
 }
+.dropdown-menu-scroll {
+  max-height: 300px; /* 根据需要调整最大高度 */
+  overflow-y: auto;
+}
 </style>

+ 290 - 0
src/components/TimerBidTable/index.vue

@@ -0,0 +1,290 @@
+<template>
+  <div class="calendar">
+    <table class="calendar-table calendar-table-hour">
+      <thead class="calendar-head">
+        <tr>
+          <th rowspan="8" class="week-td">星期 / 时间</th>
+          <th colspan="12">00:00 - 12:00</th>
+          <th colspan="12">12:00 - 24:00</th>
+          <th colspan="4" rowspan="2" class="week-td" style="display: none">小时</th>
+        </tr>
+        <tr>
+          <th colspan="1" v-for="(_, i) in 24" :key="i">{{ i }}</th>
+        </tr>
+      </thead>
+      <tbody>
+        <template v-for="(hoursList, week) in items">
+          <tr>
+            <th>{{ WeekMap[week] }}</th>
+            <td
+              v-for="(info, hour) in hoursList"
+              :key="hour"
+              @mousedown="handleMouseDown(week, hour, $event)"
+              @mouseover="handleMouseMove(week, hour)"
+              @mouseup="handleMouseUp"
+              :style="getTdStyle(info)">
+              <span class="cell-text"> {{ info.value ? info.value : '' }} </span>
+            </td>
+          </tr>
+        </template>
+        <tr>
+          <th colspan="28" class="clear-bar">
+            <span class="middle">可拖动鼠标选择时间段</span>
+            <el-button class="hover-link fr" link @click="resetAllBid" :disabled="disabled">全部重置</el-button>
+          </th>
+        </tr>
+      </tbody>
+    </table>
+
+    <el-dialog v-model="dialogVisible" title="修改出价系数" width="30%" :close-on-click-modal="false" :before-close="closeDialog">
+      <el-input-number v-model="bid" :min="0" :max="100" :step="0.1" controls-position="right" />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="cancelBid">取消</el-button>
+          <el-button type="primary" @click="submitBid">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, reactive } from 'vue'
+
+interface Props {
+  data: number[][]
+  disabled: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+})
+const bidData = ref(props.data)
+
+const mouseDown = ref(false)
+const startRowIndex = ref(0)
+const startColIndex = ref(0)
+const items = ref([])
+const WeekMap = {
+  0: '星期一',
+  1: '星期二',
+  2: '星期三',
+  3: '星期四',
+  4: '星期五',
+  5: '星期六',
+  6: '星期日',
+}
+const dialogVisible = ref(false)
+const bid = ref(0)
+const selectedItems = ref({})
+
+const getTdStyle = (info: any) => {
+  if (props.disabled) {
+    return { cursor: 'not-allowed', background: '#fff' }
+  }
+  return { background: info.selected ? '#ccdbff' : '' }
+}
+
+for (let i = 0; i < 7; i++) {
+  selectedItems.value[i] = []
+  const tmp = []
+  for (let j = 0; j < 24; j++) {
+    tmp.push({ value: bidData.value.length === 0 ? 0 : bidData.value[i][j], selected: false })
+  }
+  items.value.push(tmp)
+}
+
+watch(
+  () => props.data,
+  () => {
+    // console.log(100, 'watch', props.data)
+    if (props.data.length === 0) {
+      for (let i = 0; i < 7; i++) {
+        const tmp = []
+        for (let j = 0; j < 24; j++) {
+          tmp.push(0)
+        }
+        props.data.push(tmp)
+      }
+    }
+    for (let i = 0; i < 7; i++) {
+      for (let j = 0; j < 24; j++) {
+        items.value[i][j].value = props.data[i][j]
+      }
+    }
+    bidData.value = props.data
+  },
+  { immediate: true }
+)
+
+const handleMouseDown = (rowIndex: number, colIndex: number, event: any) => {
+  if (props.disabled) return
+  if (event.button === 0) {
+    mouseDown.value = true
+    startRowIndex.value = rowIndex
+    startColIndex.value = colIndex
+    items.value[rowIndex][colIndex].selected = true
+    selectedItems.value[rowIndex].push(colIndex)
+  }
+}
+
+const handleMouseUp = (event: any) => {
+  if (props.disabled) return
+  if (event.button === 0) {
+    mouseDown.value = false
+    startColIndex.value = 0
+    startRowIndex.value = 0
+    dialogVisible.value = true
+  }
+}
+
+const handleMouseMove = (rowIndex: number, colIndex: number) => {
+  if (props.disabled) return
+  if (mouseDown.value) {
+    selectCell(rowIndex, colIndex)
+  }
+}
+
+const selectCell = (rowIndex: number, colIndex: number) => {
+  if (props.disabled) return
+  const rowStart = Math.min(startRowIndex.value, rowIndex)
+  const rowEnd = Math.max(startRowIndex.value, rowIndex)
+  const cellStart = Math.min(startColIndex.value, colIndex)
+  const cellEnd = Math.max(startColIndex.value, colIndex)
+
+  for (let i = rowStart; i <= rowEnd; i++) {
+    for (let j = cellStart; j <= cellEnd; j++) {
+      items.value[i][j].selected = true
+      selectedItems.value[i].push(j)
+    }
+  }
+}
+
+const submitBid = () => {
+  // console.log(151, 'submitBid', bidData)
+  for (const row of Object.keys(selectedItems.value)) {
+    for (const col of selectedItems.value[row]) {
+      items.value[row][col].value = bid.value
+      bidData.value[row][col] = bid.value
+    }
+  }
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const cancelBid = () => {
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const clearCellBackgroundColor = () => {
+  for (const hoursList of items.value) {
+    for (const info of hoursList) {
+      info.selected = false
+    }
+  }
+}
+const clearSelectedItems = () => {
+  for (var i = 0; i < 7; i++) {
+    selectedItems.value[i] = []
+  }
+}
+const resetAllBid = () => {
+  for (let i = 0; i < 7; i++) {
+    for (let j = 0; j < 24; j++) {
+      items.value[i][j].value = 0
+      items.value[i][j].selected = false
+      bidData.value[i][j] = 0
+    }
+  }
+}
+const closeDialog = (done: Function) => {
+  // console.log("closeDialog")
+  cancelBid()
+  done()
+}
+</script>
+
+<style lang="scss" scoped>
+.calendar {
+  background-color: #fff;
+  -webkit-user-select: none;
+  position: relative;
+  display: inline-block;
+
+  .calendar-table {
+    border-collapse: collapse;
+  }
+
+  .week-td {
+    width: 90px;
+  }
+}
+
+table {
+  display: table;
+  border-collapse: separate;
+  box-sizing: border-box;
+  text-indent: initial;
+  border-spacing: 2px;
+  border-color: gray;
+
+  thead {
+    display: table-header-group;
+    vertical-align: middle;
+    border-color: inherit;
+  }
+
+  tr {
+    border: 1px solid #e0e5f4;
+    font-size: 12px;
+    text-align: center;
+    line-height: 32px;
+    color: rgba(0, 0, 0, 0.5);
+
+    th {
+      min-width: 40px;
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      background: #f7f8fa;
+    }
+
+    td {
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      min-width: 40px;
+
+      &:hover {
+        background: #ccdbff;
+        cursor: pointer;
+      }
+
+      .cell-text {
+        color: black;
+      }
+    }
+  }
+}
+
+.clear-bar {
+  line-height: 32px;
+  padding: 0 12px;
+
+  .hover-link {
+    color: #1c6bde;
+    font-size: 13px;
+    margin-top: 7px;
+  }
+  .fr {
+    float: right;
+  }
+  .middle {
+    float: center;
+  }
+}
+</style>

+ 407 - 0
src/components/TimerBudgetTable/index.vue

@@ -0,0 +1,407 @@
+<template>
+  <div style="width: 100%">
+    <el-row justify-content="start">
+      <el-col :span="4">
+        <span style="background-color: #11acf5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Bottom /></el-icon>
+        </span>
+        <span>基于原始预算降低(百分比)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #11acf5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Top /></el-icon>
+        </span>
+        <span>基于原始预算升高(百分比)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3fd4cf; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Bottom /></el-icon>
+        </span>
+        <span>基于原始预算降低(数值)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3fd4cf; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Top /></el-icon>
+        </span>
+        <span>基于原始预算升高(数值)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3359b5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"></el-icon>
+        </span>
+        <span>固定预算</span>
+      </el-col>
+    </el-row>
+    <div class="calendar">
+      <table class="calendar-table calendar-table-hour">
+        <thead class="calendar-head">
+          <tr>
+            <th rowspan="8" class="week-td">星期 / 时间</th>
+            <th colspan="12">00:00 - 12:00</th>
+            <th colspan="12">12:00 - 24:00</th>
+            <th colspan="4" rowspan="2" class="week-td" style="display: none">小时</th>
+          </tr>
+          <tr>
+            <th colspan="1" v-for="(_, i) in 24" :key="i">{{ i }}</th>
+          </tr>
+        </thead>
+        <tbody class="calendar-body">
+          <template v-for="(hoursList, week) in items">
+            <tr>
+              <th class="td-normal">{{ WeekMap[week] }}</th>
+              <td
+                class="un-selected"
+                v-for="(info, hour) in hoursList"
+                :key="hour"
+                @mousedown="handleMouseDown(week, hour, $event)"
+                @mouseover="handleMouseMove(week, hour)"
+                @mouseup="handleMouseUp"
+                :style="getTdStyle(info)">
+                {{ getCellText(info) }}
+                <el-icon v-if="info.type !== 'FixBudget'" style="display: inline-block; padding-top: 2px">
+                  <Top v-if="info.type === 'UpPercent' || info.type === 'UpNumber'" />
+                  <Bottom v-if="info.type === 'DownPercent' || info.type === 'DownNumber'" />
+                </el-icon>
+              </td>
+            </tr>
+          </template>
+          <tr>
+            <th colspan="28" class="clear-bar">
+              <span class="middle">可拖动鼠标选择时间段</span>
+              <el-button class="hover-link fr" link @click="resetAllBid" :disabled="disabled">全部重置</el-button>
+            </th>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <el-dialog v-model="dialogVisible" :close-on-click-modal="false" title="编辑" width="30%" :before-close="closeDialog" :append-to-body="true">
+      <el-form :model="formData" ref="formRef" :inline="true">
+        <el-form-item prop="type">
+          <el-select v-model="formData.type" @change="changeKind" style="width: 250px">
+            <el-option v-for="item in KindEnum" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="value" v-if="formData.type !== ''" :rules="{ validator: checkValue, trigger: 'blur' }">
+          <InputFloat v-model="formData.value" :prefix="isNumber ? '$' : ''" :suffix="!isNumber ? '%' : ''"></InputFloat>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelBid">取消</el-button>
+          <el-button type="primary" @click="submitBid">确认</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, Ref } from 'vue'
+import InputFloat from '/@/components/input-float/index.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  data: { value: string; type: string }[][]
+  disabled?: boolean
+}
+interface FormData {
+  type: string
+  value: string
+}
+const props = defineProps<Props>()
+
+const mouseDown = ref(false)
+const startRowIndex = ref(0)
+const startColIndex = ref(0)
+const items = ref([])
+const WeekMap = {
+  0: '星期一',
+  1: '星期二',
+  2: '星期三',
+  3: '星期四',
+  4: '星期五',
+  5: '星期六',
+  6: '星期日',
+}
+const dialogVisible = ref(false)
+const selectedItems = ref({})
+
+const KindEnum = [
+  { label: '无需调整预算', value: '' },
+  { label: '固定预算', value: 'FixBudget', color: '#3359b5' },
+  { label: '基于原始预算降低(百分比)', value: 'DownPercent', color: '#11acf5' },
+  { label: '基于原始预算升高(百分比)', value: 'UpPercent', color: '#11acf5' },
+  { label: '基于原始预算降低(数值)', value: 'DownNumber', color: '#3fd4cf' },
+  { label: '基于原始预算升高(数值)', value: 'UpNumber', color: '#3fd4cf' },
+]
+
+const formData: Ref<FormData> = ref({ type: 'FixBudget', value: '1.00' })
+const isNumber = ref(true)
+const formRef = ref()
+
+const checkValue = (rule: any, value: string, callback: any) => {
+  if (formData.value.type === 'FixBudget') {
+    if (XEUtils.toNumber(value) < 1) {
+      return new Error('固定预算必须大于1')
+    }
+  } else {
+    if (XEUtils.toNumber(value) <= 0) {
+      return new Error('数值必须大于0')
+    }
+  }
+}
+
+for (let i = 0; i < 7; i++) {
+  selectedItems.value[i] = []
+  const tmp = []
+  for (let j = 0; j < 24; j++) {
+    let type = ''
+    let value = ''
+    if (props.data.length !== 0) {
+      type = props.data[i][j].type
+      value = props.data[i][j].value
+    }
+    tmp.push({ type, value })
+  }
+  items.value.push(tmp)
+}
+
+const getCellBackgroundColor = (row: any) => {
+  if (row.selected) return '#ccdbff'
+  if (row.type) return KindEnum.find((item) => item.value === row.type).color
+  return ''
+}
+
+const getTdStyle = (info: any) => {
+  if (props.disabled) {
+    let backgroundColor = ''
+    if (info.type) {
+      backgroundColor = KindEnum.find((item) => item.value === info.type).color
+    } else {
+      backgroundColor = '#fff'
+    }
+    return { cursor: 'not-allowed', background: backgroundColor }
+  }
+  return { background: getCellBackgroundColor(info) }
+}
+
+watch(
+  () => props.data,
+  () => {
+    // console.log(191, props.data)
+    if (props.data.length === 0) {
+      for (let i = 0; i < 7; i++) {
+        const tmp = []
+        for (let j = 0; j < 24; j++) {
+          tmp.push({ type: '', value: '' })
+        }
+        props.data.push(tmp)
+      }
+    }
+    for (let i = 0; i < 7; i++) {
+      for (let j = 0; j < 24; j++) {
+        items.value[i][j].type = props.data[i][j].type
+        items.value[i][j].value = props.data[i][j].value
+      }
+    }
+  },
+  { deep: true, immediate: true }
+)
+
+const changeKind = () => {
+  if (formData.value.type === 'DownPercent' || formData.value.type === 'UpPercent') {
+    isNumber.value = false
+  } else {
+    isNumber.value = true
+  }
+  formRef.value.clearValidate('value')
+}
+
+const getCellText = (row: any) => {
+  if (row.type === 'DownPercent' || row.type === 'UpPercent') return row.value + '%'
+  if (row.value) return '$' + row.value
+  return ''
+}
+
+const handleMouseDown = (rowIndex: number, colIndex: number, event: any) => {
+  if (props.disabled) return
+  if (event.button === 0) {
+    mouseDown.value = true
+    startRowIndex.value = rowIndex
+    startColIndex.value = colIndex
+    items.value[rowIndex][colIndex].selected = true
+    selectedItems.value[rowIndex].push(colIndex)
+  }
+}
+
+const handleMouseUp = (event: any) => {
+  if (props.disabled) return
+  if (event.button === 0) {
+    mouseDown.value = false
+    startColIndex.value = 0
+    startRowIndex.value = 0
+    dialogVisible.value = true
+  }
+}
+
+const handleMouseMove = (rowIndex: number, colIndex: number) => {
+  if (props.disabled) return
+  if (mouseDown.value) {
+    selectCell(rowIndex, colIndex)
+  }
+}
+
+const selectCell = (rowIndex: number, colIndex: number) => {
+  if (props.disabled) return
+  const rowStart = Math.min(startRowIndex.value, rowIndex)
+  const rowEnd = Math.max(startRowIndex.value, rowIndex)
+  const cellStart = Math.min(startColIndex.value, colIndex)
+  const cellEnd = Math.max(startColIndex.value, colIndex)
+
+  for (let i = rowStart; i <= rowEnd; i++) {
+    for (let j = cellStart; j <= cellEnd; j++) {
+      items.value[i][j].selected = true
+      selectedItems.value[i].push(j)
+    }
+  }
+}
+
+const submitBid = () => {
+  if (formData.value.type === '') {
+    for (const row of Object.keys(selectedItems.value)) {
+      for (const col of selectedItems.value[row]) {
+        props.data[row][col].type = ''
+        props.data[row][col].value = ''
+      }
+    }
+  } else {
+    for (const row of Object.keys(selectedItems.value)) {
+      for (const col of selectedItems.value[row]) {
+        props.data[row][col].type = formData.value.type
+        props.data[row][col].value = formData.value.value
+      }
+    }
+  }
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const cancelBid = () => {
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const closeDialog = (done: Function) => {
+  cancelBid()
+  done()
+}
+
+const clearCellBackgroundColor = () => {
+  for (const hoursList of items.value) {
+    for (const info of hoursList) {
+      info.selected = false
+    }
+  }
+}
+const clearSelectedItems = () => {
+  for (var i = 0; i < 7; i++) {
+    selectedItems.value[i] = []
+  }
+}
+const resetAllBid = () => {
+  for (let i = 0; i < 7; i++) {
+    for (let j = 0; j < 24; j++) {
+      items.value[i][j].value = 0
+      items.value[i][j].selected = false
+      props.data[i][j].type = ''
+      props.data[i][j].value = ''
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.calendar {
+  background-color: #fff;
+  -webkit-user-select: none;
+  position: relative;
+  display: inline-block;
+
+  .calendar-table {
+    border-collapse: collapse;
+  }
+
+  .week-td {
+    width: 90px;
+  }
+}
+
+table {
+  display: table;
+  border-collapse: separate;
+  box-sizing: border-box;
+  text-indent: initial;
+  border-spacing: 2px;
+  border-color: gray;
+
+  thead {
+    display: table-header-group;
+    vertical-align: middle;
+    border-color: inherit;
+  }
+
+  tr {
+    border: 1px solid #e0e5f4;
+    font-size: 12px;
+    text-align: center;
+    line-height: 32px;
+    color: rgba(0, 0, 0, 0.5);
+
+    th {
+      min-width: 40px;
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      background: #f7f8fa;
+    }
+
+    td {
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      min-width: 40px;
+      color: #fff;
+      &:hover {
+        background: #ccdbff;
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+.clear-bar {
+  line-height: 32px;
+  padding: 0 12px;
+
+  .hover-link {
+    color: #1c6bde;
+    font-size: 13px;
+    margin-top: 7px;
+  }
+  .fr {
+    float: right;
+  }
+  .middle {
+    float: center;
+  }
+}
+
+.cell-text {
+  color: #fff;
+}
+</style>

+ 26 - 25
src/components/cardComponents/ShowCard.vue

@@ -1,35 +1,36 @@
 <template>
-    <div style="height: 350px;">
-        <el-row>
-            <el-col :span="6">
-                <el-card :body-style="{ padding: '0px' }">
-                    <div style="display: flex; align-items: center;">
-                        <div><img class="cardImg" :src="cardImage" alt="Card Image" style="height: 100px; border-radius: 50%;"/></div>
-                        <div>
-                            <div style="color:#909399;">{{ cardTitle }}</div>
-                            <div style="font-weight: bold;font-size: large">{{ cardMiddle }}</div>
-                            <div>
-                                <span style="color:#909399;">{{ cardBottom }}</span> ⬇️
-                                <span style="color:orangered;">{{ `${cardBottomRight}%` }}</span>
-                            </div>
-                        </div>
-                    </div>
-
-                </el-card>
-            </el-col>
-        </el-row>
-    </div>
+  <div style="height: 110px; width:24%; flex:1;min-width: 260px;">
+    <el-card :body-style="{ padding: '0px' }">
+      <div style="display: flex; align-items: center;">
+        <div><img class="cardImg" :src="cardImage" alt="Card Image" style="height: 100px; border-radius: 50%;  "/></div>
+        <div>
+          <div style="color:#909399;">{{ cardTitle }}</div>
+          <div style="font-weight: bold;font-size: large">{{ cardMiddle }}</div>
+          <div>
+            <span style="color:#909399;">{{ cardBottom }}</span> ⬇️
+            <span style="color:orangered;">{{ `${ cardBottomRight }%` }}</span>
+          </div>
+        </div>
+      </div>
+    </el-card>
+  </div>
 </template>
 
 <script setup>
 
+import { toRefs, watchEffect } from "vue"
+
 const props = defineProps({
-    cardData: {
-        type: Object,
-        required: true,
-    },
+  cardData: {
+    type: Object,
+    required: true,
+  },
+})
+const { cardImage, cardTitle, cardMiddle, cardBottom, cardBottomRight } = toRefs(props.cardData)
+
+watchEffect(() => {
+  console.log(111, props.cardData)
 })
-const { cardImage, cardTitle, cardMiddle, cardBottom, cardBottomRight } = props.cardData
 
 </script>
 

+ 201 - 0
src/components/conditionBuilder/condition-group.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="condition-group-item-wrap">
+    <div>
+      <el-form :model="formData" ref="formRef" label-suffix=":" label-position="right" label-width="100px" :inline="true">
+        <el-form-item label="数据周期" required props="day">
+          <el-select v-model="formData.day" @change="formData.exceptDay = 0">
+            <el-option v-for="item in periodEnum" :key="item.value" :label="item.label" :value="item.value"> </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排除" required props="exceptDay">
+          <el-select v-model="formData.exceptDay">
+            <el-option
+              v-for="item in excludeEnum"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+              :disabled="formData.day <= item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <el-form
+        v-for="(conditionInfo, index) of formData.items"
+        :model="conditionInfo"
+        :inline="true"
+        label-position="right"
+        label-width="100px"
+        ref="ruleFormRef"
+        :key="conditionInfo.dataType">
+        <el-form-item :required="index === 0" prop="dataType">
+          <template #label>
+            <span v-if="index === 0">条件:</span>
+            <span class="and-span" v-else>&</span>
+          </template>
+          <el-select v-model="conditionInfo.dataType" style="width: 140px" @change="conditionInfo.num = ''">
+            <el-option
+              v-for="item in candidateFields"
+              :key="item.value"
+              :disabled="selectedFields[item.value] ? true : false"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="dayType">
+          <el-select v-model="conditionInfo.dayType" style="width: 80px">
+            <el-option label="总计" value="sum"></el-option>
+            <el-option label="均值" value="avg"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="symbol">
+          <el-select v-model="conditionInfo.symbol" style="width: 120px">
+            <el-option label="大于" value="gt"></el-option>
+            <el-option label="大于等于" value="gte"></el-option>
+            <el-option label="小于" value="lt"></el-option>
+            <el-option label="小于等于" value="lte"></el-option>
+            <el-option label="等于" value="eq"></el-option>
+            <el-option label="包含" value="in"></el-option>
+            <el-option label="范围内" value="between"></el-option>
+            <el-option label="范围外" value="not_between"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="num" :rules="[{ validator: checkRight, trigger: 'blur' }]">
+          <InputFloat
+            v-model="conditionInfo.num"
+            :prefix="findPrefix(conditionInfo.dataType)"
+            :suffix="findSuffix(conditionInfo.dataType)"
+            style="width: 200px">
+          </InputFloat>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="deleteCondition(index)" type="danger" :icon="Delete" v-show="formData.items.length > 1" link></el-button>
+        </el-form-item>
+      </el-form>
+      <el-button link type="primary" @click="addCondition" :icon="Plus" style="margin-left: 100px; color:blue" v-show="showAddCondiBtn">添加条件</el-button>
+    </div>
+    <el-button type="danger" @click="deleteConditionGroup" :icon="Delete" v-show="showDelGroupBtn">删除组</el-button>
+  </div>
+  <el-divider border-style="dashed" class="condition-group-divider"> 或 </el-divider>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed } from 'vue'
+import { Delete, Plus } from '@element-plus/icons-vue'
+import InputFloat from '/@/components/input-float/index.vue'
+
+interface Props {
+  candidateFields: CandidateField[]
+  data: ConditionGroupItem
+  showDelGroupBtn: boolean
+}
+const emits = defineEmits(['deleteGroup'])
+const periodEnum = [
+  { label: '昨天', value: 1 },
+  { label: '过去7天', value: 7 },
+  { label: '过去14天', value: 14 },
+  { label: '过去30天', value: 30 },
+  { label: '过去60天', value: 60 },
+  { label: '过去90天', value: 90 },
+]
+const excludeEnum = [
+  { label: '不排除', value: 0 },
+  { label: '昨天', value: 1 },
+  { label: '最近2天', value: 2 },
+  { label: '最近3天', value: 3 },
+  { label: '最近7天', value: 7 },
+  { label: '最近14天', value: 14 },
+]
+const props = defineProps<Props>()
+const formRef = ref()
+const ruleFormRef = ref()
+const formData = reactive(props.data)
+const checkRight = (rule: any, value: string, callback: any) => {
+  if (value === '0.00' || value === '') {
+    callback(new Error('请输入大于0的数值!'))
+  } else {
+    callback()
+  }
+}
+const validate = async () => {
+  let ret = []
+  for (const info of ruleFormRef.value) {
+    await info.validate((valid:any, fields:string) => {
+      ret.push(valid)
+      if(!valid) return false
+    })
+  }
+  return ret
+}
+const showAddCondiBtn = computed(() => {
+  return formData.items.length < props.candidateFields.length
+})
+const selectedFields = computed(() => {
+  const ret = {}
+  for (const info of formData.items) {
+    ret[info.dataType] = true
+  }
+  return ret
+})
+const addCondition = () => {
+  for (const item of props.candidateFields) {
+    if (!selectedFields.value[item.value]) {
+      formData.items.push({
+        dataType: item.value,
+        dayType: 'sum',
+        symbol: 'gte',
+        num: '',
+      })
+      break
+    }
+  }
+}
+const findPrefix = (dataType: string) => {
+  return props.candidateFields.find((item) => item.value === dataType).prefix ?? ''
+}
+const findSuffix = (dataType: string) => {
+  return props.candidateFields.find((item) => item.value === dataType).suffix ?? ''
+}
+const deleteCondition = (index: number) => {
+  formData.items.splice(index, 1)
+}
+const deleteConditionGroup = () => {
+  emits('deleteGroup')
+}
+
+defineExpose({validate})
+</script>
+
+<style scoped>
+.condition-group-item-wrap {
+  background: #f4f7fe;
+  padding: 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  /* position: relative; */
+}
+.condition-group-period {
+  display: flex;
+  justify-content: flex-start;
+  gap: 20px;
+}
+.condition-item {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+.and-span {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  background: #fff;
+  text-align: center;
+  line-height: 20px;
+  border-radius: 50%;
+  margin: auto 0;
+}
+div.condition-group-divider:last-of-type {
+  display: none;
+}
+</style>

+ 265 - 0
src/components/conditionBuilder/condition-group2.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="condition-group-item-wrap">
+    <div>
+      <el-form :model="formData" ref="formRef" label-suffix=":" label-position="right" label-width="100px" :disabled="disabled">
+        <el-form-item label="数据周期" required prop="day">
+          <el-select v-model="formData.day" @change="formData.exceptDay = 0">
+            <el-option v-for="item in periodEnum" :key="item.value" :label="item.label" :value="item.value"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排除" required prop="exceptDay">
+          <el-select v-model="formData.exceptDay">
+            <el-option v-for="item in excludeEnum" :key="item.value" :label="item.label" :value="item.value" :disabled="formData.day <= item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-for="(conditionInfo, index) of formData.items"
+          :key="conditionInfo.dataType"
+          :prop="getFormProp(conditionInfo, index)"
+          :rules="{ validator: checkRight, trigger: getFormTrigger(conditionInfo) }">
+          <template #label>
+            <span v-if="index === 0">条件:</span>
+            <span class="and-span" v-else>&</span>
+          </template>
+          <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap">
+            <el-select v-model="conditionInfo.dataType" style="width: 140px" @change="changeDataType(conditionInfo)">
+              <el-option
+                v-for="item in candidateFields"
+                :key="item.value"
+                :disabled="selectedFields[item.value] ? true : false"
+                :label="item.label"
+                :value="item.value">
+              </el-option>
+            </el-select>
+            <el-select
+              v-model="conditionInfo.dayType"
+              style="width: 80px"
+              v-show="conditionInfo.symbol !== 'in' && conditionInfo.symbol !== 'not_in'">
+              <el-option label="总计" value="sum"></el-option>
+              <el-option label="均值" value="avg"></el-option>
+            </el-select>
+            <el-select v-model="conditionInfo.symbol" style="width: 120px">
+              <el-option v-for="info in getSymbolOptions(conditionInfo.dataType)" :label="info.label" :value="info.value" :key="info.value">
+              </el-option>
+            </el-select>
+            <template v-if="conditionInfo.symbol === 'in' || conditionInfo.symbol === 'not_in'">
+              <el-select v-if="getFieldInfo(conditionInfo.dataType).options" v-model="conditionInfo.values" multiple collapse-tags>
+                <el-option
+                  v-for="info in getFieldInfo(conditionInfo.dataType).options"
+                  :label="info.label"
+                  :value="info.value"
+                  :key="info.value"></el-option>
+              </el-select>
+              <div v-else style="width: 90%">
+                <TagsInput :data="conditionInfo.values" placeholder="请输入至少一个关键词,多个关键词用换行符分隔" :disabled="disabled"></TagsInput>
+              </div>
+            </template>
+            <template v-else-if="conditionInfo.symbol === 'between' || conditionInfo.symbol === 'not_between'">
+              <RangeFloat :ranges="conditionInfo.ranges" :prefix="findPrefix(conditionInfo.dataType)" :suffix="findSuffix(conditionInfo.dataType)">
+              </RangeFloat>
+            </template>
+            <InputFloat
+              v-else
+              v-model="conditionInfo.num"
+              :prefix="findPrefix(conditionInfo.dataType)"
+              :suffix="findSuffix(conditionInfo.dataType)"
+              style="width: 200px">
+            </InputFloat>
+            <el-button @click="deleteCondition(index)" type="danger" :icon="Delete" v-show="formData.items.length > 1" link></el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+      <el-button
+        link
+        type="primary"
+        @click="addConditionItem"
+        :icon="Plus"
+        :style="{'margin-left': '100px', color: disabled ? '': 'blue'}"
+        v-show="showAddCondiBtn"
+        :disabled="disabled">
+        添加条件
+      </el-button>
+    </div>
+    <el-button type="danger" @click="deleteConditionGroup" :icon="Delete" v-show="showDelGroupBtn">删除组</el-button>
+  </div>
+  <el-divider border-style="dashed" class="condition-group-divider"> 或 </el-divider>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, watch } from 'vue'
+import { Delete, Plus } from '@element-plus/icons-vue'
+import InputFloat from '/@/components/input-float/index.vue'
+import TagsInput from '/@/components/tags-input/index.vue'
+import RangeFloat from '/@/components/range-float/index.vue'
+import { useSymbolOptions } from './utils'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  candidateFields: CandidateField[]
+  data: ConditionGroupItem
+  showDelGroupBtn: boolean
+  disabled: boolean
+}
+const emits = defineEmits(['deleteGroup'])
+const periodEnum = [
+  { label: '昨天', value: 1 },
+  { label: '过去7天', value: 7 },
+  { label: '过去14天', value: 14 },
+  { label: '过去30天', value: 30 },
+  { label: '过去60天', value: 60 },
+  { label: '过去90天', value: 90 },
+]
+const excludeEnum = [
+  { label: '不排除', value: 0 },
+  { label: '昨天', value: 1 },
+  { label: '最近2天', value: 2 },
+  { label: '最近3天', value: 3 },
+  { label: '最近7天', value: 7 },
+  { label: '最近14天', value: 14 },
+]
+
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+})
+const formRef = ref()
+const formData = ref(props.data)
+const { getSymbolOptions, getFieldInfo } = useSymbolOptions(props.candidateFields)
+
+const getFormProp = (conditionInfo: ConditionItem, index: number) => {
+  if (['between', 'not_between'].includes(conditionInfo.symbol)) {
+    return `items[${index}].ranges`
+  }
+  if (['in', 'not_in'].includes(conditionInfo.symbol)) {
+    return `items[${index}].values`
+  }
+  return `items[${index}].num`
+}
+const getFormTrigger = (conditionInfo: ConditionItem) => {
+  const fieldInfo = getFieldInfo(conditionInfo.dataType)
+  if (fieldInfo.options && fieldInfo.options.length > 0) return 'change'
+  return 'blur'
+}
+
+const checkRight = (rule: any, value: string | string[], callback: any) => {
+  // console.log(139, rule, value)
+  if (rule.field.includes('num')) {
+    if (XEUtils.toNumber(value) <= 0 ) {
+      callback(new Error('请输入大于0的数值!'))
+    }
+  } else if (rule.field.includes('values')) {
+    if (value.length === 0) {
+      callback(new Error('必填项'))
+    }
+  } else if (rule.field.includes('ranges')) {
+    for (const val of value) {
+      if (XEUtils.toNumber(val) <= 0) {
+        callback(new Error('请输入大于0的数值!'))
+        break
+      }
+    }
+    if (XEUtils.toNumber(value[0]) >= XEUtils.toNumber(value[1])) {
+      callback(new Error('起始值必须小于终止值!'))
+    }
+  }
+  callback()
+}
+const validate = async () => {
+  let ret = false
+  await formRef.value.validate((valid: any) => {
+    ret = valid
+  })
+  return ret
+}
+const showAddCondiBtn = computed(() => {
+  return formData.value.items.length < props.candidateFields.length
+})
+const selectedFields = computed(() => {
+  const ret = {}
+  for (const info of formData.value.items) {
+    ret[info.dataType] = true
+  }
+  return ret
+})
+const buildConditionItem = (field: string) => {
+  const symbol = getSymbolOptions(field)[0].value
+  return {
+    dataType: field,
+    dayType: symbol === 'in' || symbol === 'not_in' ? '' : 'sum',
+    symbol: symbol,
+    num: '',
+    ranges: [],
+    values: [],
+  }
+}
+const addConditionItem = () => {
+  for (const item of props.candidateFields) {
+    if (!selectedFields.value[item.value]) {
+      formData.value.items.push(buildConditionItem(item.value))
+      break
+    }
+  }
+}
+// const changeSymbol = (conditionInfo: ConditionItem) => {
+//   if(conditionInfo.symbol === 'between' || conditionInfo.symbol === 'not_between') {
+//     if (conditionInfo.values.length !== 2) {
+//       conditionInfo.values.splice(0, conditionInfo.values.length)
+//     }
+//   }
+// }
+const changeDataType = (conditionInfo: ConditionItem) => {
+  conditionInfo.symbol = getSymbolOptions(conditionInfo.dataType)[0].value
+  conditionInfo.num = ''
+  // conditionInfo.ranges[0] = ''
+  // conditionInfo.ranges[1] = ''
+  conditionInfo.ranges.splice(0, conditionInfo.ranges.length)
+  conditionInfo.values.splice(0, conditionInfo.values.length)
+}
+
+const findPrefix = (dataType: string) => {
+  return props.candidateFields.find((item) => item.value === dataType).prefix ?? ''
+}
+const findSuffix = (dataType: string) => {
+  return props.candidateFields.find((item) => item.value === dataType).suffix ?? ''
+}
+const deleteCondition = (index: number) => {
+  formData.value.items.splice(index, 1)
+}
+const deleteConditionGroup = () => {
+  emits('deleteGroup')
+}
+
+watch(
+  () => props.data,
+  () => {
+    formData.value = props.data
+  },
+  { deep: true }
+)
+
+defineExpose({ validate })
+</script>
+
+<style scoped>
+.condition-group-item-wrap {
+  background: #f4f7fe;
+  padding: 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  /* position: relative; */
+}
+.and-span {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  background: #fff;
+  text-align: center;
+  line-height: 20px;
+  border-radius: 50%;
+  margin: auto 0;
+}
+div.condition-group-divider:last-of-type {
+  display: none;
+}
+</style>

+ 100 - 0
src/components/conditionBuilder/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div>
+    <div :class="titleClass">条件</div>
+    <ConditionGroup
+      ref="condiGroupRef"
+      :disabled="disabled"
+      v-for="(info, index) of condiGroups"
+      :candidate-fields="candidateFields"
+      :data="info"
+      :key="info.key"
+      :showDelGroupBtn="condiGroups.length > 1 && !disabled"
+      @delete-group="delGroup(info.key)" />
+  </div>
+  <el-button type="success" @click="addConditionGroup" style="margin: 5px auto; display: block" :disabled="disabled">添加条件组</el-button>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import ConditionGroup from './condition-group2.vue'
+import { useSymbolOptions } from '/@/components/conditionBuilder/utils'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  candidateFields: CandidateField[]
+  data: ConditionGroupItem[]
+  disabled: boolean
+  titleClass: string
+}
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+  titleClass: 'asj-h3',
+  candidateFields: () => {
+    return [
+      { label: '曝光量', value: 'impressions' },
+      { label: '点击量', value: 'clicks' },
+      { label: '花费', value: 'spend', prefix: '$' },
+      { label: '点击率', value: 'ctr', suffix: '%' },
+      { label: '单次点击费用', value: 'cpc', prefix: '$' },
+      { label: '转化率', value: 'cr', suffix: '%' },
+      { label: '广告订单数', value: 'order' },
+      { label: '广告销售额', value: 'sale', prefix: '$' },
+      { label: 'ACOS', value: 'acos', suffix: '%' }
+    ]
+  },
+})
+const condiGroups = ref(props.data)
+const condiGroupRef = ref()
+const { getSymbolOptions } = useSymbolOptions(props.candidateFields)
+
+const buildConditionGroup = () => {
+  const field = props.candidateFields[0].value
+  const symbol = getSymbolOptions(field)[0].value
+  return {
+    key: Math.random().toString(36).substring(2),
+    day: 1,
+    exceptDay: 0,
+    items: [
+      {
+        dataType: field,
+        dayType: symbol === 'in' || symbol === 'not_in' ? '' : 'sum',
+        symbol: symbol,
+        num: '',
+        ranges: [],
+        values: [],
+      },
+    ],
+  }
+}
+const addConditionGroup = () => {
+  condiGroups.value.push(buildConditionGroup())
+}
+if (condiGroups.value.length === 0) {
+  addConditionGroup()
+}
+
+const delGroup = (key: string) => {
+  XEUtils.remove(condiGroups.value, (item) => item.key === key)
+}
+const validate = async () => {
+  const ret = []
+  if (!condiGroupRef.value) return ret
+  for (const info of condiGroupRef.value) {
+    ret.push(await info.validate())
+  }
+  // console.log(52, ret)
+  return ret
+}
+
+watch(
+  () => props.data,
+  () => {
+    condiGroups.value = props.data
+  },
+  { deep: true }
+)
+
+defineExpose({ validate, addConditionGroup })
+</script>
+
+<style scoped></style>

+ 24 - 0
src/components/conditionBuilder/type.d.ts

@@ -0,0 +1,24 @@
+declare interface ConditionItem {
+  dataType: string,
+  dayType: string,
+  symbol: string,
+  num: string,
+  values: string[],
+  ranges: string[]
+}
+
+declare interface ConditionGroupItem {
+  key: string,
+  day: number,
+  exceptDay: number,
+  items: ConditionItem[]
+}
+
+declare interface CandidateField {
+  label: string,
+  value: string,
+  prefix?: string,
+  suffix?: string,
+  type?: string,
+  options?: {[key: string]: any}[]
+}

+ 30 - 0
src/components/conditionBuilder/utils.ts

@@ -0,0 +1,30 @@
+import XEUtils from 'xe-utils'
+
+export const useSymbolOptions = (candidateFields: CandidateField[]) => {
+  const SymbolOptionsList = [
+    { label: '大于', value: 'gt' },
+    { label: '大于等于', value: 'gte' },
+    { label: '小于', value: 'lt' },
+    { label: '小于等于', value: 'lte' },
+    { label: '等于', value: 'eq' },
+    { label: '范围内', value: 'between' },
+    { label: '范围外', value: 'not_between' },
+    // { label: '包含', value: 'in' },
+    // { label: '不包含', value: 'not_in' }
+  ]
+
+  const getFieldInfo = (field: string) => {
+    return XEUtils.find(candidateFields, (item) => item.value === field)
+  }
+
+  const getSymbolOptions = (field: string) => {
+    const FieldInfo = getFieldInfo(field)
+    if (FieldInfo.type === 'array')
+      return [
+        { label: '包含', value: 'in' },
+        { label: '不包含', value: 'not_in' },
+      ]
+    return SymbolOptionsList
+  }
+  return { getSymbolOptions, getFieldInfo }
+}

+ 56 - 0
src/components/dataCompare/index.vue

@@ -0,0 +1,56 @@
+<template>
+  <p>{{ props.value === null || props.value === 0 ? '--' : props.value }}</p>
+  <el-popover
+    effect="dark"
+    :width="260">
+    <template #reference>
+      <p :class="colorClass" v-show="props.showCompare">
+        <template v-if="props.gapVal">
+          <el-icon style="display: inline-block; padding-top: 2px">
+            <Top v-if="props.gapVal > 0"/>
+            <Bottom v-if="props.gapVal < 0"/>
+          </el-icon>
+        </template>
+        <span>{{ props.gapVal ? props.gapVal.toFixed(2) + '%' : '--'}}</span>
+      </p>
+    </template>
+    <p>对比周期:{{ compareDate[0] }} ~ {{ compareDate[1] }}</p>
+    <p>对比值:{{ props.prevVal }}</p>
+  </el-popover>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, Prop } from 'vue'
+import { getCompareDate } from '/@/views/adManage/utils/tools.js'
+
+interface Props {
+  field: string,
+  value: number,
+  prevVal: number,
+  gapVal: number,
+  dateRange: string[],
+  showCompare: boolean
+}
+
+const props = defineProps<Props>()
+const compareDate = computed(() => {
+  return getCompareDate(props.dateRange)
+})
+
+const colorClass = computed(() => {
+  if (props.gapVal) {
+    return props.gapVal > 0 ? 'green' :'red'
+  }
+  return ''
+})
+
+</script>
+
+<style scoped>
+.green {
+  color: rgb(24, 172, 54)
+}
+.red {
+  color: red;
+}
+</style>

+ 164 - 0
src/components/echartsComponents/BarChart.vue

@@ -0,0 +1,164 @@
+<template>
+    <div style="margin-left: 45%">
+        <span style="background: rgb(176,234,232); width: 18px; height: 10px; margin-top: 8px; display: inline-block; border-radius: 3px;"></span>
+        <TextSelector :modelValue="modelValue" :options="options" style="margin-top: 5px; margin-left: 8px;"/>
+        <span style="background: rgb(234,207,135); width: 18px; height: 10px; margin-top: 8px; margin-left: 20px; display: inline-block; border-radius: 3px;"></span>
+        <TextSelector :modelValue="modelValue" :options="options" style="margin-top: 5px; margin-left: 8px;"/>
+    </div>
+    <div ref="barRef" style="height: 400px;"></div>
+</template>
+
+<script lang="ts" setup>
+import {ref, onMounted, onBeforeUnmount} from 'vue'
+import * as echarts from 'echarts'
+import TextSelector from '/@/components/TextSelector/index.vue'
+
+// defineOptions({
+//     name: 'DataTendencyChart'
+// })
+
+let chartObj: any
+const barRef = ref()
+
+function resizeChart() {
+    chartObj.resize()
+}
+
+function addResize() {
+    window.addEventListener('resize', resizeChart)
+}
+
+function removeResize() {
+    window.removeEventListener('resize', resizeChart)
+}
+
+onMounted(() => {
+    initLine()
+    addResize()
+})
+onBeforeUnmount(() => {
+    if (chartObj) {
+        chartObj.dispose()
+        chartObj = null
+    }
+    removeResize()
+})
+
+function initLine() {
+    chartObj = echarts.init(barRef.value)
+    const option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'cross',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        toolbox: {
+            feature: {
+                saveAsImage: {yAxisIndex: 'none'}
+            }
+        },
+        grid: {
+            top: 70, right: 60, bottom: 30, left: 55,
+
+        },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: ['商品', '品类', '关键词-精准', '关键词-广泛', '关键词-词组'],
+            }
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                name: '数据1',
+                axisLabel: {
+                    formatter: '{value} 单位1'
+                },
+                axisLine: {
+                    show: true
+                }
+            },
+            {
+                type: 'value',
+                name: '数据2',
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    formatter: '{value} 单位2'
+                },
+                axisLine: {
+                    show: true
+                }
+            }
+        ],
+        series: [
+            {
+                name: '数据1',
+                type: 'bar',
+                // tooltip: {
+                //   valueFormatter: function (value) {
+                //     return value + ' ml';
+                //   }
+                // },
+                barWidth: '30%',
+                data: [15, 24, 21, 26, 34],
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        {offset: 0, color: 'rgba(111, 209, 206, 0.8)'},
+                        {offset: 1, color: 'rgba(111, 209, 206, 0.1)'},
+                    ]),
+                    //柱状图圆角
+                    borderRadius: [15, 15, 0, 0],
+                },
+            },
+            {
+                name: '数据2',
+                type: 'bar',
+                barWidth: '30%',
+                data: [10, 16, 28, 21, 30],
+                yAxisIndex: 1,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        {offset: 0, color: '#ebb14d'},
+                        {offset: 1, color: 'rgba(111, 209, 206, 0.1)'},
+                    ]),
+                    //柱状图圆角
+                    borderRadius: [15, 15, 0, 0],
+                },
+            },
+        ],
+    }
+    chartObj.setOption(option)
+}
+
+defineExpose({resizeChart})
+
+// 下拉框相关
+const options = [
+    {
+        value: '花费',
+        label: '花费',
+    },
+    {
+        value: 'Option2',
+        label: 'Option2',
+    },
+    {
+        value: 'Option3',
+        label: 'Option3',
+    }
+]
+const modelValue = ref(options[0].value)
+
+</script>
+
+<style scoped>
+
+</style>

+ 181 - 155
src/components/echartsComponents/BarLineChart.vue

@@ -1,176 +1,202 @@
 <template>
-    <div id="barLine" ref="barLine" style="height: 400px;"></div>
+    <div ref="barLine" style="height: 400px;"></div>
 </template>
 
-<script>
-import { onMounted, onBeforeUnmount, ref, } from 'vue'
+<script setup>
+import {onMounted, onBeforeUnmount, ref} from 'vue'
 import * as echarts from 'echarts'
 
-export default {
-    props: ['barLineData'],
+const props = defineProps({
+    barLineData: {type: Object}
+})
+let lineChart = ref()
+const barLine = ref()
 
-    setup(props, context) {
-        let lineChart = ref()
+onMounted(() => {
+    lineChart = echarts.init(barLine.value)
+    updateChart()
+    addResize()  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    }, 0)
+})
+onBeforeUnmount(() => {
+    if (lineChart) {
+        lineChart.dispose()
+        lineChart = null
+    }
+    removeResize()   // 在组件销毁前移除事件监听,避免内存泄漏
+})
+// 获取图像数据 lieChartData
+let chartData = props.barLineData
 
-        onMounted(() => {
-            lineChart = echarts.init(document.getElementById('barLine'))
-            updateChart()
-            window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
-            setTimeout(() => {
-                resizeChart()
-            },0)
-        })
-        onBeforeUnmount(() => {
-            window.removeEventListener('resize', resizeChart)   // 在组件销毁前移除事件监听,避免内存泄漏
-        })
-        // 获取图像数据 lieChartData
-        let chartData = props.barLineData
-
-        function updateChart() {
-            const option = {
-                // title: {
-                //   text: chartData.title,
+function updateChart() {
+    const option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'cross',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        // legend: {data: ['数据1', '数据2'], right: '50%',left: '50%'},
+        // toolbox: {
+        //     feature: {
+        //         saveAsImage: { yAxisIndex: 'none' }
+        //     }
+        // },
+        grid: {
+            top: 50, right: 150, bottom: 30, left: 55,
+        },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: chartData.xData,
+            }
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                name: 'ACOS',
+                splitLine: {
+                    show: true // 设置显示分割线
+                },
+                axisLabel: {
+                    formatter: '{value} 单位1'
+                },
+                axisLine: {
+                    show: true
+                }
+            },
+            {
+                type: 'value',
+                name: '点击率',
+                position: 'right',
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    formatter: '{value} 单位2'
+                },
+                axisLine: {
+                    show: true
+                }
+            },
+            {
+                type: 'value',
+                position: 'right',
+                offset: 80,
+                name: '订单数',
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    formatter: '{value} 单位3'
+                },
+                axisLine: {
+                    show: true
+                }
+            }
+        ],
+        series: [
+            {
+                name: '柱状图',
+                type: 'bar',
+                // tooltip: {
+                //   valueFormatter: function (value) {
+                //     return value + ' ml';
+                //   }
                 // },
-                tooltip: {
-                    trigger: 'axis',
-                    axisPointer: {
-                        type: 'cross',
-                        label: {
-                            backgroundColor: '#6a7985'
-                        }
-                    }
+                barWidth: '30%',
+                data: chartData.barData,
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        {offset: 0, color: 'rgba(111, 209, 206, 0.8)'},
+                        {offset: 1, color: 'rgba(111, 209, 206, 0.1)'},
+                    ]),
+                    //柱状图圆角
+                    borderRadius: [15, 15, 0, 0],
                 },
-                // legend: {data: ['数据1', '数据2'], right: '50%',left: '50%'},
-                toolbox: {
-                    feature: {
-                        saveAsImage: { yAxisIndex: 'none' }
-                    }
+            },
+            {
+                name: '数据1',
+                type: 'line',
+                symbolSize: 6,
+                symbol: 'circle',
+                smooth: true,
+                data: chartData.yData1,
+                yAxisIndex: 1,
+                lineStyle: {color: '#fe9a8b'},
+                itemStyle: {color: '#fe9a8b', borderColor: '#fe9a8b'},
+                areaStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        {offset: 0, color: '#fe9a8bb3'},
+                        {offset: 1, color: '#fe9a8b03'},
+                    ]),
                 },
-                grid: {
-                    top: 70, right: 60, bottom: 30, left: 55,
-
+            },
+            {
+                name: '数据2',
+                type: 'line',
+                symbolSize: 6,
+                symbol: 'circle',
+                smooth: true,
+                data: chartData.yData2,
+                yAxisIndex: 2,
+                lineStyle: {color: '#9E87FF'},
+                itemStyle: {color: '#9E87FF', borderColor: '#9E87FF'},
+                areaStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        {offset: 0, color: '#9E87FFb3'},
+                        {offset: 1, color: '#9E87FF03'},
+                    ]),
                 },
-                xAxis: [
-                    {
-                        type: 'category',
-                        boundaryGap: true,
-                        data: chartData.xData,
-                    }
-                ],
-                yAxis: [
-                    {
-                        type: 'value',
-                        // name: yTitle1,
-                        splitLine: {
-                            show: true // 设置显示分割线
+                emphasis: {
+                    itemStyle: {
+                        color: {
+                            type: 'radial',
+                            x: 0.5,
+                            y: 0.5,
+                            r: 0.5,
+                            colorStops: [
+                                {offset: 0, color: '#9E87FF'},
+                                {offset: 0.4, color: '#9E87FF'},
+                                {offset: 0.5, color: '#fff'},
+                                {offset: 0.7, color: '#fff'},
+                                {offset: 0.8, color: '#fff'},
+                                {offset: 1, color: '#fff'},
+                            ],
                         },
-                        axisLabel: {
-                            formatter: '{value} 单位1'
-                        }
+                        borderColor: '#9E87FF',
+                        borderWidth: 2,
                     },
-                    {
-                        type: 'value',
-                        // name: yTitle2,
-                        splitLine: {
-                            show: false
-                        },
-                        axisLabel: {
-                            formatter: '{value} 单位2'
-                        }
-                    }
-                ],
-                series: [
-                    {
-                        name: '柱状图',
-                        type: 'bar',
-                        // tooltip: {
-                        //   valueFormatter: function (value) {
-                        //     return value + ' ml';
-                        //   }
-                        // },
-                        barWidth: '30%',
-                        data: chartData.barData,
-                        yAxisIndex: 1,
-                        itemStyle: {
-                            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                                { offset: 0, color: 'rgba(111, 209, 206, 0.8)' },
-                                { offset: 1, color: 'rgba(111, 209, 206, 0.1)' },
-                            ]),
-                            //柱状图圆角
-                            borderRadius: [15, 15, 0, 0],
-                        },
-                    },
-                    {
-                        name: '数据1',
-                        type: 'line',
-                        symbolSize: 6,
-                        symbol: 'circle',
-                        smooth: true,
-                        data: chartData.yData1,
-                        yAxisIndex: 0,
-                        lineStyle: { color: '#fe9a8b' },
-                        itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' },
-                        areaStyle: {
-                            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                                { offset: 0, color: '#fe9a8bb3' },
-                                { offset: 1, color: '#fe9a8b03' },
-                            ]),
-                        },
-                    },
-                    {
-                        name: '数据2',
-                        type: 'line',
-                        symbolSize: 6,
-                        symbol: 'circle',
-                        smooth: true,
-                        data: chartData.yData2,
-                        yAxisIndex: 1,
-                        lineStyle: { color: '#9E87FF' },
-                        itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
-                        areaStyle: {
-                            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                                { offset: 0, color: '#9E87FFb3' },
-                                { offset: 1, color: '#9E87FF03' },
-                            ]),
-                        },
-                        emphasis: {
-                            itemStyle: {
-                                color: {
-                                    type: 'radial',
-                                    x: 0.5,
-                                    y: 0.5,
-                                    r: 0.5,
-                                    colorStops: [
-                                        { offset: 0, color: '#9E87FF' },
-                                        { offset: 0.4, color: '#9E87FF' },
-                                        { offset: 0.5, color: '#fff' },
-                                        { offset: 0.7, color: '#fff' },
-                                        { offset: 0.8, color: '#fff' },
-                                        { offset: 1, color: '#fff' },
-                                    ],
-                                },
-                                borderColor: '#9E87FF',
-                                borderWidth: 2,
-                            },
-                        },
-                    },
-                ],
-            }
-            lineChart.setOption(option)
-        }
+                },
+            },
+        ],
+    }
+    lineChart.setOption(option)
+}
 
-        // 自适应调整窗口
-        function resizeChart() {
-            if (lineChart) {
-                lineChart.resize()
-            }
-        }
+// 自适应调整窗口
+function resizeChart() {
+    if (lineChart) {
+        lineChart.resize()
+    }
+}
 
-        return {}
-    },
+function addResize() {
+    window.addEventListener('resize', resizeChart)
+}
 
+function removeResize() {
+    window.removeEventListener('resize', resizeChart)
 }
 
+defineExpose({resizeChart, updateChart})
 
 </script>
 

+ 214 - 0
src/components/echartsComponents/PieBarChart.vue

@@ -0,0 +1,214 @@
+<template>
+    <div>
+        <el-row :gutter="5">
+            <el-col :span="8">
+                <div>
+                    <!--<span>{{ modelValue }}</span>-->
+                    <TextSelector :modelValue="modelValue" :options="options" style="margin-top: 5px"/>
+                </div>
+
+                <div ref="pie" style="height: 400px;"></div>
+            </el-col>
+            <el-col :span="16">
+                <div style="margin-left: 40%">
+                    <span style="background: rgb(176,234,232); width: 18px; height: 10px; margin-top: 8px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector :modelValue="modelValue" :options="options" style="margin-top: 5px; margin-left: 8px;"/>
+                    <span style="background: rgb(234,207,135); width: 18px; height: 10px; margin-top: 8px; margin-left: 20px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector :modelValue="modelValue" :options="options" style="margin-top: 5px; margin-left: 8px;"/>
+                </div>
+                <div ref="bar" style="height: 400px;"></div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from "vue"
+import * as echarts from "echarts"
+import TextSelector from '/@/components/TextSelector/index.vue'
+
+let props = defineProps({
+    pieBarChartData: {
+        type: Object,
+    }
+})
+
+let pieChart = ref()
+let barChart = ref()
+const pie = ref()
+const bar = ref()
+
+onMounted(() => {
+    barChart = echarts.init(bar.value)
+    pieChart = echarts.init(pie.value)
+    setChartData()
+    window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    }, 0)
+})
+
+function setChartData() {
+    // 柱状图配置
+    const option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'cross',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        // legend: {data: ['数据1', '数据2'],},
+        toolbox: {
+            feature: {
+                saveAsImage: { yAxisIndex: 'none' }
+            }
+        },
+        grid: {
+            top: 70, right: 60, bottom: 30, left: 55,
+
+        },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: ['商品', '品类', '关键词-精准', '关键词-广泛', '关键词-词组'],
+            }
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                name: '数据1',
+                axisLabel: {
+                    formatter: '{value} 单位1'
+                },
+                axisLine: {
+                    show: true
+                }
+            },
+            {
+                type: 'value',
+                name: '数据2',
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    formatter: '{value} 单位2'
+                },
+                axisLine: {
+                    show: true
+                }
+            }
+        ],
+        series: [
+            {
+                name: '数据1',
+                type: 'bar',
+                // tooltip: {
+                //   valueFormatter: function (value) {
+                //     return value + ' ml';
+                //   }
+                // },
+                barWidth: '30%',
+                data: props.pieBarChartData.barData[0],
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        { offset: 0, color: 'rgba(111, 209, 206, 0.8)' },
+                        { offset: 1, color: 'rgba(111, 209, 206, 0.1)' },
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [15, 15, 0, 0],
+                },
+            },
+            {
+                name: '数据2',
+                type: 'bar',
+                barWidth: '30%',
+                data: props.pieBarChartData.barData[1],
+                yAxisIndex: 1,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        { offset: 0, color: '#ebb14d' },
+                        { offset: 1, color: 'rgba(111, 209, 206, 0.1)' },
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [15, 15, 0, 0],
+                },
+            },
+        ],
+    }
+    barChart.setOption(option)
+    // 饼图配置
+    const option2 = {
+        tooltip: {
+            trigger: 'item',
+        },
+        series: [
+            {
+                name: 'Access From',
+                type: 'pie',
+                radius: ['25%', '50%'],
+                avoidLabelOverlap: false,
+                itemStyle: {
+                    borderWidth: 1, // 设置边框的宽度
+                    borderColor: '#fff', // 将边框颜色设置为白色或图表背景颜色
+                },
+                emphasis: {
+                    label: {
+                        show: true,
+                        // fontSize: 40,
+                        fontWeight: 'bold'
+                    }
+                },
+                label: {
+                    normal: {
+                        show: true,
+                        position: 'outside',
+                        // formatter: '{b}: {c} ({d}%)' // b为数据名,c为数据值,d为百分比
+                    }
+                },
+                labelLine: {
+                    normal: {
+                        show: true
+                    }
+                },
+                data: props.pieBarChartData.pieData
+            }
+        ]
+    }
+    pieChart.setOption(option2)
+    resizeChart()
+}
+
+function resizeChart() {
+    barChart.resize()
+    pieChart.resize()
+}
+
+defineExpose({ resizeChart })
+
+// 下拉框相关
+const options = [
+    {
+        value: '花费',
+        label: '花费',
+    },
+    {
+        value: 'Option2',
+        label: 'Option2',
+    },
+    {
+        value: 'Option3',
+        label: 'Option3',
+    }
+]
+const modelValue = ref(options[0].value)
+
+</script>
+
+<style scoped>
+
+</style>

+ 56 - 0
src/components/echartsComponents/ScatterChart.vue

@@ -0,0 +1,56 @@
+<template>
+    <div>
+        <div id="scatter" style="height: 400px;"></div>
+    </div>
+</template>
+
+<script setup>
+import { onBeforeUnmount, onMounted, ref } from "vue"
+import * as echarts from "echarts"
+
+let scatterChart = ref()
+onMounted(() => {
+    scatterChart = echarts.init(document.getElementById('scatter'))
+    setChartData()
+    window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    },0)
+})
+onBeforeUnmount(() => {
+    window.removeEventListener('resize', resizeChart)   // 在组件销毁前移除事件监听,避免内存泄漏
+})
+const props = defineProps({
+    scatterData: {
+        type: Object,
+    }
+})
+
+function setChartData() {
+    const option = {
+        xAxis: {},
+        yAxis: {},
+        series: [
+            {
+                symbolSize: 20,
+                data: props.scatterData,
+                type: 'scatter'
+            }
+        ]
+    }
+    scatterChart.setOption(option)
+}
+
+function resizeChart() {
+    if (scatterChart) {
+        scatterChart.resize()
+    }
+}
+defineExpose({ resizeChart })
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 268 - 0
src/components/echartsComponents/dataTendency.vue

@@ -0,0 +1,268 @@
+<template>
+  <div ref="chartRef" :style="{height:height, width:width}"></div>
+  <p>{{ props.showMetrics }}</p>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import * as echarts from 'echarts'
+import XEUtils from 'xe-utils'
+
+defineOptions({
+  name: 'DataTendencyChart'
+})
+interface Props {
+  dataset: {[key: string]: any}[],
+  showMetrics: ShowMetric[],
+  width: string,
+  height: string
+}
+
+const props = withDefaults(defineProps<Props>(), { width: "100%", height: "500px" })
+const chartRef = ref()
+let chartObj:any
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+const option: {[key: string]: any} = {
+  dataset: {
+    source: props.dataset
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross',
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    show: false
+  },
+  grid: {
+    top: 50, right: 150, bottom: 30, left: 55,
+  },
+  xAxis: {
+    type: 'time'
+  },
+  yAxis: [
+    {
+      type: 'value',
+      name: '',
+      splitLine: {
+        show: true // 设置显示分割线
+      },
+      // axisLabel: {
+      //   formatter: '{value}'
+      // },
+      axisLine: {
+        show: true
+      },
+      show: false
+    },
+    {
+      type: 'value',
+      name: '',
+      position: 'right',
+      splitLine: {
+        show: false
+      },
+      // axisLabel: {
+      //   formatter: '{value} 单位2'
+      // },
+      axisLine: {
+        show: true
+      },
+      show: false
+    },
+    {
+      type: 'value',
+      position: 'right',
+      offset: 80,
+      name: '',
+      splitLine: {
+        show: false
+      },
+      // axisLabel: {
+      //   formatter: '{value} 单位3'
+      // },
+      axisLine: {
+        show: true
+      },
+      show: false
+    }
+  ],
+  series: [
+    {
+      name: '',
+      type: 'bar',
+      encode: {
+        x: 'date',
+        y: ''
+      },
+      // tooltip: {
+      //   valueFormatter: function (value) {
+      //     return value + ' ml';
+      //   }
+      // },
+      barWidth: '20px',
+      yAxisIndex: 0,
+      itemStyle: {
+        // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+        //     { offset: 0, color: 'rgba(111, 209, 206, 0.8)' },
+        //     { offset: 1, color: 'rgba(111, 209, 206, 0.1)' },
+        // ]),
+        color: '',
+        //柱状图圆角
+        borderRadius: [15, 15, 0, 0],
+      }
+    },
+    {
+      name: '',
+      type: 'line',
+      encode: {
+        x: 'date',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 1,
+      // lineStyle: { color: '#fe9a8b' },
+      itemStyle: { color: '#fe9a8b', borderColor: '#fe9a8b' },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#fe9a8bb3' },
+          { offset: 1, color: '#fe9a8b03' },
+        ]),
+      },
+    },
+    {
+      name: '',
+      type: 'line',
+      encode: {
+        x: 'date',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 2,
+      // lineStyle: { color: '#9E87FF' },
+      itemStyle: { color: '#9E87FF', borderColor: '#9E87FF' },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#9E87FFb3' },
+          { offset: 1, color: '#9E87FF03' },
+        ]),
+      },
+      emphasis: {
+        itemStyle: {
+          color: {
+            type: 'radial',
+            x: 0.5,
+            y: 0.5,
+            r: 0.5,
+            colorStops: [
+              { offset: 0, color: '#9E87FF' },
+              { offset: 0.4, color: '#9E87FF' },
+              { offset: 0.5, color: '#fff' },
+              { offset: 0.7, color: '#fff' },
+              { offset: 0.8, color: '#fff' },
+              { offset: 1, color: '#fff' },
+            ],
+          },
+          borderColor: '#9E87FF',
+          borderWidth: 2,
+        },
+      }
+    }
+  ]
+}
+
+onMounted(() => {
+	initLine()
+	addResize()
+})
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+  removeResize()
+})
+watch(
+  props.showMetrics,
+  () => {
+    // const option: any = {
+    //   legend: {
+    //     selected: {},
+    //     yAxis: [
+    //       {name: '', show: false},
+    //       {name: '', show: false},
+    //       {name: '', show: false}
+    //     ],
+    //     series: [
+    //       {name: '', encode: {y: ''}, itemStyle: {color: ''}},
+    //       {name: '', encode: {y: ''}, itemStyle: {color: ''}},
+    //       {name: '', encode: {y: ''}, itemStyle: {color: ''}}
+    //     ]
+    //   } 
+    // }
+    const tmp:{[key: string]: boolean} = {}
+    for (const info of props.showMetrics) { tmp[info.metric] = true }
+    for (const key of XEUtils.keys(option.legend.selected)) {
+      if (XEUtils.has(tmp, key)) {
+        option.legend.selected[key] = true
+      } else {
+        option.legend.selected[key] = false
+      }
+    }
+    
+    for (const [metricInfo, yInfo, sInfo] of XEUtils.zip(props.showMetrics, option.yAxis, option.series)) {
+      if (metricInfo) {
+        yInfo.name = metricInfo.label
+        yInfo.show = true
+
+        sInfo.name = metricInfo.label
+        sInfo.encode.y = metricInfo.metric
+        sInfo.itemStyle.color = metricInfo.color
+        
+      } else {
+        yInfo.show = false
+      }
+    }
+
+    console.log(option)
+    
+    chartObj.setOption(option)
+  }
+)
+
+const initLine = () => {
+	chartObj = echarts.init(chartRef.value)
+	
+  const tmp:{[key: string]: boolean} = {}
+  for (const info of props.showMetrics) { tmp[info.metric] = true }
+  option.legend.selected = tmp
+
+  props.showMetrics.forEach((info, index) => {
+    option.yAxis[index].name = info.label
+    option.yAxis[index].show = true
+
+    option.series[index].name = info.label
+    option.series[index].encode.y = info.metric
+    option.series[index].itemStyle.color = info.color
+  })
+  console.log(option)
+	chartObj.setOption(option)
+}
+
+defineExpose({resizeChart})
+
+</script>
+
+<style scoped>
+</style>

+ 69 - 0
src/components/input-float/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <el-input v-model="data" @input="onInput" @blur="onBlur" class="asj-input-float" style="width: 100%;">
+    <template v-if="prefix.length > 0" #prepend>{{ props.prefix }}</template>
+    <template v-if="suffix.length > 0" #append>{{ props.suffix }}</template>
+  </el-input>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+  },
+  precision: {
+    type: Number,
+    default: 2,
+  },
+  prefix: {
+    type: String,
+    default: '',
+  },
+  suffix: {
+    type: String,
+    default: '',
+  }
+})
+const data = ref(props.modelValue)
+const emits = defineEmits(['update:modelValue', 'blur'])
+
+const onInput = (val: string) => {
+  data.value = val
+    .replace(/[^\d.]/g, '')
+    .replace(/\.{2,}/g, '.')
+    .replace('.', '$#$')
+    .replace(/\./g, '')
+    .replace('$#$', '.')
+    .replace(/^\./g, '')
+  const index = data.value.indexOf('.')
+  if (index > 0) {
+    if (data.value.length - index - 1 > props.precision) {
+      data.value = val.substring(0, index + props.precision + 1)
+    }
+  }
+  emits('update:modelValue', data.value)
+}
+
+const onBlur = () => {
+  if (data.value !== '') {
+    data.value = Number(data.value).toFixed(props.precision)
+    emits('update:modelValue', data.value)
+    emits('blur')
+  }
+}
+
+watch(
+  () => props.modelValue,
+  () => (data.value = props.modelValue)
+)
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-input-group__append) {
+  padding: 0 10px !important;
+}
+::v-deep(.el-input-group__prepend) {
+  padding: 0 10px !important;
+}
+</style>

+ 30 - 0
src/components/range-float/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div>
+    <InputFloat v-model="ranges[0]" :prefix="prefix" :suffix="suffix" style="width: 150px">
+    </InputFloat>
+    <span> ~ </span>
+    <InputFloat v-model="ranges[1]" :prefix="prefix" :suffix="suffix" style="width: 150px">
+    </InputFloat>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import InputFloat from '/@/components/input-float/index.vue'
+
+interface Props {
+  ranges: string[],
+  prefix: string,
+  suffix: string
+}
+const props = withDefaults(defineProps<Props>(), {
+  prefix: '',
+  suffix: ''
+})
+if (props.ranges.length === 0) {
+  props.ranges.push('')
+  props.ranges.push('')
+}
+</script>
+
+<style scoped></style>

+ 1 - 1
src/components/searchInput/index.vue

@@ -1,6 +1,6 @@
 <template>
     <div>
-        <div class="mt-4" style="margin-bottom: 5px">
+        <div class="mt-1" style="margin-bottom: 5px">
             <el-input
                     v-model="input3"
                     placeholder="快速查询"

+ 53 - 0
src/components/select-button/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <el-popover :placement="placement" trigger="hover">
+    <template #reference>
+      <el-button :type="btnType" :color="btnColor" :icon="btnIcon" style="color: #fff">{{ btnTitle }}</el-button>
+    </template>
+    <div class="popver-content">
+      <span class="popver-content-item" v-for="info of options" @click="handleClick(info.value)">{{ info.label }}</span>
+    </div>
+  </el-popover>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+interface Props {
+  btnTitle: string,
+  btnType: string,
+  btnColor: string,
+  btnIcon: any,
+  placement: string,
+  // modelValue: string | number,
+  options: { label: string; value: string | number }[]
+}
+const props = withDefaults(defineProps<Props>(), {
+  placement: "bottom-start",
+  btnType: 'primary',
+  btnColor: '#3359b5',
+  btnIcon: ''
+})
+const emits = defineEmits(['click'])
+
+const handleClick = (value: string | number) => {
+  // if (props.modelValue === value) return
+  // emits('update:modelValue', value)
+  emits('click', value)
+}
+</script>
+
+<style lang="scss" scoped>
+.popver-content {
+  display: flex;
+  flex-direction: column;
+  .popver-content-item {
+    padding: 5px 0;
+
+    &:hover {
+      background-color: #f4f7fd;
+      color: blue;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 3 - 17
src/components/shopSelector/index.vue

@@ -22,31 +22,17 @@ import { useShopInfo } from '/@/stores/shopInfo'
 defineOptions({
   name: 'ShopSelector'
 })
-
-const selectedRow: Ref<Profile> = ref({
-  id: 0,
-  profile_id: "",
-	account_name: "",
-	time_zone: "",
-	advertiser_id: "",
-	country_code: "",
-	currency_code: "",
-	marketplace_str_id: ""
-})
-const allShops: Ref<Profile[]> = ref([])
 const shopInfo = useShopInfo()
+const selectedRow: Ref<Profile> = ref(shopInfo.profile)
+const allShops: Ref<Profile[]> = ref([])
 
 onBeforeMount(async () => {
-  const resp = await GetList({ pageSize: 1000, query: '{-refresh_token}' })
+  const resp = await GetList({ limit: 999, query: '{-refresh_token}' })
   allShops.value = resp.data
-  selectedRow.value = allShops.value[0]
-  shopInfo.updateShopInfo(selectedRow.value)
 })
-
 const flagIcon = computed(()=> {
   return "fi fi-" + selectedRow.value.country_code.toLowerCase()
 })
-
 const changedVal = () => {
   shopInfo.updateShopInfo(selectedRow.value)
 }

+ 1 - 0
src/components/shopSelector/types.ts

@@ -6,5 +6,6 @@ export interface Profile {
 	advertiser_id: string,
 	country_code: string,
 	currency_code: string,
+	currency_symbol: string,
 	marketplace_str_id: string
 }

+ 78 - 0
src/components/tags-input/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="tags-input-wrapper">
+    <div class="tags-box">
+      <el-tag v-for="tag in tags" :key="tag" color="#F5F5F5" :closable="!disabled" @close="handleClose(tag)">
+        <span style="color: blue">{{ tag }}</span>
+      </el-tag>
+      <el-popconfirm title="确定清空吗?" @confirm="handleClear" :hide-after="10" placement="top">
+        <template #reference>
+          <el-button size="small" :disabled="tags.length === 0">{{ tags.length }}/{{ maxLength }}</el-button>
+        </template>
+      </el-popconfirm>
+    </div>
+    <el-input
+      type="textarea"
+      v-model="text"
+      @blur="onBlur"
+      :disabled="tags.length >= maxLength"
+      :placeholder="placeholder"
+      :autosize="{ minRows: 3, maxRows: 10 }"></el-input>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  data: string[]
+  placeholder: string
+  maxLength: number,
+  disabled?: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+  placeholder: '请输入',
+  maxLength: 100,
+})
+const tags = ref(props.data)
+const emits = defineEmits(['update:modelValue'])
+const text = ref('')
+const tagsMap = computed(() => {
+  const map = {}
+  for (const tag of tags.value) {
+    map[tag] = true
+  }
+  return map
+})
+
+const onBlur = () => {
+  const newTags = XEUtils.uniq(text.value.split(/[\r\n]/))
+  for (const tag of newTags) {
+    if (tag !== '' && !tagsMap.value[tag] && tags.value.length < props.maxLength) {
+      tags.value.push(tag)
+    }
+  }
+  text.value = ''
+}
+const handleClose = (tag: string) => {
+  tags.value.splice(tags.value.indexOf(tag), 1)
+}
+const handleClear = () => {
+  tags.value.splice(0, tags.value.length)
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-box {
+  width: 100%;
+  display: flex;
+  overflow: auto;
+  flex-wrap: wrap;
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  gap: 5px;
+  background: #fff;
+  padding: 3px;
+  margin-bottom: 1px;
+}
+</style>

+ 3 - 3
src/layout/logo/index.vue

@@ -1,10 +1,10 @@
 <template>
 	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
-		<img :src="logoMini" class="layout-logo-medium-img" />
-		<span style="font-size: x-large">{{ themeConfig.globalTitle }}</span>
+		<el-image style="width: 100px; height: 100px" src="src/assets/ansjer_image.png" fit="contain" />
+		<!-- <span style="font-size: x-large">{{ themeConfig.globalTitle }}</span> -->
 	</div>
 	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
-		<img :src="logoMini" class="layout-logo-size-img" />
+		<img src="src/assets/ansjer_image.png" class="layout-logo-size-img" />
 	</div>
 </template>
 

+ 8 - 6
src/layout/navBars/breadcrumb/user.vue

@@ -2,7 +2,8 @@
 	<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
 		<DynamicTime></DynamicTime>
 		<ShopSelector class="layout-navbars-breadcrumb-user-icon"></ShopSelector>
-		<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
+		<!-- 字体切换移除 -->
+		<!-- <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
 			<div class="layout-navbars-breadcrumb-user-icon">
 				<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
 			</div>
@@ -13,8 +14,9 @@
 					<el-dropdown-item command="small" :disabled="state.disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
 				</el-dropdown-menu>
 			</template>
-		</el-dropdown>
-		<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
+		</el-dropdown> -->
+		<!-- 语言切换移除 -->
+		<!-- <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
 			<div class="layout-navbars-breadcrumb-user-icon">
 				<i
 					class="iconfont"
@@ -29,12 +31,12 @@
 					<el-dropdown-item command="zh-tw" :disabled="state.disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
 				</el-dropdown-menu>
 			</template>
-		</el-dropdown>
-		<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
+		</el-dropdown> -->
+		<!-- <div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
 			<el-icon :title="$t('message.user.title2')">
 				<ele-Search />
 			</el-icon>
-		</div>
+		</div> -->
 		<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
 			<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
 		</div>

+ 2 - 0
src/main.ts

@@ -37,10 +37,12 @@ import dayjs from 'dayjs'
 import UTC from 'dayjs/plugin/utc'
 import Timezon from 'dayjs/plugin/timezone'
 import IsSameOrBefore from 'dayjs/plugin/isSameOrBefore'
+import 'dayjs/locale/zh-cn'
 
 dayjs.extend(UTC)
 dayjs.extend(Timezon)
 dayjs.extend(IsSameOrBefore)
+dayjs.locale('zh-cn')
 
 let forIconfont = analyzingIconForIconfont(iconfont); //解析class
 iconList.addIcon(forIconfont.list); // 添加iconfont dvadmin3的icon

+ 12 - 0
src/router/backEnd.ts

@@ -2,6 +2,8 @@ import { RouteRecordRaw } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import pinia from '/@/stores/index';
 import { useUserInfo } from '/@/stores/userInfo';
+import { useShopInfo } from '/@/stores/shopInfo';
+import { usePublicData } from '/@/stores/publicData'
 import { useRequestOldRoutes } from '/@/stores/requestOldRoutes';
 import { Session } from '/@/utils/storage';
 import { NextLoading } from '/@/utils/loading';
@@ -36,6 +38,8 @@ const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...lay
  * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  */
 export async function initBackEndControlRoutes() {
+	console.log("--------initBackEndControlRoutes------------")
+
 	// 界面 loading 动画开始执行
 	if (window.nextLoading === undefined) NextLoading.start();
 	// 无 token 停止执行下一步
@@ -43,6 +47,14 @@ export async function initBackEndControlRoutes() {
 	// 触发初始化用户信息 pinia
 	// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
 	await useUserInfo().setUserInfos();
+	await useShopInfo().initShopInfo();
+	const publicData = usePublicData()
+	publicData.initData();
+	publicData.$subscribe((mutation, state) => {
+		// console.log(mutation)
+		// console.log(state.dateRange)
+		publicData.setDateRange(state.dateRange)
+	}, { detached: true })
 	// 获取路由菜单数据
 	const res = await getBackEndControlRoutes();
 

+ 89 - 89
src/router/index.ts

@@ -1,15 +1,15 @@
-import {createRouter, createWebHashHistory} from 'vue-router';
-import NProgress from 'nprogress';
-import 'nprogress/nprogress.css';
-import pinia from '/@/stores/index';
-import {storeToRefs} from 'pinia';
-import {useKeepALiveNames} from '/@/stores/keepAliveNames';
-import {useRoutesList} from '/@/stores/routesList';
-import {useThemeConfig} from '/@/stores/themeConfig';
-import {Session} from '/@/utils/storage';
-import {staticRoutes} from '/@/router/route';
-import {initFrontEndControlRoutes} from '/@/router/frontEnd';
-import {initBackEndControlRoutes} from '/@/router/backEnd';
+import {createRouter, createWebHashHistory} from 'vue-router'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import pinia from '/@/stores/index'
+import {storeToRefs} from 'pinia'
+import {useKeepALiveNames} from '/@/stores/keepAliveNames'
+import {useRoutesList} from '/@/stores/routesList'
+import {useThemeConfig} from '/@/stores/themeConfig'
+import {Session} from '/@/utils/storage'
+import {staticRoutes} from '/@/router/route'
+import {initFrontEndControlRoutes} from '/@/router/frontEnd'
+import {initBackEndControlRoutes} from '/@/router/backEnd'
 
 /**
  * 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。
@@ -21,9 +21,9 @@ import {initBackEndControlRoutes} from '/@/router/backEnd';
  */
 
 // 读取 `/src/stores/themeConfig.ts` 是否开启后端控制路由配置
-const storesThemeConfig = useThemeConfig(pinia);
-const {themeConfig} = storeToRefs(storesThemeConfig);
-const {isRequestRoutes} = themeConfig.value;
+const storesThemeConfig = useThemeConfig(pinia)
+const {themeConfig} = storeToRefs(storesThemeConfig)
+const {isRequestRoutes} = themeConfig.value
 
 /**
  * 创建一个可以被 Vue 应用程序使用的路由实例
@@ -31,9 +31,9 @@ const {isRequestRoutes} = themeConfig.value;
  * @link 参考:https://next.router.vuejs.org/zh/api/#createrouter
  */
 export const router = createRouter({
-    history: createWebHashHistory(),
-    routes: staticRoutes,
-});
+  history: createWebHashHistory(),
+  routes: staticRoutes,
+})
 
 /**
  * 路由多级嵌套数组处理成一维数组
@@ -41,13 +41,13 @@ export const router = createRouter({
  * @returns 返回处理后的一维路由菜单数组
  */
 export function formatFlatteningRoutes(arr: any) {
-    if (arr.length <= 0) return false;
-    for (let i = 0; i < arr.length; i++) {
-        if (arr[i].children) {
-            arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
-        }
+  if (arr.length <= 0) return false
+  for (let i = 0; i < arr.length; i++) {
+    if (arr[i].children) {
+      arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1))
     }
-    return arr;
+  }
+  return arr
 }
 
 /**
@@ -58,81 +58,81 @@ export function formatFlatteningRoutes(arr: any) {
  * @returns 返回将一维数组重新处理成 `定义动态路由(dynamicRoutes)` 的格式
  */
 export function formatTwoStageRoutes(arr: any) {
-    if (arr.length <= 0) return false;
-    const newArr: any = [];
-    const cacheList: Array<string> = [];
-    arr.forEach((v: any) => {
-        if (v.path === '/') {
-            newArr.push({
-                component: v.component,
-                name: v.name,
-                path: v.path,
-                redirect: v.redirect,
-                meta: v.meta,
-                children: []
-            });
-        } else {
-            // 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
-            // 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
-            if (v.path.indexOf('/:') > -1) {
-                v.meta['isDynamic'] = true;
-                v.meta['isDynamicPath'] = v.path;
-            }
-            newArr[0].children.push({...v});
-            // 存 name 值,keep-alive 中 include 使用,实现路由的缓存
-            // 路径:/@/layout/routerView/parent.vue
-            if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive && v.component_name != "") {
-                cacheList.push(v.name);
-                const stores = useKeepALiveNames(pinia);
-                stores.setCacheKeepAlive(cacheList);
-            }
-        }
-    });
-    return newArr;
+  if (arr.length <= 0) return false
+  const newArr: any = []
+  const cacheList: Array<string> = []
+  arr.forEach((v: any) => {
+    if (v.path === '/') {
+      newArr.push({
+        component: v.component,
+        name: v.name,
+        path: v.path,
+        redirect: v.redirect,
+        meta: v.meta,
+        children: []
+      })
+    } else {
+      // 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
+      // 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+      if (v.path.indexOf('/:') > -1) {
+        v.meta['isDynamic'] = true
+        v.meta['isDynamicPath'] = v.path
+      }
+      newArr[0].children.push({...v})
+      // 存 name 值,keep-alive 中 include 使用,实现路由的缓存
+      // 路径:/@/layout/routerView/parent.vue
+      if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive && v.component_name != '') {
+        cacheList.push(v.name)
+        const stores = useKeepALiveNames(pinia)
+        stores.setCacheKeepAlive(cacheList)
+      }
+    }
+  })
+  return newArr
 }
 
 // 路由加载前
 router.beforeEach(async (to, from, next) => {
-    NProgress.configure({showSpinner: false});
-    if (to.meta.title) NProgress.start();
-    const token = Session.get('token');
-    if (to.path === '/login' && !token) {
-        next();
-        NProgress.done();
+  NProgress.configure({showSpinner: false})
+  if (to.meta.title) NProgress.start()
+  const token = Session.get('token')
+  if (to.path === '/login' && !token) {
+    next()
+    NProgress.done()
+  } else {
+    if (!token) {
+      next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`)
+      Session.clear()
+      NProgress.done()
+    } else if (token && to.path === '/login') {
+      next('/home')
+      NProgress.done()
     } else {
-        if (!token) {
-            next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
-            Session.clear();
-            NProgress.done();
-        } else if (token && to.path === '/login') {
-            next('/home');
-            NProgress.done();
+      const storesRoutesList = useRoutesList(pinia)
+      const {routesList} = storeToRefs(storesRoutesList)
+      if (routesList.value.length === 0) {
+        if (isRequestRoutes) {
+          // 后端控制路由:路由数据初始化,防止刷新时丢失
+          await initBackEndControlRoutes()
+          // 动态添加路由:防止非首页刷新时跳转回首页的问题
+          // 确保 addRoute() 时动态添加的路由已经被完全加载上去
+          next({...to, replace: true})
         } else {
-            const storesRoutesList = useRoutesList(pinia);
-            const {routesList} = storeToRefs(storesRoutesList);
-            if (routesList.value.length === 0) {
-                if (isRequestRoutes) {
-                    // 后端控制路由:路由数据初始化,防止刷新时丢失
-                    await initBackEndControlRoutes();
-                    // 动态添加路由:防止非首页刷新时跳转回首页的问题
-                    // 确保 addRoute() 时动态添加的路由已经被完全加载上去
-                    next({...to, replace: true});
-                } else {
-                    // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
-                    await initFrontEndControlRoutes();
-                    next({...to, replace: true});
-                }
-            } else {
-                next();
-            }
+          // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
+          await initFrontEndControlRoutes()
+          next({...to, replace: true})
         }
+      } else {
+        next()
+      }
     }
-});
+  }
+})
 
 // 路由加载后
 router.afterEach(() => {
-    NProgress.done();
-});
+  NProgress.done()
+})
 
 // 导出路由
-export default router;
+export default router

+ 8 - 8
src/router/route.ts

@@ -46,14 +46,14 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			icon: 'iconfont icon-gerenzhongxin',
 		},
 	},
-	{
-		path: '/createcampaigns',
-		name: 'createcampaigns',
-		component: () => import('/@/views/adManage/sb/campaigns/CreateCampaigns/index.vue'),
-		meta: {
-			title: '新建广告活动',
-		},
-	},
+	// {
+	// 	path: '/createcampaigns',
+	// 	name: 'createcampaigns',
+	// 	component: () => import('/@/views/adManage/sb/campaigns/CreateCampaigns/index.vue'),
+	// 	meta: {
+	// 		title: '新建广告活动',
+	// 	},
+	// },
 ];
 
 /**

+ 7 - 0
src/settings.ts

@@ -52,6 +52,13 @@ export default {
 							}
 						},
 					},
+					toolbar: {
+						buttons: {
+							compact: {
+								show: false
+							}
+						}
+					},
 					/* search: {
 						layout: 'multi-line',
 						collapse: true,

+ 26 - 0
src/stores/publicData.ts

@@ -0,0 +1,26 @@
+import { defineStore } from 'pinia'
+import { ref, Ref } from 'vue'
+import { Session } from '/@/utils/storage'
+import { recentDaysRange } from '/@/views/adManage/utils/tools'
+import { useShopInfo } from './shopInfo'
+
+export const usePublicData = defineStore('publicData', () => {
+  const dateRange: Ref<string[]> = ref([])
+
+  function setDateRange(val: string[]) {
+    dateRange.value = val
+    Session.set('dateRange', val)
+  }
+
+  function initData() {
+    const dateRangeVal = Session.get('dateRange')
+    if (dateRangeVal) {
+      dateRange.value = dateRangeVal
+    } else {
+      setDateRange(recentDaysRange(useShopInfo().profile.time_zone, 7))
+    }
+  }
+
+  return { dateRange, initData, setDateRange }
+})
+

+ 13 - 9
src/stores/shopInfo.ts

@@ -14,6 +14,7 @@ export const useShopInfo = defineStore('shopInfo', () => {
     advertiser_id: "",
     country_code: "",
     currency_code: "",
+    currency_symbol: "",
     marketplace_str_id: ""
   })
 
@@ -25,25 +26,28 @@ export const useShopInfo = defineStore('shopInfo', () => {
     profile.value.advertiser_id = obj.advertiser_id
     profile.value.country_code = obj.country_code
     profile.value.currency_code = obj.currency_code
+    profile.value.currency_symbol = obj.currency_symbol
     profile.value.marketplace_str_id = obj.marketplace_str_id
     Session.set('shopInfo', profile.value);
   }
 
-  async function reqShopInfo(id: number) {
+  async function reqShopInfo() {
     return request({
-      url: '/api/adManage/profiles/' + id + '/',
-      method: 'GET'
+      url: '/api/ad_manage/profiles/',
+      method: 'GET',
+      params: { limit: 1 }
     })
   }
 
-  async function setShopInfo(id: number) {
-    if (Session.get('shopInfo')) {
-      profile.value = Session.get('shopInfo');
+  async function initShopInfo() {
+    const data = Session.get('shopInfo')
+    if (data?.profile_id) {
+      profile.value = data
     } else {
-      let resp: any = await reqShopInfo(id);
-      updateShopInfo(resp.data)
+      const resp: any = await reqShopInfo();
+      updateShopInfo(resp.data[0])
     }
   }
 
-  return { profile, updateShopInfo }
+  return { profile, updateShopInfo, initShopInfo }
 })

+ 5 - 5
src/stores/themeConfig.ts

@@ -64,7 +64,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 			// 是否开启菜单手风琴效果
 			isUniqueOpened: true,
 			// 是否开启固定 Header
-			isFixedHeader: false,
+			isFixedHeader: true,
 			// 初始化变量,用于更新菜单 el-scrollbar 的高度,请勿删除
 			isFixedHeaderChange: false,
 			// 是否开启经典布局分割菜单(仅经典布局生效)
@@ -111,7 +111,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 			 */
 			// Tagsview 风格:可选值"<tags-style-one|tags-style-four|tags-style-five>",默认 tags-style-five
 			// 定义的值与 `/src/layout/navBars/tagsView/tagsView.vue` 中的 class 同名
-			tagsStyle: 'tags-style-five',
+			tagsStyle: 'tags-style-one',
 			// 主页面切换动画:可选值"<slide-right|slide-left|opacitys>",默认 slide-right
 			animation: 'slide-right',
 			// 分栏高亮风格:可选值"<columns-round|columns-card>",默认 columns-round
@@ -137,11 +137,11 @@ export const useThemeConfig = defineStore('themeConfig', {
 			 * 全局网站标题 / 副标题
 			 */
 			// 网站主标题(菜单导航、浏览器当前网页标题)
-			globalTitle: 'DVAdmin',
+			globalTitle: 'Ansjer',
 			// 网站副标题(登录页顶部文字)
-			globalViceTitle: 'DVAdmin',
+			globalViceTitle: 'Ansjer',
 			// 网站副标题(登录页顶部文字)
-			globalViceTitleMsg: '企业级快速开发平台',
+			globalViceTitleMsg: '广告管理系统',
 			// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
 			globalI18n: 'zh-cn',
 			// 默认全局组件大小,可选值"<large|'default'|small>",默认 'large'

+ 521 - 243
src/theme/app.scss

@@ -1,356 +1,634 @@
 /* 初始化样式
 ------------------------------- */
 * {
-	margin: 0;
-	padding: 0;
-	box-sizing: border-box;
-	outline: none !important;
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  outline: none !important;
 }
 
 :root {
-	--next-color-white: #ffffff;
-	--next-bg-main-color: #f8f8f8;
-	--next-bg-color: #f5f5ff;
-	--next-border-color-light: #f1f2f3;
-	--next-color-primary-lighter: #ecf5ff;
-	--next-color-success-lighter: #f0f9eb;
-	--next-color-warning-lighter: #fdf6ec;
-	--next-color-danger-lighter: #fef0f0;
-	--next-color-dark-hover: #0000001a;
-	--next-color-menu-hover: rgba(0, 0, 0, 0.2);
-	--next-color-user-hover: rgba(0, 0, 0, 0.04);
-	--next-color-seting-main: #e9eef3;
-	--next-color-seting-aside: #d3dce6;
-	--next-color-seting-header: #b3c0d1;
+  --next-color-white: #ffffff;
+  --next-bg-main-color: #f8f8f8;
+  --next-bg-color: #f5f5ff;
+  --next-border-color-light: #f1f2f3;
+  --next-color-primary-lighter: #ecf5ff;
+  --next-color-success-lighter: #f0f9eb;
+  --next-color-warning-lighter: #fdf6ec;
+  --next-color-danger-lighter: #fef0f0;
+  --next-color-dark-hover: #0000001a;
+  --next-color-menu-hover: rgba(0, 0, 0, 0.2);
+  --next-color-user-hover: rgba(0, 0, 0, 0.04);
+  --next-color-seting-main: #e9eef3;
+  --next-color-seting-aside: #d3dce6;
+  --next-color-seting-header: #b3c0d1;
 }
 
 html,
 body,
 #app {
-	margin: 0;
-	padding: 0;
-	width: 100%;
-	height: 100%;
-	font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
-	font-weight: 400;
-	-webkit-font-smoothing: antialiased;
-	-webkit-tap-highlight-color: transparent;
-	background-color: var(--next-bg-main-color);
-	font-size: 14px;
-	overflow: hidden;
-	position: relative;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
+  font-weight: 400;
+  -webkit-font-smoothing: antialiased;
+  -webkit-tap-highlight-color: transparent;
+  background-color: var(--next-bg-main-color);
+  font-size: 14px;
+  overflow: hidden;
+  position: relative;
 }
 
 /* 主布局样式
 ------------------------------- */
 .layout-container {
-	width: 100%;
-	height: 100%;
-	.layout-pd {
-		padding: 15px !important;
-	}
-	.layout-flex {
-		display: flex;
-		flex-direction: column;
-		flex: 1;
-	}
-	.layout-aside {
-		background: var(--next-bg-menuBar);
-		box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
-		height: inherit;
-		position: relative;
-		z-index: 1;
-		display: flex;
-		flex-direction: column;
-		overflow-x: hidden !important;
-		.el-scrollbar__view {
-			overflow: hidden;
-		}
-	}
-	.layout-header {
-		padding: 0 !important;
-		height: auto !important;
-	}
-	.layout-main {
-		padding: 0 !important;
-		overflow: hidden;
-		width: 100%;
-		background-color: var(--next-bg-main-color);
-		display: flex;
-		flex-direction: column;
-		// 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
-		.layout-main-scroll {
-			@extend .layout-flex;
-			.layout-parent {
-				@extend .layout-flex;
-				position: relative;
-			}
-		}
-	}
-	// 用于界面高度自适应
-	.layout-padding {
-		@extend .layout-pd;
-		position: absolute;
-		left: 0;
-		top: 0;
-		height: 100%;
-		overflow: hidden;
-		@extend .layout-flex;
-		&-auto {
-			height: inherit;
-			@extend .layout-flex;
-		}
-		&-view {
-			background: var(--el-color-white);
-			width: 100%;
-			height: 100%;
-			border-radius: 4px;
-			border: 1px solid var(--el-border-color-light, #ebeef5);
-			overflow: hidden;
-		}
-	}
-	// 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
-	.layout-padding-unset {
-		padding: 0 !important;
-		&-view {
-			border-radius: 0 !important;
-			border: none !important;
-		}
-	}
-	// 用于设置 iframe loading 时的高度(loading 垂直居中显示)
-	.layout-iframe {
-		.el-loading-parent--relative {
-			height: 100%;
-		}
-	}
-	.el-scrollbar {
-		width: 100%;
-	}
-	.layout-el-aside-br-color {
-		border-right: 1px solid var(--el-border-color-light, #ebeef5);
-	}
-	// pc端左侧导航样式
-	.layout-aside-pc-220 {
-		width: 220px !important;
-		transition: width 0.3s ease;
-	}
-	.layout-aside-pc-64 {
-		width: 64px !important;
-		transition: width 0.3s ease;
-	}
-	.layout-aside-pc-1 {
-		width: 1px !important;
-		transition: width 0.3s ease;
-	}
-	// 手机端左侧导航样式
-	.layout-aside-mobile {
-		position: fixed;
-		top: 0;
-		left: -220px;
-		width: 220px;
-		z-index: 9999999;
-	}
-	.layout-aside-mobile-close {
-		left: -220px;
-		transition: all 0.3s cubic-bezier(0.39, 0.58, 0.57, 1);
-	}
-	.layout-aside-mobile-open {
-		left: 0;
-		transition: all 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
-	}
-	.layout-aside-mobile-mode {
-		position: fixed;
-		top: 0;
-		right: 0;
-		bottom: 0;
-		left: 0;
-		height: 100%;
-		background-color: rgba(0, 0, 0, 0.5);
-		z-index: 9999998;
-		animation: error-img 0.3s;
-	}
-	.layout-mian-height-50 {
-		height: calc(100vh - 50px);
-	}
-	.layout-columns-warp {
-		flex: 1;
-		display: flex;
-		overflow: hidden;
-	}
-	.layout-hide {
-		display: none;
-	}
+  width: 100%;
+  height: 100%;
+
+  .layout-pd {
+    padding: 15px !important;
+  }
+
+  .layout-flex {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+  }
+
+  .layout-aside {
+    background: var(--next-bg-menuBar);
+    box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
+    height: inherit;
+    position: relative;
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    overflow-x: hidden !important;
+
+    .el-scrollbar__view {
+      overflow: hidden;
+    }
+  }
+
+  .layout-header {
+    padding: 0 !important;
+    height: auto !important;
+  }
+
+  .layout-main {
+    padding: 0 !important;
+    overflow: hidden;
+    width: 100%;
+    background-color: var(--next-bg-main-color);
+    display: flex;
+    flex-direction: column;
+    // 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
+    .layout-main-scroll {
+      @extend .layout-flex;
+
+      .layout-parent {
+        @extend .layout-flex;
+        position: relative;
+      }
+    }
+  }
+
+  // 用于界面高度自适应
+  .layout-padding {
+    @extend .layout-pd;
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    overflow: hidden;
+    @extend .layout-flex;
+
+    &-auto {
+      height: inherit;
+      @extend .layout-flex;
+    }
+
+    &-view {
+      background: var(--el-color-white);
+      width: 100%;
+      height: 100%;
+      border-radius: 4px;
+      border: 1px solid var(--el-border-color-light, #ebeef5);
+      overflow: hidden;
+    }
+  }
+
+  // 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
+  .layout-padding-unset {
+    padding: 0 !important;
+
+    &-view {
+      border-radius: 0 !important;
+      border: none !important;
+    }
+  }
+
+  // 用于设置 iframe loading 时的高度(loading 垂直居中显示)
+  .layout-iframe {
+    .el-loading-parent--relative {
+      height: 100%;
+    }
+  }
+
+  .el-scrollbar {
+    width: 100%;
+  }
+
+  .layout-el-aside-br-color {
+    border-right: 1px solid var(--el-border-color-light, #ebeef5);
+  }
+
+  // pc端左侧导航样式
+  .layout-aside-pc-220 {
+    width: 220px !important;
+    transition: width 0.3s ease;
+  }
+
+  .layout-aside-pc-64 {
+    width: 64px !important;
+    transition: width 0.3s ease;
+  }
+
+  .layout-aside-pc-1 {
+    width: 1px !important;
+    transition: width 0.3s ease;
+  }
+
+  // 手机端左侧导航样式
+  .layout-aside-mobile {
+    position: fixed;
+    top: 0;
+    left: -220px;
+    width: 220px;
+    z-index: 9999999;
+  }
+
+  .layout-aside-mobile-close {
+    left: -220px;
+    transition: all 0.3s cubic-bezier(0.39, 0.58, 0.57, 1);
+  }
+
+  .layout-aside-mobile-open {
+    left: 0;
+    transition: all 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
+  }
+
+  .layout-aside-mobile-mode {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 9999998;
+    animation: error-img 0.3s;
+  }
+
+  .layout-mian-height-50 {
+    height: calc(100vh - 50px);
+  }
+
+  .layout-columns-warp {
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+  }
+
+  .layout-hide {
+    display: none;
+  }
 }
 
 /* element plus 全局样式
 ------------------------------- */
 .layout-breadcrumb-seting {
-	.el-divider {
-		background-color: rgb(230, 230, 230);
-	}
+  .el-divider {
+    background-color: rgb(230, 230, 230);
+  }
 }
 
 /* nprogress 进度条跟随主题颜色
 ------------------------------- */
 #nprogress {
-	.bar {
-		background: var(--el-color-primary) !important;
-		z-index: 9999999 !important;
-	}
+  .bar {
+    background: var(--el-color-primary) !important;
+    z-index: 9999999 !important;
+  }
 }
 
 /* flex 弹性布局
 ------------------------------- */
 .flex {
-	display: flex;
+  display: flex;
 }
+
 .flex-auto {
-	flex: 1;
-	overflow: hidden;
+  flex: 1;
+  overflow: hidden;
 }
+
 .flex-center {
-	@extend .flex;
-	flex-direction: column;
-	width: 100%;
-	overflow: hidden;
+  @extend .flex;
+  flex-direction: column;
+  width: 100%;
+  overflow: hidden;
 }
+
 .flex-margin {
-	margin: auto;
+  margin: auto;
 }
+
 .flex-warp {
-	display: flex;
-	flex-wrap: wrap;
-	align-content: flex-start;
-	margin: 0 -5px;
-	.flex-warp-item {
-		padding: 5px;
-		.flex-warp-item-box {
-			width: 100%;
-			height: 100%;
-		}
-	}
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  margin: 0 -5px;
+
+  .flex-warp-item {
+    padding: 5px;
+
+    .flex-warp-item-box {
+      width: 100%;
+      height: 100%;
+    }
+  }
 }
 
 /* cursor 鼠标形状
 ------------------------------- */
 // 默认
 .cursor-default {
-	cursor: default !important;
+  cursor: default !important;
 }
+
 // 帮助
 .cursor-help {
-	cursor: help !important;
+  cursor: help !important;
 }
+
 // 手指
 .cursor-pointer {
-	cursor: pointer !important;
+  cursor: pointer !important;
 }
+
 // 移动
 .cursor-move {
-	cursor: move !important;
+  cursor: move !important;
 }
 
 /* 宽高 100%
 ------------------------------- */
 .w100 {
-	width: 100% !important;
+  width: 100% !important;
 }
+
 .h100 {
-	height: 100% !important;
+  height: 100% !important;
 }
+
 .vh100 {
-	height: 100vh !important;
+  height: 100vh !important;
 }
+
 .max100vh {
-	max-height: 100vh !important;
+  max-height: 100vh !important;
 }
+
 .min100vh {
-	min-height: 100vh !important;
+  min-height: 100vh !important;
 }
 
 /* 颜色值
 ------------------------------- */
 .color-primary {
-	color: var(--el-color-primary);
+  color: var(--el-color-primary);
 }
+
 .color-success {
-	color: var(--el-color-success);
+  color: var(--el-color-success);
 }
+
 .color-warning {
-	color: var(--el-color-warning);
+  color: var(--el-color-warning);
 }
+
 .color-danger {
-	color: var(--el-color-danger);
+  color: var(--el-color-danger);
 }
+
 .color-info {
-	color: var(--el-color-info);
+  color: var(--el-color-info);
 }
 
 /* 字体大小全局样式
 ------------------------------- */
 @for $i from 10 through 32 {
-	.font#{$i} {
-		font-size: #{$i}px !important;
-	}
+  .font#{$i} {
+    font-size: #{$i}px !important;
+  }
 }
 
 /* 外边距、内边距全局样式
 ------------------------------- */
 @for $i from 1 through 35 {
-	.mt#{$i} {
-		margin-top: #{$i}px !important;
-	}
-	.mr#{$i} {
-		margin-right: #{$i}px !important;
-	}
-	.mb#{$i} {
-		margin-bottom: #{$i}px !important;
-	}
-	.ml#{$i} {
-		margin-left: #{$i}px !important;
-	}
-	.pt#{$i} {
-		padding-top: #{$i}px !important;
-	}
-	.pr#{$i} {
-		padding-right: #{$i}px !important;
-	}
-	.pb#{$i} {
-		padding-bottom: #{$i}px !important;
-	}
-	.pl#{$i} {
-		padding-left: #{$i}px !important;
-	}
+  .mt#{$i} {
+    margin-top: #{$i}px !important;
+  }
+  .mr#{$i} {
+    margin-right: #{$i}px !important;
+  }
+  .mb#{$i} {
+    margin-bottom: #{$i}px !important;
+  }
+  .ml#{$i} {
+    margin-left: #{$i}px !important;
+  }
+  .pt#{$i} {
+    padding-top: #{$i}px !important;
+  }
+  .pr#{$i} {
+    padding-right: #{$i}px !important;
+  }
+  .pb#{$i} {
+    padding-bottom: #{$i}px !important;
+  }
+  .pl#{$i} {
+    padding-left: #{$i}px !important;
+  }
 }
 
 // 自定义全局样式
 .asj-container {
-  padding: 3px 10px 3px 5px;
+  padding: 0px 10px 3px 5px;
+  background-color: #fafafa;
 }
 
 .fs-page-custom {
-	position: initial !important;
+  position: initial !important;
+
+  .fs-search-col > * {
+    margin-left: 0 !important;
+  }
 }
 
-.asj-header {
+.asj-tabs {
+  background-color: #fff;
+  position: sticky;
+  top: 48px;
+  z-index: 9;
+  box-shadow: 0px 0px 12px rgba(51, 89, 181, 0.16);
+
+  padding: 0 12px;
+  display: flex;
+  height: 40px;
+  gap: 24px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.asj-tab {
+  display: flex;
+  align-items: center;
+  padding: 0 4px;
+  height: 40px;
+  font-weight: 700;
+  line-height: 40px;
+  cursor: pointer;
+  border-bottom: 2px solid #fff;
+}
+
+.asj-tab.active {
+  color: #409eff;
+  border-color: #409eff
+}
+
+// 详细页里面的样式
+.asj-detail-header {
   position: sticky;
   background-color: #fff;
-  box-shadow: 0px 0px 12px rgba(51,89,181,0.16);
+  box-shadow: 0px 0px 12px rgba(51, 89, 181, 0.16);
   z-index: 10;
-  top: 1px;
-  height: 80px;
-  margin-bottom: 3px;
+  top: 0;
+  height: 95px;
+  margin-bottom: -1px;
   display: flex;
   flex-direction: column;
 }
-.asj-info {
+
+.asj-detail-info {
   margin: 5px;
   display: flex;
   flex-direction: row;
   gap: 30px;
+  .label {
+    color: #999;
+  }
+  .value {
+    color: #333;
+  }
+}
+
+.asj-detail-tabs > .el-tabs__header.is-top {
+  position: sticky;
+  top: 95px;
+  z-index: 10;
+  border-top: 1px solid #aeafb0;
+}
+
+// 顶层公共搜索样式
+.public-search {
+  display: flex;
+  gap: 8px;
+  padding-bottom: 8px;
+  position: sticky;
+  top: 0;
+  padding-top: 8px;
+  z-index: 10;
+  width: 100%;
+  background-color: #f8f8f8;
+  box-shadow: 0px 0px 0px rgba(51, 89, 181, 0.16);
+
+  /*.el-input__wrapper {
+    border-radius: 0;
+  }*/
+}
+
+.chart-tabs {
+  margin: 5px 0 15px 0;
+
+  .el-tabs__nav {
+    padding-left: 0 !important;
+  }
 }
-.asj-tabs > .el-tabs__header.is-top {
+
+.overview-tabs {
+  display: flex;
+  gap: 8px;
+  padding-bottom: 8px;
   position: sticky;
-  top: 82px;
+  top: 40px;
+  padding-top: 8px;
   z-index: 10;
+  width: 100%;
+  background-color: #f8f8f8;
+  box-shadow: 0px 0px 0px rgba(51, 89, 181, 0.16);
+
+  /*.el-input__wrapper {
+    border-radius: 0;
+  }*/
+}
+
+// 广告总览顶部样式
+.overview-top {
+  background-color: #fff;
+  position: sticky;
+  top: 0;
+  z-index: 9;
+  box-shadow: 0 0 12px rgba(51, 89, 181, 0.16);
+  padding: 0 12px;
+  display: flex;
+  height: 40px;
+  gap: 24px;
+}
+
+// 图表卡片样式
+.el-tabs.el-tabs--top.el-tabs--border-card.chart-tabs {
+  border-radius: 10px;
+  overflow: hidden;
 }
+
+// 表格样式
+.fs-crud-container .fs-crud-header {
+  padding: 0 0 0 0 !important;
+}
+
+.fs-crud-container .fs-crud-header .fs-crud-actionbar {
+  display: flex;
+  flex: 10000;
+   align-items: center;
+  min-width: 1px;
+  border: 0.5px solid #dddfe6;
+  border-top-left-radius: 10px;
+  border-bottom: 0;
+  border-right: 0;
+  margin: 0 0 0 0;
+  padding: 10px 0 10px 10px;
+  background-color: white;
+}
+
+.fs-page-content .fs-crud-container .fs-crud-header .fs-crud-toolbar {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  margin-right: 0;
+  padding: 0 10px 0 0;
+  flex: 1;
+  border: 0.5px solid #dddfe6;
+  border-bottom: 0;
+  border-top-right-radius: 10px;
+  background-color: white;
+}
+
+.fs-header-middle+div {
+  border-left: 0.5px solid #dddfe6;
+  border-top-left-radius: 10px;
+}
+.fs-page-content .fs-crud-container .fs-crud-header .fs-header-middle+div.fs-crud-toolbar{
+  padding: 10px 10px 10px 0;
+}
+.fs-page-content .fs-crud-container .fs-crud-header .fs-crud-actionbar+div {
+  border-left: 0;
+}
+
+.fs-container .box .inner .body {
+  border-radius: 10px;
+  flex: 1;
+  padding: 0 10px 10px 10px;
+  border: 0.5px solid #dddfe6;
+  background-color: white;
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+  border-top: 0;
+  border-bottom-color: #ffffff00;
+}
+
+.fs-container .box .inner .footer {
+  flex-shrink: 0;
+  background: #fff;
+  border: 0.5px solid #dddfe6;
+  border-top: 0;
+  border-bottom-right-radius: 10px;
+  border-bottom-left-radius: 10px;
+}
+
+// 表格单元格样式
+.el-table__footer td.el-table__cell {
+  border: none !important;
+  border-bottom: 0.5px solid rgb(211, 211, 211) !important;
+}
+
+.no-hover-effect {
+  background-color: #f0f9eb;
+  color: #67c23a;
+  border-color: #b3e19d;
+  margin-top: -3px;
+}
+.no-hover-effect:hover,
+.no-hover-effect:focus,
+.no-hover-effect:active {
+  background-color: #f0f9eb !important;
+  color: #67c23a !important;
+  border-color: #b3e19d !important;
+}
+
+.no-hover-effect-ban {
+  background-color: #fef0f0;
+  color: #f56c6c;
+  border-color: #fab6b6;
+  margin-top: -3px;
+}
+.no-hover-effect-ban:hover,
+.no-hover-effect-ban:focus,
+.no-hover-effect-ban:active {
+  background-color: #fef0f0 !important;
+  color: #f56c6c !important;
+  border-color: #fab6b6 !important;
+}
+
+.asj-h2 {
+  height: 60px;
+  line-height: 60px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.asj-h3 {
+  font-size: 15px;
+  font-weight: 750;
+  color: #333;
+  line-height: 22px;
+  padding: 12px 0;
+  position: relative;
+}
+
+.auto-page-foot {
+  background-color: #fff;
+  padding: 24px;
+  position: sticky;
+  bottom: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: center;
+}

+ 3 - 3
src/theme/element.scss

@@ -191,9 +191,9 @@
 
 /* Tabs 标签页
 ------------------------------- */
-.el-tabs__nav-wrap::after {
-	height: 1px !important;
-}
+// .el-tabs__nav-wrap::after {
+// 	height: 1px !important;
+// }
 
 /* Dropdown 下拉菜单
 ------------------------------- */

+ 5 - 0
src/types/pinia.d.ts

@@ -89,3 +89,8 @@ declare interface ThemeConfigState {
 		globalComponentSize: string;
 	};
 }
+
+declare interface publicData {
+	dateRange: string[],
+	[key:string]: any
+}

+ 314 - 244
src/types/views.d.ts

@@ -2,164 +2,166 @@
  * views personal
  */
 type NewInfo = {
-	title: string;
-	date: string;
-	link: string;
-};
+  title: string
+  date: string
+  link: string
+}
 type Recommend = {
-	title: string;
-	msg: string;
-	icon: string;
-	bg: string;
-	iconColor: string;
-};
+  title: string
+  msg: string
+  icon: string
+  bg: string
+  iconColor: string
+}
 declare type PersonalState = {
-	newsInfoList: NewInfo[];
-	personalForm: {
-		avatar:string,
-		username: string;
-		name: string;
-		email: string;
-		mobile: string;
-		gender: number | string;
-		dept_info: {
-			dept_id: number;
-			dept_name: string;
-		}
-		role_info: [{
-			id: number;
-			name: string;
-		}]
-	};
-};
+  newsInfoList: NewInfo[]
+  personalForm: {
+    avatar: string
+    username: string
+    name: string
+    email: string
+    mobile: string
+    gender: number | string
+    dept_info: {
+      dept_id: number
+      dept_name: string
+    }
+    role_info: [
+      {
+        id: number
+        name: string
+      }
+    ]
+  }
+}
 
 /**
  * views visualizing
  */
 declare type Demo2State<T = any> = {
-	time: {
-		txt: string;
-		fun: number;
-	};
-	dropdownList: T[];
-	dropdownActive: string;
-	skyList: T[];
-	dBtnList: T[];
-	chartData4Index: number;
-	dBtnActive: number;
-	earth3DBtnList: T[];
-	chartData4List: T[];
-	myCharts: T[];
-};
+  time: {
+    txt: string
+    fun: number
+  }
+  dropdownList: T[]
+  dropdownActive: string
+  skyList: T[]
+  dBtnList: T[]
+  chartData4Index: number
+  dBtnActive: number
+  earth3DBtnList: T[]
+  chartData4List: T[]
+  myCharts: T[]
+}
 
 /**
  * views params
  */
 declare type ParamsState = {
-	value: string;
-	tagsViewName: string;
-	tagsViewNameIsI18n: boolean;
-};
+  value: string
+  tagsViewName: string
+  tagsViewNameIsI18n: boolean
+}
 
 /**
  * views system
  */
 // role
 declare interface RowRoleType {
-	roleName: string;
-	roleSign: string;
-	describe: string;
-	sort: number;
-	status: boolean;
-	createTime: string;
+  roleName: string
+  roleSign: string
+  describe: string
+  sort: number
+  status: boolean
+  createTime: string
 }
 
 interface SysRoleTableType extends TableType {
-	data: RowRoleType[];
+  data: RowRoleType[]
 }
 
 declare interface SysRoleState {
-	tableData: SysRoleTableType;
+  tableData: SysRoleTableType
 }
 
 declare type TreeType = {
-	id: number;
-	label: string;
-	children?: TreeType[];
-};
+  id: number
+  label: string
+  children?: TreeType[]
+}
 
 // user
 declare type RowUserType<T = any> = {
-	username: string;
-	userNickname: string;
-	roleSign: string;
-	department: string[];
-	phone: string;
-	email: string;
-	sex: string;
-	password: string;
-	overdueTime: T;
-	status: boolean;
-	describe: string;
-	createTime: T;
-};
+  username: string
+  userNickname: string
+  roleSign: string
+  department: string[]
+  phone: string
+  email: string
+  sex: string
+  password: string
+  overdueTime: T
+  status: boolean
+  describe: string
+  createTime: T
+}
 
 interface SysUserTableType extends TableType {
-	data: RowUserType[];
+  data: RowUserType[]
 }
 
 declare interface SysUserState {
-	tableData: SysUserTableType;
+  tableData: SysUserTableType
 }
 
 declare type DeptTreeType = {
-	deptName: string;
-	createTime: string;
-	status: boolean;
-	sort: number;
-	describe: string;
-	id: number | string;
-	children?: DeptTreeType[];
-};
+  deptName: string
+  createTime: string
+  status: boolean
+  sort: number
+  describe: string
+  id: number | string
+  children?: DeptTreeType[]
+}
 
 // dept
 declare interface RowDeptType extends DeptTreeType {
-	deptLevel: string[];
-	person: string;
-	phone: string;
-	email: string;
+  deptLevel: string[]
+  person: string
+  phone: string
+  email: string
 }
 
 interface SysDeptTableType extends TableType {
-	data: DeptTreeType[];
+  data: DeptTreeType[]
 }
 
 declare interface SysDeptState {
-	tableData: SysDeptTableType;
+  tableData: SysDeptTableType
 }
 
 // dic
 type ListType = {
-	id: number;
-	label: string;
-	value: string;
-};
+  id: number
+  label: string
+  value: string
+}
 
 declare interface RowDicType {
-	dicName: string;
-	fieldName: string;
-	describe: string;
-	status: boolean;
-	createTime: string;
-	list: ListType[];
+  dicName: string
+  fieldName: string
+  describe: string
+  status: boolean
+  createTime: string
+  list: ListType[]
 }
 
 interface SysDicTableType extends TableType {
-	data: RowDicType[];
+  data: RowDicType[]
 }
 
 declare interface SysDicState {
-	tableData: SysDicTableType;
+  tableData: SysDicTableType
 }
 
 /**
@@ -167,173 +169,173 @@ declare interface SysDicState {
  */
 //  filtering
 declare type FilteringChilType = {
-	id: number | string;
-	label: string;
-	active: boolean;
-};
+  id: number | string
+  label: string
+  active: boolean
+}
 
 declare type FilterListType = {
-	img: string;
-	title: string;
-	evaluate: string;
-	collection: string;
-	price: string;
-	monSales: string;
-	id: number | string;
-	loading?: boolean;
-};
+  img: string
+  title: string
+  evaluate: string
+  collection: string
+  price: string
+  monSales: string
+  id: number | string
+  loading?: boolean
+}
 
 declare type FilteringRowType = {
-	title: string;
-	isMore: boolean;
-	isShowMore: boolean;
-	id: number | string;
-	children: FilteringChilType[];
-};
+  title: string
+  isMore: boolean
+  isShowMore: boolean
+  id: number | string
+  children: FilteringChilType[]
+}
 
 // tableRules
 declare type TableRulesHeaderType = {
-	prop: string;
-	width: string | number;
-	label: string;
-	isRequired?: boolean;
-	isTooltip?: boolean;
-	type: string;
-};
+  prop: string
+  width: string | number
+  label: string
+  isRequired?: boolean
+  isTooltip?: boolean
+  type: string
+}
 
 declare type TableRulesState = {
-	tableData: {
-		data: EmptyObjectType[];
-		header: TableRulesHeaderType[];
-		option: SelectOptionType[];
-	};
-};
+  tableData: {
+    data: EmptyObjectType[]
+    header: TableRulesHeaderType[]
+    option: SelectOptionType[]
+  }
+}
 
 declare type TableRulesOneProps = {
-	name: string;
-	email: string;
-	autograph: string;
-	occupation: string;
-};
+  name: string
+  email: string
+  autograph: string
+  occupation: string
+}
 
 // tree
 declare type RowTreeType = {
-	id: number;
-	label: string;
-	label1: string;
-	label2: string;
-	isShow: boolean;
-	children?: RowTreeType[];
-};
+  id: number
+  label: string
+  label1: string
+  label2: string
+  isShow: boolean
+  children?: RowTreeType[]
+}
 
 // workflow index
 declare type NodeListState = {
-	id: string | number;
-	nodeId: string | undefined;
-	class: HTMLElement | string;
-	left: number | string;
-	top: number | string;
-	icon: string;
-	name: string;
-};
+  id: string | number
+  nodeId: string | undefined
+  class: HTMLElement | string
+  left: number | string
+  top: number | string
+  icon: string
+  name: string
+}
 
 declare type LineListState = {
-	sourceId: string;
-	targetId: string;
-	label: string;
-};
+  sourceId: string
+  targetId: string
+  label: string
+}
 
 declare type XyState = {
-	x: string | number;
-	y: string | number;
-};
+  x: string | number
+  y: string | number
+}
 
 declare type WorkflowState<T = any> = {
-	leftNavList: T[];
-	dropdownNode: XyState;
-	dropdownLine: XyState;
-	isShow: boolean;
-	jsPlumb: T;
-	jsPlumbNodeIndex: null | number;
-	jsplumbDefaults: T;
-	jsplumbMakeSource: T;
-	jsplumbMakeTarget: T;
-	jsplumbConnect: T;
-	jsplumbData: {
-		nodeList: NodeListState[];
-		lineList: LineListState[];
-	};
-};
+  leftNavList: T[]
+  dropdownNode: XyState
+  dropdownLine: XyState
+  isShow: boolean
+  jsPlumb: T
+  jsPlumbNodeIndex: null | number
+  jsplumbDefaults: T
+  jsplumbMakeSource: T
+  jsplumbMakeTarget: T
+  jsplumbConnect: T
+  jsplumbData: {
+    nodeList: NodeListState[]
+    lineList: LineListState[]
+  }
+}
 
 // workflow drawer
 declare type WorkflowDrawerNodeState<T = any> = {
-	node: { [key: string]: T };
-	nodeRules: T;
-	form: T;
-	tabsActive: string;
-	loading: {
-		extend: boolean;
-	};
-};
+  node: { [key: string]: T }
+  nodeRules: T
+  form: T
+  tabsActive: string
+  loading: {
+    extend: boolean
+  }
+}
 
 declare type WorkflowDrawerLabelType = {
-	type: string;
-	label: string;
-};
+  type: string
+  label: string
+}
 
 declare type WorkflowDrawerState<T = any> = {
-	isOpen: boolean;
-	nodeData: {
-		type: string;
-	};
-	jsplumbConn: T;
-};
+  isOpen: boolean
+  nodeData: {
+    type: string
+  }
+  jsplumbConn: T
+}
 
 /**
  * views make
  */
 // tableDemo
 declare type TableDemoPageType = {
-	pageNum: number;
-	pageSize: number;
-};
+  pageNum: number
+  pageSize: number
+}
 
 declare type TableHeaderType = {
-	key: string;
-	width: string;
-	title: string;
-	type: string | number;
-	colWidth: string;
-	width?: string | number;
-	height?: string | number;
-	isCheck: boolean;
-};
+  key: string
+  width: string
+  title: string
+  type: string | number
+  colWidth: string
+  width?: string | number
+  height?: string | number
+  isCheck: boolean
+}
 
 declare type TableSearchType = {
-	label: string;
-	prop: string;
-	placeholder: string;
-	required: boolean;
-	type: string;
-	options?: SelectOptionType[];
-};
+  label: string
+  prop: string
+  placeholder: string
+  required: boolean
+  type: string
+  options?: SelectOptionType[]
+}
 
 declare type TableDemoState = {
-	tableData: {
-		data: EmptyObjectType[];
-		header: TableHeaderType[];
-		config: {
-			total: number;
-			loading: boolean;
-			isBorder: boolean;
-			isSelection: boolean;
-			isSerialNo: boolean;
-			isOperate: boolean;
-		};
-		search: TableSearchType[];
-		param: EmptyObjectType;
-	};
-};
+  tableData: {
+    data: EmptyObjectType[]
+    header: TableHeaderType[]
+    config: {
+      total: number
+      loading: boolean
+      isBorder: boolean
+      isSelection: boolean
+      isSerialNo: boolean
+      isOperate: boolean
+    }
+    search: TableSearchType[]
+    param: EmptyObjectType
+  }
+}
 
 declare interface MetricOptions {
   label: string
@@ -341,34 +343,102 @@ declare interface MetricOptions {
   disabled?: boolean
 }
 
+declare interface ShowMetric {
+  label: string
+  metric: string
+  color: string
+}
+
 declare interface MetricData extends MetricOptions {
-  metricVal: string,
-  preVal?: string,
-  gapVal?: string
+  metricVal: string
+  preVal?: any
+  gapVal?: any
 }
 
 declare interface Portfolio {
-	name: string,
-	portfolioId: string
+  name: string
+  portfolioId: string
 }
 
 declare interface SpCampaign {
-	id?: number,
-	campaignId?: string,
-	campaignName?: string,
-	budgetType?: string,
-	budget?: string,
-	startDate?: string,
-	endDate?: string,
-	targetingType?: string,
-	state?: string,
-	dynBidStrategy?: string,
-	portfolio?: Portfolio
+  id?: number
+  campaignId?: string
+  campaignName?: string
+  budgetType?: string
+  budget?: string
+  startDate?: string
+  endDate?: string
+  targetingType?: string
+  state?: string
+  dynBidStrategy?: string
+  portfolioName?: string
+  servingStatus?: string
 }
 
 declare type SpAdGroup = {
-	id?: number,
-	adGroupName?: string,
-	state?: string,
-	defaultBid?: string
+  id?: number
+  adGroupId?: string
+  adGroupName?: string
+  state?: string
+  defaultBid?: string
+  startDate?: string
+  endDate?: string
+  targetingType?: string
+}
+
+declare interface RuleCampaignAd {
+  campaignType: string
+  campaignId: string
+  campaignName?: string
+  adGroupId: string
+  adGroupName?: string
+}
+
+declare interface SearchTermBid {
+  bidType: string
+  type: string
+  defaultBidding: string
+  min: string
+  max: string
+  matchType: string
+  numType: string
+  use: boolean
+}
+
+declare interface RuleSearchTermAction {
+  keywords: SearchTermBid[]
+  target: SearchTermBid
+}
+
+declare interface AutoRule {
+  id?: number
+  type: number
+  campaignType: string
+  campaignAd: RuleCampaignAd[]
+  action: { [key:string]: any }
+  activeModel?: string
+  weekdays?: number[]
+  conditions: any[]
+  setTime?: string
+}
+
+declare interface AutoTemplate {
+  id?: number
+  name: string
+  rule: AutoRule
+}
+
+declare interface AutoCampaignRule {
+  campaignId: string
+  campaignType: string
+  profileId: string
+  ruleType: number
+
+  // id?: number,
+  templateId?: number
+  templateName?: string
+  useTmpl?: boolean
+  template?: null | AutoTemplate
+  rule: null | AutoRule
+  RuleStatusButton?: { [key: string]: any }
 }

+ 5 - 0
src/utils/emitter.ts

@@ -0,0 +1,5 @@
+import mitt from 'mitt'
+
+const emitter = mitt()
+
+export default emitter

+ 3 - 3
src/utils/service.ts

@@ -16,7 +16,7 @@ import { getBaseURL } from './baseUrl';
 function createService() {
 	// 创建一个 axios 实例
 	const service = axios.create({
-		timeout: 20000,
+		timeout: 60000,
 		headers: {
 			'Content-Type': 'application/json;charset=utf-8',
 		},
@@ -172,11 +172,11 @@ function createRequestFunction(service: any) {
 			headers: {
 				'Content-Type': get(config, 'headers.Content-Type', 'application/json'),
 			},
-			timeout: 5000,
+			timeout: 60000,
 			baseURL: getBaseURL(),
 			data: {},
 		};
-
+		delete config.headers
 		// const token = userStore.getToken;
 		const token = Session.get('token');
 		if (token != null) {

+ 312 - 0
src/views/adManage/ad-overview/chartComponents/dataTendency.vue

@@ -0,0 +1,312 @@
+<template>
+  <div v-loading="loading">
+    <MetricsCards v-model="metrics" :metric-items="metricsItems" @change="changeMetric"></MetricsCards>
+    <div style="height: 350px;" ref="chartRef"></div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref,onMounted, onBeforeUnmount, Ref, unref, watch, computed } from 'vue'
+import * as echarts from 'echarts'
+import { spCampaignMetricsEnum } from '/@/views/adManage/utils/enum.js'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import XEUtils from 'xe-utils'
+import { buildChartOpt, parseQueryParams } from '/@/views/adManage/utils/tools.js'
+
+defineOptions({
+  name: "DataTendencyChart"
+})
+
+interface Props {
+  fetchCard: Function,
+  fetchLine: Function,
+  fetchLineMonth?: Function,
+  fetchLineWeek?: Function,
+  query: {[key: string]: any},
+  initMetric?: ShowMetric[],
+  metricEnum?: {[key: string]: string}[]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  initMetric: () => [
+    {metric: 'Impression', color: '#0085ff', 'label': '曝光量'},
+    {metric: 'Click', color: '#3fd4cf', 'label': '点击量'},
+    {metric: 'Spend', color: '#ff9500', 'label': '花费'},
+  ],
+  metricEnum: () => spCampaignMetricsEnum
+})
+
+const metrics = ref(props.initMetric)
+// const shopInfo = useShopInfo()
+// const publicData = usePublicData()
+// const { dateRange } = storeToRefs(publicData)
+const metricsItems: Ref<MetricData[]> = ref([])
+let chartObj:any
+const chartRef = ref()
+const statDim = ref('day')
+const option: any = {
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    show: false
+  },
+  grid: {
+    top: 50, right: 150, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category'
+  },
+  yAxis: [
+    {
+      id: 0,
+      type: 'value',
+      name: '',
+      splitLine: {
+        show: true // 设置显示分割线
+      },
+      axisLine: {
+        show: true,
+				lineStyle: { color: '' }
+      },
+      show: true
+    },
+    {
+      id: 1,
+      type: 'value',
+      name: '',
+      position: 'right',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+				lineStyle: {
+					color: ''
+				}
+      },
+      show: true
+    },
+    {
+      id: 2,
+      type: 'value',
+      position: 'right',
+      offset: 90,
+      name: '',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+				lineStyle: {
+					color: ''
+				}
+      },
+      show: true
+    }
+  ],
+  series: [
+    {
+      id: 0,
+      name: '',
+      type: 'bar',
+      encode: {
+        x: 'Name',
+        y: ''
+      },
+      barWidth: '20px',
+      yAxisIndex: 0,
+      itemStyle: {
+        color: '',
+        borderRadius: [6, 6, 6, 6],
+      }
+    },
+    {
+      id: 1,
+      name: '',
+      type: 'line',
+      encode: {
+        x: 'Name',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 1,
+      itemStyle: {
+        // color: '#ff9500',
+        // borderColor: '#ff9500'
+      },
+      areaStyle: {
+        // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+        //   { offset: 0, color: '#3fd4cf53' },
+        //   { offset: 1, color: '#3fd4cf03' },
+        // ]),
+      },
+      emphasis: {
+        focus:'series'
+      }
+    },
+    {
+      id: 2,
+      name: '',
+      type: 'line',
+      encode: {
+        x: 'Name',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 2,
+      itemStyle: {},
+      areaStyle: {},
+      emphasis: {
+        focus:'series'
+      }
+    }
+  ]
+}
+const loading = ref(true)
+const queryParams = computed(() => parseQueryParams(props.query))
+
+onMounted(() => {
+  getMetricsItems()
+	addResize()
+  // initLine()
+  setTimeout(() => { initLine() }, 0)
+})
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+  removeResize()
+})
+
+const initLine = async () => {
+	chartObj = echarts.init(chartRef.value)
+	const items = await getDataset()
+	option.dataset.source = items
+
+  XEUtils.arrayEach(option.series, (info:any, index) => {
+    const color = metrics.value[index].color
+    info.name = metrics.value[index].label
+    info.encode.y = metrics.value[index].metric
+    if (info.type === 'bar') {
+      info.itemStyle.color = color
+    } else {
+      info.itemStyle = { color: color, borderColor: color }
+      info.areaStyle = {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: color + '53' },
+          { offset: 1, color: color + '03' },
+        ])
+      }
+    }
+  })
+  XEUtils.arrayEach(option.yAxis, (info:any, index) => {
+    info.name = metrics.value[index].label
+    info.axisLine.lineStyle.color = metrics.value[index].color
+  })
+
+  XEUtils.arrayEach(props.metricEnum, info => {
+    option.legend.selected[info.label] = false
+  })
+  for(const info of metrics.value) {
+    option.legend.selected[info.label] = true
+  }
+  // console.log(option)
+	chartObj.setOption(option)
+  loading.value = false
+}
+const getDataset = async () => {
+	if (statDim.value === 'week') {
+    if (props.fetchLineWeek) {
+      const resp = await props.fetchLineWeek(queryParams.value)
+      return resp.data
+    }
+  } else if (statDim.value === 'month') {
+    if (props.fetchLineMonth) {
+      const resp = await props.fetchLineMonth(queryParams.value)
+      return resp.data
+    }
+  } else {
+    const resp = await props.fetchLine(queryParams.value)
+    return resp.data
+  }
+}
+const getMetricsItems = async () => {
+	const resp = await props.fetchCard(queryParams.value)
+	const data = resp.data
+  metricsItems.value.length = 0
+	XEUtils.arrayEach(props.metricEnum, info => {
+		const tmp:MetricData = {
+			label: info.label,
+			value: info.value,
+			metricVal: data[info.value],
+			gapVal: data[`gap${info.value}`],
+			preVal: data[`prev${info.value}`],
+		}
+		metricsItems.value.push(tmp)
+	})
+}
+
+const changeMetric = () => {
+  const opt = buildChartOpt(option, metrics.value)
+  chartObj.setOption(opt)
+}
+
+const changeStatDim = async () => {
+  loading.value = true
+  let source = await getDataset()
+  if (source.length > 0) {
+    chartObj.setOption({dataset: {source: source}})
+  }
+  loading.value = false
+}
+
+watch(
+  props.query,
+  async () => {
+    // console.log("------watch-----queryParams", props.query)
+    loading.value = true
+    await getMetricsItems()
+    const items = await getDataset()
+    const opt = { dataset: { source: items } }
+    chartObj.setOption(opt)
+    loading.value = false
+  }
+)
+
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+
+
+</script>
+
+<style scoped>
+.metrics-cards {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 12px;
+  width: 100%;
+}
+
+.chart-button-group {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 5px;
+}
+</style>

+ 80 - 0
src/views/adManage/ad-overview/daily/api.ts

@@ -0,0 +1,80 @@
+import {request} from '/@/utils/service'
+import {UserPageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud'
+import XEUtils from 'xe-utils'
+
+export const apiPrefix = '/api/ad_manage/summary/report/trend/'
+
+export function GetList(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + 'daily',
+    method: 'get',
+    params: query,
+  })
+}
+
+export function GetObj(id: any) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'get',
+  })
+}
+
+export function AddObj(obj: AddReq) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: obj,
+  })
+}
+
+export function UpdateObj(obj: EditReq) {
+  return request({
+    url: apiPrefix + obj.id + '/',
+    method: 'put',
+    data: obj,
+  })
+}
+
+export function DelObj(id: DelReq) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'delete',
+    data: {id},
+  })
+}
+
+export function getCardData(query: UserPageQuery) {
+  return request({
+    url: '/api/ad_manage/summary/report/total',
+    method: 'GET',
+    params: query,
+  })
+}
+
+export function getLineData(query: UserPageQuery) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'daily',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+  query['dateRangeType'] = 'W'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+  query['dateRangeType'] = 'M'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+

+ 400 - 0
src/views/adManage/ad-overview/daily/crud.tsx

@@ -0,0 +1,400 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        showSummary: true,
+        stripe: false,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          borderRight: 'none',
+        },
+        // rowClassName() {
+        //   return '.el-table__footer td.el-table__cell'
+        // },
+        cellStyle:  {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd'
+        },
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: false,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        Name: {
+          title: '日期',
+          column: {
+            width: 100,
+            align: 'left',
+            fixed: 'left',
+            // border: '0.5px solid #ddd',
+            cellStyle: {
+              border: '1px solid #ddd',
+            }
+          },
+        },
+        Spend: {
+          title: '花费',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            border: '0.5px solid #ddd',
+
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top"
+                                content="来自亚马逊广告API,亚马逊系统会在3天内将无效点击从统计数据中删除,因此过去3天内的花费可能会有所变化">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>花费</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalSales: {
+          title: '销售额',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br/>出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销售额</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ACOS: {
+          title: 'ACOS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告投入产出比,系统计算,广告花费/广告带来的销售额。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ACOS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ROAS: {
+          title: 'ROAS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告支出回报,系统计算,广告带来的销售额/广告花费。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ROAS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPC: {
+          title: '点击成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="单次点击成本,系统计算,花费/点击量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPA: {
+          title: '订单成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="平均每笔订单的花费,系统计算,花费/广告订单量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Click: {
+          title: '点击量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被点击的次数。亚马逊系统会在3天内将无效点击去除。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CTR: {
+          title: '点击率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalPurchases: {
+          title: '订单数',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单数</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalUnitOrdered: {
+          title: '销量',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Impression: {
+          title: '曝光量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被展示的次数。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>曝光量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        PurchasesRate: {
+          title: '转化率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告转化率,系统计算,广告订单量/点击量*100%,展示型推广vCPM成本类型的广告活动不予计算。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>转化率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPM: {
+          title: '千次曝光成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 150,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>千次曝光成本</span>
+                  </span>
+              )
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 131 - 0
src/views/adManage/ad-overview/daily/index.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="overview-tabs">
+    <DateRangePicker v-model="dateRange"></DateRangePicker>
+    <el-select v-model="selectedPortfolios" placeholder="SP" style="width: 200px" collapse-tags collapse-tags-tooltip :max-collapse-tags="3">
+      <el-option v-for="info of portfolios" :label="info.label" :value="info.value" :disabled="info.disabled"></el-option>
+    </el-select>
+  </div>
+  <fs-page class="fs-page-custom" style="margin-top: -11px">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #header-middle>
+        <el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card">
+          <DataTendencyChart
+            v-if="tabActiveName === 'dataTendency'"
+            :query="queryParams"
+            :fetchCard="getCardData"
+            :fetchLine="getLineData"
+            :fetch-line-month="getLineMonthData"
+            :fetch-line-week="getLineWeekData"
+          >
+          </DataTendencyChart>
+        </el-tabs>
+      </template>
+      <template #cell_percentTimeInBudget="scope">
+        <el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0" />
+      </template>
+      <template #cell_campaignName="scope">
+        <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+      </template>
+      <template #cell_MissedImpressions="scope">
+        {{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+      </template>
+      <template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }} </template>
+      <template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }} </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+          :field="field"
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]"
+          :date-range="dateRange"
+          :show-compare="showCompare"
+        />
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, Ref, ref, watch } from 'vue'
+import { FsPage, useFs } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { useRouter } from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/ad-overview/chartComponents/dataTendency.vue'
+import { getCardData, getLineData, getLineMonthData, getLineWeekData } from './api'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const selectedPortfolios = ref('sp')
+const portfolios = [
+  {
+    value: 'sp/sb/sd',
+    label: 'SP/SB/SD',
+  },
+  {
+    value: 'sp',
+    label: 'SP',
+  },
+  {
+    value: 'sb',
+    label: 'SB',
+  },
+  {
+    value: 'sd',
+    label: 'SD',
+  },
+  {
+    value: 'dsp',
+    label: 'DSP',
+    disabled: true,
+  },
+]
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  dateRange,
+  profileId: profile.value.profile_id,
+  campaignType: selectedPortfolios.value,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const router = useRouter()
+const showCompare = ref(false)
+
+console.log(111, profile.value.profile_id)
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: { campaignId: row.campaignId, tagsViewName: row.campaignName },
+  })
+}
+
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+  flex: none;
+}
+::v-deep(.el-table--border .el-table__footer-wrapper) {
+  border: none;
+}
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+</style>

+ 88 - 0
src/views/adManage/ad-overview/hourly/api.ts

@@ -0,0 +1,88 @@
+import {request} from '/@/utils/service'
+import {UserPageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud'
+import XEUtils from 'xe-utils'
+
+export const apiPrefix = '/api/ad_manage/summary/ams/'
+
+export function GetList(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + 'hourly_trend',
+    method: 'get',
+    params: query,
+  })
+}
+
+export function GetObj(id: any) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'get',
+  })
+}
+
+export function AddObj(obj: AddReq) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: obj,
+  })
+}
+
+export function UpdateObj(obj: EditReq) {
+  return request({
+    url: apiPrefix + obj.id + '/',
+    method: 'put',
+    data: obj,
+  })
+}
+
+export function DelObj(id: DelReq) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'delete',
+    data: {id},
+  })
+}
+
+export function getCardData(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + 'hourly_total',
+    method: 'GET',
+    params: query,
+  })
+}
+
+export function getLineData(query: UserPageQuery) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'hourly_trend',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+  query['dateRangeType'] = 'W'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+  query['dateRangeType'] = 'M'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getMyList(query: UserPageQuery) {
+  query['dateRangeType'] = 'M'
+  return request({
+    url: '/api/ad_manage/summary/ams/hourly',
+    method: 'GET',
+    params: query
+  })
+}

+ 329 - 0
src/views/adManage/ad-overview/hourly/crud.tsx

@@ -0,0 +1,329 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        showSummary: true,
+        stripe: false,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          borderRight: 'none',
+        },
+        cellStyle:  {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd'
+        },
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: true,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        Name: {
+          title: '日期',
+          column: {
+            width: 100,
+            align: 'left',
+            fixed: 'left',
+            // border: '0.5px solid #ddd',
+            cellStyle: {
+              border: '1px solid #ddd',
+            }
+          },
+        },
+        Spend: {
+          title: '花费',
+          column: {
+            align: 'center',
+            sortable: true,
+            border: '0.5px solid #ddd',
+
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top"
+                                content="来自亚马逊广告API,亚马逊系统会在3天内将无效点击从统计数据中删除,因此过去3天内的花费可能会有所变化">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>花费</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalSales: {
+          title: '销售额',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br/>出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销售额</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ACOS: {
+          title: 'ACOS',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告投入产出比,系统计算,广告花费/广告带来的销售额。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ACOS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ROAS: {
+          title: 'ROAS',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告支出回报,系统计算,广告带来的销售额/广告花费。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ROAS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPC: {
+          title: '点击成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="单次点击成本,系统计算,花费/点击量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPA: {
+          title: '订单成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="平均每笔订单的花费,系统计算,花费/广告订单量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Click: {
+          title: '点击量',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被点击的次数。亚马逊系统会在3天内将无效点击去除。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CTR: {
+          title: '点击率',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalPurchases: {
+          title: '订单数',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单数</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalUnitOrdered: {
+          title: '销量',
+          column: {
+            align: 'center',
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销量</span>
+                  </span>
+              )
+            }
+          }
+        },
+      }
+    }
+  }
+}

+ 135 - 0
src/views/adManage/ad-overview/hourly/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="overview-tabs">
+    <DateRangePicker v-model="dateRange"></DateRangePicker>
+    <el-select
+        v-model="selectedPortfolios"
+        placeholder="SP"
+        style="width: 200px"
+        collapse-tags
+        collapse-tags-tooltip
+        :max-collapse-tags="3"
+    >
+      <el-option v-for="info of portfolios" :label="info.label" :value="info.value" :disabled="info.disabled"></el-option>
+    </el-select>
+  </div>
+	<fs-page class="fs-page-custom" style="margin-top: -11px">
+		<fs-crud ref="crudRef" v-bind="crudBinding">
+			<template #header-middle>
+				<el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card">
+						<DataTendencyChart
+							v-if="tabActiveName === 'dataTendency'"
+							:query="queryParams"
+							:fetchCard="getCardData"
+							:fetchLine="getLineData"
+							:fetch-line-month="getLineMonthData"
+							:fetch-line-week="getLineWeekData">
+						</DataTendencyChart>
+				</el-tabs>
+			</template>
+			<template #cell_percentTimeInBudget="scope">
+				<el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0" />
+			</template>
+			<template #cell_campaignName="scope">
+				<el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+			</template>
+			<template #cell_MissedImpressions="scope">
+				{{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+			</template>
+			<template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }} </template>
+			<template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }} </template>
+			<template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+				<DataCompare
+          :field="field"
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]"
+          :date-range="dateRange"
+          :show-compare="showCompare"/>
+			</template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {useRouter} from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/ad-overview/chartComponents/dataTendency.vue'
+import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const selectedPortfolios = ref('sp')
+const portfolios = [
+  {
+    value: 'sp/sb/sd',
+    label: 'SP/SB/SD'
+  },
+  {
+    value: 'sp',
+    label: 'SP'
+  },
+  {
+    value:'sb',
+    label:'SB'
+  },
+  {
+    value:'sd',
+    label:'SD'
+  },
+  {
+    value: 'dsp',
+    label: 'DSP',
+    disabled: true
+  }
+]
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  dateRange,
+  // startDate: '2023-11-01',
+  // endDate: '2023-11-15',
+  profileId: profile.value.profile_id,
+  campaignType: selectedPortfolios.value
+})
+
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const router = useRouter()
+const showCompare = ref(false)
+
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: { campaignId: row.campaignId, tagsViewName: row.campaignName },
+  })
+}
+
+onMounted(async () => {
+	crudExpose.doRefresh()
+})
+
+watch(queryParams, async () => {
+  crudExpose.doRefresh()
+}, { deep: true })
+
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+	flex: none;
+}
+::v-deep(.el-table--border .el-table__footer-wrapper) {
+  border: none;
+}
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+</style>

+ 57 - 0
src/views/adManage/ad-overview/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="asj-container">
+    <div class="overview-top">
+      <div v-for="tab of tabs" :key="tab.name" :class="['asj-tab', { active: tabActiveName === tab.name }]" @click="tabActiveName = tab.name">
+        {{ tab.label }}
+      </div>
+    </div>
+    <component :is="tabsComponents[tabActiveName]"></component>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {onBeforeMount, provide, Ref, ref} from 'vue'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {GetAllPortfolios} from '/@/views/adManage/portfolios/api'
+import Total from './total/index.vue'
+import Hour from '/@/views/adManage/ad-overview/hourly/index.vue'
+import Daily from '/@/views/adManage/ad-overview/daily/index.vue'
+import Weekly from '/@/views/adManage/ad-overview/weekly/index.vue'
+import Monthly from '/@/views/adManage/ad-overview/monthly/index.vue'
+
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const selectedPortfolios: Ref<string[]> = ref([])
+const portfolios: Ref<Portfolio[]> = ref([])
+const { dateRange } = storeToRefs(publicData)
+const tabActiveName = ref('Total')
+const tabs = [
+  { label: '总览', name: 'Total' },
+  { label: '按小时', name: 'Hour' },
+  { label: '按日', name: 'Daily' },
+  { label: '按周', name: 'Weekly' },
+  { label: '按月', name: 'Monthly' },
+]
+const tabsComponents: any = {
+  Total,
+  Hour,
+  Daily,
+  Weekly,
+  Monthly,
+}
+
+provide('dateRange', dateRange)
+
+onBeforeMount(async () => {
+  const resp: APIResponseData = await GetAllPortfolios()
+  portfolios.value = resp.data
+})
+
+</script>
+
+<style scoped>
+::v-deep(.el-table .el-table__header-wrapper .cell) {
+  border-right: 1px solid rgb(218, 221, 223);
+}
+</style>

+ 80 - 0
src/views/adManage/ad-overview/monthly/api.ts

@@ -0,0 +1,80 @@
+import {request} from '/@/utils/service'
+import {UserPageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud'
+import XEUtils from 'xe-utils'
+
+export const apiPrefix = '/api/ad_manage/summary/report/trend/'
+
+export function GetList(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + 'monthly',
+    method: 'get',
+    params: query,
+  })
+}
+
+export function GetObj(id: any) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'get',
+  })
+}
+
+export function AddObj(obj: AddReq) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: obj,
+  })
+}
+
+export function UpdateObj(obj: EditReq) {
+  return request({
+    url: apiPrefix + obj.id + '/',
+    method: 'put',
+    data: obj,
+  })
+}
+
+export function DelObj(id: DelReq) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'delete',
+    data: {id},
+  })
+}
+
+export function getCardData(query: UserPageQuery) {
+  return request({
+    url: '/api/ad_manage/summary/report/total',
+    method: 'GET',
+    params: query,
+  })
+}
+
+export function getLineData(query: UserPageQuery) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'monthly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+  query['dateRangeType'] = 'W'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+  query['dateRangeType'] = 'M'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+

+ 397 - 0
src/views/adManage/ad-overview/monthly/crud.tsx

@@ -0,0 +1,397 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        showSummary: true,
+        stripe: false,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          borderRight: 'none',
+        },
+        cellStyle:  {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd'
+        },
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: false,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        Name: {
+          title: '日期',
+          column: {
+            width: 100,
+            align: 'left',
+            fixed: 'left',
+            // border: '0.5px solid #ddd',
+            cellStyle: {
+              border: '1px solid #ddd',
+            }
+          },
+        },
+        Spend: {
+          title: '花费',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            border: '0.5px solid #ddd',
+
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top"
+                                content="来自亚马逊广告API,亚马逊系统会在3天内将无效点击从统计数据中删除,因此过去3天内的花费可能会有所变化">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>花费</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalSales: {
+          title: '销售额',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br/>出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销售额</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ACOS: {
+          title: 'ACOS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告投入产出比,系统计算,广告花费/广告带来的销售额。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ACOS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ROAS: {
+          title: 'ROAS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告支出回报,系统计算,广告带来的销售额/广告花费。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ROAS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPC: {
+          title: '点击成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="单次点击成本,系统计算,花费/点击量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPA: {
+          title: '订单成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="平均每笔订单的花费,系统计算,花费/广告订单量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Click: {
+          title: '点击量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被点击的次数。亚马逊系统会在3天内将无效点击去除。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CTR: {
+          title: '点击率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalPurchases: {
+          title: '订单数',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单数</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalUnitOrdered: {
+          title: '销量',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Impression: {
+          title: '曝光量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被展示的次数。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>曝光量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        PurchasesRate: {
+          title: '转化率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告转化率,系统计算,广告订单量/点击量*100%,展示型推广vCPM成本类型的广告活动不予计算。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>转化率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPM: {
+          title: '千次曝光成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 150,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>千次曝光成本</span>
+                  </span>
+              )
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 131 - 0
src/views/adManage/ad-overview/monthly/index.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="overview-tabs">
+    <DateRangePicker v-model="dateRange"></DateRangePicker>
+    <el-select
+        v-model="selectedPortfolios"
+        placeholder="SP"
+        style="width: 200px"
+        collapse-tags
+        collapse-tags-tooltip
+        :max-collapse-tags="3"
+    >
+      <el-option v-for="info of portfolios" :label="info.label" :value="info.value" :disabled="info.disabled"></el-option>
+    </el-select>
+  </div>
+  <fs-page class="fs-page-custom" style="margin-top: -11px">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #header-middle>
+        <el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card">
+          <DataTendencyChart
+              v-if="tabActiveName === 'dataTendency'"
+              :query="queryParams"
+              :fetchCard="getCardData"
+              :fetchLine="getLineData"
+              :fetch-line-month="getLineMonthData"
+              :fetch-line-week="getLineWeekData">
+          </DataTendencyChart>
+        </el-tabs>
+      </template>
+      <template #cell_percentTimeInBudget="scope">
+        <el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0" />
+      </template>
+      <template #cell_campaignName="scope">
+        <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+      </template>
+      <template #cell_MissedImpressions="scope">
+        {{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+      </template>
+      <template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }} </template>
+      <template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }} </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+            :field="field"
+            :value="scope.row[field]"
+            :prev-val="scope.row[`prev${field}`]"
+            :gap-val="scope.row[`gap${field}`]"
+            :date-range="dateRange"
+            :show-compare="showCompare"/>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, Ref, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {useRouter} from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/ad-overview/chartComponents/dataTendency.vue'
+import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const selectedPortfolios = ref('sp')
+const portfolios = [
+  {
+    value: 'sp/sb/sd',
+    label: 'SP/SB/SD'
+  },
+  {
+    value: 'sp',
+    label: 'SP'
+  },
+  {
+    value:'sb',
+    label:'SB'
+  },
+  {
+    value:'sd',
+    label:'SD'
+  },
+  {
+    value: 'dsp',
+    label: 'DSP',
+    disabled: true
+  }
+]
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  dateRange,
+  profileId: profile.value.profile_id,
+  campaignType: selectedPortfolios.value
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const router = useRouter()
+const showCompare = ref(false)
+
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: { campaignId: row.campaignId, tagsViewName: row.campaignName },
+  })
+}
+
+watch(queryParams, async () => {
+  crudExpose.doRefresh()
+}, { deep: true })
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+  flex: none;
+}
+::v-deep(.el-table--border .el-table__footer-wrapper) {
+  border: none;
+}
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+</style>

+ 74 - 0
src/views/adManage/ad-overview/total/api.ts

@@ -0,0 +1,74 @@
+import { request } from '/src/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import XEUtils from 'xe-utils';
+
+export const apiPrefix = '/api/ad_manage/summary/report/';
+export function GetList(query: UserPageQuery) {
+  return request({
+    url: apiPrefix,
+    method: 'get',
+    params: query,
+  })
+}
+export function GetObj(id: any) {
+  return request({
+    url: apiPrefix + id + "/",
+    method: 'get',
+  });
+}
+
+export function AddObj(obj: AddReq) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: obj,
+  });
+}
+
+export function UpdateObj(obj: EditReq) {
+  return request({
+    url: apiPrefix + obj.id + '/',
+    method: 'put',
+    data: obj,
+  });
+}
+
+export function DelObj(id: DelReq) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'delete',
+    data: { id },
+  });
+}
+
+export function getCardData(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + "total/",
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineData(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + "daily/",
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getCardTotalData(query) {
+  return request({
+    url: apiPrefix + "total",
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getChartTotalData(query) {
+  return request({
+    url: apiPrefix + "trend/daily",
+    method: 'GET',
+    params: query
+  })
+}

+ 0 - 0
src/views/adManage/ad-overview/total/crud.tsx


+ 914 - 0
src/views/adManage/ad-overview/total/index.vue

@@ -0,0 +1,914 @@
+<template>
+  <div>
+    <div class="container-main">
+      <div class="overview-tabs">
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+        <el-select
+            v-model="selectedPortfolios"
+            placeholder="SP"
+            style="width: 200px"
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="3"
+        >
+          <el-option v-for="info of portfolios" :key="info.value" :label="info.label" :value="info.value" :disabled="info.disabled"></el-option>
+        </el-select>
+      </div>
+      <!-- 卡片内容 不要删除这个类 -->
+      <div class="home-container" style="margin-top: 0" v-loading="cardLoading">
+        <el-row :gutter="15" class="home-card-one mb15">
+          <el-col
+              :xs="24"
+              :sm="12"
+              :md="12"
+              :lg="6"
+              :xl="6"
+              v-for="(v, k) in state.homeOne"
+              :key="k"
+              :class="{ 'home-media home-media-lg': k > 1, 'home-media-sm': k === 1 }"
+          >
+            <div class="home-card-item flex">
+              <div class="flex-margin flex w100" :class="` home-one-animation${k}`">
+                <div class="flex-auto" style="margin-top: -10px">
+                  <div class="mt10">{{ v.cardTitle }}</div>
+                  <div class="font30">{{ v.num1 }}</div>
+                  <div style="display: inline-block; margin-right: 10px; margin-left: 3px;">
+                    {{ v.compareNum }}
+                  </div>
+                  <el-icon :style="{ color: String(v.num2).includes('-') ? '#59b939' : '#e36f53' }" style="display: inline-block; padding-top: 2px">
+                    <template v-if="String(v.num2).includes('-')">
+                      <Bottom/> <!-- num2 是负数时显示向下箭头 -->
+                    </template>
+                    <template v-else>
+                      <Top/> <!-- num2 不是负数时显示向上箭头 -->
+                    </template>
+                  </el-icon>
+                  <span class="l-indent" :style="{ color: String(v.num2).includes('-') ? '#59b939' : '#e36f53' }">{{ v.num2 }}%</span>
+                </div>
+                <div class="home-card-item-icon flex" :style="{ background: `var(${v.color2})` }">
+                  <i class="flex-margin font32" :class="v.num4" :style="{ color: `var(${v.color3})` }"></i>
+                </div>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+      <!-- 折线图 -->
+      <el-card v-loading="loading" style="margin-top: -5px;">
+        <div style="height: 350px;" ref="chartRefOne"></div>
+      </el-card>
+      <el-row :gutter="5" style="margin-top: 10px">
+        <el-col :span="12">
+          <el-card v-loading="loading">
+            <div style="height: 350px;" ref="chartRefAcos"></div>
+          </el-card>
+        </el-col>
+        <el-col :span="12">
+          <el-card v-loading="loading">
+            <div style="height: 350px;" ref="chartRefCPCandCTR"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+      <el-row :gutter="5" style="margin-top: 10px">
+        <el-col :span="12">
+          <el-card v-loading="loading">
+            <div style="height: 350px;" ref="chartRefTotalPurchases"></div>
+          </el-card>
+        </el-col>
+        <el-col :span="12">
+          <el-card v-loading="loading">
+            <div style="height: 350px;" ref="chartRefImpandCli"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+    <!--<el-button @click="changeCardData">按钮</el-button>-->
+  </div>
+
+</template>
+
+<script lang="ts" setup>
+import {nextTick, onBeforeUnmount, onMounted, reactive, Ref, ref, watch} from 'vue'
+import * as echarts from 'echarts'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {createCrudOptions} from '/@/views/adManage/sp/targets/crud'
+import {useFs} from '@fast-crud/fast-crud'
+import {getCardTotalData, getChartTotalData} from '/src/views/adManage/ad-overview/total/api'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const loading = ref(true)
+const cardLoading = ref(true)
+const publicData = usePublicData()
+const {dateRange} = storeToRefs(publicData)
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const tabActiveName = ref('Campaigns')
+const queryParams = ref({
+  profileId: profile.value.profile_id,
+  dateRange
+})
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: queryParams})
+
+// 广告类型下拉框
+const selectedPortfolios = ref('SP')
+const portfolios = [
+  {
+    value: 'SP/SB/SD',
+    label: 'SP/SB/SD'
+  },
+  {
+    value: 'SP',
+    label: 'SP'
+  },
+  {
+    value:'SB',
+    label:'SB'
+  },
+  {
+    value:'SD',
+    label:'SD'
+  },
+  {
+    value: 'DSP',
+    label: 'DSP',
+    disabled: true
+  }
+]
+
+// 发送请求获取数据
+async function setTotalData() {
+  try {
+    cardLoading.value = true
+    const resp = await getCardTotalData({ startDate: dateRange.value[0], endDate: dateRange.value[1], profileId: queryParams.value.profileId })
+    state.homeOne[0].num1 = resp.data.Spend
+    state.homeOne[0].compareNum = resp.data.prevSpend
+    state.homeOne[0].num2 = resp.data.gapSpend
+    state.homeOne[1].num1 = resp.data.TotalSales
+    state.homeOne[1].compareNum = resp.data.prevTotalSales
+    state.homeOne[1].num2 = resp.data.gapTotalSales
+    state.homeOne[2].num1 = resp.data.TotalPurchases
+    state.homeOne[2].compareNum = resp.data.prevTotalPurchases
+    state.homeOne[2].num2 = resp.data.gapTotalPurchases
+    state.homeOne[3].num1 = resp.data.ACOS
+    state.homeOne[3].compareNum = resp.data.prevACOS
+    state.homeOne[3].num2 = resp.data.gapACOS
+  } catch (error) {
+    console.log('获取数据失败:', error)
+  } finally {
+    cardLoading.value = false
+  }
+}
+
+let optionSource
+async function setChartTotalData() {
+  try {
+    loading.value = true
+    const resp = await getChartTotalData({ startDate: dateRange.value[0], endDate: dateRange.value[1], profileId: queryParams.value.profileId })
+    optionSource = resp.data
+    await setChartOptions()
+    return resp.data
+  } catch (error) {
+    console.log('获取数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+
+onMounted( () => {
+  // crudExpose.doRefresh()
+  setTotalData()
+  setChartTotalData()
+})
+
+watch(queryParams, async() => {
+  try {
+    loading.value = true
+    console.log('queryParams.value', queryParams.value)
+    await initLine()
+    await setTotalData()
+    await setChartTotalData()
+    loading.value = false
+  } catch (error) {
+    console.log(error)
+  }
+
+}, {deep: true})
+
+
+// 卡片相关功能
+const state = reactive({
+  homeOne: [
+    {
+      num1: '',
+      compareNum: '',
+      num2: '',
+      cardTitle: '花费',
+      num4: 'fa fa-meetup',
+      // color1: '#FF6462',
+      color2: '--next-color-primary-lighter',
+      color3: '--el-color-primary',
+    },
+    {
+      num1: '',
+      compareNum: '',
+      num2: '',
+      cardTitle: '销售额',
+      num4: 'iconfont icon-ditu',
+      color2: '--next-color-success-lighter',
+      color3: '--el-color-success',
+    },
+    {
+      num1: '',
+      compareNum: '',
+      num2: '',
+      cardTitle: '订单数',
+      num4: 'iconfont icon-zaosheng',
+      color2: '--next-color-warning-lighter',
+      color3: '--el-color-warning',
+    },
+    {
+      num1: '',
+      compareNum: '',
+      num2: '',
+      cardTitle: 'ACOS',
+      num4: 'fa fa-github-alt',
+      color2: '--next-color-danger-lighter',
+      color3: '--el-color-danger',
+    },
+  ],
+
+  myCharts: [],
+  charts: {
+    theme: '',
+    bgColor: '',
+    color: '#303133',
+  },
+})
+
+
+// 折线图相关功能
+const chartRefOne = ref()
+let chartObjOne
+const option = {
+  title: {text: '花费 & 销售额'},
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    data: ['花费', '销售额'],
+    show: true
+  },
+  grid: {
+    top: 50, right: 65, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category',
+    // boundaryGap: false,
+  },
+  yAxis: [
+    {
+      type: 'value',
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3a83f7' // 第一个 Y 轴的颜色
+        }
+      }
+    },
+    {
+      type: 'value',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#f19a37' // 第二个 Y 轴的颜色
+        }
+      }
+    }
+  ],
+  series: [
+    {
+      name: '花费',
+      yAxisIndex: 0,
+      encode: {
+        x: 'Name',
+        y: 'Spend',
+      },
+      type: 'line',
+      smooth: true,
+      itemStyle: {
+        color: '#3a83f7',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(58, 131, 247, 0.5)' // 顶部不透明
+          }, {
+            offset: 0.2, color: 'rgba(58, 131, 247, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(58, 131, 247, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+    {
+      name: '销售额',
+      yAxisIndex: 1,
+      encode: {
+        x: 'Name',
+        y: 'TotalSales',
+      },
+      type: 'line',
+      smooth: true,
+      lineStyle: {
+        type: 'dashed'
+      },
+      itemStyle: {
+        color: '#f19a37',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(241, 154, 55, 0.5)' // 顶部半透明
+          }, {
+            offset: 0.2, color: 'rgba(241, 154, 55, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(241, 154, 55, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+  ]
+}
+
+const chartRefAcos = ref()
+let chartObjAcos
+const option2 = {
+  title: {text: 'Acos'},
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    data: ['Acos'],
+    show: true
+  },
+  grid: {
+    top: 50, right: 65, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category',
+    // boundaryGap: false,
+  },
+  yAxis: [
+    {
+      type: 'value',
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3a83f7'
+        }
+      }
+    }
+  ],
+  series: [
+    {
+      name: 'Acos',
+      yAxisIndex: 0,
+      encode: {
+        x: 'Name',
+        y: 'Acos',
+      },
+      type: 'line',
+      smooth: true,
+      lineStyle: {
+        type: 'dashed'
+      },
+      legendHoverLink: false,
+      itemStyle: {
+        color: '#3a83f7',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(58, 131, 247, 0.5)' // 顶部不透明
+          }, {
+            offset: 0.2, color: 'rgba(58, 131, 247, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(58, 131, 247, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+  ]
+}
+
+const chartRefCPCandCTR = ref()
+let chartObjCPCandCTR
+const option3 = {
+  title: {text: '点击成本 & 点击率'},
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    data: ['点击成本', '点击率'],
+    show: true
+  },
+  grid: {
+    top: 50, right: 65, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category',
+    // boundaryGap: false,
+  },
+  yAxis: [
+    {
+      type: 'value',
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3a83f7' // 第一个 Y 轴的颜色
+        }
+      }
+    },
+    {
+      type: 'value',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#f19a37' // 第二个 Y 轴的颜色
+        }
+      }
+    }
+  ],
+  series: [
+    {
+      name: '点击成本',
+      yAxisIndex: 0,
+      encode: {
+        x: 'Name',
+        y: 'CPC',
+      },
+      type: 'line',
+      smooth: true,
+      itemStyle: {
+        color: '#3a83f7',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(58, 131, 247, 0.5)' // 顶部不透明
+          }, {
+            offset: 0.2, color: 'rgba(58, 131, 247, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(58, 131, 247, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+    {
+      name: '点击率',
+      yAxisIndex: 1,
+      encode: {
+        x: 'Name',
+        y: 'CTR',
+      },
+      type: 'line',
+      smooth: true,
+      itemStyle: {
+        color: '#f19a37',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(241, 154, 55, 0.5)' // 顶部半透明
+          }, {
+            offset: 0.2, color: 'rgba(241, 154, 55, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(241, 154, 55, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+  ]
+}
+
+const chartRefTotalPurchases = ref()
+let chartObjTotalPurchases
+const option4 = {
+  title: {text: '订单数'},
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    data: ['订单数'],
+    show: true
+  },
+  grid: {
+    top: 50, right: 65, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category',
+    // boundaryGap: false,
+  },
+  yAxis: [
+    {
+      type: 'value',
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3a83f7'
+        }
+      }
+    }
+  ],
+  series: [
+    {
+      name: '订单数',
+      yAxisIndex: 0,
+      encode: {
+        x: 'Name',
+        y: 'TotalPurchases',
+      },
+      type: 'line',
+      smooth: true,
+      lineStyle: {
+        type: 'dashed'
+      },
+      legendHoverLink: false,
+      itemStyle: {
+        color: '#3a83f7',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(58, 131, 247, 0.5)' // 顶部不透明
+          }, {
+            offset: 0.2, color: 'rgba(58, 131, 247, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(58, 131, 247, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+  ]
+}
+
+const chartRefImpandCli = ref()
+let chartObjImpandCli
+const option5 = {
+  title: {text: '曝光量 & 点击量'},
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    data: ['曝光量', '点击量'],
+    show: true
+  },
+  grid: {
+    top: 50, right: 65, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category',
+    // boundaryGap: false,
+  },
+  yAxis: [
+    {
+      type: 'value',
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3a83f7' // 第一个 Y 轴的颜色
+        }
+      }
+    },
+    {
+      type: 'value',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#f19a37' // 第二个 Y 轴的颜色
+        }
+      }
+    }
+  ],
+  series: [
+    {
+      name: '曝光量',
+      yAxisIndex: 0,
+      encode: {
+        x: 'Name',
+        y: 'Impression',
+      },
+      type: 'line',
+      smooth: true,
+      itemStyle: {
+        color: '#3a83f7',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(58, 131, 247, 0.5)' // 顶部不透明
+          }, {
+            offset: 0.2, color: 'rgba(58, 131, 247, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(58, 131, 247, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+    {
+      name: '点击量',
+      yAxisIndex: 1,
+      encode: {
+        x: 'Name',
+        y: 'Click',
+      },
+      type: 'line',
+      smooth: true,
+      itemStyle: {
+        color: '#f19a37',
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [{
+            offset: 0, color: 'rgba(241, 154, 55, 0.5)' // 顶部半透明
+          }, {
+            offset: 0.2, color: 'rgba(241, 154, 55, 0.2)' // 中间更透明
+          }, {
+            offset: 1, color: 'rgba(241, 154, 55, 0)' // 底部完全透明
+          }],
+          global: false // 缺省为 false
+        }
+      }
+    },
+  ]
+}
+
+function initLine() {
+  if (chartRefOne.value) {
+    chartObjOne = echarts.init(chartRefOne.value)
+    chartObjAcos = echarts.init(chartRefAcos.value)
+    chartObjCPCandCTR = echarts.init(chartRefCPCandCTR.value)
+    chartObjTotalPurchases = echarts.init(chartRefTotalPurchases.value)
+    chartObjImpandCli = echarts.init(chartRefImpandCli.value)
+  }
+}
+
+function setChartOptions() {
+  nextTick(() => {
+    const chartOptions = [
+      {chart: chartObjOne, option: option},
+      {chart: chartObjAcos, option: option2},
+      {chart: chartObjCPCandCTR, option: option3},
+      {chart: chartObjTotalPurchases, option: option4},
+      {chart: chartObjImpandCli, option: option5}
+    ]
+
+    chartOptions.forEach(chartOption => {
+      try {
+        chartOption.option.dataset.source = optionSource
+        chartOption.chart.setOption(chartOption.option)
+      } catch (error) {
+        console.error('设置图表选项失败:', error)
+      }
+    })
+  })
+}
+
+function resizeChart() {
+  if (chartObjOne) {
+    chartObjOne.resize();
+    chartObjAcos.resize()
+    chartObjCPCandCTR.resize()
+    chartObjTotalPurchases.resize()
+    chartObjImpandCli.resize()
+  }
+}
+
+onMounted(async () => {
+  initLine()
+  window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+  setTimeout(() => {
+    resizeChart()
+  }, 10)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeChart)
+});
+
+</script>
+
+<style scoped lang="scss">
+$homeNavLengh: 8;
+.home-container {
+  overflow: hidden;
+  margin-top: 10px;
+
+  .home-card-one,
+  .home-card-two,
+  .home-card-three {
+    .home-card-item {
+      width: 100%;
+      height: 130px;
+      border-radius: 4px;
+      transition: all ease 0.3s;
+      padding: 20px;
+      overflow: hidden;
+      background: #ffffff;
+      box-shadow: var(--el-box-shadow-light);
+      color: var(--el-text-color-primary);
+      border: 1px solid var(--next-border-color-light);
+
+      &:hover {
+        box-shadow: 0 2px 12px var(--next-color-dark-hover);
+        transition: all ease 0.3s;
+      }
+
+      &-icon {
+        width: 70px;
+        height: 70px;
+        border-radius: 100%;
+        flex-shrink: 1;
+
+        i {
+          color: var(--el-text-color-placeholder);
+        }
+      }
+
+      &-title {
+        font-size: 15px;
+        font-weight: bold;
+        height: 30px;
+      }
+    }
+  }
+
+  .home-card-one {
+    @for $i from 0 through 3 {
+      .home-one-animation#{$i} {
+        opacity: 0;
+        animation-name: error-num;
+        animation-duration: 0.5s;
+        animation-fill-mode: forwards;
+        animation-delay: calc($i/10) + s;
+      }
+    }
+  }
+
+  .home-card-two,
+  .home-card-three {
+    .home-card-item {
+      height: 400px;
+      width: 100%;
+      overflow: hidden;
+
+      .home-monitor {
+        height: 100%;
+
+        .flex-warp-item {
+          width: 25%;
+          height: 111px;
+          display: flex;
+
+          .flex-warp-item-box {
+            margin: auto;
+            text-align: center;
+            color: var(--el-text-color-primary);
+            display: flex;
+            border-radius: 5px;
+            background: var(--next-bg-color);
+            cursor: pointer;
+            transition: all 0.3s ease;
+
+            &:hover {
+              background: var(--el-color-primary-light-9);
+              transition: all 0.3s ease;
+            }
+          }
+
+          @for $i from 0 through $homeNavLengh {
+            .home-animation#{$i} {
+              opacity: 0;
+              animation-name: error-num;
+              animation-duration: 0.5s;
+              animation-fill-mode: forwards;
+              animation-delay: calc($i/10) + s;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.down {
+  margin-top: 10px;
+}
+.l-indent {
+  margin-left: 1px;
+}
+</style>

+ 80 - 0
src/views/adManage/ad-overview/weekly/api.ts

@@ -0,0 +1,80 @@
+import {request} from '/@/utils/service'
+import {UserPageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud'
+import XEUtils from 'xe-utils'
+
+export const apiPrefix = '/api/ad_manage/summary/report/trend/'
+
+export function GetList(query: UserPageQuery) {
+  return request({
+    url: apiPrefix + 'weekly',
+    method: 'get',
+    params: query,
+  })
+}
+
+export function GetObj(id: any) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'get',
+  })
+}
+
+export function AddObj(obj: AddReq) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: obj,
+  })
+}
+
+export function UpdateObj(obj: EditReq) {
+  return request({
+    url: apiPrefix + obj.id + '/',
+    method: 'put',
+    data: obj,
+  })
+}
+
+export function DelObj(id: DelReq) {
+  return request({
+    url: apiPrefix + id + '/',
+    method: 'delete',
+    data: {id},
+  })
+}
+
+export function getCardData(query: UserPageQuery) {
+  return request({
+    url: '/api/ad_manage/summary/report/total',
+    method: 'GET',
+    params: query,
+  })
+}
+
+export function getLineData(query: UserPageQuery) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'weekly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+  query['dateRangeType'] = 'W'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+  query['dateRangeType'] = 'M'
+  return request({
+    url: apiPrefix + 'hourly',
+    method: 'GET',
+    params: query
+  })
+}
+

+ 397 - 0
src/views/adManage/ad-overview/weekly/crud.tsx

@@ -0,0 +1,397 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        showSummary: true,
+        stripe: false,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          borderRight: 'none',
+        },
+        cellStyle:  {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd'
+        },
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: true,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        Name: {
+          title: '日期',
+          column: {
+            width: 130,
+            align: 'left',
+            fixed: 'left',
+            // border: '0.5px solid #ddd',
+            cellStyle: {
+              border: '1px solid #ddd',
+            }
+          },
+        },
+        Spend: {
+          title: '花费',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            border: '0.5px solid #ddd',
+
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top"
+                                content="来自亚马逊广告API,亚马逊系统会在3天内将无效点击从统计数据中删除,因此过去3天内的花费可能会有所变化">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>花费</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalSales: {
+          title: '销售额',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br/>出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售额,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的销售额;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销售额</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ACOS: {
+          title: 'ACOS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告投入产出比,系统计算,广告花费/广告带来的销售额。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ACOS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        ROAS: {
+          title: 'ROAS',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告支出回报,系统计算,广告带来的销售额/广告花费。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>ROAS</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPC: {
+          title: '点击成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="单次点击成本,系统计算,花费/点击量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPA: {
+          title: '订单成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 130,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="平均每笔订单的花费,系统计算,花费/广告订单量">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单成本</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Click: {
+          title: '点击量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被点击的次数。亚马逊系统会在3天内将无效点击去除。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CTR: {
+          title: '点击率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>点击率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalPurchases: {
+          title: '订单数',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      订单数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>订单数</span>
+                  </span>
+              )
+            }
+          }
+        },
+        TotalUnitOrdered: {
+          title: '销量',
+          column: {
+            align: 'center',
+            width: 100,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="
+                      Seller类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。<br />
+                      <br />
+                      Vendor类型店铺:<br />
+                      销售件数,来自亚马逊广告API。<br />
+                      在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+                      亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+                      付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。" raw-content>
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>销量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        Impression: {
+          title: '曝光量',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="来自亚马逊广告API,广告被展示的次数。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>曝光量</span>
+                  </span>
+              )
+            }
+          }
+        },
+        PurchasesRate: {
+          title: '转化率',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="广告转化率,系统计算,广告订单量/点击量*100%,展示型推广vCPM成本类型的广告活动不予计算。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>转化率</span>
+                  </span>
+              )
+            }
+          }
+        },
+        CPM: {
+          title: '千次曝光成本',
+          column: {
+            align: 'center',
+            sortable: true,
+            width: 150,
+            renderHeader() {
+              return (
+                  <span>
+                    <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+                      <span>
+                        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+                      </span>
+                    </el-tooltip>
+                    <span>千次曝光成本</span>
+                  </span>
+              )
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 131 - 0
src/views/adManage/ad-overview/weekly/index.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="overview-tabs">
+    <DateRangePicker v-model="dateRange"></DateRangePicker>
+    <el-select
+        v-model="selectedPortfolios"
+        placeholder="SP"
+        style="width: 200px"
+        collapse-tags
+        collapse-tags-tooltip
+        :max-collapse-tags="3"
+    >
+      <el-option v-for="info of portfolios" :label="info.label" :value="info.value" :disabled="info.disabled"></el-option>
+    </el-select>
+  </div>
+  <fs-page class="fs-page-custom" style="margin-top: -11px">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #header-middle>
+        <el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card">
+          <DataTendencyChart
+              v-if="tabActiveName === 'dataTendency'"
+              :query="queryParams"
+              :fetchCard="getCardData"
+              :fetchLine="getLineData"
+              :fetch-line-month="getLineMonthData"
+              :fetch-line-week="getLineWeekData">
+          </DataTendencyChart>
+        </el-tabs>
+      </template>
+      <template #cell_percentTimeInBudget="scope">
+        <el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0" />
+      </template>
+      <template #cell_campaignName="scope">
+        <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+      </template>
+      <template #cell_MissedImpressions="scope">
+        {{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+      </template>
+      <template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }} </template>
+      <template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }} </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+            :field="field"
+            :value="scope.row[field]"
+            :prev-val="scope.row[`prev${field}`]"
+            :gap-val="scope.row[`gap${field}`]"
+            :date-range="dateRange"
+            :show-compare="showCompare"/>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, Ref, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {useRouter} from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/ad-overview/chartComponents/dataTendency.vue'
+import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const selectedPortfolios = ref('sp')
+const portfolios = [
+  {
+    value: 'sp/sb/sd',
+    label: 'SP/SB/SD'
+  },
+  {
+    value: 'sp',
+    label: 'SP'
+  },
+  {
+    value:'sb',
+    label:'SB'
+  },
+  {
+    value:'sd',
+    label:'SD'
+  },
+  {
+    value: 'dsp',
+    label: 'DSP',
+    disabled: true
+  }
+]
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  dateRange,
+  profileId: profile.value.profile_id,
+  campaignType: selectedPortfolios.value
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const router = useRouter()
+const showCompare = ref(false)
+
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: { campaignId: row.campaignId, tagsViewName: row.campaignName },
+  })
+}
+
+watch(queryParams, async () => {
+  crudExpose.doRefresh()
+}, { deep: true })
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+  flex: none;
+}
+::v-deep(.el-table--border .el-table__footer-wrapper) {
+  border: none;
+}
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+</style>

+ 0 - 24
src/views/adManage/portfolios/PortfoliosSelector.vue

@@ -1,24 +0,0 @@
-<template>
-  <el-select v-model="portfolios" placeholder="广告组合">
-    <el-options v-for="info in portfolios" :label="info.name" :value="info.portfolioId"></el-options>
-  </el-select>
-</template>
-
-<script lang="ts" setup>
-import { ref, onBeforeMount, Ref } from 'vue'
-import { GetList } from './api'
-
-defineOptions({
-  name: 'PortfoliosSelector'
-})
-const portfolios:Ref<portfolios[]> = ref([])
-onBeforeMount(async () => {
-  const resp:APIResponseData = await GetList({ limit: 999 })
-  portfolios.value = resp.data
-})
-
-</script>
-
-<style scoped>
-
-</style>

+ 66 - 27
src/views/adManage/portfolios/api.ts

@@ -1,41 +1,80 @@
-import { request } from '/@/utils/service';
-import { AddReq, DelReq, EditReq, InfoReq, UserPageQuery } from '@fast-crud/fast-crud';
+import { request } from '/@/utils/service'
+import { AddReq, DelReq, EditReq, InfoReq, UserPageQuery } from '@fast-crud/fast-crud'
 
-export const apiPrefix = '/api/ad_manage/portfolios/';
+export const apiPrefix = '/api/ad_manage/portfolios/'
 export function GetList(query: UserPageQuery) {
-    return request({
-        url: apiPrefix,
-        method: 'get',
-        params: query,
-    })
+	return request({
+		url: apiPrefix,
+		method: 'get',
+		params: query,
+	})
+}
+export function GetAllPortfolios() {
+	return request({
+		url: apiPrefix + 'select_list',
+		method: 'get',
+		params: { limit: 999 },
+	})
 }
 export function GetObj(id: InfoReq) {
-    return request({
-        url: apiPrefix + id,
-        method: 'get',
-    });
+	return request({
+		url: apiPrefix + id,
+		method: 'get',
+	})
 }
 
 export function AddObj(obj: AddReq) {
-    return request({
-        url: apiPrefix,
-        method: 'post',
-        data: obj,
-    });
+	return request({
+		url: apiPrefix,
+		method: 'post',
+		data: obj,
+	})
 }
 
 export function UpdateObj(obj: EditReq) {
-    return request({
-        url: apiPrefix + obj.id + '/',
-        method: 'put',
-        data: obj,
-    });
+	return request({
+		url: apiPrefix + obj.id + '/',
+		method: 'put',
+		data: obj,
+	})
 }
 
 export function DelObj(id: DelReq) {
-    return request({
-        url: apiPrefix + id + '/',
-        method: 'delete',
-        data: { id },
-    });
+	return request({
+		url: apiPrefix + id + '/',
+		method: 'delete',
+		data: { id },
+	})
+}
+
+export function getCardData(query: UserPageQuery) {
+	return request({
+		url: apiPrefix + 'report/amount',
+		method: 'GET',
+		params: query,
+	})
+}
+
+export function getLineData(query: UserPageQuery) {
+	return request({
+		url: apiPrefix + 'report/trend/daily',
+		method: 'GET',
+		params: query,
+	})
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+	return request({
+		url: apiPrefix + 'report/trend/weekly',
+		method: 'GET',
+		params: query,
+	})
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+	return request({
+		url: apiPrefix + 'report/trend/monthly',
+		method: 'GET',
+		params: query,
+	})
 }

+ 88 - 72
src/views/adManage/portfolios/crud.tsx

@@ -1,26 +1,25 @@
-import * as api from './api';
-import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
-import { inject, nextTick, ref } from 'vue';
-import { successMessage } from '/@/utils/message';
-import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js';
+import * as api from './api'
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject, nextTick, ref } from 'vue'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
 
 export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
 	const pageRequest = async (query: UserPageQuery) => {
-		return await api.GetList(query);
-	};
+		return await api.GetList(query)
+	}
 	const editRequest = async ({ form, row }: EditReq) => {
-		form.id = row.id;
-		return await api.UpdateObj(form);
-	};
+		form.id = row.id
+		return await api.UpdateObj(form)
+	}
 	const delRequest = async ({ row }: DelReq) => {
-		return await api.DelObj(row.id);
-	};
+		return await api.DelObj(row.id)
+	}
 	const addRequest = async ({ form }: AddReq) => {
-		return await api.AddObj(form);
-	};
+		return await api.AddObj(form)
+	}
 
 	//权限判定
-	const hasPermissions = inject('$hasPermissions');
+	const hasPermissions = inject('$hasPermissions')
 
 	return {
 		crudOptions: {
@@ -28,8 +27,8 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 				height: 800,
 			},
 			container: {
-        fixedHeight: false
-      },
+				fixedHeight: false,
+			},
 			request: {
 				pageRequest,
 				addRequest,
@@ -46,13 +45,13 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					edit: {
 						iconRight: 'Edit',
 						type: 'text',
-            text: null
+						text: null,
 						// show: hasPermissions('dictionary:Update'),
 					},
 					remove: {
 						iconRight: 'Delete',
 						type: 'text',
-            text: null
+						text: null,
 						// show: hasPermissions('dictionary:Delete'),
 					},
 					// custom: {
@@ -75,6 +74,23 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					// },
 				},
 			},
+			actionbar: {
+				buttons: { 
+					add: {
+						text: '新建广告组合'
+					}
+				}
+			},
+			toolbar: {
+				buttons: {
+					search: {
+						show: true,
+					},
+					compact: {
+						show: false,
+					},
+				},
+			},
 			columns: {
 				// _index: {
 				// 	title: '序号',
@@ -93,36 +109,36 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 				// 		},
 				// 	},
 				// },
-        name: {
-          title: '广告组合',
-          column: {
-            width: '150px'
-          },
+				name: {
+					title: '广告组合',
+					column: {
+						width: '150px',
+					},
 					search: {
 						show: true,
 						component: {
 							props: {
-								clearable: true
-							}
-						}
+								clearable: true,
+							},
+						},
 					},
 					form: {
-						rules: [{required: true, message:'必填项'}]
-					}
-        },
-        state: {
-          title: '状态',
-          type: 'dict-select',
-          dict: dict({
-            data:[
-              {value:'enabled', label:'投放中'},
-              {value:'disable', label:'禁用'},
-            ] 
-          }),
+						rules: [{ required: true, message: '必填项' }],
+					},
+				},
+				state: {
+					title: '状态',
+					type: 'dict-select',
+					dict: dict({
+						data: [
+							{ value: 'enabled', label: '投放中' },
+							{ value: 'disable', label: '禁用' },
+						],
+					}),
 					form: {
-						show: false
-					}
-        },
+						show: false,
+					},
+				},
 				budget_policy: {
 					title: '预算类型',
 					type: 'dict-select',
@@ -131,49 +147,49 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 							{ value: '', label: '无预算上限' },
 							{ value: 'dateRange', label: '日期范围' },
 							{ value: 'monthlyRecurring', label: '按月' },
-						]
+						],
 					}),
 					form: {
-						value: ''
-					}
+						value: '',
+					},
 				},
-        budget_startDate: {
-          title: '开始日期',
+				budget_startDate: {
+					title: '开始日期',
 					type: 'date',
 					form: {
-						show: compute(context => context.form.budget_policy === "dateRange"),
-						rules: [{required: true, message:'必填项'}]
-					}
-        },
-        budget_endDate: {
-          title: '结束日期',
+						show: compute((context) => context.form.budget_policy === 'dateRange'),
+						rules: [{ required: true, message: '必填项' }],
+					},
+				},
+				budget_endDate: {
+					title: '结束日期',
 					type: 'date',
 					form: {
-						show: compute(context => context.form.budget_policy !== '')
-					}
-        },
-        budget_amount: {
-          title: '预算',
+						show: compute((context) => context.form.budget_policy !== ''),
+					},
+				},
+				budget_amount: {
+					title: '预算',
 					type: 'number',
 					form: {
 						value: 0,
-						show: compute(context => context.form.budget_policy !== ''),
-						rules: [{required: true, message:'必填项'}],
+						show: compute((context) => context.form.budget_policy !== ''),
+						rules: [{ required: true, message: '必填项' }],
 						component: {
 							min: 0,
 							precision: 2,
-							controlsPosition: "right"
-						}
-					}
-        },
-        inBudget: {
-          title: '是否预算内',
+							controlsPosition: 'right',
+						},
+					},
+				},
+				inBudget: {
+					title: '是否预算内',
 					form: {
-						show: false
-					}
-        },
-        ...BaseColumn
+						show: false,
+					},
+				},
+				...BaseColumn,
 			},
 		},
-	};
-};
+	}
+}

+ 30 - 5
src/views/adManage/portfolios/index.vue

@@ -1,12 +1,28 @@
 <template>
 	<div class="asj-container">
-		<div class="public-search">
-			<DateRangePicker v-model="dateRange" timezone="America/Los_Angeles" ></DateRangePicker>
-		</div>
 		<fs-page class="fs-page-custom">
 			<fs-crud ref="crudRef" v-bind="crudBinding">
+				<template #search-left>
+					<DateRangePicker v-model="dateRange" timezone="America/Los_Angeles"></DateRangePicker>
+				</template>
 				<template #header-middle>
-					<el-card style="height: 500px;">此处用于显示可视化图形</el-card>
+					<el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card">
+						<el-tab-pane label="数据趋势" name="dataTendency">
+							<DataTendencyChart 
+								v-if="tabActiveName === 'dataTendency'" 
+								:fetch-card="getCardData" 
+								:fetch-line="getLineData"
+								:fetch-line-month="getLineMonthData"
+								:fetch-line-week="getLineWeekData">
+							</DataTendencyChart>
+						</el-tab-pane>
+						<el-tab-pane label="广告结构" name="adStruct" >
+							<!-- <AdStructChart v-if="tabActiveName === 'adStruct'"/> -->
+						</el-tab-pane>
+						<el-tab-pane label="散点视图" name="scatterView">
+							<div v-if="tabActiveName === 'scatterView'">散点视图</div>
+						</el-tab-pane>
+					</el-tabs>
 				</template>
 			</fs-crud>
 		</fs-page>
@@ -18,7 +34,16 @@ import { ref, onMounted } from 'vue';
 import { useFs } from '@fast-crud/fast-crud';
 import { createCrudOptions } from './crud';
 import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+import { getCardData, getLineData, getLineMonthData, getLineWeekData } from './api'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
 
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const tabActiveName = ref("dataTendency")
 const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
 
 // 页面打开后获取列表数据
@@ -26,7 +51,7 @@ onMounted(() => {
 	crudExpose.doRefresh();
 });
 
-const dateRange = ref([])
+
 
 </script>
 

+ 0 - 25
src/views/adManage/sb/campaigns/CreateCampaigns/adFormat/CommoditySet.vue

@@ -1,25 +0,0 @@
-<template>
-    <div class="container" style="margin-top: 20px">
-        <p style="font-weight: bold">需要帮助创建图片或品牌旗舰店?</p>
-        <p style="color: dodgerblue">深入了解广告创意服务(链接未完成)</p>
-        <div>
-            <el-radio-group v-model="radio1">
-                <el-radio label="1" size="large" border>
-                    Option A
-                </el-radio>
-                <el-radio label="2" size="large" border>Option B</el-radio>
-            </el-radio-group>
-        </div>
-    </div>
-</template>
-
-<script lang="ts" setup>
-
-import { ref } from 'vue'
-const radio1 = ref('1')
-
-</script>
-
-<style scoped>
-
-</style>

+ 160 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/api/index.ts

@@ -0,0 +1,160 @@
+import { request } from '/@/utils/service'
+
+
+export function getAdMixSelect() {
+  return request({
+      url: '/api/ad_manage/portfolios/select_list',
+      method: 'GET',
+  })
+}
+
+export function postCampaignsData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbcampaigns/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postGroupData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbgroups/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postNegativeWordData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sptargets/add/negative/keywords/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function getAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getLifeStyleAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getBrands(query) {
+  return request({
+    url: '/api/ad_manage/sb/getbrands/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getStoreurl(query) {
+  return request({
+    url: '/api/ad_manage/sb/storeurl/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getPageAsins(query) {
+  return request({
+    url: '/api/ad_manage/sb/getpageasins/',
+      method: 'get',
+      params: query
+  })
+}
+export function getCommodityCard(query) {
+  return request({
+    url: '/api/sellers/listings/all/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getVideoAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function videoDetailCreate(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/video/create/',
+      method: 'post',
+      data: obj,
+  })
+}
+
+export function uploadFile(obj) {
+  return request({
+      url: '/api/ad_manage/assets/upload/',
+      method: 'post',
+      data: obj,
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+  })
+}
+
+export function checkAsset(obj) {
+  return request({
+      url: '/api/ad_manage/assets/checkasset/',
+      method: 'post',
+      data: obj,
+  })
+}
+
+export function getDefaultSpotlightAsin(query) {
+  return request({
+      url: '/api/ad_manage/sb/defaultspotlightasin/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getSellerInStock(obj) {
+  return request({
+      url: '/api/sellers/listings/sellerinstock/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postStoreSpotlight(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/storespotlight/create/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postBrandVideo(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/brandvideo/create/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postVideo(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/video/create/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postProductset(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/productcollection/create/',
+      method: 'post',
+      data: obj
+  })
+}
+

+ 335 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdCampaign.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;" v-loading="campaignLoading">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">设置</span>
+      </div>
+      <el-form
+        ref="campaignRuleFormRef"
+        :model="campaignRuleForm"
+        :rules="campaignRules"
+        label-position="top"
+        label-width="120px"
+        class="demo-ruleForm"
+        :size="formSize"
+        status-icon>
+        <div class="flex-between">
+          <el-form-item label="广告活动名称" prop="campaignName" style="width: 48%">
+            <el-input v-model="campaignRuleForm.campaignName" placeholder="请输入广告活动名称" />
+          </el-form-item>
+          <el-form-item label="广告组合" prop="adMix" style="width: 48%">
+            <el-select v-model="campaignRuleForm.adMix" placeholder="请选择" style="width: 100%">
+              <el-option v-for="item in adMixOptions" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </div>
+        <div class="flex-between">
+          <div class="flex-between" style="width: 48%">
+            <el-form-item label="开始时间" prop="startDate" style="width: 49%">
+              <el-date-picker
+                v-model="campaignRuleForm.startDate"
+                type="date"
+                label="Pick a date"
+                placeholder="开始时间"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+                style="width: 100%" />
+            </el-form-item>
+
+            <el-form-item label="结束时间" prop="endDate" style="width: 49%">
+              <el-date-picker
+                v-model="campaignRuleForm.endDate"
+                type="date"
+                label="Pick a date"
+                placeholder="开始时间"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+                style="width: 100%" />
+            </el-form-item>
+          </div>
+          <div class="flex-between" style="width: 48%">
+            <el-form-item label="预算" required prop="budget" style="width: 65%">
+              <el-input v-model="campaignRuleForm.budget" minlength="1" maxlength="7" placeholder="请输入" style="width: 100%">
+                <template #prepend>$</template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="频率" prop="frequency" style="width: 34%">
+              <el-select v-model="campaignRuleForm.frequency" placeholder="请选择" style="width: 100%">
+                <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
+              </el-select>
+            </el-form-item>
+          </div>
+        </div>
+        <el-form-item label="品牌" prop="brand" required style="width: 48%">
+          <el-select v-model="campaignRuleForm.brand" placeholder="请选择" style="width: 100%">
+            <el-option v-for="item in brandOptions" :key="item.brandId" :label="item.brandRegistryName" :value="item.brandEntityId" />
+          </el-select>
+        </el-form-item>
+        <div style="font-weight: 700; padding-bottom: 18px">
+          <span style="color: #306cd7; font-size: 26px">|</span>
+          <span style="font-size: 18px; padding-left: 5px">竞价</span>
+        </div>
+        <el-form-item label="自动竞价" style="margin-bottom: -13px">
+          <el-form-item>
+            <el-switch v-model="campaignRuleForm.isBid" />
+            <span style="margin-left: 10px; color: #88909b">允许亚马逊自动优化搜索结果首页以外的广告位竞价</span>
+          </el-form-item>
+        </el-form-item>
+        <div style="width: 55%" v-if="campaignRuleForm.isBid == false">
+          <el-card shadow="never" body-style="padding: 10px 10px 5px 10px;">
+            <div style="margin-bottom: 10px; font-weight: 500">
+              <span style="color:#f56c6c;margin-right: 4px;">*</span>
+              展示位置出价调整
+            </div>
+            <div style="display: flex; align-items: center">
+              <div class="left">
+                <div class="title">商品页面</div>
+                <div class="tip">产品详情页面为顾客提供在亚马逊所售卖商品的详情信息</div>
+              </div>
+              <el-form-item prop="commodityPage" style="margin-bottom: 0px !important; width: 37%;">
+                <el-input v-model="campaignRuleForm.commodityPage" maxlength="3" placeholder="-99 ~ 900">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="display: flex; align-items: center; margin-top: 10px">
+              <div class="left">
+                <div class="title">搜索结果顶部(首页)</div>
+                <div class="tip">亚马逊首页 http://www.amazon.com</div>
+              </div>
+              <el-form-item prop="firstPage" style="margin-bottom: 0px !important; width: 37%">
+                <el-input v-model="campaignRuleForm.firstPage" maxlength="3" placeholder="-99 ~ 900" style="width: 100%">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="display: flex; align-items: center; margin-top: 10px">
+              <div class="left">
+                <div class="title">搜索结果的其余位置</div>
+                <div class="tip">其他位置集合, 例如搜索页</div>
+              </div>
+              <el-form-item prop="otherPlace" style="margin-bottom: 0px !important; width: 37%">
+                <el-input v-model="campaignRuleForm.otherPlace" maxlength="3" placeholder="-99 ~ 900">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="color: #8d9095; padding-left: 60%; margin-top: 10px">示例: $5.00 竞价降低 40% 将变为 $3.00</div>
+          </el-card>
+        </div>
+        <el-form-item style="margin: 20px 0 -10px 48%">
+          <el-button type="primary" plain @click="submitCampaignForm(campaignRuleFormRef)">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { defineEmits, onMounted, reactive, ref, watch } from 'vue'
+import { getAdMixSelect, getBrands, postCampaignsData } from '../api/index'
+import { useShopInfo } from '/@/stores/shopInfo'
+import emitter from '/@/utils/emitter'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const formSize = ref('default')
+const campaignRuleFormRef = ref<FormInstance>()
+interface campaignRuleForm {
+  campaignName: string
+  adMix: string
+  startDate: string
+  endDate: string
+  budget: string
+  frequency: string
+  brand: string
+  isBid: boolean
+  commodityPage: string
+  otherPlace: string
+  firstPage: string
+}
+const campaignRuleForm = reactive<campaignRuleForm>({
+  campaignName: '',
+  adMix: '',
+  startDate: '',
+  endDate: '',
+  budget: '',
+  frequency: 'DAILY',
+  brand: '',
+  isBid: false,
+  commodityPage: '',
+  otherPlace: '',
+  firstPage: '',
+})
+const campaignRules = reactive<FormRules<campaignRuleForm>>({
+  campaignName: [{ required: true, message: '请输入广告活动', trigger: 'blur' }],
+  startDate: [{ type: 'date', required: true, message: '请选择时间', trigger: 'blur' }],
+  budget: [
+    { required: true, message: '请输入预算', trigger: 'blur' },
+    { pattern: /^(?:[1-9]\d{0,5}|1000000)(?:\.\d{1,2})?$/, message: '预算必须是1到1000000之间的数字,小数点后最多两位', trigger: 'blur' },
+  ],
+  commodityPage: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+  otherPlace: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+  firstPage: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+})
+const frequencyOptions = [
+  {
+    value: 'DAILY',
+    label: '每日',
+  },
+  {
+    value: 'lifeCycle',
+    label: '生命周期',
+    disabled: true,
+  },
+]
+const brandOptions = ref([])
+
+const submitCampaignForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createCampaigns()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+async function getBrandOption() {
+  const response = await getBrands({ profile_id: profile.value.profile_id })
+  brandOptions.value = response.data
+}
+
+const adMixOptions = ref([])
+async function getAdMix() {
+  try {
+    const response = await getAdMixSelect()
+    adMixOptions.value = response.data.map((option) => {
+      return {
+        value: option.portfolioId,
+        label: option.name,
+      }
+    })
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+const campaignLoading = ref(false)
+const prevCampaignId = ref('')
+const prevCampaignName = ref('')
+const respCampaignId = ref('')
+const respCampaignName = ref('')
+
+async function createCampaigns() {
+  campaignLoading.value = true
+
+  // 构建基础请求体
+  const campaignData = {
+    profile_id: profile.value.profile_id,
+    budget: campaignRuleForm.budget,
+    budgetType: campaignRuleForm.frequency,
+    name: campaignRuleForm.campaignName,
+    brandEntityId: campaignRuleForm.brand,
+    bidOptimization: campaignRuleForm.isBid,
+    bidOptimizationStrategy: '',
+    startDate: campaignRuleForm.startDate,
+    endDate: campaignRuleForm.endDate,
+    smartDefault: 'MANUAL',
+    costType: 'CPC',
+    goal: 'PAGE_VISIT',
+    state: 'PAUSED',
+    ...(campaignRuleForm.firstPage && { h_percentage: campaignRuleForm.firstPage }),
+    ...(campaignRuleForm.commodityPage && { d_percentage: campaignRuleForm.commodityPage }),
+    ...(campaignRuleForm.otherPlace && { o_percentage: campaignRuleForm.otherPlace }),
+    ...(campaignRuleForm.adMix && { portfolioId: campaignRuleForm.adMix }),
+  }
+
+  try {
+    const response = await postCampaignsData(campaignData)
+
+    respCampaignId.value = response.data.campaignId
+    respCampaignName.value = response.data.campaignName
+
+    if (response.data.campaignId) {
+      // 如果创建成功,更新 respCampaignId 和 respCampaignName,同时保留上一次成功的值
+      prevCampaignId.value = respCampaignId.value
+      prevCampaignName.value = respCampaignName.value
+      respCampaignId.value = response.data.campaignId
+      respCampaignName.value = response.data.campaignName
+      ElMessage({ message: '广告活动创建成功', type: 'success' })
+    } else {
+      // 如果创建失败,检查是否有上一次成功的值,如果有就保留
+      if (prevCampaignId.value) {
+        respCampaignId.value = prevCampaignId.value
+        respCampaignName.value = prevCampaignName.value
+      }
+      ElMessage.error('广告活动创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    campaignLoading.value = false
+  }
+}
+
+const emit = defineEmits(['update-campaign'])
+
+watch([respCampaignId, respCampaignName], () => {
+  if (respCampaignId.value && respCampaignName.value) {
+    emit('update-campaign', {
+      id: respCampaignId.value,
+      name: respCampaignName.value,
+    })
+  }
+})
+
+watch(
+  () => campaignRuleForm.brand,
+  () => {
+    emitter.emit('brandEntityId', brandOptions.value)
+  }
+)
+
+onMounted(() => {
+  getAdMix()
+  getBrandOption()
+})
+</script>
+
+<style scoped>
+.flex-between {
+  display: flex;
+  justify-content: space-between;
+}
+.left {
+  margin-right: 12px;
+  width: 60%;
+}
+.title {
+  font-size: 14px;
+  line-height: 20px;
+  color: #1d2129;
+}
+.tip {
+  font-size: 14px;
+  line-height: 20px;
+  color: #4e5969;
+}
+</style>

+ 366 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue

@@ -0,0 +1,366 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 20px 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">广告格式</span>
+      </div>
+      <div class="ad-format-radios">
+        <el-radio-group v-model="adFormatRadio" style="display: flex; justify-content: space-between">
+          <el-radio class="ad-format-radio" label="productSet" border>
+            <div style="text-align: center; color: #333333">商品集</div>
+            <img src="src/views/adManage/sb/campaigns/CreateCampaigns/img/img_1.jpg" class="img-style">
+            <!-- <div style="background-color: #1e2128; width: 200px; height: 200px; margin: 0 auto"></div> -->
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">使用图片将流量引导至商品详情页面, 以推广多件商品</div>
+          </el-radio>
+          <el-radio class="ad-format-radio" label="focus" border>
+            <div style="text-align: center; color: #333333">品牌旗舰店焦点</div>
+            <div style="background-color: #4c649d; width: 200px; height: 200px; margin: 0 auto"></div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">将流量引流到品牌旗舰店, 包括子页面</div>
+          </el-radio>
+          <el-radio class="ad-format-radio" label="video" border>
+            <div style="text-align: center; color: #333333">视频</div>
+            <div style="background-color: #43abc3; width: 200px; height: 200px; margin: 0 auto"></div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">
+              使用视频宣传您的品牌或产品, 将流量吸引至您的品牌旗舰店或商品详情页
+            </div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+      <div class="customize-font" v-if="adFormatRadio === 'productSet'">需要帮助创建图片或品牌旗舰店?</div>
+      <div class="customize-font" v-if="adFormatRadio === 'focus'">在创建或编辑品牌旗舰店时需要帮助?</div>
+      <div class="customize-font" v-if="adFormatRadio === 'video'">在创建或编辑视频时需要帮助?</div>
+
+      <div style="display: flex; align-items: center; margin: 20px 0 5px 0">
+        <div style="color: #616266; font-weight: 450">着陆页</div>
+        <el-tooltip content="顾客在与您的广告互动后将被引导至着陆页" placement="top">
+          <el-icon color="#616266"><InfoFilled /></el-icon>
+        </el-tooltip>
+      </div>
+
+      <div class="land-Page" v-if="adFormatRadio === 'productSet' || adFormatRadio === 'video'">
+        <el-radio-group v-model="arrivalsRadio" style="display: flex; justify-content: space-between">
+          <el-radio label="flagshipStore" border style="height: auto; width: 100%; flex: 2; align-items: flex-start; padding: 15px 10px 0 10px">
+            <div>亚马逊品牌旗舰店(包括子页面)</div>
+            <div>
+              <el-form
+                ref="ruleFormRef"
+                :model="ruleForm"
+                :rules="rules"
+                label-position="top"
+                label-width="120px"
+                class="demo-ruleForm"
+                size="default"
+                status-icon>
+                <div style="display: flex; margin-top: 10px">
+                  <el-form-item label="选择一个店铺" prop="shop" style="width: 48%; margin-right: 10px">
+                    <el-select
+                      v-model="ruleForm.shop"
+                      clearable
+                      style="width: 100%"
+                      @change="shopChanged"
+                      @blur="validateField('shop')"
+                      :disabled="arrivalsRadio == 'newArrivals' || arrivalsRadio == 'productDetailsPage'">
+                      <el-option v-for="item in shopOptions" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item label="选择一个页面" prop="page" style="width: 48%">
+                    <el-select
+                      v-model="ruleForm.page"
+                      clearable
+                      style="width: 100%"
+                      @blur="validateField('page')"
+                      :disabled="arrivalsRadio == 'newArrivals' || arrivalsRadio == 'productDetailsPage'">
+                      <el-option v-for="item in pageOptions" :key="item.storePageId" :label="item.storePageName" :value="item.storePageUrl" />
+                    </el-select>
+                  </el-form-item>
+                </div>
+              </el-form>
+            </div>
+          </el-radio>
+          <el-radio class="land-page-radio" label="newArrivals" border v-if="adFormatRadio === 'productSet'">
+            <div>新着陆页</div>
+            <div>选择要推广的商品, 我们将为您创建一个落地页</div>
+          </el-radio>
+          <el-radio class="land-page-radio" label="productDetailsPage" border v-if="adFormatRadio === 'video'">
+            <div>商品详情页</div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+
+      <div v-if="adFormatRadio === 'focus'">
+        <p style="padding: 10px 0 15px 0">亚马逊上的品牌旗舰店(必须有4个或更多页面, 每个页面有1个或更多独特的商品)</p>
+        <el-form
+          ref="flagshipStoreRuleFormRef2"
+          :model="flagshipStoreRuleForm2"
+          :rules="flagshipStoreRules2"
+          label-position="top"
+          label-width="120px"
+          class="demo-ruleForm"
+          size="default"
+          status-icon>
+          <el-form-item label="选择一个店铺" prop="focusShop">
+            <el-select
+              v-model="flagshipStoreRuleForm2.focusShop"
+              clearable
+              @blur="validateField2('focusShop')"
+              style="padding-top: 10px; margin-top: -15px; width: 500px">
+              <el-option v-for="item in focusShopOptions" :key="item.brandId" :label="item.brandRegistryName" :value="item.brandEntityId" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <div>
+        <div
+          style="font-weight: 700; padding: 20px 0 10px 0"
+          v-if="
+            (adFormatRadio === 'productSet' && arrivalsRadio === 'newArrivals') ||
+            (adFormatRadio === 'video' && arrivalsRadio === 'productDetailsPage')
+          ">
+          <span style="color: #306cd7; font-size: 26px">|</span>
+          <span style="font-size: 18px; padding-left: 5px">商品</span>
+        </div>
+        <ProductSetCommodity v-if="adFormatRadio === 'productSet' && arrivalsRadio === 'newArrivals'"></ProductSetCommodity>
+        <VideoCommodity
+          @update-added-data="handleUpdateAddedData"
+          v-if="adFormatRadio === 'video' && arrivalsRadio === 'productDetailsPage'"></VideoCommodity>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { FormInstance, FormRules } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { defineEmits, onMounted, reactive, ref, watch } from 'vue'
+import { getBrands, getStoreurl } from '../api/index'
+import ProductSetCommodity from '../component/ProductSetCommodity.vue'
+import VideoCommodity from '../component/VideoCommodity.vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import emitter from '/@/utils/emitter'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const adFormatRadio = ref('productSet')
+const arrivalsRadio = ref('flagshipStore')
+const ruleFormRef = ref<FormInstance>()
+interface RuleForm {
+  shop: string
+  page: string
+}
+const ruleForm = reactive<RuleForm>({
+  shop: '',
+  page: '',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  shop: [{ required: true, message: '请选择', trigger: 'change' }],
+  page: [{ required: true, message: '请选择', trigger: 'change' }],
+})
+const validateField = (fieldName) => {
+  ruleFormRef.value.validateField(fieldName, () => {})
+}
+
+const flagshipStoreRuleFormRef2 = ref<FormInstance>()
+interface flagshipStoreRuleForm2 {
+  focusShop: string
+}
+const flagshipStoreRuleForm2 = reactive<flagshipStoreRuleForm2>({
+  focusShop: '',
+})
+const flagshipStoreRules2 = reactive<FormRules<flagshipStoreRuleForm2>>({
+  focusShop: [{ required: true, message: '请选择', trigger: 'change' }],
+})
+const validateField2 = (fieldName) => {
+  flagshipStoreRuleFormRef2.value.validateField(fieldName, () => {})
+}
+
+const shopOptions = ref([])
+const pageOptions = ref([])
+const focusShopOptions = ref([])
+
+async function getShopOptions() {
+  try {
+    const response = await getBrands({ profile_id: profile.value.profile_id })
+    const shopOption = response.data.map((item) => {
+      return {
+        value: item.brandRegistryName,
+        label: 'ZOSI',
+      }
+    })
+    focusShopOptions.value = response.data
+    shopOptions.value = shopOption
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+watch(
+  () => ruleForm.shop,
+  () => {
+    setTimeout(() => {
+      emitter.emit('video-shop', focusShopOptions.value[0])
+    }, 2000)
+  },
+  { deep: true }
+)
+
+watch(
+  () => flagshipStoreRuleForm2.focusShop,
+  () => {
+    setTimeout(() => {
+      emitter.emit('spotlight-shop', focusShopOptions.value[0])
+    }, 2000)
+  }
+)
+
+async function getPageOptions() {
+  try {
+    const response = await getStoreurl({ profile_id: profile.value.profile_id })
+    pageOptions.value = response.data.storePageInfo
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+let selectedPage: any = ''
+
+watch(
+  () => ruleForm.page,
+  (newPageValue) => {
+    selectedPage = pageOptions.value.find((page) => page.storePageUrl === newPageValue)
+    if (selectedPage) {
+      setTimeout(() => {
+        emitter.emit('page', selectedPage.storePageUrl)
+      }, 2000)
+    } else {
+      console.log('No page selected or matching page not found')
+    }
+  }
+)
+
+const emit = defineEmits([
+  'update:adFormatRadio',
+  'update:arrivalsRadio',
+  'update:flagshipStoreShop',
+  'update:pageOptions',
+  'update:addedTableData',
+  'update:focusShopSelect',
+])
+
+function handleUpdateAddedData(data) {
+  emit('update:addedTableData', data)
+}
+// 监听 adFormatRadio 的变化并触发事件
+watch(
+  adFormatRadio,
+  (newValue) => {
+    emit('update:adFormatRadio', newValue)
+  },
+  { immediate: true }
+)
+
+watch(
+  arrivalsRadio,
+  (newValue) => {
+    emit('update:arrivalsRadio', newValue)
+  },
+  { immediate: true }
+)
+
+watch(
+  () => ruleForm.shop,
+  (newValue) => {
+    emit('update:flagshipStoreShop', newValue)
+    if (newValue === 'ZOSI') {
+      getPageOptions()
+    }
+    if (!ruleForm.shop) {
+      ruleForm.page = ''
+      pageOptions.value = []
+    }
+  }
+)
+
+watch(
+  () => ruleForm.page,
+  (newValue) => {
+    emit('update:pageOptions', newValue)
+  }
+)
+
+watch(
+  () => flagshipStoreRuleForm2.focusShop,
+  (newValue) => {
+    emit('update:focusShopSelect', newValue)
+  },
+  { deep: true }
+)
+
+function shopChanged() {
+  setTimeout(() => {
+    emitter.emit('send-brandEntityId', { brandEntityId: focusShopOptions.value })
+  }, 2000)
+}
+
+watch([adFormatRadio, arrivalsRadio], () => {
+  ruleForm.shop = ''
+  ruleForm.page = ''
+
+  if (ruleFormRef.value) {
+    ruleFormRef.value.clearValidate(['shop', 'page'])
+  }
+})
+
+onMounted(() => {
+  getShopOptions()
+})
+</script>
+
+<style scoped>
+.customize-font {
+  color: #1e2128;
+  font-weight: 600;
+  margin-top: 10px;
+}
+.ad-format-radio {
+  height: auto;
+  width: 100%;
+  flex: 1;
+  flex-direction: column-reverse;
+  align-items: center;
+}
+.land-page-radio {
+  height: 132px;
+  flex: 1;
+  align-items: flex-start;
+  padding: 15px 10px 0 10px;
+}
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.ad-format-radios .el-radio-group .el-radio__inner) {
+  /* 广告格式单选按钮 */
+  margin-bottom: 3px;
+}
+
+::v-deep(.land-Page .el-radio-group .el-radio__inner) {
+  /* 着陆页单选按钮 */
+  margin-top: 3px;
+}
+::v-deep(.land-Page .el-radio__label) {
+  width: 100%;
+}
+
+::v-deep(.ad-format-radios label.el-radio.is-bordered.is-checked.el-radio--default) {
+  background: #f5f7fe;
+}
+::v-deep(.land-Page label.el-radio.is-bordered.is-checked.el-radio--default) {
+  background: #f5f7fe;
+}
+.img-style {
+  width: 200px;
+  height: 200px;
+  margin: 0 auto;
+}
+</style>

+ 111 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdGroup.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;" v-loading="groupLoading">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">广告组</span>
+      </div>
+      <el-form
+        ref="groupRuleFormRef"
+        :model="groupRuleForm"
+        :rules="groupRules"
+        label-position="left"
+        label-width="120px"
+        class="demo-ruleForm"
+        :size="formSize"
+        status-icon>
+        <el-form-item label="广告组名称" prop="groupName">
+          <el-input v-model="groupRuleForm.groupName" style="width: 600px" placeholder="请输入广告组名称" />
+          <el-button type="primary" plain :disabled="!respCampaignId" @click="submitGroupForm(groupRuleFormRef)" style="margin-left: 30px">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, watch, Ref } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { postGroupData } from '../api/index'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respCampaignName = inject<Ref>('respCampaignName')
+
+const groupLoading = ref(false)
+
+const formSize = ref('default')
+const groupRuleFormRef = ref<FormInstance>()
+interface groupRuleForm {
+  groupName: string
+}
+const groupRuleForm = reactive<groupRuleForm>({
+  groupName: '',
+})
+const groupRules = reactive<FormRules<groupRuleForm>>({
+  groupName: [{ required: true, message: '请输入广告活动', trigger: 'blur' }],
+})
+
+const submitGroupForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createGroups()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const respAdGroupId = ref('')
+async function createGroups() {
+  groupLoading.value = true
+  const groupData = {
+    profile_id: profile.value.profile_id,
+    campaignId: respCampaignId.value,
+    name: respCampaignName.value,
+  }
+
+  const filteredRequestData = Object.fromEntries(Object.entries(groupData).filter(([_, v]) => v != null))
+  try {
+    const response = await postGroupData(filteredRequestData)
+    respAdGroupId.value = response.data.adGroupId
+    ElMessage({
+      message: '广告组创建成功',
+      type: 'success',
+    })
+    groupRuleForm.groupName = ''
+  } catch (error) {
+    ElMessage.error('广告组创建失败!')
+    console.error('请求失败:', error)
+  } finally {
+    groupLoading.value = false
+  }
+}
+
+const emit = defineEmits(['update-groupId'])
+
+watch(respAdGroupId, () => {
+  if (respAdGroupId.value) {
+    emit('update-groupId', {
+      id: respAdGroupId.value,
+    })
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.el-form-item__label) {
+  font-weight: 500;
+  color: #505968;
+}
+</style>

+ 60 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/DeliveryType.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 20px 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">投放类型</span>
+      </div>
+      <div class="delivery-type-radio-group">
+        <el-radio-group v-model="deliveryTypeRadio" style="display: flex; justify-content: space-between">
+          <el-radio class="delivery-type-radio" label="keyword" border>
+            <div>关键词定向</div>
+            <div style="color: #88909b; font-weight: 400; padding-top: 5px;">选择关键词以帮助您的商品出现在购物者搜索中</div>
+          </el-radio>
+          <el-radio class="delivery-type-radio" label="commodity" border>
+            <div>商品定向</div>
+            <div style="color: #88909b; font-weight: 400; padding-top: 5px;">选择要在亚马逊上投放的目标商品</div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+
+      <KeywordTarget v-if="deliveryTypeRadio === 'keyword'"></KeywordTarget>
+      <NegativeWord v-if="deliveryTypeRadio === 'keyword'"></NegativeWord>
+      <ProductOrientation v-if="deliveryTypeRadio === 'commodity'"></ProductOrientation>
+      <NegativeGood v-if="deliveryTypeRadio === 'commodity'"></NegativeGood>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import KeywordTarget from './KeywordTarget.vue'
+import NegativeWord from './NegativeWord.vue'
+import ProductOrientation from './ProductOrientation.vue'
+import NegativeGood from './NegativeGood.vue'
+
+const deliveryTypeRadio = ref('keyword')
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.delivery-type-radio-group .el-radio__label) {
+  width: 100%;
+}
+.delivery-type-radio {
+  height: auto;
+  flex: 1;
+  align-items: flex-start;
+  padding: 15px 10px 15px 10px;
+}
+::v-deep(.delivery-type-radio-group .el-radio-group .el-radio__inner) {
+  /* 着陆页单选按钮 */
+  margin-top: 3px;
+}
+::v-deep(.delivery-type-radio-group .el-radio.is-bordered.is-checked.el-radio--default.delivery-type-radio) {
+  /* 选中后的背景颜色 */
+  background: #f5f7fe;
+}
+</style>

+ 986 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/FocusCreativity.vue

@@ -0,0 +1,986 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">创意</span>
+      </div>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    list-type="picture-card"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openDialog" disabled="true">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title>编辑品牌旗舰店页面</template>
+                  <div v-for="(storePage, index) in topStorePages" :key="index" style="margin-bottom: 10px; width: 85%">
+                    <el-card shadow="hover" body-style="padding: 10px;">
+                      <div style="margin-right: 8px; line-height: normal; display: flex; align-items: center">
+                        <el-image class="img-box" :src="storePage.storePageLink" />
+                        <div style="margin-left: 15px">
+                          <span><span style="color: #6d7784">当前名称:</span>{{ storePage.storePageName }}</span>
+                          <div style="margin-bottom: 5px"><span style="color: #6d7784">ASIN: </span>{{ storePage.asin }}</div>
+                          <el-input v-model="storePage.inputName" style="width: 300px" placeholder="修改品牌页面名称"></el-input>
+                        </div>
+                        <div class="card-operation">
+                          <el-button link type="primary" @click="changePicture(storePage.storePageUrl, index)" style="margin-bottom: 10px"
+                            >更换图片</el-button
+                          >
+                          <el-button link type="primary" @click="changePage(storePage.storePageUrl, index)">更换页面</el-button>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-dialog v-model="commodityDialog" title="更换图片" width="50%">
+                  <el-radio-group
+                    v-loading="dialogLoading3"
+                    v-model="selectedCommodity"
+                    style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+                    <div v-for="(item, index) in stock" :key="index">
+                      <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+                        <div style="padding: 10px; display: flex; align-items: center">
+                          <div style="margin-right: 8px; line-height: normal">
+                            <el-image class="img-box" :src="item.image_link" />
+                          </div>
+                          <div style="position: relative">
+                            <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                              <div class="double-line">{{ item.title }}</div>
+                            </el-tooltip>
+                            <span>
+                              <span style="color: #6d7784">ASIN: </span>
+                              <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                            </span>
+                          </div>
+                        </div>
+                      </el-radio>
+                    </div>
+                  </el-radio-group>
+                  <div style="margin-top: 20px; display: flex; justify-content: center">
+                    <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedStore">确定</el-button>
+                  </div>
+                </el-dialog>
+                <el-dialog v-model="pageDialog" title="更换页面" width="50%">
+                  <el-radio-group v-loading="pageDialogLoading" v-model="selectedCommodity" @change="handlePageChange" class="radio-group-item">
+                    <div v-for="(item, index) in storePageData.storePageInfo" :key="index" style="width: 100%">
+                      <el-radio
+                        :label="item.storePageId"
+                        class="radio-item"
+                        :disabled="topStorePages.some((storePage) => storePage.storePageName === item.storePageName)">
+                        <div class="radio-item-content">
+                          <div style="position: relative">
+                            <el-tooltip class="box-item" effect="dark" :content="item.storePageName" placement="top">
+                              <div class="double-line">{{ item.storePageName }}</div>
+                            </el-tooltip>
+                          </div>
+                        </div>
+                      </el-radio>
+                    </div>
+                  </el-radio-group>
+                  <!-- <div style="margin-top: 20px; display: flex; justify-content: center">
+                    <el-button type="primary" :disabled="!selectedCommodity">确定</el-button>
+                  </div> -->
+                </el-dialog>
+
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="submitForm(ruleFormRef)" :disabled="!fileList.length" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <!-- <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, computed, onMounted, onUnmounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import emitter from '/@/utils/emitter'
+import {
+  getAssets,
+  getLifeStyleAssets,
+  getPageAsins,
+  getCommodityCard,
+  getStoreurl,
+  getDefaultSpotlightAsin,
+  getSellerInStock,
+  postStoreSpotlight,
+  uploadFile,
+  checkAsset,
+} from '../api/index'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const createLoading = ref(false)
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createStoreSpotlight()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    // getCommodityCardData()
+  }
+}
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const disabled = ref(false)
+const fileList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+
+    // 将新的文件对象添加到 fileList 中
+    fileList.value.push(newFile)
+  }
+
+  // 清空选中卡片
+  selectedCards.value = []
+  // 关闭对话框
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  console.log('🚀 ~ getAssetsData ~ response-->>', response)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+// function openLifeStyleDialog() {
+//   lifeStyleDialog.value = true
+//   getLifeStyleAssetsData()
+// }
+
+// 获取商品数据
+const topStorePages = ref([])
+const commodityLoading = ref(false)
+const dialogLoading3 = ref(false)
+let subpageslist = []
+
+async function getDefaultCommodityData() {
+  try {
+    commodityLoading.value = true
+    const resp = await getDefaultSpotlightAsin({ profile_id: '3006125408623189' })
+    if (resp.code === 2000) {
+      topStorePages.value = resp.data.defaultlist.map((item) => ({
+        storePageName: item.storePageName,
+        storePageLink: item.image_link,
+        storePageUrl: item.storePageUrl,
+        asin: item.asin,
+        inputName: item.storePageName, // 新增字段用于存储输入值
+      }))
+      subpageslist = topStorePages.value.map((item) => ({
+      asin: item.asin,
+      pageTitle: item.inputName,
+      url: item.storePageUrl,
+  }))
+    }
+  } catch (error) {
+    console.error('Error in getDefaultCommodityData:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+const asinList = ref([])
+const stock = ref([])
+const currentEditingIndex = ref(null)
+const selectedCommodityData = ref(null)
+
+async function changePicture(pageUrl, index) {
+  commodityDialog.value = true
+  dialogLoading3.value = true
+
+  currentEditingIndex.value = index
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageUrl,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    getStock()
+    dialogLoading3.value = false
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+async function getStock() {
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      asinlist: asinList.value,
+    }
+    const response = await getSellerInStock(obj)
+    stock.value = response.data
+  } catch (error) {
+    console.error('Error in getStock:', error)
+  }
+}
+
+function handleSelectedStore() {
+  if (currentEditingIndex.value !== null) {
+    selectedCommodityData.value = stock.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData.value) {
+      // 更新图片链接和ASIN
+      topStorePages.value[currentEditingIndex.value].storePageLink = selectedCommodityData.value.image_link
+      topStorePages.value[currentEditingIndex.value].asin = selectedCommodityData.value.asin
+      // 更新输入框的绑定值以便可以继续编辑名称
+      topStorePages.value[currentEditingIndex.value].inputName = topStorePages.value[currentEditingIndex.value].inputName
+      console.log(topStorePages.value)
+    }
+    // 重置当前编辑索引和选中的商品
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+    commodityDialog.value = false
+  }
+}
+
+// 更换页面功能
+const pageDialog = ref(false)
+const storePageData = ref({ storePageInfo: [] })
+const pageDialogLoading = ref(false)
+const selectedPageUrl = ref('')
+const firstObj: any = ref({})
+const pageAsinList = ref([])
+const selectedPageName = ref('')
+
+async function changePage(pageUrl, index) {
+  pageDialog.value = true
+  pageDialogLoading.value = true
+  currentEditingIndex.value = index
+  try {
+    const response = await getStoreurl({ profile_id: profile.value.profile_id })
+    storePageData.value = response.data
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    pageDialogLoading.value = false
+  }
+}
+
+async function getAsinList(pageurl) {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageurl,
+    }
+    const resp = await getPageAsins(query)
+    pageAsinList.value = resp.data.asinList
+
+    changeCardData()
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+async function handlePageChange(selectedPageId) {
+  const selectedPage = storePageData.value.storePageInfo.find((page) => page.storePageId === selectedPageId)
+  if (selectedPage) {
+    selectedPageUrl.value = selectedPage.storePageUrl
+    selectedPageName.value = selectedPage.storePageName
+    pageDialog.value = false
+    await getAsinList(selectedPageUrl.value)
+  } else {
+    console.error('Selected page not found in storePageData')
+  }
+}
+
+async function changeCardData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      asinlist: pageAsinList.value,
+    }
+    const response = await getSellerInStock(query)
+    if (response && response.data.length > 0) {
+      // 将response的第一个元素赋值给firstObj
+      firstObj.value = response.data[0]
+
+      // 更新图片链接和ASIN
+      topStorePages.value[currentEditingIndex.value].storePageLink = firstObj.value.image_link
+      topStorePages.value[currentEditingIndex.value].asin = firstObj.value.asin
+      topStorePages.value[currentEditingIndex.value].storePageName = selectedPageName.value
+      topStorePages.value[currentEditingIndex.value].inputName = selectedPageName.value
+      currentEditingIndex.value = null
+    } else {
+      console.error('No data')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  }
+}
+
+// 创建创意
+let brandName = ''
+let brandEntityId = ''
+const respAdGroupId = inject<Ref>('respAdGroupId')
+let brandLogoCrop = {}
+
+const upLoading = ref(false)
+let respAssetId = ''
+
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+// TODO: url对应的是Home 暂时写死
+async function createStoreSpotlight() {
+  createLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      url: 'https://www.amazon.com/stores/page/1D1DD2FD-CF54-4FE5-B1A0-9E01F12F8144',
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: brandName,
+      brandLogoAssetID: respAssetId,
+      brandLogoCrop: brandLogoCrop,
+      consentToTranslate: false,
+      subpageslist: subpageslist,
+      headline: ruleForm.title,
+    }
+    const response = await postStoreSpotlight(query)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+watch(topStorePages, () => {
+    subpageslist = topStorePages.value.map((item) => ({
+    asin: item.asin,
+    pageTitle: item.inputName,
+    url: item.storePageUrl,
+  }))
+  console.log('subpageslist', subpageslist)
+},{deep: true})
+
+onMounted(() => {
+  emitter.on('spotlight-shop', (newValue: any) => {
+    brandName = newValue.brandRegistryName
+    brandEntityId = newValue.brandEntityId
+  })
+})
+
+// 接收数据端在组件卸载时解绑事件
+onUnmounted(() => {
+  emitter.off('spotlight-shop')
+})
+
+const focusShop = inject<Ref>('focusShop')
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: focusShop.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    console.log('asinList', asinList.value)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// let lastQueriedAsins = []
+const commodityCard = ref([])
+// async function getCommodityCardData() {
+//   try {
+//     commodityLoading.value = true
+//     const topAsins = asinList.value.slice(0, 3)
+
+//     const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+//     if (newAsins.length === 0) {
+//       commodityLoading.value = false
+//       return // 如果没有新的 ASIN,直接返回
+//     }
+
+//     lastQueriedAsins = [...topAsins]
+
+//     // 清空commodityCard,为新数据做准备
+//     commodityCard.value = []
+
+//     // 对每个新的 ASIN 发送请求
+//     for (const asin of newAsins) {
+//       const query = {
+//         profile_id: profile.value.profile_id,
+//         asin: asin,
+//       }
+
+//       try {
+//         const response = await getCommodityCard(query)
+//         commodityCard.value.push(response.data)
+//         // console.log('Response for ASIN', asin, ':', response)
+//       } catch (error) {
+//         console.log('Error for ASIN', asin, ':', error)
+//       }
+//     }
+//   } catch (error) {
+//     console.log('Outer error:', error)
+//   } finally {
+//     commodityLoading.value = false
+//   }
+// }
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+        console.log('🚀 ~ getAdditionalCommodityData ~ replaceableCommodity-->>', replaceableCommodity.value)
+        // console.log('Response for additional ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+// watch(focusShop.value,
+//   async () => {
+//     await getCommodityCollapseData()
+//     getCommodityCardData()
+//     getAdditionalCommodityData()
+//   }
+// )
+onMounted(async () => {
+  await getDefaultCommodityData()
+  // await getCommodityCollapseData()
+  // getCommodityCardData()
+  // getAdditionalCommodityData()
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  min-width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.card-operation {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  margin-left: 30px;
+}
+.radio-group-item {
+  display: flex;
+  flex-direction: column;
+  align-content: flex-start;
+  align-items: flex-start;
+}
+.radio-item {
+  height: 80px;
+  width: 100%;
+  border-bottom: 1px solid #ccc;
+}
+.radio-item-content {
+  padding: 10px;
+  display: flex;
+  align-items: center;
+}
+</style>

+ 296 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/KeywordTarget.vue

@@ -0,0 +1,296 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px; font-weight: 700">关键词定向</span>
+    </el-divider>
+    <div style="width: 100%; height: 600px; display: flex; border: 1px solid #e5e7eb; border-radius: 6px" v-loading="keywordsLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7eb">
+        <el-tabs v-model="keyWordsTabs" class="demo-tabs">
+          <div style="margin: 8px">
+            <div style="display: flex; align-items: center">
+              <div style="min-width: 40px; margin-left: 8px; font-weight: 500; color: #616266">竞价:</div>
+              <el-select v-model="bidType" class="m-2" placeholder="Select" style="width: 450px">
+                <el-option v-for="item in bidTypeOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
+              </el-select>
+              <el-input v-model="bidInput" :disabled="!(bidType == 'customBid')" placeholder="Please input">
+                <template #prepend>$</template>
+              </el-input>
+            </div>
+            <div style="display: flex; align-items: center">
+              <span style="margin: 0 10px 0 8px; font-weight: 500; color: #616266">匹配类型: </span>
+              <el-checkbox v-model="broadType" label="广泛" />
+              <el-checkbox v-model="phraseType" label="词组" />
+              <el-checkbox v-model="exactType" label="精确" />
+            </div>
+          </div>
+          <el-tab-pane label="建议" name="first">
+            <el-table
+              height="425"
+              style="width: 100%; padding-left: 5px"
+              :data="keyWordsTableData"
+              :header-cell-style="headerCellStyle"
+              :header-row-style="changeKeyWordsTableHeader">
+              <el-table-column prop="asin" label="关键词"> </el-table-column>
+              <el-table-column prop="matchType" label="匹配类型"> </el-table-column>
+              <el-table-column prop="adviceBid" label="建议出价" width="120"> </el-table-column>
+            </el-table>
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input v-model="keyWordsTextarea" :rows="10" type="textarea" style="padding-left: 5px" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button type="primary" text bg @click="addKeyWords">添加</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedKeyWordsTableData.length }}</span>
+              <span style="color: #529b2e">成功: {{ successCount }}</span>
+              <span style="color: #c45656">失败: {{ failureCount }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllKeyWords">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body" body-style="padding-bottom: -20px;">
+            <el-table
+              :data="addedKeyWordsTableData"
+              style="width: 100%; height: 450px"
+              :header-row-style="changeKeyWordsTableHeader"
+              :header-cell-style="headerCellStyle">
+              <el-table-column prop="keyword" label="关键词" width="auto" />
+              <el-table-column prop="matchType" label="匹配类型" />
+              <el-table-column prop="bid" label="出价">
+                <template #default="scope">
+                  <el-input v-model="scope.row.bid" placeholder="Please input bid" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="suggestBid" label="建议出价" align="center">
+                <template #default="{ row }">
+                  <div>{{ row.adviceBid ? row.adviceBid : '--' }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="operate" label="操作" width="60" align="right">
+                <template #default="scope">
+                  <el-button type="danger" size="small" link @click="delSingleKeyWord(scope)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around; padding-top: 0px">
+          <el-button type="primary" plain @click="keyWordsSave" :disabled="!addedKeyWordsTableData.length">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { Ref, inject, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const keyWordsTabs = ref('first')
+const bidType = ref('customBid')
+const keywordsLoading = ref(false)
+const bidTypeOptions = [
+  {
+    value: 'suggestBid',
+    label: '建议出价',
+    disabled: true,
+  },
+  {
+    value: 'customBid',
+    label: '自定义出价',
+  },
+  {
+    value: 'defaultBid',
+    label: '默认出价',
+    disabled: true,
+  },
+]
+
+const bidInput = ref('0.75')
+const keyWordsTableData = ref([]) // 关键词定向左侧表格数据
+const addedKeyWordsTableData = ref([]) // 关键词定向右侧表格数据
+const keyWordsTextarea = ref('')
+let broadType = ref(true)
+let phraseType = ref(true)
+let exactType = ref(true)
+const MATCH_TYPE = {
+  broad: '广泛',
+  phrase: '词组',
+  exact: '精确',
+}
+const MATCH_TYPE_MAP = {
+  广泛: 'broad',
+  词组: 'phrase',
+  精确: 'exact',
+}
+const successCount = ref('')
+const failureCount = ref('')
+
+function addKeyWords() {
+  const trimmedText = keyWordsTextarea.value.trim()
+  const items = trimmedText.split(/,|\n/)
+
+  items.forEach((item) => {
+    const trimmedItem = item.trim()
+    if (trimmedItem) {
+      if (broadType.value) {
+        addKeyWordEntry(trimmedItem, MATCH_TYPE.broad)
+      }
+      if (phraseType.value) {
+        addKeyWordEntry(trimmedItem, MATCH_TYPE.phrase)
+      }
+      if (exactType.value) {
+        addKeyWordEntry(trimmedItem, MATCH_TYPE.exact)
+      }
+    } else {
+      ElMessage({
+        message: '有空项目,未被添加到列表中',
+        type: 'warning',
+      })
+    }
+  })
+  keyWordsTextarea.value = ''
+}
+
+function addKeyWordEntry(keyword, matchType) {
+  let keyWordEntry = {
+    keyword: keyword,
+    matchType: matchType,
+    bid: bidInput.value,
+  }
+
+  if (!addedKeyWordsTableData.value.some((n) => n.keyword === keyWordEntry.keyword && n.matchType === keyWordEntry.matchType)) {
+    addedKeyWordsTableData.value.push(keyWordEntry)
+  } else {
+    ElMessage({
+      message: `关键词 ${keyword} (${matchType}) 已存在,未被添加到列表中`,
+      type: 'warning',
+    })
+  }
+}
+
+function delSingleKeyWord(scope) {
+  const index = addedKeyWordsTableData.value.findIndex((item) => item.keyword === scope.row.keyword && item.matchType === scope.row.matchType)
+  if (index !== -1) {
+    addedKeyWordsTableData.value.splice(index, 1)
+  } else {
+    console.log('无效的索引,无法删除条目')
+  }
+}
+
+function delAllKeyWords() {
+  addedKeyWordsTableData.value = []
+}
+
+// TODO: 必须要创建了商品之后才能创建关键词定向
+async function keyWordsSave() {
+  keywordsLoading.value = true
+  successCount.value = ''
+  failureCount.value = ''
+
+  const keywordList = addedKeyWordsTableData.value.map((kw) => ({
+    keywordText: kw.keyword,
+    bid: kw.bid,
+    matchType: MATCH_TYPE_MAP[kw.matchType],
+  }))
+
+  const requestData = {
+    profile_id: profile.value.profile_id,
+    campaignId: respCampaignId.value,
+    adGroupId: respAdGroupId.value,
+    keywordlist: keywordList,
+  }
+  const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/sbtargets/add/keywords/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    if (resp.data.success.length !== 0) {
+      ElMessage({
+        message: '关键词创建成功',
+        type: 'success',
+      })
+      successCount.value = resp.data.success.length
+      failureCount.value = resp.data.error.length
+      delAllKeyWords()
+    } else {
+      ElMessage.error('关键词创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    keywordsLoading.value = false
+  }
+}
+
+function headerCellStyle(args) {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+function changeKeyWordsTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 左侧表格Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+</style>

+ 354 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeGood.vue

@@ -0,0 +1,354 @@
+<template>
+  <div prop="matchType" style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px; font-weight: 700">否定商品</span>
+    </el-divider>
+    <div style="width: 100%; height: 600px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px" v-loading="negativeGoodsLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="topTabs" stretch>
+          <el-tab-pane label="排除商品" name="first">
+            <el-tabs v-model="negativeTabs" class="demo-tabs">
+              <el-tab-pane label="搜索" name="first">
+                <div style="margin-bottom: 10px">
+                  <el-input placeholder="按ASIN搜索" v-model="negativeInput" @change="searchNegativeGoods" clearable />
+                </div>
+                <el-table
+                  height="495"
+                  style="width: 100%"
+                  v-loading="loading"
+                  :data="negativeTableData"
+                  :header-cell-style="headerCellStyle"
+                  :show-header="false">
+                  <el-table-column prop="asin" label="商品">
+                    <template #default="scope">
+                      <div style="display: flex; align-items: center">
+                        <div style="margin-right: 8px; line-height: normal">
+                          <el-image class="img-box" :src="scope.row.image_link" />
+                        </div>
+                        <div>
+                          <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                            <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                          </el-tooltip>
+                          <span>
+                            ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="name" label="Name" width="120" align="right">
+                    <template #header> </template>
+                    <template #default="scope">
+                      <el-button type="primary" size="small" @click="addSingleNegativeGoods(scope)" text>添加</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-tab-pane>
+              <el-tab-pane label="输入" name="second">
+                <el-input
+                  v-model="negativeGoodsTextarea"
+                  :rows="17"
+                  type="textarea"
+                  disabled="true"
+                  maxlength="11000"
+                  style="padding: 10px 10px" />
+                <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+                  <el-button style="margin-right: 10px" type="primary" text bg @click="addNegativeGoods">添加</el-button>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+          <el-tab-pane label="排除品牌" name="second">
+
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedNegetiveTableData.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllNegativeGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -30px">
+          <el-table
+            :data="addedNegetiveTableData"
+            height="473"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedNegGoods">
+            <el-table-column prop="asin" label="商品">
+              <template #default="scope">
+                <div style="display: flex; align-items: center">
+                  <div style="margin-right: 8px; line-height: normal">
+                    <el-image class="img-box" :src="scope.row.image_link" />
+                  </div>
+                  <div>
+                    <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                      <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                    </el-tooltip>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="120" align="right">
+              <template #default="scope">
+                <el-button type="primary" size="small" @click="delSingleNegativeGoods(scope)" text>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div style="display: flex; justify-content: space-around; padding-top: 10px">
+          <el-button plain type="primary" @click="negativeGoodsSave" :disabled="!addedNegetiveTableData.length">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Ref, inject, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { request } from '/@/utils/service'
+
+let selections = [] // 添加选中的项
+let addedSels = [] // 删除选中的项
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const negativeTabs = ref('first')
+const topTabs = ref('first')
+const loading = ref(false)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const negativeTableData = ref([])
+const addedNegetiveTableData = ref([])
+let negativeGoodsLoading = ref(false)
+let negativeList = reactive([])
+const tableData = negativeList
+let inputAddedNegGoods = ref([])
+const negativeInput = ref('')
+
+function setNegativeTableData(asin = '') {
+  negativeGoodsLoading.value = true
+  return request({
+    url: '/api/sellers/listings/all/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+    },
+  })
+    .then((resp) => {
+      negativeTableData.value = resp.data
+      inputAddedNegGoods.value = resp.data
+      negativeGoodsLoading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      negativeGoodsLoading.value = false
+    })
+}
+
+ const negativeGoodsTextarea = ref('')
+// 输入tab的textarea
+function addNegativeGoods() {
+  console.log('negativeGoodsTextarea', negativeGoodsTextarea.value)
+  loading.value = true
+
+  setNegativeTableData(negativeGoodsTextarea.value)
+    .then(() => {
+      addedNegetiveTableData.value = [...addedNegetiveTableData.value, ...inputAddedNegGoods.value]
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+
+function addSingleNegativeGoods(scope) {
+  const isAlreadyAdded = addedNegetiveTableData.value.some((item) => item.asin === scope.row.asin)
+  if (!isAlreadyAdded) {
+    addedNegetiveTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function delAllNegativeGoods() {
+  addedNegetiveTableData.value = []
+}
+
+function delSingleNegativeGoods(scope) {
+  const index = addedNegetiveTableData.value.findIndex((item) => item.asin === scope.row.asin)
+
+  if (index !== -1) {
+    addedNegetiveTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function searchNegativeGoods(e) {
+  console.log(e)
+  if (e === '') {
+    negativeTableData.value = []
+  } else {
+    setNegativeTableData(e)
+  }
+}
+
+function handleAddedNegGoods(selection) {
+  addedSels = selection
+}
+
+
+async function negativeGoodsSave() {
+  console.log(addedNegetiveTableData.value)
+  const asinList = addedNegetiveTableData.value.map((item) => item.asin)
+  console.log('🚀 ~ negativeGoodsSave ~ asinList-->>', asinList)
+  negativeGoodsLoading.value = true
+  console.log('addedNegetiveTableData', addedNegetiveTableData.value)
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinList: asinList,
+      matchType: 'ASIN_SAME_AS',
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/sptargets/add/negative/targets/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ negativeWordsSave ~ resp-->>', resp)
+    negativeGoodsLoading.value = false
+    if (resp.data.success.length !== 0) {
+      ElMessage({
+        message: '否定商品创建成功',
+        type: 'success',
+      })
+      delAllNegative()
+    } else {
+      ElMessage.error('否定商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function delAllNegative() {
+  negativeList.length = 0
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.column-margin-bottom label.el-radio.is-bordered {
+  margin-bottom: 10px;
+  padding: 35px;
+}
+::v-deep(.column-margin-bottom label.el-radio.is-bordered span.el-radio__inner) {
+  margin-top: -18px;
+  margin-left: -15px;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.demo-tabs .el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-tabs__nav-wrap::after) {
+  height: 2px !important;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+// 输入底部样式
+::v-deep(.card-box .el-card__body) {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  // margin: 10px 0 10px 10px;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+::v-deep(.goods-orientation-tabs .el-tabs__nav-scroll) {
+  margin-left: -20px !important;
+}
+::v-deep(.category-tabs .el-tabs__nav) {
+  margin-left: 20px;
+}
+::v-deep(.goods-orientation-tabs #tab-1) {
+  /* 商品定向Tab栏 */
+  border-right: 0;
+}
+</style>

+ 247 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeWord.vue

@@ -0,0 +1,247 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px;font-weight: 700;">否定词</span>
+    </el-divider>
+    <div style="width: 100%; height: 520px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <div style="margin: 10px 0">
+          <span style="margin-left: 25px; color: #e47470">*</span>
+          <span style="color: #666666; margin-right: 10px">匹配类型: </span>
+          <el-checkbox v-model="NEGATIVE_PHRASE" label="词组否定" />
+          <el-checkbox v-model="NEGATIVE_EXACT" label="精确否定" />
+        </div>
+        <el-input
+          v-model="negativeWordsTextarea"
+          :rows="17"
+          type="textarea"
+          placeholder="请输入关键词,多个关键词使用逗号或者换行符分隔。(最多添加1000个关键词)"
+          maxlength="11000"
+          style="padding: 0 20px" />
+        <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+          <el-button style="margin-right: 18px" type="primary" text bg @click="addNegative">添加</el-button>
+        </div>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ negativeList.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllNegative">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body">
+            <el-table :data="negativeList" style="width: 100%; height: 370px; padding-bottom: 0" :header-row-style="changeNegTableHeader">
+              <el-table-column prop="negativeWords" label="否定词" width="auto" />
+              <el-table-column prop="operate" label="操作" width="60" align="right">
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="delSingleNegative(scope)" text>删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around">
+          <el-button type="primary" plain @click="negativeWordsSave" :disabled="!negativeList.length">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { Ref, inject, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { postNegativeWordData } from '../api/index'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+const tableData = reactive([])
+let negativeWordsLoading = ref(false)
+let negativeList = reactive([])
+let negativeWordsTextarea = ref('')
+let NEGATIVE_PHRASE = ref(true)
+let NEGATIVE_EXACT = ref(true)
+let exactNegativeList = reactive([]) // 用于存储精确否定的数组
+let phraseNegativeList = reactive([]) // 用于存储词组否定的数组
+
+function addNegative() {
+  const trimmedText = negativeWordsTextarea.value.trim()
+  const items = trimmedText.split(/,|\n/)
+
+  items.forEach((item) => {
+    const trimmedItem = item.trim()
+    if (trimmedItem) {
+      let phraseEntry = '词组: ' + trimmedItem
+      let exactEntry = '精确: ' + trimmedItem
+
+      if (NEGATIVE_PHRASE.value && !negativeList.some((n) => n.negativeWords === phraseEntry)) {
+        negativeList.push({ negativeWords: phraseEntry })
+        phraseNegativeList.push(trimmedItem) // 添加到词组否定数组
+      }
+      if (NEGATIVE_EXACT.value && !negativeList.some((n) => n.negativeWords === exactEntry)) {
+        negativeList.push({ negativeWords: exactEntry })
+        exactNegativeList.push(trimmedItem) // 添加到精确否定数组
+      }
+    } else {
+      console.log('有空项目,未被添加到列表中')
+    }
+  })
+
+  negativeWordsTextarea.value = ''
+  console.log('🚀 ~ exactNegativeList-->>', exactNegativeList)
+  console.log('🚀 ~ phraseNegativeList-->>', phraseNegativeList)
+}
+
+function delAllNegative() {
+  // negativeList.splice(0, negativeList.length)
+  negativeList.length = 0
+  exactNegativeList.length = 0
+  phraseNegativeList.length = 0
+}
+
+function delSingleNegative(scope) {
+  const index = negativeList.findIndex((item) => item.negativeWords === scope.row.negativeWords)
+  if (index !== -1) {
+    // 确定被删除的项是词组还是精确
+    const isPhrase = scope.row.negativeWords.startsWith('词组: ')
+    const isExact = scope.row.negativeWords.startsWith('精确: ')
+
+    // 从 negativeList 删除
+    if (negativeList.length) {
+      negativeList.splice(index, 1)
+      console.log(`已删除索引为 ${index} 的条目`)
+    } else {
+      console.log('无效的索引,无法删除条目')
+    }
+
+    // 从 exactNegativeList 或 phraseNegativeList 删除
+    const trimmedItem = scope.row.negativeWords.substring(4).trim() // 从 '词组: ' 或 '精确: ' 后开始截取
+    if (isPhrase) {
+      const phraseIndex = phraseNegativeList.findIndex((item) => item === trimmedItem)
+      if (phraseIndex !== -1) {
+        phraseNegativeList.splice(phraseIndex, 1)
+      }
+    } else if (isExact) {
+      const exactIndex = exactNegativeList.findIndex((item) => item === trimmedItem)
+      if (exactIndex !== -1) {
+        exactNegativeList.splice(exactIndex, 1)
+      }
+    }
+  } else {
+    console.log('无效的索引,无法删除条目')
+  }
+}
+
+async function negativeWordsSave() {
+  negativeWordsLoading.value = true
+  console.log('negativeList', negativeList)
+  createNegativeWords()
+  // try {
+  //   const requestData = {
+  //     profile_id: profile.value.profile_id,
+  //     campaignId: respCampaignId.value,
+  //     adGroupId: respAdGroupId.value,
+  //     state: 'PAUSED',
+  //     EkeywordList: exactNegativeList,
+  //     PkeywordList: phraseNegativeList,
+  //   }
+  //   const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+
+  //   console.log('🚀 ~ negativeWordsSave ~ resp-->>', resp)
+  //   negativeWordsLoading.value = false
+  //   if (resp.data.negativeKeyworderror.length !== 0) {
+  //     ElMessage({
+  //       message: '否定词创建成功',
+  //       type: 'success',
+  //     })
+  //     delAllNegative()
+  //   } else {
+  //     ElMessage.error('否定词创建失败!')
+  //   }
+  // } catch (error) {
+  //   console.error('请求失败:', error)
+  // }
+}
+
+async function createNegativeWords() {
+  const negativeWordsData = {
+    profile_id: profile.value.profile_id,
+    campaignId: respCampaignId.value,
+    adGroupId: respAdGroupId.value,
+    state: 'PAUSED',
+    EkeywordList: exactNegativeList,
+    PkeywordList: phraseNegativeList,
+  }
+  const filteredRequestData = Object.fromEntries(Object.entries(negativeWordsData).filter(([_, v]) => v != null))
+  try {
+    const response = await postNegativeWordData(filteredRequestData)
+    console.log('🚀 ~ createCampaigns ~ response-->>', response)
+      if (response.data.negativeKeyworderror.length !== 0) {
+      ElMessage({
+        message: '否定词创建成功',
+        type: 'success',
+      })
+      delAllNegative()
+    } else {
+      ElMessage.error('否定词创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    negativeWordsLoading.value = false
+  }
+}
+
+function changeNegTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 左侧表格Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+</style>

+ 810 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductOrientation.vue

@@ -0,0 +1,810 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px; font-weight: 700">商品定向</span>
+    </el-divider>
+    <div style="width: 100%; height: 600px; display: flex; border: 1px solid #e5e7eb; border-radius: 6px" v-loading="productOrientationLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7eb">
+        <el-tabs
+          type="border-card"
+          stretch
+          class="goods-orientation-tabs"
+          style="border: 0; border-right: 0; border-bottom-left-radius: 6px; border-top-left-radius: 5px; overflow: hidden">
+          <el-tab-pane label="品类" style="border-top-left-radius: 6px">
+            <div style="display: flex; align-items: center">
+              <span style="width: 40px">竞价:</span>
+              <el-select v-model="categoryBiddingType" @change="singleGoodsBidSelectChanged" class="m-2" placeholder="Select">
+                <el-option v-for="item in categoryBiddingTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="singleGoodsBidInput" :disabled="categoryBiddingType === 'defaultBid'" style="width: 200px">
+                <template #prepend>$</template>
+              </el-input>
+            </div>
+
+            <el-tabs v-model="categoryTabs" class="category-tabs">
+              <el-tab-pane label="建议" name="first">
+                <el-table :data="proposalTableData" style="width: 100%" height="422">
+                  <el-table-column prop="proposal" label="建议" width="520">
+                    <template #header> 0建议 </template>
+                  </el-table-column>
+                  <el-table-column prop="address" label="Address">
+                    <template #header>
+                      <el-button type="primary" size="normal" link @click="handleGoodsAdd">全部添加</el-button>
+                    </template>
+                    <template #default="scope">
+                      <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-tab-pane>
+              <el-tab-pane label="搜索" name="second">
+                <el-input placeholder="请输入关键词过滤" />
+                <el-scrollbar height="390px">
+                  <el-tree :data="searchClassifyTableData" :props="defaultProps">
+                    <template #default="{ node, data }">
+                      <span class="custom-tree-node">
+                        <span style="width: 75%">{{ node.label }}</span>
+                        <span style="color: rgb(50, 108, 216)" v-if="data.ta == true">
+                          <a @click="refine(data)"> 细化 </a>
+                          <a style="margin-left: 8px" @click="orientate(node, data)"> 定向 </a>
+                        </span>
+                      </span>
+                    </template>
+                  </el-tree>
+                </el-scrollbar>
+                <el-dialog v-model="visible" :title="`细化分类: ${dialogTitle}`" @close="dialogClose" destroy-on-close>
+                  <div style="display: flex; justify-content: space-between">
+                    <span>根据特定品牌、价格范围、星级和Prime配送资格,细化分类</span>
+                    <span>
+                      <el-checkbox v-model="dialogForm.isCount" label="显示商品数量" @change="isCountChanged" />
+                    </span>
+                  </div>
+                  <el-form :model="dialogForm" :rules="dialogRules" ref="dialogFormRef" style="margin-top: 20px">
+                    <el-form-item style="padding-left: 140px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">品牌</span>
+                      <el-select
+                        v-model="dialogForm.dialogselectValue"
+                        @change="dialogSelectChange"
+                        multiple
+                        placeholder="请选择"
+                        :loading="dialogSelectLoading">
+                        <el-option v-for="item in dialogForm.dialogOptions" :key="item.value" :label="item.label" :value="item.value" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item prop="prices" style="padding-left: 112px; margin-top: 10px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">价格范围</span>
+                      <el-input-number v-model="dialogForm.prices.lowest" :min="1" :controls="false" placeholder="无最低商品价格" />
+                      --
+                      <el-input-number v-model="dialogForm.prices.highest" :min="1" :controls="false" placeholder="无最高商品价格" />
+                    </el-form-item>
+                    <el-form-item prop="starRating" style="padding-left: 85px; margin-top: 10px">
+                      <span style="margin-right: 15px; color: #616266; font-weight: 500">查看星级评定</span>
+                      <el-slider v-model="dialogForm.starRating" range show-stops :max="5" :marks="marks" style="width: 70%" />
+                    </el-form-item>
+                    <el-form-item prop="delivery" style="padding-left: 140px; margin-top: 30px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">配送</span>
+                      <el-radio-group v-model="dialogForm.delivery">
+                        <el-radio label="all" style="font-weight: 400">所有</el-radio>
+                        <el-radio label="eligible" style="font-weight: 400">具备Prime资格</el-radio>
+                        <el-radio label="diseligible" style="font-weight: 400">不具备Prime资格</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                  </el-form>
+                  <template #footer>
+                    <div style="display: flex; justify-content: space-between">
+                      <span v-loading="countLoadig">定位到的商品数量:
+                        <span v-if="dialogForm.isCount == true">{{ commodityCount[0]?.min }} - {{ commodityCount[0]?.max }}</span>
+                      </span>
+                      <span class="dialog-footer">
+                        <el-button @click="visible = false">取消</el-button>
+                        <el-button type="primary" @click="dialogFormSubmit">确定</el-button>
+                      </span>
+                    </div>
+                  </template>
+                </el-dialog>
+              </el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+          <el-tab-pane label="单个商品">
+            <div style="display: flex; align-items: center">
+              <span style="width: 40px">竞价:</span>
+              <el-select class="m-2" v-model="singleGoodsBidSelect" @change="singleGoodsBidSelectChanged">
+                <el-option v-for="item in singleGoodsBidTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="singleGoodsBidInput" :disabled="singleGoodsBidSelect == 'defaultBid'" style="width: 200px">
+                <template #prepend>$</template>
+              </el-input>
+              <!-- <div style="margin-left: 20px">
+                <span style="margin-right: 10px">类型:</span>
+                <el-checkbox v-model="expand" label="扩展" />
+                <el-checkbox v-model="accurate" label="精准" />
+              </div> -->
+            </div>
+            <el-tabs v-model="singleGoodsTabs" class="category-tabs">
+              <el-tab-pane label="建议" name="first">
+                <el-table :data="proposalTableData" style="width: 100%" height="342">
+                  <el-table-column prop="proposal" label="商品" width="520" />
+                  <el-table-column prop="address" label="类型" />
+                  <el-table-column prop="operational" label="操作" />
+                </el-table>
+              </el-tab-pane>
+              <el-tab-pane label="搜索" name="second">
+                <el-input v-model="singleGoodsSearchInp" @change="singleGoodsSearchChaneged" placeholder="按ASIN搜索"></el-input>
+                <el-table :data="searchTableData" style="width: 100%" height="309">
+                  <el-table-column prop="asin" label="商品" width="520">
+                    <template #default="{ row }">
+                      <div style="display: flex; align-items: center">
+                        <img :src="row.image_link" style="width: 40px; height: 40px; margin-right: 10px" />
+                        <span>{{ row.title }}</span>
+                      </div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="productTypes" label="类型">
+                    <template #default="scope">
+                      <div v-if="expand">扩展</div>
+                      <div v-if="accurate">精准</div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="operational" label="操作">
+                    <template #default="scope">
+                      <el-button class="button" text @click="addSingleSearch(scope)">添加</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-tab-pane>
+              <!-- TODO: 商品定向TextArea -->
+              <el-tab-pane label="输入" name="third">待完成</el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ productOrientationTableData.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllCna">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body">
+            <el-table
+              height="460"
+              :data="productOrientationTableData"
+              style="width: 100%"
+              :header-row-style="changeKeyWordsTableHeader"
+              :header-cell-style="headerCellStyle">
+              <el-table-column prop="cna" label="分类 & 商品" width="300">
+                <template #default="scope">
+                  <div v-if="scope.row.cna || scope.row.classification">
+                    分类: <span style="color: #000000">{{ scope.row.cna ? scope.row.cna : scope.row.classification }}</span>
+                  </div>
+                  <div v-if="scope.row.asin">
+                    {{ scope.row.asin ? scope.row.asin : '--' }}
+                  </div>
+                  <div v-if="scope.row.brand">
+                    品牌: <span style="color: #000000">{{ scope.row.brand }}</span>
+                  </div>
+                  <div v-if="scope.row.low_price || scope.row.high_price">
+                    品牌价格:
+                    <span style="color: #000000">
+                      {{ scope.row.low_price ? '$' + scope.row.low_price : '--' }} -
+                      {{ scope.row.high_price ? '$' + scope.row.high_price : '--' }}
+                    </span>
+                  </div>
+                  <div v-if="scope.row.low_rating || scope.row.high_rating">
+                    评分: <span style="color: #000000">{{ scope.row.low_rating }} - {{ scope.row.high_rating }}</span>
+                  </div>
+                  <div v-if="scope.row.deliveryText">
+                    配送: <span style="color: #000000">{{ scope.row.deliveryText }}</span>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="type" label="类型">
+                <template #default="scope">
+                  {{ scope.row.productTypeText ? scope.row.productTypeText : '--' }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="bid" label="竞价">
+                <template #default="scope">
+                  <el-input-number v-model="scope.row.bid" :min="0.02" :max="1000000" :controls="false" size="small" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="operate" label="操作" width="60" align="right">
+                <template #default="scope">
+                  <el-button text size="small" @click="delCna(scope.$index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around; margin-top: -8px">
+          <el-button type="primary" plain @click="productTagetSave">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { CSSProperties, Ref, inject, onMounted, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { request } from '/@/utils/service'
+
+
+let adsTableData = ref([])
+let selections = [] // 添加选中的项
+let addedSels = [] // 删除选中的项
+
+const singleGoodsTabs = ref('first')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const categoryTabs = ref('first')
+const categoryBiddingType = ref('customBid')
+const categoryBiddingTypeOptions = [
+  {
+    value: 'defaultBid',
+    label: '默认竞价',
+  },
+  {
+    value: 'customBid',
+    label: '自定义竞价',
+  },
+]
+const categoryBidInput = ref('0.75')
+const singleGoodsBidSelect = ref('customBid')
+const singleGoodsBidTypeOptions = [
+  {
+    value: 'defaultBid',
+    label: '默认竞价',
+  },
+  {
+    value: 'customBid',
+    label: '自定义竞价',
+  },
+]
+const singleGoodsBidInput = ref('0.75')
+const expand = ref(true)
+const accurate = ref(false)
+const proposalTableData = ref([])
+const searchClassifyTableData = ref([])
+const productOrientationLoading = ref(false)
+const dialogSelectLoading = ref(false)
+const defaultProps = {
+  children: 'ch',
+  label: 'cna',
+}
+const countLoadig = ref(false)
+const visible = ref(false)
+let dialogTitle = ref('')
+let categoryId = ref('')
+const dialogselectValue = ref('')
+let dialogOptions: any = ref([])
+const dialogForm: any = reactive({
+  prices: {
+    lowest: undefined,
+    highest: undefined,
+  },
+  starRating: [0, 5],
+  dialogselectValue: [],
+  delivery: 'all',
+  isCount: false,
+})
+const dialogFormRef = ref()
+const dialogRules = reactive({
+  prices: [{ validator: validatePrices, trigger: 'blur' }],
+})
+
+interface Mark {
+  style: CSSProperties
+  label: string
+}
+type Marks = Record<number, Mark | string>
+const marks = reactive<Marks>({
+  0: '0',
+  1: '1',
+  2: '2',
+  3: '3',
+  4: '4',
+  5: '5',
+})
+let commodityCount = ref([])
+let currentDialogIndex = ref(0)
+let productOrientationTableData = ref([])
+
+async function validatePrices(rule, value) {
+  if (value.highest !== '' && value.lowest !== '' && value.highest <= value.lowest) {
+    return Promise.reject('最高价格必须大于最低价格')
+  }
+  return Promise.resolve()
+}
+
+async function setProductOrientationData() {
+  productOrientationLoading.value = true
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/targetable/categories/',
+      method: 'GET',
+      params: {
+        profile_id: profile.value.profile_id,
+      },
+    })
+    searchClassifyTableData.value = resp.data
+    productOrientationLoading.value = false
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+async function setDialogOption() {
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/categories/brands/',
+      method: 'GET',
+      params: {
+        profile_id: profile.value.profile_id,
+        category_id: categoryId.value,
+      },
+    })
+    const options = resp.data
+    dialogForm.dialogOptions = options.brands.map((brand) => {
+      return {
+        label: brand.name,
+        value: brand.id,
+      }
+    })
+    dialogSelectLoading.value = false
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+async function getCount(instanceId) {
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/products/count/',
+      method: 'POST',
+      data: {
+        profile_id: profile.value.profile_id,
+        category_id: categoryId.value,
+      },
+    })
+    if (instanceId === currentDialogIndex.value) {
+      commodityCount.value = resp.data.AsinCounts
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    if (instanceId === currentDialogIndex.value) {
+      countLoadig.value = false
+    }
+  }
+}
+
+function dialogClose() {
+  currentDialogIndex.value++
+  resetDialogForm()
+  dialogForm.isCount = false
+  commodityCount.value = []
+  countLoadig.value = false
+}
+
+function resetDialogForm() {
+  dialogForm.prices.lowest = undefined
+  dialogForm.prices.highest = undefined
+  dialogForm.starRating = [0, 5]
+  dialogForm.dialogselectValue = []
+  dialogForm.delivery = 'all'
+  dialogForm.isCount = false
+}
+
+function isCountChanged() {
+  if (dialogForm.isCount) {
+    const instanceId = currentDialogIndex.value
+    countLoadig.value = true
+    getCount(instanceId)
+  } else {
+    countLoadig.value = false
+    commodityCount.value = []
+  }
+}
+
+function delCna(index) {
+  productOrientationTableData.value.splice(index, 1)
+}
+
+function delAllCna() {
+  productOrientationTableData.value = []
+}
+
+function singleGoodsBidSelectChanged() {
+  if (singleGoodsBidSelect.value === 'defaultBid' || categoryBiddingType.value === 'defaultBid') {
+    singleGoodsBidInput.value = ''
+  }
+}
+
+let singleGoodsSearchInp = ref('')
+let searchTableData = ref([])
+function setSearchTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      searchTableData.value = resp.data
+      productOrientationLoading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      productOrientationLoading.value = false
+    })
+}
+function singleGoodsSearchChaneged() {
+  productOrientationLoading.value = true
+  setSearchTableData()
+}
+function addSingleSearch(scope) {
+  console.log('🚀 ~ addSingleSearch ~ scope-->>', scope);
+
+  const typesToAdd = [];
+  if (expand.value) {
+    typesToAdd.push('ASIN_EXPANDED_FROM');
+  }
+  if (accurate.value) {
+    typesToAdd.push('ASIN_SAME_AS');
+  }
+  const productTypeMap = {
+    ASIN_EXPANDED_FROM: '扩展',
+    ASIN_SAME_AS: '精确',
+  };
+
+  typesToAdd.forEach((productType) => {
+    const isAlreadyAdded = productOrientationTableData.value.some(item => item.sku === scope.row.sku && item.productType === productType);
+
+    if (!isAlreadyAdded) {
+      const newData = {
+        type: 'p',
+        asin: scope.row.asin,
+        sku: scope.row.sku,
+        productType: productType,
+        productTypeText: productTypeMap[productType],
+      };
+      productOrientationTableData.value.push(newData);
+    } else {
+      console.log(`${productType} item is already added.`);
+    }
+  });
+}
+
+
+let selectedLabels = ref([]) // 选中的label数组
+function dialogSelectChange(event) {
+  console.log('🚀 ~ dialogSelectChange ~ event-->>', event)
+
+  // 使用 map 来转换每个选中项的 value 为其对应的 label
+  selectedLabels.value = event.map((selectedValue) => {
+    const selectedOption = dialogForm.dialogOptions.find((option) => option.value === selectedValue)
+    return selectedOption ? selectedOption.label : ''
+  })
+
+  console.log('🚀 ~ dialogSelectChange ~ selectedLabels-->>', selectedLabels.value)
+}
+
+let refineItem = ref([])
+// 细化按钮功能
+function refine(data) {
+  console.log('🚀 ~ refine ~ data-->>', data)
+  commodityCount.value = []
+  dialogTitle.value = data.cna
+  categoryId.value = data.cid
+  refineItem.value.push(data)
+  visible.value = true
+  dialogSelectLoading.value = true
+  setDialogOption()
+}
+// 弹框提交功能
+function dialogFormSubmit() {
+  dialogFormRef.value.validate((valid) => {
+    if (valid) {
+      console.log('表单提交')
+      visible.value = false
+      const dialogClassification = dialogTitle.value
+      const dialogPrices_low = dialogForm.prices.lowest
+      const dialogPrices_high = dialogForm.prices.highest
+      const dialogStartRating = dialogForm.starRating
+      const ratingLow = dialogStartRating[0]
+      const ratingHigh = dialogStartRating[1]
+      const dialogDelivery = dialogForm.delivery
+      console.log('🚀 ~ dialogFormRef.value.validate ~ dialogDelivery-->>', dialogDelivery)
+      const deliveryMap = {
+        all: '所有',
+        eligible: '具备Prime资格',
+        diseligible: '不具备Prime资格',
+      }
+
+      selectedLabels.value.forEach((brandLabel) => {
+        // 查找与当前 brandLabel 相对应的选项
+        const selectedOption = dialogForm.dialogOptions.find((option) => option.label === brandLabel)
+        // 获取对应的 brandId,如果没有找到则默认为空
+        const brandId = selectedOption ? selectedOption.value : ''
+        const refineObj = {
+          type: 'c',
+          classification: dialogClassification,
+          classificationId: categoryId.value,
+          brand: brandLabel,
+          brandId: brandId, // 使用找到的 brandId
+          low_price: dialogPrices_low,
+          high_price: dialogPrices_high,
+          low_rating: ratingLow,
+          high_rating: ratingHigh,
+          delivery: dialogDelivery,
+          deliveryText: deliveryMap[dialogDelivery],
+        }
+        console.log('🚀 ~ dialogFormRef.value.validate ~ refineObj-->>', refineObj)
+        productOrientationTableData.value.push(refineObj)
+      })
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+
+// 定向按钮功能
+function orientate(node, data) {
+  console.log('🚀 ~ orientate ~ data-->>', data);
+  const exists = productOrientationTableData.value.some(item => item.cid === data.cid);
+
+  if (!exists) {
+    const newData = {
+      type: 'c',
+      classification: data.cna,
+      classificationId: data.cid,
+    };
+    productOrientationTableData.value.push(newData);
+  }
+}
+
+
+
+let productTargetBidList = ref([])
+async function productTagetSave() {
+  console.log('tableData', productOrientationTableData.value)
+  // 检查是否存在 bid 为空的行
+  const hasEmptyBid = productOrientationTableData.value.some(row => row.bid == null || row.bid === '')
+  // 直接返回,不继续执行
+  if (hasEmptyBid) {
+    console.log('存在空的 bid,不发送请求')
+    ElMessage.error('存在空的 bid,无法创建商品!')
+    return
+  }
+  productOrientationTableData.value.forEach((row) => {
+    productTargetBidList.value.push(row.bid)
+  })
+  console.log('productTargetBidList', productTargetBidList.value)
+  productOrientationLoading.value = true
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      adGroupId: respAdGroupId.value,
+      campaignId: respCampaignId.value,
+      expressionList: productOrientationTableData.value,
+      state: "PAUSED"
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/sptargets/manual/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+
+    console.log('🚀 ~ createTargetGroup ~ resp-->>', resp)
+    productOrientationLoading.value = false
+
+    if (respAdGroupId.value) {
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+
+  // 清空表格和 bid 列表
+  productOrientationTableData.value = []
+  productTargetBidList.value = []
+}
+
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !adsTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    adsTableData.value.push(...newSelections)
+  }
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = adsTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    adsTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+function changeKeyWordsTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+onMounted(() => {
+  setProductOrientationData()
+})
+
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.column-item .el-radio-group {
+  display: inline-flex;
+  font-size: 0;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.radio-description {
+  font-size: 12px;
+  color: #666;
+  margin-top: -18px;
+  margin-left: 22px;
+}
+.radio-description-2 {
+  font-size: 12px;
+  color: #666;
+  margin-top: -10px;
+}
+.column-margin-bottom label.el-radio.is-bordered {
+  margin-bottom: 10px;
+  padding: 35px;
+}
+::v-deep(.column-margin-bottom label.el-radio.is-bordered span.el-radio__inner) {
+  margin-top: -18px;
+  margin-left: -15px;
+}
+.gap-items {
+  display: flex;
+  justify-content: flex-start;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.gap-item {
+  width: 200px;
+  margin-left: 30px;
+  color: #0b0d0d;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-tabs__nav-wrap::after) {
+  height: 2px !important;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+// 输入底部样式
+::v-deep(.card-box .el-card__body) {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  // margin: 10px 0 10px 10px;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.target-group-item {
+  margin-top: 15px;
+}
+.suggested-bid-item {
+  margin-left: 230px;
+  margin-right: 60px;
+}
+.bid-input {
+  width: 200px;
+  margin-left: 15px;
+}
+
+::v-deep(.goods-orientation-tabs .el-tabs__nav-scroll) {
+  margin-left: -20px !important;
+}
+::v-deep(.category-tabs .el-tabs__nav) {
+  margin-left: 20px;
+}
+::v-deep(.goods-orientation-tabs #tab-1) {
+  /* 商品定向Tab栏 */
+  border-right: 0;
+}
+.custom-tree-node {
+  /* el-tree自定义样式 */
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+.dialog-head {
+  /* 弹窗样式 */
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+</style>
+

+ 444 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCommodity.vue

@@ -0,0 +1,444 @@
+<template>
+  <div prop="commodity" style="width: 100%" v-loading="productLoading">
+    <div style="width: 100%; height: 620px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="productTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input v-model="searchInp" placeholder="Please input" class="input-with-select" @change="inpChange" clearable>
+                <template #prepend>
+                  <el-select v-model="leftSelect" style="width: 100px" @change="selChange">
+                    <el-option label="名称" value="name" />
+                    <el-option label="ASIN" value="asin" />
+                    <el-option label="SKU" value="sku" />
+                  </el-select>
+                </template>
+                <template #append>
+                  <el-select v-model="rightSelect" style="width: 100px">
+                    <el-option label="最新优先" value="latest" />
+                    <el-option label="最早优先" value="earliest" />
+                    <el-option label="优选广告" value="optimal" />
+                  </el-select>
+                </template>
+              </el-input>
+            </div>
+            <el-table
+              height="490"
+              style="width: 100%"
+              v-loading="loading"
+              :data="productTableData"
+              :header-cell-style="headerCellStyle"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="50" />
+              <el-table-column prop="asin" label="商品">
+                <template #default="scope">
+                  <div style="display: flex; align-items: center">
+                    <div style="margin-right: 8px; line-height: normal">
+                      <el-image class="img-box" :src="scope.row.image_link" />
+                    </div>
+                    <div>
+                      <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                        <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                      </el-tooltip>
+                      <div class="data-color">
+                        <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                        <span style="margin: 0 5px; color: #cacdd4">|</span>
+                        <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                      </div>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                      <span>
+                        SKU: <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="Name" width="120" align="right">
+                <template #header>
+                  <el-button type="primary" size="normal" link @click="handleGoodsAdd">添加已选中</el-button>
+                </template>
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :total="totalItems"
+              layout="prev, pager, next" />
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              style="padding: 10px"
+              v-model="productTextarea"
+              :rows="20"
+              type="textarea"
+              placeholder="请输入ASIN,多个ASIN使用逗号、空格或换行符分隔。(未完成)"
+              maxlength="11000" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button v-for="button in buttons" :key="button.text" :type="button.type" link @click="addGods">{{ button.text }}</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: 0">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedTableData.length }}</span>
+              <el-tooltip content="添加最少3件商品。我们建议添加至少5件商品,以降低在商品缺货时出现广告活动暂停的可能性。" placement="top">
+                <el-text type="warning" truncated style="width: 350px;">添加最少3件商品。我们建议添加至少5件商品,以降低在商品缺货时出现广告活动暂停的可能性</el-text>
+              </el-tooltip>
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -12px">
+          <el-table
+            :data="addedTableData"
+            height="510"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedGoodsChange">
+            <el-table-column type="selection" width="50" />
+            <el-table-column prop="asin" label="ASIN">
+              <template #default="scope">
+                <div style="display: flex; align-items: center">
+                  <div style="margin-right: 8px; line-height: normal">
+                    <el-image class="img-box" :src="scope.row.image_link" />
+                  </div>
+                  <div>
+                    <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                      <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                    </el-tooltip>
+                    <div class="data-color">
+                      <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                      <span style="margin: 0 5px; color: #cacdd4">|</span>
+                      <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                    </div>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                    <span
+                      >SKU:
+                      <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="Name" width="120" align="right">
+              <template #header>
+                <el-button type="danger" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
+              </template>
+              <template #default="scope">
+                <el-button type="primary" size="small" @click="delSingleGoods(scope)" text>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <!-- <div style="display: flex; justify-content: space-around; padding-top: 5px">
+          <el-button type="primary" plain :disabled="addedTableData.length < 3"  @click="submitProductForm">保存</el-button>
+        </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { TabsPaneContext } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import type { Ref } from 'vue'
+import { inject, onMounted, ref, watch } from 'vue'
+import emitter from '/@/utils/emitter'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const productTextarea = ref('')
+const productLoading = ref(false)
+let addedAdsTableItems = ref([])
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const productTableData = ref([]) // 左侧表格数据
+const loading = ref(false)
+let addedTableData = ref([])
+let selections = []
+let addedSels = []
+const searchInp = ref('')
+const leftSelect = ref('name')
+const buttons = [{ type: 'primary', text: '添加' }] as const
+const productTabs = ref('first')
+const rightSelect = ref('latest')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respCampaignName = inject<Ref>('respCampaignName')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+function setTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      productTableData.value = resp.data
+      totalItems.value = resp.total
+      currentPage.value = resp.page
+      loading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      loading.value = false
+    })
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = addedTableData.value.some((item) => item.asin === scope.row.asin)
+  if (!isAlreadyAdded) {
+    addedTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function addGods() {
+  const inputData = productTextarea.value
+  const asins = inputData.split(/[\n,]+/)
+
+  asins.forEach((asin) => {
+    if (asin.trim()) {
+      setTableData(asin.trim())
+        .then((response) => {
+          console.log(`Data for ASIN ${asin}:`, response) // 更新这里来正确地访问数据
+        })
+        .catch((error) => {
+          console.error(`Error fetching data for ASIN ${asin}:`, error)
+        })
+    }
+  })
+}
+
+function delSingleGoods(scope) {
+  const index = addedTableData.value.findIndex((item) => item.asin === scope.row.asin)
+  if (index !== -1) {
+    addedTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function delAllGoods() {
+  addedTableData.value = []
+  // addedTableData.value.splice(0, addedTableData.value.length)
+}
+
+// 删除第二个table中已经选中的项
+function delSelectedGoods() {
+  addedTableData.value = addedTableData.value.filter((item) => !addedSels.includes(item))
+  addedSels = []
+}
+
+function inpChange(e) {
+  const value = e
+  if (leftSelect.value === 'asin') {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku') {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+
+function selChange(e) {
+  console.log('e', e)
+  const value = e
+  if (leftSelect.value === 'asin' && searchInp.value) {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku' && searchInp.value) {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !addedTableData.value.some((added) => added.asin === sel.asin) // 使用asin作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    addedTableData.value.push(...newSelections)
+  }
+}
+// 点击Tab
+const handleGoodsTabs = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+function isItemInList(item, list) {
+  return list.some((listItem) => listItem.sku === item.sku && listItem.asin === item.asin)
+}
+// 监听商品右侧表格已添加的数据并转化数据格式
+watch(
+  addedTableData,
+  (newValue, oldValue) => {
+    newValue.forEach((item) => {
+      if (!isItemInList(item, addedAdsTableItems.value)) {
+        addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
+      }
+    })
+  },
+  { deep: true }
+)
+
+async function createAds() {
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinsku: addedAdsTableItems.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/spads/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ createAds ~ resp-->>', resp)
+    productLoading.value = false
+    if (resp.data.success.length > 0) {
+      addedTableData.value = []
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function submitProductForm() {
+  productLoading.value = true
+  createAds()
+}
+
+// 处理分页器当前页变化
+function handleCurrentChange(newPage) {
+  currentPage.value = newPage
+  loading.value = true
+  setTableData()
+}
+// 处理分页器每页显示条目数变化
+function handleSizeChange(newSize) {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+watch(addedTableData, () => {
+  if (addedTableData.value.length > 0) {
+    emitter.emit('addedTableData', addedTableData.value)
+  }
+},{deep:true})
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+onMounted(() => {
+  setTableData()
+})
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+/* 商品定向Tab栏 */
+::v-deep(.goods-orientation-tabs #tab-1) {
+  border-right: 0;
+}
+</style>

+ 896 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity1.vue

@@ -0,0 +1,896 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">创意</span>
+      </div>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    :on-remove="handleRemove"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    list-type="picture-card"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+                <el-collapse-item name="2" style="padding-right: 10px">
+                  <template #title>自定义图片(可选)</template>
+                  <el-upload
+                    v-model:file-list="customFileList"
+                    :on-change="changeCustomFile"
+                    :on-remove="handleCustomRemove"
+                    v-loading="customUpLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    list-type="picture-card"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="customPictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">图片规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openLifeStyleDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 1200 x 628 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 5MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">4、内容: 图片中未添加文本、图形或徽标</div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="customDialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="(item, index) in flattenedCommodityCard" :key="index" style="margin: 0 0 5px 0">
+                    <el-card shadow="hover" body-style="padding: 10px; display: flex;">
+                      <div style="margin-right: 8px; line-height: normal">
+                        <el-image class="img-box" :src="item.image_link" />
+                      </div>
+                      <div style="position: relative">
+                        <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                          <div class="double-line">{{ item.title }}</div>
+                        </el-tooltip>
+                        <span>
+                          <span style="color: #6d7784">ASIN: </span>
+                          <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                        </span>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          link
+                          @click="() => openCommodityDialog(index)"
+                          style="position: absolute; bottom: 2px; right: 0">
+                          更换商品
+                        </el-button>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button
+              type="primary"
+              plain
+              @click="submitForm(ruleFormRef)"
+              :disabled="!fileList.length"
+              style="position: absolute; top: 92%; left: 46%">
+              保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="commodityDialog" title="更换商品" width="50%">
+      <el-radio-group
+        v-loading="dialogLoading3"
+        v-model="selectedCommodity"
+        style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+        <div v-for="(item, index) in flattenedReplaceableCommodity" :key="index">
+          <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+            <div style="padding: 10px; display: flex; align-items: center">
+              <div style="margin-right: 8px; line-height: normal">
+                <el-image class="img-box" :src="item.image_link" />
+              </div>
+              <div style="position: relative">
+                <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                  <div class="double-line">{{ item.title }}</div>
+                </el-tooltip>
+                <span>
+                  <span style="color: #6d7784">ASIN: </span>
+                  <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                </span>
+              </div>
+            </div>
+          </el-radio>
+        </div>
+      </el-radio-group>
+      <div style="margin-top: 20px; display: flex; justify-content: center">
+        <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedCommodity">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Delete, Picture, Plus, Search, ZoomIn } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules, UploadFile, UploadProps } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { Ref, computed, inject, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
+import { checkAsset, getAssets, getCommodityCard, getLifeStyleAssets, getPageAsins, postProductset, uploadFile } from '../api/index'
+import { useShopInfo } from '/@/stores/shopInfo'
+import emitter from '/@/utils/emitter'
+
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createCreativity()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    getCommodityCardData()
+  }
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const customDialogVisible = ref(false)
+const disabled = ref(false)
+const pictureList = ref([])
+const fileList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+// 图片上传功能
+const upLoading = ref(false)
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handleCustomRemove(file: UploadFile) {
+  customFileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function customPictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  customDialogVisible.value = true
+}
+
+// 自定义文件上传
+const customFileList = ref([])
+const customUpLoading = ref(false)
+let customImageAssetId = ''
+
+function changeCustomFile(file: UploadFile) {
+  handleCustomUpload(file)
+}
+
+async function handleCustomUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId.value)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    customImageAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    customImageCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+const createLoading = ref(false)
+const casins = ref('')
+let brandLogoCrop = {}
+let customImageCrop = {}
+let url = ''
+let brandName = ''
+let brandLogoAssetID = ''
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId.value)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    brandLogoAssetID = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+async function createCreativity() {
+  createLoading.value = true
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: casins.value,
+      lasins: [],
+      url: url,
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: brandName,
+      brandLogoCrop: brandLogoCrop,
+      brandLogoAssetID: brandLogoAssetID,
+      customImageCrop: customImageCrop,
+      customImageAssetId: customImageAssetId,
+      headline: ruleForm.title,
+    }
+    const response = await postProductset(obj)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+// 选择卡片功能
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+    fileList.value.push(newFile) // 将新的文件对象添加到 fileList 中
+  }
+
+  selectedCards.value = [] // 清空选中卡片
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+function openLifeStyleDialog() {
+  lifeStyleDialog.value = true
+  getLifeStyleAssetsData()
+}
+
+const pageOptionsValue = inject<Ref>('pageOptionsValue')
+const asinList = ref([])
+const commodityLoading = ref(false)
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageOptionsValue.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+let lastQueriedAsins = []
+const commodityCard = ref([])
+async function getCommodityCardData() {
+  try {
+    commodityLoading.value = true
+    const topAsins = asinList.value.slice(0, 3)
+
+    const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+    if (newAsins.length === 0) {
+      commodityLoading.value = false
+      return // 如果没有新的 ASIN,直接返回
+    }
+
+    lastQueriedAsins = [...topAsins]
+
+    // 清空commodityCard,为新数据做准备
+    commodityCard.value = []
+
+    // 对每个新的 ASIN 发送请求
+    for (const asin of newAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        commodityCard.value.push(response.data)
+        // console.log('Response for ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+const dialogLoading3 = ref(false)
+let currentEditingIndex = ref(null)
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+        console.log('🚀 ~ getAdditionalCommodityData ~ replaceableCommodity-->>', replaceableCommodity.value)
+        // console.log('Response for additional ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+function handleSelectedCommodity() {
+  if (currentEditingIndex.value !== null && selectedCommodity.value) {
+    const selectedCommodityData = flattenedReplaceableCommodity.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData) {
+      commodityCard.value[currentEditingIndex.value] = selectedCommodityData
+    }
+    commodityDialog.value = false
+    console.log('commodityCard', commodityCard.value)
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+  }
+}
+
+watch(
+  () => pageOptionsValue.value,
+  async () => {
+    await getCommodityCollapseData()
+    getCommodityCardData()
+    getAdditionalCommodityData()
+  }
+)
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+
+watch(
+  flattenedCommodityCard,
+  (newValue: any) => {
+    casins.value = newValue.map((item) => item.asin)
+  },
+  { deep: true }
+)
+
+const brandEntityId = ref('')
+
+onMounted(() => {
+  emitter.on('send-brandEntityId', (value: any) => {
+    brandEntityId.value = value.brandEntityId[0].brandEntityId
+  })
+  emitter.on('page', (value: any) => {
+    url = value
+  })
+  emitter.on('video-shop', (value: any) => {
+    brandName = value.brandRegistryName
+  })
+})
+
+// 接收数据端在组建卸载时解绑事件
+onUnmounted(() => {
+  emitter.off('send-brandEntityId')
+  emitter.off('page')
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

+ 461 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity2.vue

@@ -0,0 +1,461 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">创意</span>
+      </div>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none; padding-right: 10px;">
+                <el-collapse-item name="1">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 10px 5px 0"></el-input>
+                  </el-form-item>
+                  <el-upload
+                    drag
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :auto-upload="false"
+                    :on-remove="handleRemove"
+                    style="padding-right: 10px">
+                    <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+                    <div class="el-upload__text">Drop file here or <em>click to upload</em></div>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                </el-collapse-item>
+                <el-collapse-item name="2">
+                  <template #title>自定义图片(可选)</template>
+                  <el-upload
+                    class="avatar-uploader"
+                    v-model:file-list="customFileList"
+                    :on-change="changeCustomFile"
+                    v-loading="upCustomLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :auto-upload="false"
+                    :on-remove="handleRemove"
+                    style="padding-right: 10px">
+                    <img v-if="imageUrl" :src="imageUrl" class="avatar" />
+                    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">图片规格</span>
+                          <el-button type="primary" :icon="Picture">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 1200 x 628 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 5MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">4、内容: 图片中未添加文本、图形或徽标</div>
+                      </div>
+                    </template>
+                  </el-upload>
+                </el-collapse-item>
+                <el-collapse-item name="3">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="item in commodityCard" :key="item.asin" style="padding-bottom: 5px;">
+                    <el-card shadow="hover" body-style="padding: 10px">
+                      <div style="padding: 10px; display: flex; align-items: center">
+                        <div style="margin-right: 8px; line-height: normal">
+                          <el-image class="img-box" :src="item.image_link" />
+                        </div>
+                        <div style="position: relative">
+                          <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                            <div class="single-line">{{ item.title }}</div>
+                          </el-tooltip>
+                          <span>
+                            <span style="color: #6d7784">ASIN: </span>
+                            <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                          </span>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button
+              type="primary"
+              plain
+              @click="submitForm(ruleFormRef)"
+              :disabled="!fileList.length"
+              style="position: absolute; top: 92%; left: 46%">
+              保存
+            </el-button>
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, onMounted, onUnmounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { checkAsset, uploadFile, postProductset } from '../api/index'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture } from '@element-plus/icons-vue'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const setBrandName = inject<Ref>('setBrandName')
+const addedTableDataForVc2 = inject<Ref>('addedTableDataForVc2')
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+
+const ruleFormRef = ref<FormInstance>()
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createCreativity()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  console.log(val)
+}
+
+const fileList = ref<UploadUserFile[]>([])
+
+const customFileList = ref<UploadUserFile[]>([])
+
+const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
+  fileList.value = []
+}
+
+const handleCustomRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
+  fileList.value = []
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+const upLoading = ref(false)
+let brandLogoAssetID = ''
+let brandLogoCrop = {}
+let createLoading = ref(false)
+const brandEntityId = inject<Ref>('brandEntityId')
+
+function changeFile(file) {
+  handleUpload(file)
+}
+
+async function handleUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId.value)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    brandLogoAssetID = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+// 自定义图片上传
+const upCustomLoading = ref(false)
+const lasins = ref('')
+let customImageAssetId = ''
+let customImageCrop = {}
+
+
+function changeCustomFile(file) {
+  handleCustomUpload(file)
+}
+
+async function handleCustomUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId.value)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upCustomLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    customImageAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upCustomLoading.value = false
+  }
+}
+
+// TODO: url对应的是Home 暂时写死
+async function createCreativity() {
+  createLoading.value = true
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: lasins.value,
+      lasins: lasins.value,
+      url: 'https://www.amazon.com/stores/page/1D1DD2FD-CF54-4FE5-B1A0-9E01F12F8144',
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: setBrandName.value,
+      brandLogoCrop: brandLogoCrop,
+      brandLogoAssetID: brandLogoAssetID,
+      customImageCrop: customImageCrop,
+      customImageAssetId: customImageAssetId,
+      headline: ruleForm.title,
+    }
+    const response = await postProductset(obj)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+let asins = ref([])
+
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    asins.value = []
+    if (Array.isArray(newValue) && newValue.length > 0) {
+      newValue.forEach((item) => {
+        if (item.asin && !asins.value.includes(item.asin)) {
+          asins.value.push(item.asin)
+        }
+      })
+    }
+    console.log('Updated ASINs:', asins.value)
+  },
+  { deep: true }
+)
+
+const commodityCard = ref([])
+
+watch(
+  commodityCard,
+  (newValue: any) => {
+    lasins.value = newValue.map(item => item.asin)
+  },
+  { deep: true }
+)
+
+onMounted(() => {
+  emitter.on('addedTableData', (data: any) => {
+    commodityCard.value = data
+  })
+})
+
+// 接收数据端在组件卸载时解绑事件
+onUnmounted(() => {
+  emitter.off('addedTableData')
+})
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+</style>

+ 452 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCommodity.vue

@@ -0,0 +1,452 @@
+<template>
+  <div prop="commodity" style="width: 100%" v-loading="productLoading">
+    <div style="width: 100%; height: 620px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="productTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input v-model="searchInp" placeholder="Please input" class="input-with-select" @change="inpChange" clearable>
+                <template #prepend>
+                  <el-select v-model="leftSelect" style="width: 100px" @change="selChange">
+                    <el-option label="名称" value="name" />
+                    <el-option label="ASIN" value="asin" />
+                    <el-option label="SKU" value="sku" />
+                  </el-select>
+                </template>
+                <template #append>
+                  <el-select v-model="rightSelect" style="width: 100px">
+                    <el-option label="最新优先" value="latest" />
+                    <el-option label="最早优先" value="earliest" />
+                    <el-option label="优选广告" value="optimal" />
+                  </el-select>
+                </template>
+              </el-input>
+            </div>
+            <el-table
+              height="490"
+              style="width: 100%"
+              v-loading="loading"
+              :data="productTableData"
+              :header-cell-style="headerCellStyle"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="50" />
+              <el-table-column prop="asin" label="商品">
+                <template #default="scope">
+                  <div style="display: flex; align-items: center">
+                    <div style="margin-right: 8px; line-height: normal">
+                      <el-image class="img-box" :src="scope.row.image_link" />
+                    </div>
+                    <div>
+                      <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                        <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                      </el-tooltip>
+                      <div class="data-color">
+                        <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                        <span style="margin: 0 5px; color: #cacdd4">|</span>
+                        <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                      </div>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                      <span>
+                        SKU: <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="Name" width="120" align="right">
+                <template #header>
+                  <el-button type="primary" size="normal" link :disabled="addedTableData.length >= 1" @click="handleGoodsAdd">添加已选中</el-button>
+                </template>
+                <template #default="scope">
+                  <el-button type="primary" size="small" :disabled="addedTableData.length >= 1" @click="addSingleGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :total="totalItems"
+              layout="prev, pager, next" />
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              style="padding: 10px"
+              v-model="productTextarea"
+              :rows="20"
+              type="textarea"
+              placeholder="请输入ASIN,多个ASIN使用逗号、空格或换行符分隔。(未完成)"
+              maxlength="11000" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button v-for="button in buttons" :key="button.text" :type="button.type" link @click="addGods">{{ button.text }}</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: 0">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedTableData.length }}</span>
+
+                <el-text type="warning" truncated>最多添加一个产品</el-text>
+
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -12px">
+          <el-table
+            :data="addedTableData"
+            height="475"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedGoodsChange">
+            <el-table-column type="selection" width="50" />
+            <el-table-column prop="asin" label="ASIN">
+              <template #default="scope">
+                <div style="display: flex; align-items: center">
+                  <div style="margin-right: 8px; line-height: normal">
+                    <el-image class="img-box" :src="scope.row.image_link" />
+                  </div>
+                  <div>
+                    <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                      <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                    </el-tooltip>
+                    <div class="data-color">
+                      <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                      <span style="margin: 0 5px; color: #cacdd4">|</span>
+                      <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                    </div>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                    <span
+                      >SKU:
+                      <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="Name" width="120" align="right">
+              <template #header>
+                <el-button type="danger" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
+              </template>
+              <template #default="scope">
+                <el-button type="primary" size="small" @click="delSingleGoods(scope)" text>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <!-- <div style="display: flex; justify-content: space-around; padding-top: 5px">
+          <el-button type="primary" plain :disabled="productSave" @click="submitProductForm">保存</el-button>
+        </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject, onMounted, ref, watch, provide, onUnmounted } from 'vue'
+import type { Ref } from 'vue'
+import type { TabsPaneContext } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const productTextarea = ref('')
+const productLoading = ref(false)
+let addedAdsTableItems = ref([])
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const productTableData = ref([]) // 左侧表格数据
+const loading = ref(false)
+let addedTableData = ref([])
+let selections = []
+let addedSels = []
+const searchInp = ref('')
+const leftSelect = ref('name')
+let productSave = ref(true)
+const buttons = [{ type: 'primary', text: '添加' }] as const
+const productTabs = ref('first')
+const rightSelect = ref('latest')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+function setTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      productTableData.value = resp.data
+      totalItems.value = resp.total
+      currentPage.value = resp.page
+      loading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      loading.value = false
+    })
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = addedTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    addedTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function addGods() {
+  const inputData = productTextarea.value
+  const asins = inputData.split(/[\n,]+/)
+
+  asins.forEach((asin) => {
+    if (asin.trim()) {
+      setTableData(asin.trim())
+        .then((response) => {
+          console.log(`Data for ASIN ${asin}:`, response) // 更新这里来正确地访问数据
+        })
+        .catch((error) => {
+          console.error(`Error fetching data for ASIN ${asin}:`, error)
+        })
+    }
+  })
+}
+
+function delSingleGoods(scope) {
+  const index = addedTableData.value.findIndex((item) => item.sku === scope.row.sku)
+  if (index !== -1) {
+    addedTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function delAllGoods() {
+  addedTableData.value = []
+  // addedTableData.value.splice(0, addedTableData.value.length)
+}
+
+// 删除第二个table中已经选中的项
+function delSelectedGoods() {
+  addedTableData.value = addedTableData.value.filter((item) => !addedSels.includes(item))
+  addedSels = []
+}
+
+function inpChange(e) {
+  const value = e
+  if (leftSelect.value === 'asin') {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku') {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+
+function selChange(e) {
+  console.log('e', e)
+  const value = e
+  if (leftSelect.value === 'asin' && searchInp.value) {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku' && searchInp.value) {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !addedTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    addedTableData.value.push(...newSelections)
+  }
+}
+// 点击Tab
+const handleGoodsTabs = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+function isItemInList(item, list) {
+  return list.some((listItem) => listItem.sku === item.sku && listItem.asin === item.asin)
+}
+// 监听商品右侧表格已添加的数据并转化数据格式
+watch(
+  addedTableData,
+  (newValue, oldValue) => {
+    newValue.forEach((item) => {
+      if (!isItemInList(item, addedAdsTableItems.value)) {
+        addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
+      }
+    })
+    if (addedTableData.value.length !== 0) {
+      productSave.value = false
+    } else {
+      productSave.value = true
+    }
+  },
+  { deep: true }
+)
+
+async function createAds() {
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinsku: addedAdsTableItems.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/spads/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ createAds ~ resp-->>', resp)
+    productLoading.value = false
+    if (resp.data.success.length > 0) {
+      productSave.value = false
+      addedTableData.value = []
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function submitProductForm() {
+  productLoading.value = true
+  createAds()
+}
+
+// 处理分页器当前页变化
+function handleCurrentChange(newPage) {
+  currentPage.value = newPage
+  loading.value = true
+  setTableData()
+}
+// 处理分页器每页显示条目数变化
+function handleSizeChange(newSize) {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+const emit = defineEmits(['update-added-data']);
+
+// 当数据发生变化时,触发事件
+watch(addedTableData, (newValue) => {
+  emit('update-added-data', newValue);
+}, {deep: true});
+
+onMounted(() => {
+  setTableData()
+})
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+/* 商品定向Tab栏 */
+::v-deep(.goods-orientation-tabs #tab-1) {
+  border-right: 0;
+}
+</style>

+ 908 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity1.vue

@@ -0,0 +1,908 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">创意</span>
+      </div>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    list-type="picture-card"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" disabled="true" @click="openDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+                <el-collapse-item name="2" style="padding-right: 10px">
+                  <template #title><span style="color: #e47470; margin-right: 4px">*</span>视频</template>
+                  <div style="display: flex; justify-content: space-between">
+                    <div style="font-weight: 700; color: #1d2129; line-height: 18px; font-size: 14px">选择视频</div>
+                    <el-button type="primary" link>查看视频批准提示</el-button>
+                  </div>
+                  <div style="margin: 10px 0; font-size: 12px; font-weight: 400; color: #666; line-height: 18px">
+                    保持视频简短并紧扣主题。视频会自动播放,因此请确保前 2
+                    秒极具吸引力,并且不依靠声音来传递信息。如果您在视频中使用了文字,请确保文字清晰易辨。字幕或音频必须与将展示您广告的区域相匹配。
+                  </div>
+                  <el-upload
+                    v-model:file-list="videoList"
+                    :on-change="changeVideo"
+                    v-loading="videoLoading"
+                    accept=".mp4, .mov"
+                    action="#"
+                    :limit="1"
+                    :auto-upload="false"
+                    :on-remove="handleRemoveVideo"
+                    class="upload-demo">
+                    <el-button type="primary">上传视频</el-button>
+                    <template #tip>
+                      <el-button type="primary" disabled="true" style="margin-left: 20px">从素材库中选择</el-button>
+                      <!-- <div class="el-upload__tip">div> -->
+                      <hr style="margin: 10px 0" />
+                      <div>
+                        <div style="display: flex; justify-content: space-between">
+                          <span style="font-weight: 700; color: #1d2129; line-height: 18px; font-size: 14px">视频文件要求:</span>
+                          <el-button type="primary" link>了解更多</el-button>
+                        </div>
+                        <div class="tip-list-title">视频格式</div>
+                        <div class="tip-item">1.纵横比:16:9</div>
+                        <div class="tip-item">2.尺寸:1280 x 720 像素、1920 x 1080 像素或 3840 x 2160 像素</div>
+                        <div class="tip-item">3.文件大小:500MB 或更小</div>
+                        <div class="tip-item">4.文件格式:MP4 或 MOV</div>
+                        <div class="tip-item">5.长度:6-45 秒</div>
+                        <div class="tip-item">6.帧率:23.976、23.98、24、25、29.97 或 29.98 fps</div>
+                        <div class="tip-item">7.比特率:1 Mbps 或更高</div>
+                        <div class="tip-item">8.编解码器:H.264 或 H.2651</div>
+                        <div class="tip-item">9.配置文件:主配置文件或基线配置文件</div>
+                        <div class="tip-item">10.视频流:仅为 1</div>
+                        <div class="tip-list-title">音频规格</div>
+                        <div class="tip-item">1.语言:必须与广告投放区域匹配</div>
+                        <div class="tip-item">2.采样率:44.1 kHz 或更高</div>
+                        <div class="tip-item">3.编解码器:PCM、AAC 或 MP3</div>
+                        <div class="tip-item">4.比特率:96 kbps 或更高</div>
+                        <div class="tip-item">5.格式:立体声或单声道</div>
+                        <div class="tip-item">6.音频流:仅为 1</div>
+                      </div>
+                    </template>
+                  </el-upload>
+
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="(item, index) in flattenedCommodityCard" :key="index" style="margin: 0 0 5px 0">
+                    <el-card shadow="hover" body-style="padding: 10px; display: flex;">
+                      <div style="margin-right: 8px; line-height: normal">
+                        <el-image class="img-box" :src="item.image_link" />
+                      </div>
+                      <div style="position: relative">
+                        <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                          <div class="double-line">{{ item.title }}</div>
+                        </el-tooltip>
+                        <span>
+                          <span style="color: #6d7784">ASIN: </span>
+                          <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                        </span>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          link
+                          @click="() => openCommodityDialog(index)"
+                          style="position: absolute; bottom: 2px; right: 0">
+                          更换商品
+                        </el-button>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="submitForm(ruleFormRef)" :disabled="!fileList.length" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="commodityDialog" title="更换商品" width="50%">
+      <el-radio-group
+        v-loading="dialogLoading3"
+        v-model="selectedCommodity"
+        style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+        <div v-for="(item, index) in flattenedReplaceableCommodity" :key="index">
+          <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+            <div style="padding: 10px; display: flex; align-items: center">
+              <div style="margin-right: 8px; line-height: normal">
+                <el-image class="img-box" :src="item.image_link" />
+              </div>
+              <div style="position: relative">
+                <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                  <div class="double-line">{{ item.title }}</div>
+                </el-tooltip>
+                <span>
+                  <span style="color: #6d7784">ASIN: </span>
+                  <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                </span>
+              </div>
+            </div>
+          </el-radio>
+        </div>
+      </el-radio-group>
+      <div style="margin-top: 20px; display: flex; justify-content: center">
+        <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedCommodity">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, computed, onMounted, onUnmounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import { getAssets, getLifeStyleAssets, getPageAsins, getCommodityCard, uploadFile, checkAsset, postBrandVideo } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createBrandVideo()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    getCommodityCardData()
+  }
+}
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const disabled = ref(false)
+const fileList = ref([])
+const videoList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+const upLoading = ref(false)
+
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+let brandLogoCrop = {}
+let respAssetId = ''
+let brandEntityId = ''
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+const videoLoading = ref(false)
+let videoAssetIds = ''
+function handleRemoveVideo(file: UploadFile) {
+  videoList.value = []
+}
+
+function changeVideo(file: UploadFile) {
+  uploadVideo(file)
+}
+
+// 上传视频
+async function uploadVideo(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'VIDEO')
+  formData.append('assetSubTypeList', JSON.stringify([]))
+  videoLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    videoAssetIds = resp.data.assetId
+    
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    videoLoading.value = false
+  }
+}
+
+const respAdGroupId = inject<Ref>('respAdGroupId')
+let brandName = ''
+let selectedPage = ''
+const createLoading = ref(false)
+async function createBrandVideo() {
+  createLoading.value = true
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: asinListFromCommodityCard.value,
+      url: selectedPage,
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: brandName,
+      brandLogoAssetID: respAssetId,
+      brandLogoCrop: brandLogoCrop,
+      consentToTranslate: false,
+      videoAssetIds: videoAssetIds,
+      headline: ruleForm.title,
+    }
+    const response = await postBrandVideo(obj)
+    console.log('response', response)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+onMounted(() => {
+  emitter.on('video-shop', (value: any) => {
+    brandName = value.brandRegistryName
+    brandEntityId = value.brandEntityId
+  })
+  emitter.on('page', (newPageValue: any) => {
+    selectedPage = newPageValue
+  })
+})
+
+// 接收数据端在组件卸载时解绑事件
+onUnmounted(() => {
+  emitter.off('video-shop')
+  emitter.off('page')
+})
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+
+    // 将新的文件对象添加到 fileList 中
+    fileList.value.push(newFile)
+  }
+
+  // 清空选中卡片
+  selectedCards.value = []
+  // 关闭对话框
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  console.log('🚀 ~ getAssetsData ~ response-->>', response)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+function openLifeStyleDialog() {
+  lifeStyleDialog.value = true
+  getLifeStyleAssetsData()
+}
+
+const pageOptionsValue = inject<Ref>('pageOptionsValue')
+const asinList = ref([])
+const commodityLoading = ref(false)
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageOptionsValue.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    console.log('asinList', asinList.value)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+function clickSave() {
+  console.log(123, fileList.value)
+}
+
+let lastQueriedAsins = []
+const commodityCard = ref([])
+async function getCommodityCardData() {
+  try {
+    commodityLoading.value = true
+    const topAsins = asinList.value.slice(0, 3)
+
+    const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+    if (newAsins.length === 0) {
+      commodityLoading.value = false
+      return // 如果没有新的 ASIN,直接返回
+    }
+    lastQueriedAsins = [...topAsins]
+    // 清空commodityCard,为新数据做准备
+    commodityCard.value = []
+    // 对每个新的 ASIN 发送请求
+    for (const asin of newAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        commodityCard.value.push(response.data)
+        // console.log('Response for ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+const dialogLoading3 = ref(false)
+let currentEditingIndex = ref(null)
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+function handleSelectedCommodity() {
+  if (currentEditingIndex.value !== null && selectedCommodity.value) {
+    const selectedCommodityData = flattenedReplaceableCommodity.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData) {
+      commodityCard.value[currentEditingIndex.value] = selectedCommodityData
+    }
+    commodityDialog.value = false
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+  }
+}
+
+watch(
+  () => pageOptionsValue.value,
+  async () => {
+    await getCommodityCollapseData()
+    getCommodityCardData()
+    getAdditionalCommodityData()
+  }
+)
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const asinListFromCommodityCard = computed(() => {
+  return flattenedCommodityCard.value.map((item) => item.asin)
+})
+
+// watch(asinListFromCommodityCard, (newAsinList) => {
+//   console.log('New ASIN list:', newAsinList);
+// });
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.tip-list-title {
+  line-height: 17px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #1d2129;
+  padding-top: 8px;
+  padding-bottom: 4px;
+}
+.tip-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #86909c;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

+ 511 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity2.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;" v-loading="loading">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">创意</span>
+      </div>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="video" style="padding: 0 10px 0 5px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>视频 </template>
+                  <div>
+                    <div style="display: flex; align-items: center; justify-content: space-between">
+                      <span style="color: #1e2128; font-size: 15px; font-weight: 450">选择视频</span>
+                      <el-button type="primary" link>查看视频批准提示</el-button>
+                    </div>
+                    <div style="color: #666666; margin-bottom: 10px">
+                      保持视频简短并紧扣主题。视频会自动播放,因此请确保前 2
+                      秒极具吸引力,并且不依靠声音来传递信息。如果您在视频中使用了文字,请确保文字清晰易辨。字幕或音频必须与将展示您广告的区域相匹配。
+                    </div>
+                    <div class="upload-button-group">
+                      <el-upload
+                        v-model:file-list="fileList"
+                        :on-change="changeFile"
+                        v-loading="upLoading"
+                        accept=".mp4, .mov"
+                        action="#"
+                        :limit="1"
+                        :auto-upload="false"
+                        :on-remove="handleRemove"
+                        class="upload-demo">
+                        <el-button type="primary">上传文件</el-button>
+
+                        <!-- <template #tip>
+                          <div class="el-upload__tip"><el-button type="primary" :icon="Picture"  :disabled="true" @click="handleSelect">从素材库中选择</el-button></div>
+                        </template> -->
+                      </el-upload>
+                      <!-- <el-button type="primary" :icon="Picture" style="margin-left: -120px" :disabled="true" @click="handleSelect">从素材库中选择</el-button> -->
+                    </div>
+                  </div>
+                </el-collapse-item>
+                <div style="display: flex; align-items: center; justify-content: space-between; padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 15px; font-weight: 450">视频文件要求:</span>
+                  <el-button type="primary" link>了解更多</el-button>
+                </div>
+                <div style="padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 13px; font-weight: 450">视频规格</span>
+                  <div class="introduce-item">1.纵横比:16:9</div>
+                  <div class="introduce-item">2.尺寸:1280 x 720 像素、1920 x 1080 像素或 3840 x 2160 像素</div>
+                  <div class="introduce-item">3.文件大小:500MB 或更小</div>
+                  <div class="introduce-item">4.文件格式:MP4 或 MOV</div>
+                  <div class="introduce-item">5.长度:6-45 秒</div>
+                  <div class="introduce-item">6.帧率:23.976、23.98、24、25、29.97 或 29.98 fps</div>
+                  <div class="introduce-item">7.比特率:1 Mbps 或更高</div>
+                  <div class="introduce-item">8.编解码器:H.264 或 H.265</div>
+                  <div class="introduce-item">9.配置文件:主配置文件或基线配置文件</div>
+                  <div class="introduce-item">10.视频流:仅为 1</div>
+                </div>
+                <div style="padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 13px; font-weight: 450">音频规格</span>
+                  <div class="introduce-item">1.语言:必须与广告投放区域匹配</div>
+                  <div class="introduce-item">2.采样率:44.1 kHz 或更高</div>
+                  <div class="introduce-item">3.编解码器:PCM、AAC 或 MP3</div>
+                  <div class="introduce-item">4.比特率:96 kbps 或更高</div>
+                  <div class="introduce-item">5.格式:立体声或单声道</div>
+                  <div class="introduce-item">6.音频流:仅为 1</div>
+                </div>
+                <hr style="color: #eceef4; margin: 8px 10px 0 5px" />
+                <el-collapse-item name="commodity" style="padding: 0 10px 0 5px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品 </template>
+                  <div v-for="item in addedTableDataForVc2" :key="item.asin">
+                    <el-card shadow="hover" body-style="padding: 10px">
+                      <div style="padding: 10px; display: flex; align-items: center">
+                        <div style="margin-right: 8px; line-height: normal">
+                          <el-image class="img-box" :src="item.image_link" />
+                        </div>
+                        <div style="position: relative">
+                          <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                            <div class="double-line">{{ item.title }}</div>
+                          </el-tooltip>
+                          <span>
+                            <span style="color: #6d7784">ASIN: </span>
+                            <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                          </span>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button
+              type="primary"
+              plain
+              @click="submitForm(ruleFormRef)"
+              :disabled="!commodityCard.length && addedTableDataForVc2"
+              style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <video class="image" :src="item.imageUrl" controls preload="none" @click.stop></video>
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">背景视频</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch, inject, Ref, onMounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search } from '@element-plus/icons-vue'
+import { getVideoAssets, videoDetailCreate, uploadFile, checkAsset, postVideo } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const addedTableDataForVc2 = inject<Ref>('addedTableDataForVc2')
+
+interface RuleForm {
+  name: string
+}
+const ruleFormRef = ref<FormInstance>()
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+})
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createVideo()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['video'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+}
+
+const fileList = ref<UploadUserFile[]>([]) // 上传的文件列表
+
+const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
+  fileList.value.length = 0
+  // console.log(file, uploadFiles)
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+function changeFile(file) {
+  // console.log('file', file)
+  handleUpload(file)
+}
+
+const upLoading = ref(false)
+let brandLogoCrop = {}
+let respAssetId = ''
+let brandEntityId = ''
+
+async function handleUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'VIDEO')
+  formData.append('assetSubTypeList', JSON.stringify([]))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+const createLoading = ref(false)
+async function createVideo() {
+  createLoading.value = true
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: asins.value,
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      videoAssetIds: respAssetId,
+      consentToTranslate: false,
+    }
+    const response = await postVideo(obj)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+let asins = ref([])
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    asins.value = []
+    if (Array.isArray(newValue) && newValue.length > 0) {
+      newValue.forEach((item) => {
+        if (item.asin && !asins.value.includes(item.asin)) {
+          asins.value.push(item.asin)
+        }
+      })
+    }
+    console.log('Updated ASINs:', asins.value)
+  },
+  { deep: true }
+)
+
+// dialog以及选择card相关功能
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+let selectedAssetId = ''
+
+function resetSelection() {
+  selectedCards.value = []
+  selectedId.value = null
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'VIDEO',
+    specCheckApprovedPrograms: 'SPONSORED_BRANDS_VIDEO',
+  }
+  const response = await getVideoAssets(query)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.resolutionWidth,
+      height: asset.fileMetadata.resolutionHeight,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function isSelected(id) {
+  return selectedCards.value.some((card) => card.id === id)
+}
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push({
+      id: item.id,
+      assetId: item.id,
+      title: item.title,
+      imageUrl: item.imageUrl,
+    })
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 获取选中卡片的 assetId
+    selectedAssetId = selectedCards.value[0].assetId
+    const newFile = {
+      name: selectedCards.value[0].title, // 任何可以用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+    fileList.value.push(newFile)
+
+    // 清空选中卡片
+    resetSelection()
+    centerDialogVisible.value = false
+  }
+}
+
+function handleSelect() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+// 创建商品创意相关
+const loading = ref(false)
+async function createCommodity() {
+  try {
+    loading.value = true
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: commodityCard.value[0].asin,
+      name: ruleForm.name,
+      state: 'ENABLED',
+      adGroupId: respAdGroupId.value,
+      consentToTranslate: false,
+      videoAssetIds: selectedAssetId,
+    }
+    const response = await videoDetailCreate(obj)
+    if (response.data.creative_state == 'success') {
+      ElMessage({
+        message: '商品创意创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创意创建失败!')
+    }
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+function clickSave() {
+  console.log(123, fileList.value)
+  createCommodity()
+  commodityCard.value.length = 0
+}
+
+// 已选择的商品
+const commodityCard = ref([])
+
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    commodityCard.value = newValue
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

BIN
src/views/adManage/sb/campaigns/CreateCampaigns/img/img_1.jpg


+ 90 - 248
src/views/adManage/sb/campaigns/CreateCampaigns/index.vue

@@ -1,264 +1,106 @@
 <template>
-    <!--<p>新建广告页面</p>-->
-    <div>
-        <!-- 设置 -->
-        <el-card style="margin: 5px">
-            <div style="font-size: 19px; font-weight: bold"><span style="background-color: rgb(53, 105, 214); margin-right: 5px">|</span>设置</div>
-            <div class="container">
-                <el-form
-                        ref="ruleFormRef"
-                        :model="ruleForm"
-                        :rules="rules"
-                        label-width="120px"
-                        class="demo-ruleForm"
-                        :size="formSize"
-                        status-icon
-                        :label-position="labelPosition"
-                >
-                    <el-form-item class="form-item" label="广告活动名称" prop="name" style="width: 50%;">
-                        <el-input v-model="ruleForm.name"/>
-                    </el-form-item>
-                    <el-form-item class="form-item" label="广告组合" prop="portfolios" style="margin-left: 9px">
-                        <el-select v-model="ruleForm.portfolios" placeholder="请选择" style="margin-left: -9px">
-                            <el-option label="Zone one" value="shanghai"/>
-                            <el-option label="Zone two" value="beijing"/>
-                        </el-select>
-                    </el-form-item>
-                    <el-form-item class="form-item" label="预算" prop="budget">
-                        <el-select-v2
-                                v-model="ruleForm.budget"
-                                placeholder="Activity budget"
-                                :options="options"
-                        />
-                    </el-form-item>
-                    <el-form-item class="form-item" label="活动时间" required>
-                        <el-col :span="11">
-                            <el-form-item prop="date1">
-                                <el-date-picker
-                                        v-model="ruleForm.date1"
-                                        type="date"
-                                        label="Pick a date"
-                                        placeholder="开始时间"
-                                        style="width: 100%"
-                                />
-                            </el-form-item>
-                        </el-col>
-                        <el-col class="text-center" style="margin-bottom: 22px !important;" :span="1">
-                            <span class="text-center">—</span>
-                        </el-col>
-                        <el-col :span="11">
-                            <el-form-item prop="date2">
-                                <el-date-picker
-                                        v-model="ruleForm.date2"
-                                        type="date"
-                                        label="Pick a date"
-                                        placeholder="结束时间"
-                                        style="width: 100%"
-                                />
-                            </el-form-item>
-                        </el-col>
-                    </el-form-item>
-                    <el-form-item class="form-item" label="自动竞价" prop="delivery" @click.prevent style="margin-left: 9px">
-                        <el-switch v-model="ruleForm.delivery" style="margin-left: -9px"/>
-                        <span style="margin-left: 15px; color: #7a7a7a">允许亚马逊自动优化搜索结果首页以外的广告位竞价</span>
-                    </el-form-item>
-                    <el-form-item>
-                        <el-button type="primary" @click="submitForm(ruleFormRef)">
-                            Create
-                        </el-button>
-                        <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
-                    </el-form-item>
-                </el-form>
-            </div>
-        </el-card>
-        <!-- 广告组 -->
-        <el-card style="margin: 5px">
-            <div style="font-size: 19px; font-weight: bold">
-                <span style="background-color: rgb(53, 105, 214); margin-right: 5px">|</span>
-                广告组
-            </div>
-            <div>
-                <el-form ref="ruleFormRef"
-                           :model="ruleForm"
-                           :rules="rules"
-                           label-width="120px"
-                           class="demo-ruleForm"
-                           :size="formSize"
-                           status-icon
-                           :label-position="labelPosition">
-                    <el-form-item class="form-item" label="广告组名称" prop="groupName" style="width: 50%;">
-                        <el-input v-model="ruleForm.groupName"/>
-                    </el-form-item>
-                </el-form>
-            </div>
-        </el-card>
-        <!-- 广告格式 -->
-        <el-card style="margin: 5px">
-            <div style="font-size: 19px; font-weight: bold">
-                <span style="background-color: rgb(53, 105, 214); margin-right: 5px">|</span>
-                广告格式
-            </div>
-            <div>
-                <el-form ref="ruleFormRef"
-                         :model="ruleForm"
-                         :rules="rules"
-                         label-width="120px"
-                         class="demo-ruleForm"
-                         :size="formSize"
-                         status-icon
-                         :label-position="labelPosition">
-                </el-form>
-            </div>
-            <el-tabs>
-                <el-tab-pane label="商品集"><CommoditySet/></el-tab-pane>
-                <el-tab-pane label="品牌旗舰店焦点">品牌旗舰店焦点</el-tab-pane>
-                <el-tab-pane label="视频">视频</el-tab-pane>
-            </el-tabs>
-        </el-card>
-    </div>
+  <div class="page-container">
+    <AdCampaign @update-campaign="handleCampaignUpdate"></AdCampaign>
+    <AdGroup @update-group-id="handleGroupIdUpdate"></AdGroup>
+    <AdFormat
+      @update:adFormatRadio="handleAdFormatRadioChange"
+      @update:arrivalsRadio="handleArrivalsRadioChange"
+      @update:flagshipStoreShop="handleFlagshipStoreShopChange"
+      @update:pageOptions="handlePageOptionsChange"
+      @update:addedTableData="handleUpdateAddedData"
+      @update:focusShopSelect="handleFocusShopSelectChange"></AdFormat>
+    
+    <ProductSetCreativity1 v-if="adFormatRadioValue === 'productSet' && arrivalsRadioValue === 'flagshipStore' && flagshipStoreShopValue === 'ZOSI'">
+    </ProductSetCreativity1>
+    <FocusCreativity v-if="adFormatRadioValue === 'focus' && focusShop === 'ENTITY2NJKG6JSUTTPB'"></FocusCreativity>
+    <VideoCreativity1 v-if="adFormatRadioValue === 'video' && arrivalsRadioValue === 'flagshipStore' && flagshipStoreShopValue === 'ZOSI'">
+    </VideoCreativity1>
+    <VideoCreativity2 v-if="adFormatRadioValue === 'video' && arrivalsRadioValue === 'productDetailsPage'"></VideoCreativity2>
+    <ProductSetCreativity2 v-if="adFormatRadioValue === 'productSet' && arrivalsRadioValue === 'newArrivals'"></ProductSetCreativity2>
+    <DeliveryType></DeliveryType>
+  </div>
 </template>
 
 <script lang="ts" setup>
-import {reactive, ref} from 'vue'
-import type {FormInstance, FormRules, FormProps} from 'element-plus'
-import CommoditySet from './adFormat/CommoditySet.vue'
+import { provide, ref, onMounted, onUnmounted} from 'vue'
+import AdCampaign from './component/AdCampaign.vue'
+import AdGroup from './component/AdGroup.vue'
+import AdFormat from './component/AdFormat.vue'
+import DeliveryType from './component/DeliveryType.vue'
+import VideoCreativity1 from './component/VideoCreativity1.vue'
+import VideoCreativity2 from './component/VideoCreativity2.vue'
+import ProductSetCreativity1 from './component/ProductSetCreativity1.vue'
+import ProductSetCreativity2 from './component/ProductSetCreativity2.vue'
+import FocusCreativity from './component/FocusCreativity.vue'
+import emitter from '/@/utils/emitter'
 
-interface RuleForm {
-    name: string
-    portfolios: string
-    budget: string
-    date1: string
-    date2: string
-    delivery: boolean
-    type: string[]
-    resource: string
-    desc: string
-    groupName: string
-}
-
-const formSize = ref('default')
-const ruleFormRef = ref<FormInstance>()
-const ruleForm = reactive<RuleForm>({
-    name: '',
-    portfolios: '',
-    budget: '',
-    date1: '',
-    date2: '',
-    delivery: false,
-    type: [],
-    resource: '',
-    desc: '',
-    groupName: '',
-})
+const respCampaignId = ref('')
+const respCampaignName = ref('')
+const respAdGroupId = ref('')
+const adFormatRadioValue = ref('')
+const arrivalsRadioValue = ref('')
+const flagshipStoreShopValue = ref('')
+const pageOptionsValue = ref('')
+const addedTableDataForVc2 = ref('')
+const focusShop = ref('')
+const focusShopLabel = ref('')
+const brandEntityId = ref('')
+const setBrandName = ref('')
 
-const rules = reactive<FormRules<RuleForm>>({
-    name: [
-        {required: true, message: 'Please input Activity name', trigger: 'blur'},
-        {min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur'},
-    ],
-    portfolios: [
-        {
-            required: false,
-            message: 'Please select Activity zone',
-            trigger: 'change',
-        },
-    ],
-    budget: [
-        {
-            required: true,
-            message: 'Please select Activity budget',
-            trigger: 'change',
-        },
-    ],
-    date1: [
-        {
-            type: 'date',
-            required: true,
-            message: 'Please pick a date',
-            trigger: 'change',
-        },
-    ],
-    date2: [
-        {
-            type: 'date',
-            required: false,
-            message: 'Please pick a time',
-            trigger: 'change',
-        },
-    ],
-    type: [
-        {
-            type: 'array',
-            required: true,
-            message: 'Please select at least one activity type',
-            trigger: 'change',
-        },
-    ],
-    resource: [
-        {
-            required: true,
-            message: 'Please select activity resource',
-            trigger: 'change',
-        },
-    ],
-    desc: [
-        {required: true, message: 'Please input activity form', trigger: 'blur'},
-    ],
-    groupName: [
-        {required: true, message: 'Please input Group Name', trigger: 'blur'},
-    ]
-})
+provide('respCampaignId', respCampaignId)
+provide('respCampaignName', respCampaignName)
+provide('respAdGroupId', respAdGroupId)
+provide('pageOptionsValue', pageOptionsValue)
+provide('addedTableDataForVc2', addedTableDataForVc2)
+provide('focusShop', focusShop)
+provide('focusShopLabel', focusShopLabel)
+provide('brandEntityId', brandEntityId)
+provide('setBrandName', setBrandName)
 
-const submitForm = async (formEl: FormInstance | undefined) => {
-    if (!formEl) return
-    await formEl.validate((valid, fields) => {
-        if (valid) {
-            console.log('submit!')
-            console.log(ruleForm)
-        } else {
-            console.log('error submit!', fields)
-        }
-    })
+const handleCampaignUpdate = (data) => {
+  respCampaignId.value = data.id
+  respCampaignName.value = data.name
 }
-
-const resetForm = (formEl: FormInstance | undefined) => {
-    if (!formEl) return
-    formEl.resetFields()
+const handleGroupIdUpdate = (data) => {
+  respAdGroupId.value = data.id
+}
+const handleAdFormatRadioChange = (newValue) => {
+  adFormatRadioValue.value = newValue // 更新 adFormatRadioValue
+}
+const handleArrivalsRadioChange = (newValue) => {
+  arrivalsRadioValue.value = newValue
+}
+const handleFlagshipStoreShopChange = (newValue) => {
+  flagshipStoreShopValue.value = newValue
+}
+const handlePageOptionsChange = (newValue) => {
+  pageOptionsValue.value = newValue
 }
 
-const options = Array.from({length: 10000}).map((_, idx) => ({
-    value: `${idx + 1}`,
-    label: `${idx + 1}`,
-}))
+function handleUpdateAddedData(data) {
+  addedTableDataForVc2.value = data
+}
 
-const labelPosition = ref<FormProps['labelPosition']>('left')
+function handleFocusShopSelectChange(newValue) {
+  focusShop.value = newValue
+  focusShopLabel.value = 'ZOSI'
+}
 
+onMounted(()=>{
+  emitter.on('brandEntityId',(value: any)=>{
+    brandEntityId.value = value[0].brandEntityId
+    setBrandName.value = value[0].brandRegistryName
+  })
+})
 </script>
 
 <style scoped>
-    .form-item :deep(.el-form-item__label) {
-        font-weight: bold;
-    }
-    :deep(.el-tabs__nav-scroll) {
-        display: flex;
-        justify-content: space-around;
-    }
-    :deep([id^="tab"]) {
-        padding: 0;
-        margin-right: 230px;
-        border: 1px solid #D3D3D3;
-        border-radius: 5px;
-    }
-    :deep(div#tab-0) {
-        padding: 15px;
-    }
-    :deep(div#tab-1) {
-        padding: 15px;
-    }
-    :deep(div#tab-2) {
-        padding: 15px;
-    }
-    :deep(.el-tabs__nav-wrap::after) {
-        width: 0;
-    }
+.page-container {
+  padding: 12px;
+  background-color: #fafafa;
+}
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+  color: #505968;
+}
 </style>

+ 49 - 7
src/views/adManage/sb/campaigns/api.ts

@@ -1,18 +1,17 @@
 import { request } from '/@/utils/service';
-import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
-import XEUtils from 'xe-utils';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
 
-export const apiPrefix = '/api/ad_manage/spCampaigns/';
-export function GetList(query: PageQuery) {
+export const apiPrefix = '/api/ad_manage/sbcampaigns/';
+export function GetList(query: UserPageQuery) {
     return request({
-        url: apiPrefix,
+        url: apiPrefix + 'list/',
         method: 'get',
         params: query,
     })
 }
-export function GetObj(id: InfoReq) {
+export function GetObj(id: any) {
     return request({
-        url: apiPrefix + id,
+        url: apiPrefix + id + "/",
         method: 'get',
     });
 }
@@ -40,3 +39,46 @@ export function DelObj(id: DelReq) {
         data: { id },
     });
 }
+
+export function getCardData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "total/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    query["dateRangeType"] = "D"
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+    query["dateRangeType"] = "W"
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+    query["dateRangeType"] = "M"
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getAdStructureData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "structure/",
+        method: 'GET',
+        params: query
+    })
+}

+ 59 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/api.ts

@@ -0,0 +1,59 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/ads/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}
+export function GetObj(id: InfoReq) {
+    return request({
+        url: apiPrefix + id + "/",
+        method: 'get',
+    });
+}
+
+export function AddObj(obj: AddReq) {
+    return request({
+        url: apiPrefix,
+        method: 'post',
+        data: obj,
+    });
+}
+
+export function UpdateObj(obj: EditReq) {
+    return request({
+        url: apiPrefix + obj.id + '/',
+        method: 'put',
+        data: obj,
+    });
+}
+
+export function DelObj(id: DelReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'delete',
+        data: { id },
+    });
+}
+
+
+export function getCardData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "total/",
+        method: 'GET',
+        params: query
+    })
+  }
+
+export function getLineData(query: UserPageQuery) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'daily/',
+    method: 'GET',
+    params: query
+  })
+}

+ 130 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/crud.tsx

@@ -0,0 +1,130 @@
+import * as api from './api'
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: UserPageQuery) => {
+		const params = parseQueryParams(context.value)
+		XEUtils.assign(query, params)
+		return await api.GetList(query);
+	};
+	const editRequest = async ({ form, row }: EditReq) => {
+		form.id = row.id;
+		return await api.UpdateObj(form);
+	};
+	const delRequest = async ({ row }: DelReq) => {
+		return await api.DelObj(row.id);
+	};
+	const addRequest = async ({ form }: AddReq) => {
+		return await api.AddObj(form);
+	};
+
+	//权限判定
+	const hasPermissions = inject('$hasPermissions');
+
+	return {
+		crudOptions: {
+			table: {
+				height: 750,
+				headerCellStyle: {
+					backgroundColor: '#f6f7fa', // 直接设置背景颜色
+					height: '20px',
+					// border: '0.5px solid #ddd',
+				},
+				cellStyle: {
+					border: 'none',
+					borderBottom: '0.5px solid #ddd',
+				},
+			},
+			container: {
+        fixedHeight: false
+      },
+			actionbar: {
+				show: true,
+				color: "#626aef",
+				buttons: {
+					add: {
+						show: false
+					},
+					create: {
+						text: '添加广告',
+						// type: 'primary',
+						color: "#626aef",
+						plain: true,
+						show: true,
+						click() {
+
+						}
+					},
+				}
+			},
+			search: {
+				show: true,
+				buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+			},
+			toolbar: {
+        buttons: {
+					search: {
+						show: true
+					},
+					compact: {
+						show: false
+					}
+				}
+			},
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			rowHandle: {
+				fixed: 'right',
+				width: 80,
+				buttons: {
+					view: {
+						show: false,
+					},
+					edit: {
+						iconRight: 'Edit',
+						type: 'text',
+            text: null
+						// show: hasPermissions('dictionary:Update'),
+					},
+					remove: {
+						iconRight: 'Delete',
+						type: 'text',
+            text: null
+						// show: hasPermissions('dictionary:Delete'),
+					},
+				},
+			},
+			columns: {
+        adGroupName: {
+					title: '广告组名称',
+					column: {
+						width: 230,
+						fixed: 'left',
+					}
+				},
+				state: {
+					title: '状态',
+					column: {
+						width: "100px"
+					}
+				},
+				...BaseColumn
+			}
+		}
+	}
+}

+ 73 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/index.vue

@@ -0,0 +1,73 @@
+<template>
+	<fs-page class="fs-page-custom">
+		<fs-crud ref="crudRef" v-bind="crudBinding">
+			<template #search-left>
+				<DateRangePicker v-model="dateRange"></DateRangePicker>
+			</template>
+			<template #header-middle>
+				<DataTendencyChart :query="queryParams" :fetch-card="getCardData" :fetch-line="getLineData"> </DataTendencyChart>
+			</template>
+			<template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+          :field="field" 
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]" 
+          :date-range="dateRange"
+          :show-compare="showCompare"/>
+      </template>
+      <template #toolbar-left>
+        <div>
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { LocationQueryValue } from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { getCardData, getLineData } from './api'
+import { storeToRefs } from 'pinia'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+
+
+interface Props {
+	adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const showCompare = ref(false)
+const queryParams = ref({
+	adGroupId: props.adGroupId,
+	dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(() => {
+	crudExpose.doRefresh()
+})
+watch(queryParams, async () => {
+	crudExpose.doRefresh()
+}, { deep: true })
+</script>
+
+<style lang="scss">
+.chart-tabs {
+	margin: 5px 0;
+
+	.el-tabs__nav {
+		padding-left: 0 !important;
+	}
+}
+</style>

+ 12 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/api.ts

@@ -0,0 +1,12 @@
+import { request } from '/@/utils/service'
+import { LocationQueryValue } from 'vue-router'
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/'
+
+export function GetObj(adGroupId: LocationQueryValue | LocationQueryValue[]) {
+	return request({
+		url: apiPrefix + 'head/',
+		method: 'get',
+		params: { adGroupId },
+	})
+}

+ 11 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/targets/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 95 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/crud.tsx

@@ -0,0 +1,95 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {},
+      container: {
+        fixedHeight: true,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        keyword: {
+          title: '目标群体',
+          column: {
+            width: '200px',
+            fixed: 'left',
+            align: 'center',
+          },
+        },
+        state: {
+          title: '状态',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: '60px',
+            align: 'center',
+          },
+        },
+        // currentBid: {
+        //   title: '当前竞价',
+        //   column: {
+        //     width: '100px',
+        //     align: 'center',
+        //   },
+        // },
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 67 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+
+      <template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+          :field="field"
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]"
+          :date-range="dateRange"
+          :show-compare="showCompare"
+        />
+      </template>
+      <template #toolbar-left>
+        <div class="campare-switch">
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { LocationQueryValue } from 'vue-router'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

+ 84 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="asj-container">
+    <div class="asj-detail-header">
+      <span style="font-size: x-large; font-weight: bold; color: #0f1111;margin: 5px;">
+        <span> {{ adGroupInfo.adGroupName }} </span>
+      </span>
+      <div class="asj-detail-info">
+        <span style="color: rgb(177, 177, 177);">状态: </span>
+        <span>
+          <el-button class="no-hover-effect" type="success" size="small" round plain>{{ dynStatusEnum[adGroupInfo.state] }}</el-button>
+        </span>
+        <span class="head-span">投放类型: </span> <span>{{ targetTypeEnum[adGroupInfo.creative_type] }}</span>
+        <!-- <span>默认竞价:{{ profile.currency_symbol + adGroupInfo.defaultBid }}</span> -->
+        <span class="head-span">投放日期: </span> <span>{{ adGroupInfo.startDate }} ~ {{ adGroupInfo.endDate ?? '无结束日期' }}</span>
+      </div>
+    </div>
+    <el-tabs type="border-card" class="asj-detail-tabs" v-model="tabActiveName">
+      <el-tab-pane label="广告" name="ads">
+        <Ads v-if="tabActiveName==='ads'" :adGroupId="route.query.adGroupId"></Ads>
+      </el-tab-pane>
+      <template v-if="route.query.targetingType ==='target'">
+        <el-tab-pane label="商品投放" name="tab2">
+          <ManualTarget v-if="tabActiveName === 'tab2'" :adGroupId="route.query.adGroupId">商品投放</ManualTarget>
+        </el-tab-pane>
+        <!--<el-tab-pane label="否定商品" name="tab3">-->
+        <!--  <NegProduct v-if="tabActiveName === 'tab3'" :adGroupId="route.query.adGroupId">否定商品</NegProduct>-->
+        <!--</el-tab-pane>-->
+      </template>
+      <template v-else>
+        <el-tab-pane label="关键词" name="tab2">
+          <Keyword v-if="tabActiveName === 'tab2'" :ad-group-id="route.query.adGroupId">关键词</Keyword>
+        </el-tab-pane>
+        <el-tab-pane label="否定词" name="tab3">
+          <NegKeyword v-if="tabActiveName === 'tab3'" :ad-group-id="route.query.adGroupId">否定词</NegKeyword>
+        </el-tab-pane>
+        <el-tab-pane label="搜索关键词" name="searchTerm">
+          <SearchTerm v-if="tabActiveName === 'searchTerm'" :adGroupId="route.query.adGroupId" />
+        </el-tab-pane>
+      </template>
+    </el-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, Ref} from 'vue'
+import {useRoute} from 'vue-router'
+import {GetObj} from './api'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {storeToRefs} from 'pinia'
+import Ads from './ads/index.vue'
+import SearchTerm from './searchTerm/index.vue'
+import Keyword from './keyword/index.vue'
+import AutoTarget from './autoTarget/index.vue'
+import ManualTarget from './manualTarget/index.vue'
+import NegProduct from './negProduct/index.vue'
+import NegKeyword from './negKeyword/index.vue'
+import NegTarget from './negTarget/index.vue'
+import {dynStatusEnum, targetTypeEnum} from '/@/views/adManage/utils/enum'
+// import { usePublicData } from '/@/stores/publicData'
+
+
+const tabActiveName = ref('ads')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const route = useRoute()
+const adGroupInfo: Ref<SpAdGroup> = ref({})
+
+onMounted(async () => {
+  const resp = await GetObj(route.query.adGroupId)
+  adGroupInfo.value = resp.data
+  console.log(111, route.query)
+})
+
+</script>
+
+<style scoped>
+.head-span {
+  color: rgb(177, 177, 177);
+  margin-left: 40px;
+}
+:deep(.el-tabs--border-card) {
+  border: none;
+}
+</style>

+ 28 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api.ts

@@ -0,0 +1,28 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/keywords/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}
+
+export function getCardData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "total/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    query["dateRangeType"] = "D"
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}

+ 122 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/crud.tsx

@@ -0,0 +1,122 @@
+import * as api from './api'
+import {CreateCrudOptionsProps, CreateCrudOptionsRet, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 750,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          // border: '0.5px solid #ddd',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+      },
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: true,
+        color: "#626aef",
+        buttons: {
+          add: {
+            show: false
+          },
+          create: {
+            text: '添加关键词',
+            // type: 'primary',
+            color: "#626aef",
+            plain: true,
+            show: true,
+            click() {
+
+            }
+          },
+        }
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        keywordText: {
+          title: '关键词',
+          column: {
+            width: '200px',
+            fixed: 'left',
+            align: 'center',
+          },
+        },
+        state: {
+          title: '状态',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: '60px',
+            align: 'center',
+          },
+        },
+        // currentBid: {
+        //   title: '当前竞价',
+        //   column: {
+        //     width: '100px',
+        //     align: 'center',
+        //   },
+        // },
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 71 - 0
src/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+      <template #header-middle>
+        <DataTendencyChart :query="queryParams" :fetch-card="getCardData" :fetch-line="getLineData"> </DataTendencyChart>
+      </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+          :field="field"
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]"
+          :date-range="dateRange"
+          :show-compare="showCompare"
+        />
+      </template>
+      <template #toolbar-left>
+        <div class="campare-switch">
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { LocationQueryValue } from 'vue-router'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import {getCardData, getLineData} from '/@/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

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