Bläddra i källkod

✨ feat: 修改SP自动化规则分时调价和定向规则界面UI; 新增规则执行时区tag

WanGxC 1 år sedan
förälder
incheckning
74fd70dd1d

+ 67 - 48
src/views/components/auto/auto-campaigns/select-tmpl.vue

@@ -1,39 +1,20 @@
-<template>
-  <div>
-    <div class="asj-h3">选择模板</div>
-    <el-radio-group v-model="ruleUsage" @change="changeRuleUsage">
-      <div style="display: flex; justify-content: flex-start; flex-direction: column">
-        <el-radio label="custom">自定义规则</el-radio>
-        <el-input
-          v-model="data.templateName"
-          v-show="ruleUsage === 'custom'"
-          style="margin-left: 22px"
-          placeholder="将规则同时保存为模板(可选)"></el-input>
-        <el-radio label="tmpl">使用已有模板</el-radio>
-        <el-select v-show="ruleUsage === 'tmpl'" @change="changeTmpl" v-model="tmplId" style="margin-left: 22px">
-          <el-option v-for="info in tmplList" :label="info.name" :value="info.id" :key="info.id"></el-option>
-        </el-select>
-      </div>
-    </el-radio-group>
-  </div>
-</template>
-
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, onMounted, watch } from 'vue'
-import * as api from './api'
-import XEUtils from 'xe-utils'
+import { ref, onMounted, watch } from 'vue';
+import * as api from './api';
+import XEUtils from 'xe-utils';
 
 
 interface Props {
 interface Props {
-  data: AutoCampaignRule
+  data: AutoCampaignRule;
 }
 }
-const props = defineProps<Props>()
-const ruleUsage = ref('custom')
-const tmplId = ref(0)
+
+const props = defineProps<Props>();
+const ruleUsage = ref('custom');
+const tmplId = ref(0);
 if (props.data.template) {
 if (props.data.template) {
-  tmplId.value = props.data.template.id
+  tmplId.value = props.data.template.id;
 }
 }
 
 
-const tmplList = ref([])
+const tmplList = ref([]);
 // const emits = defineEmits(['change-tmpl'])
 // const emits = defineEmits(['change-tmpl'])
 
 
 const getTmplList = async () => {
 const getTmplList = async () => {
@@ -41,42 +22,80 @@ const getTmplList = async () => {
     page: 1,
     page: 1,
     pageSize: 999,
     pageSize: 999,
     type: props.data.ruleType,
     type: props.data.ruleType,
-  })
-  tmplList.value = resp.data
-}
+  });
+  tmplList.value = resp.data;
+};
 const changeTmpl = () => {
 const changeTmpl = () => {
-  const tmpl = XEUtils.find(tmplList.value, (item) => item.id === tmplId.value)
-  props.data.rule = XEUtils.clone(tmpl.rule, true)
-  props.data.template = tmpl
+  const tmpl = XEUtils.find(tmplList.value, (item) => item.id === tmplId.value);
+  props.data.rule = XEUtils.clone(tmpl.rule, true);
+  props.data.template = tmpl;
   // emits('change-tmpl', tmpl)
   // emits('change-tmpl', tmpl)
-}
+};
 const changeRuleUsage = () => {
 const changeRuleUsage = () => {
-  props.data.useTmpl = ruleUsage.value === 'tmpl'
+  props.data.useTmpl = ruleUsage.value === 'tmpl';
   if (props.data.useTmpl) {
   if (props.data.useTmpl) {
     if (!tmplId.value && tmplList.value.length > 0) {
     if (!tmplId.value && tmplList.value.length > 0) {
-      tmplId.value = tmplList.value[0].id
+      tmplId.value = tmplList.value[0].id;
     }
     }
-    changeTmpl()
+    changeTmpl();
   }
   }
-}
+};
 
 
 onMounted(async () => {
 onMounted(async () => {
-  await getTmplList()
-})
+  await getTmplList();
+});
 
 
 watch(
 watch(
   () => props.data.useTmpl,
   () => props.data.useTmpl,
   () => {
   () => {
     if (props.data.useTmpl) {
     if (props.data.useTmpl) {
-      ruleUsage.value = 'tmpl'
-      tmplId.value = props.data.template.id
+      ruleUsage.value = 'tmpl';
+      tmplId.value = props.data.template.id;
     } else {
     } else {
-      ruleUsage.value = 'custom'
+      ruleUsage.value = 'custom';
     }
     }
   }
   }
-)
+);
 
 
-defineExpose({ getTmplList })
+defineExpose({ getTmplList });
 </script>
 </script>
 
 
-<style scoped></style>
+<template>
+  <el-card class="mt-2">
+    <div class="asj-h3">
+      <span class="custom-title-icon"></span>
+      选择模板
+    </div>
+    <el-radio-group v-model="ruleUsage" @change="changeRuleUsage">
+      <div style="display: flex; justify-content: flex-start; flex-direction: column">
+        <el-radio label="custom">自定义规则</el-radio>
+        <el-input
+          v-model="data.templateName"
+          v-show="ruleUsage === 'custom'"
+          style="margin-left: 22px"
+          placeholder="将规则同时保存为模板(可选)"></el-input>
+        <el-radio label="tmpl">使用已有模板</el-radio>
+        <el-select v-show="ruleUsage === 'tmpl'" @change="changeTmpl" v-model="tmplId" style="margin-left: 22px">
+          <el-option v-for="info in tmplList" :label="info.name" :value="info.id" :key="info.id"></el-option>
+        </el-select>
+      </div>
+    </el-radio-group>
+  </el-card>
+</template>
+
+<style scoped>
+.custom-title-icon {
+  padding: 0 5px;
+}
+
+.custom-title-icon:before {
+  content: '';
+  width: 4px;
+  height: 15px;
+  background: #3569d6;
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  left: 0;
+}
+</style>

+ 51 - 46
src/views/components/auto/auto-campaigns/target-rule.vue

@@ -1,16 +1,58 @@
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import TargetSelect from '../target-select.vue';
+import SelectTmpl from './select-tmpl.vue';
+import FreqSetting from '../freq-setting.vue';
+import { userFormData } from './common';
+import TargetRuleSetting from '../target-rule-setting.vue';
+import XEUtils from 'xe-utils';
+
+interface Props {
+  data: {
+    campaignId: string;
+    campaignType: string;
+    profileId: string;
+    ruleType: number;
+  };
+  RuleStatusButton?: { [key: string]: any };
+}
+
+const props = defineProps<Props>();
+const emits = defineEmits(['refresh']);
+const formRef = ref();
+const ruleSettingRef = ref();
+const { formData, submitFormData } = userFormData(props);
+
+const submitForm = async () => {
+  const valid2 = await ruleSettingRef.value.validateForm();
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2) {
+      await submitFormData();
+      emits('refresh');
+    } else {
+      console.log('验证失败:', [valid, valid2]);
+    }
+  });
+};
+const cancel = () => {
+  emits('refresh');
+};
+</script>
+
 <template>
 <template>
   <div>
   <div>
     <div class="asj-h2">定向规则</div>
     <div class="asj-h2">定向规则</div>
     <SelectTmpl :data="formData" />
     <SelectTmpl :data="formData" />
-    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
+    <el-form class="custom-card" :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
       <el-form-item>
       <el-form-item>
-        <template #label>
-          <span class="asj-h3">生效对象</span>
-        </template>
+        <!--<template #label>-->
+        <!--  <span class="custom-title-icon"></span>-->
+        <!--  <span class="asj-h3">生效对象</span>-->
+        <!--</template>-->
         <TargetSelect mode="auto" :data="formData.rule" :useTmpl="formData.useTmpl" :campaign-id="data.campaignId"></TargetSelect>
         <TargetSelect mode="auto" :data="formData.rule" :useTmpl="formData.useTmpl" :campaign-id="data.campaignId"></TargetSelect>
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
-    <TargetRuleSetting :rule="formData.rule" ref="ruleSettingRef" :disabled="formData.useTmpl"/>
+    <TargetRuleSetting :rule="formData.rule" ref="ruleSettingRef" :disabled="formData.useTmpl" />
     <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl" />
     <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl" />
     <div class="auto-page-foot">
     <div class="auto-page-foot">
       <el-button style="width: 200px" @click="cancel">取消</el-button>
       <el-button style="width: 200px" @click="cancel">取消</el-button>
@@ -19,45 +61,8 @@
   </div>
   </div>
 </template>
 </template>
 
 
-<script lang="ts" setup>
-import { ref, computed } from 'vue'
-import TargetSelect from '../target-select.vue'
-import SelectTmpl from './select-tmpl.vue'
-import FreqSetting from '../freq-setting.vue'
-import { userFormData } from './common'
-import TargetRuleSetting from '../target-rule-setting.vue'
-import XEUtils from 'xe-utils'
-
-interface Props {
-  data: {
-    campaignId: string
-    campaignType: string
-    profileId: string
-    ruleType: number
-  }
-  RuleStatusButton?: { [key: string]: any }
+<style lang="scss" scoped>
+:deep(.custom-card .el-form-item__content) {
+  display: block;
 }
 }
-const props = defineProps<Props>()
-const emits = defineEmits(['refresh'])
-const formRef = ref()
-const ruleSettingRef = ref()
-const { formData, submitFormData } = userFormData(props)
-
-const submitForm = async () => {
-  const valid2 = await ruleSettingRef.value.validateForm()
-  formRef.value.validate(async (valid: boolean) => {
-    if (valid && valid2) {
-      await submitFormData()
-      emits('refresh')
-    } else {
-      console.log('验证失败:', [valid, valid2])
-    }
-  })
-}
-const cancel = () => {
-  emits('refresh')
-}
-</script>
-
-<style lang="scss" scoped></style>
-
+</style>

+ 167 - 45
src/views/components/auto/auto-campaigns/timer-bid.vue

@@ -1,68 +1,190 @@
-<template>
-  <div>
-    <div class="asj-h2">分时调价</div>
-    <SelectTmpl :data="formData" />
-    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
-      <el-form-item prop="rule.conditions" :rules="[{ validator: checkConditions, trigger: 'xxx' }]">
-        <template #label>
-          <span class="asj-h3">设置竞价</span>
-        </template>
-        <TimerBidTable :data="formData.rule.conditions" @click="formRef.clearValidate('rule.conditions')" :disabled="formData.useTmpl" />
-      </el-form-item>
-    </el-form>
-    <div class="auto-page-foot">
-      <el-button style="width: 200px" @click="cancel">取消</el-button>
-      <el-button style="width: 200px" type="primary" @click="submit">提交</el-button>
-    </div>
-  </div>
-</template>
-
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { ref, onMounted, Ref, watch } from 'vue'
-import TimerBidTable from '/@/components/TimerBidTable/index.vue'
-import SelectTmpl from './select-tmpl.vue'
-import { userFormData } from './common'
-import XEUtils from 'xe-utils'
+import { ref, onMounted, Ref, watch, reactive } from 'vue';
+import TimerBidTable from '/@/components/TimerBidTable/index.vue';
+import SelectTmpl from './select-tmpl.vue';
+import { userFormData } from './common';
+import XEUtils from 'xe-utils';
+import dayjs from 'dayjs';
 
 
 interface Props {
 interface Props {
   data: {
   data: {
-    campaignId: string
-    campaignType: string
-    profileId: string
-    ruleType: number
-  }
-  RuleStatusButton?: { [key: string]: any }
+    campaignId: string;
+    campaignType: string;
+    profileId: string;
+    ruleType: number;
+  };
+  RuleStatusButton?: { [key: string]: any };
 }
 }
-const props = defineProps<Props>()
-const emits = defineEmits(['refresh'])
-const formRef = ref()
-const { formData, submitFormData } = userFormData(props)
+
+const props = defineProps<Props>();
+const emits = defineEmits(['refresh']);
+
+const formRef = ref();
+const { formData, submitFormData } = userFormData(props);
 
 
 const checkConditions = (rule: any, value: any, callback: any) => {
 const checkConditions = (rule: any, value: any, callback: any) => {
   for (const bidList of formData.value.rule.conditions) {
   for (const bidList of formData.value.rule.conditions) {
     for (const val of bidList) {
     for (const val of bidList) {
       if (val > 0) {
       if (val > 0) {
-        return callback()
+        return callback();
       }
       }
     }
     }
   }
   }
-  callback(new Error('请先设置竞价!'))
-}
+  callback(new Error('请先设置竞价!'));
+};
 const submit = async () => {
 const submit = async () => {
   formRef.value.validate(async (valid: any) => {
   formRef.value.validate(async (valid: any) => {
     if (valid) {
     if (valid) {
-      await submitFormData()
-      emits('refresh')
+      await submitFormData();
+      emits('refresh');
     } else {
     } else {
-      console.log('验证失败')
+      console.log('验证失败');
     }
     }
-  })
+  });
+};
+const cancel = async () => {
+  emits('refresh');
+};
+
+const timeRange = ref('Option1');
+const schedule = ref('Option1');
+const bid = ref('');
+const timeRangeOptions = [
+  {
+    value: 'Option1',
+    label: '24小时: 00:00-23:59',
+  },
+  {
+    value: 'Option2',
+    label: '凌晨: 00:00-06:59',
+  },
+  {
+    value: 'Option3',
+    label: '上午: 7:00-11:59',
+  },
+  {
+    value: 'Option4',
+    label: '工作时: 9:00-16:59',
+  },
+  {
+    value: 'Option5',
+    label: '下午: 12:00-16:59',
+  },
+  {
+    value: 'Option6',
+    label: '晚上: 17:00-20:59',
+  },
+  {
+    value: 'Option7',
+    label: '深夜: 21:00-23:59',
+  },
+];
+const scheduleOptions = [
+  {
+    value: 'Option1',
+    label: '每一天',
+  },
+  {
+    value: 'Option2',
+    label: '仅在工作日',
+  },
+  {
+    value: 'Option3',
+    label: '仅在周末',
+  },
+];
+
+const isVisible = ref(true)
+function handleClose() {
+  isVisible.value = false
 }
 }
+</script>
 
 
-const cancel = async () => {
-  emits('refresh')
+<template>
+  <div class="mx-5">
+    <div class="asj-h2">分时调价</div>
+    <div class="mt-3.5">
+      规则执行时区: PDT
+      <div>
+        <el-tag v-if="isVisible" closable @close="handleClose" color="#e7edf4" class="custom-tag">
+          <template #default>
+            <div class="tag-content">
+              <strong>自动化分时规则:</strong>
+              <p>
+                1. 应用分时调价后,如需手动修改竞价,只能在Xmars操作。在亚马逊后台或其他第三方系统进行的调价操作,竞价将会被Xmars当前时段的自动化执行结果覆盖。
+              </p>
+              <p>2. 广告活动开启分时调价,规则的修改将在下一个整点生效。</p>
+            </div>
+          </template>
+        </el-tag>
+      </div>
+    </div>
+    <SelectTmpl :data="formData" />
+    <el-card class="mt-3">
+      <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
+        <el-form-item prop="rule.conditions" :rules="[{ validator: checkConditions, trigger: 'xxx' }]">
+          <template #label>
+            <span class="custom-title-icon"></span>
+            <span class="asj-h3">设置竞价</span>
+          </template>
+          <div class="flex flex-col">
+            <div class="flex gap-2 my-2">
+              <el-select v-model="timeRange">
+                <el-option v-for="item in timeRangeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-select v-model="schedule">
+                <el-option v-for="item in scheduleOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="bid" style="width: 150px" placeholder="1.0"></el-input>
+              <el-button link class="active-btn" style="color: #3c58af">应用</el-button>
+            </div>
+            <TimerBidTable
+              :data="formData.rule.conditions"
+              @click="formRef.clearValidate('rule.conditions')"
+              :disabled="formData.useTmpl" />
+          </div>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submit">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.active-btn:hover {
+  color: #7a9ce4 !important; /* 如果需要改变字体颜色 */
+  transition: all 0.3s; /* 添加过渡效果 */
 }
 }
 
 
-</script>
+.custom-title-icon {
+  padding-right: 10px;
+}
 
 
-<style scoped></style>
+.custom-title-icon:before {
+  content: '';
+  width: 4px;
+  height: 15px;
+  background: #3569d6;
+  position: absolute;
+  transform: translateY(25%);
+}
+
+.custom-tag {
+  height: auto;
+  padding: 8px 8px 8px 16px;
+  color: #505968;
+  border-color: #abbbd8;
+  align-items: flex-start !important;
+  white-space: normal;
+  line-height: 1.5;
+}
+
+.tag-content {
+  display: block;
+  max-width: 100%;
+}
+</style>

+ 19 - 3
src/views/components/auto/target-rule-setting.vue

@@ -1,6 +1,9 @@
 <template>
 <template>
-  <div>
-    <div class="asj-h3">规则设置</div>
+  <el-card>
+    <div class="asj-h3">
+      <span class="custom-title-icon"></span>
+      规则设置
+    </div>
     <div class="rule-setting-container">
     <div class="rule-setting-container">
       <div v-for="(info, index) in rule.conditions" class="rule-setting-item" :key="info.actionType + info.ordering">
       <div v-for="(info, index) in rule.conditions" class="rule-setting-item" :key="info.actionType + info.ordering">
         <div class="rule-setting-title">
         <div class="rule-setting-title">
@@ -61,7 +64,7 @@
         </div>
         </div>
       </el-popover>
       </el-popover>
     </div>
     </div>
-  </div>
+  </el-card>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
@@ -254,4 +257,17 @@ defineExpose({ validateForm })
     }
     }
   }
   }
 }
 }
+
+.custom-title-icon {
+  padding-right: 10px;
+}
+
+.custom-title-icon:before {
+  content: '';
+  width: 4px;
+  height: 15px;
+  background: #3569d6;
+  position: absolute;
+  transform: translateY(25%);
+}
 </style>
 </style>

+ 49 - 32
src/views/components/auto/target-select.vue

@@ -1,35 +1,3 @@
-<template>
-  <div style="display: flex; flex-direction: column; align-items: flex-start">
-    <div style="margin: 5px 0">
-      <span>广告类型:</span>
-      <el-select v-model="data.campaignType" :disabled="mode==='edit' || useTmpl">
-        <el-option label="SP" value="sp"></el-option>
-        <el-option label="SB" value="sb"></el-option>
-        <el-option label="SD" value="sd"></el-option>
-      </el-select>
-    </div>
-    <el-radio-group v-model="data.activeModel" :disabled="mode==='edit' || useTmpl">
-      <div class="target-radio-group">
-        <el-radio label="campaign">当前广告活动(所有定向)</el-radio>
-        <div class="target-radio-group-item">
-          <el-radio label="adGroup">当前广告活动的指定广告组(所有定向)</el-radio>
-          <AdGroupSelect
-            v-show="mode === 'auto' && data.activeModel === 'adGroup'"
-            v-model="data.campaignAd"
-            style="padding-left: 23px;width: 450px;"
-            :multiple="true"
-            :query="{ profileId: '3006125408623189', campaignType: data.campaignType, campaignId: campaignId }">
-          </AdGroupSelect>
-        </div>
-        <div class="target-radio-group-item">
-          <el-radio label="specified">指定定向</el-radio>
-          <el-button v-show="mode === 'auto' && data.activeModel === 'specified'" style="margin-left: 20px; color: blue" link>选择定向</el-button>
-        </div>
-      </div>
-    </el-radio-group>
-  </div>
-</template>
-
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { ref, watch } from 'vue'
 import { ref, watch } from 'vue'
 import AdGroupSelect from '/@/views/components/ad-group-select/index.vue'
 import AdGroupSelect from '/@/views/components/ad-group-select/index.vue'
@@ -43,6 +11,42 @@ interface Props {
 const props = defineProps<Props>()
 const props = defineProps<Props>()
 </script>
 </script>
 
 
+<template>
+  <el-card body-style="width: 100%;">
+    <span class="custom-title-icon"></span>
+    <span class="asj-h3">生效对象</span>
+    <div style="display: flex; flex-direction: column; align-items: flex-start">
+      <div style="margin: 5px 0">
+        <span>广告类型:</span>
+        <el-select v-model="data.campaignType" :disabled="mode==='edit' || useTmpl">
+          <el-option label="SP" value="sp"></el-option>
+          <el-option label="SB" value="sb"></el-option>
+          <el-option label="SD" value="sd"></el-option>
+        </el-select>
+      </div>
+      <el-radio-group v-model="data.activeModel" :disabled="mode==='edit' || useTmpl">
+        <div class="target-radio-group">
+          <el-radio label="campaign">当前广告活动(所有定向)</el-radio>
+          <div class="target-radio-group-item">
+            <el-radio label="adGroup">当前广告活动的指定广告组(所有定向)</el-radio>
+            <AdGroupSelect
+                v-show="mode === 'auto' && data.activeModel === 'adGroup'"
+                v-model="data.campaignAd"
+                style="padding-left: 23px;width: 450px;"
+                :multiple="true"
+                :query="{ profileId: '3006125408623189', campaignType: data.campaignType, campaignId: campaignId }">
+            </AdGroupSelect>
+          </div>
+          <div class="target-radio-group-item">
+            <el-radio label="specified">指定定向</el-radio>
+            <el-button v-show="mode === 'auto' && data.activeModel === 'specified'" style="margin-left: 20px; color: blue" link>选择定向</el-button>
+          </div>
+        </div>
+      </el-radio-group>
+    </div>
+  </el-card>
+</template>
+
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .target-radio-group {
 .target-radio-group {
   display: flex;
   display: flex;
@@ -55,4 +59,17 @@ const props = defineProps<Props>()
     align-items: flex-start;
     align-items: flex-start;
   }
   }
 }
 }
+
+.custom-title-icon {
+  padding-right: 10px;
+}
+
+.custom-title-icon:before {
+  content: '';
+  width: 4px;
+  height: 15px;
+  background: #3569d6;
+  position: absolute;
+  transform: translateY(55%);
+}
 </style>
 </style>

+ 1 - 1
vite.config.ts

@@ -41,7 +41,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
     server: {
     server: {
       host: '0.0.0.0',
       host: '0.0.0.0',
       port: env.VITE_PORT as unknown as number,
       port: env.VITE_PORT as unknown as number,
-      open: true,
+      open: false,
       hmr: true,
       hmr: true,
       proxy: {
       proxy: {
         '/gitee': {
         '/gitee': {