guojing_wu 1 рік тому
батько
коміт
0f00bc667d
49 змінених файлів з 2814 додано та 1326 видалено
  1. 219 209
      src/components/TimerBidTable/index.vue
  2. 71 61
      src/components/TimerBudgetTable/index.vue
  3. 0 43
      src/components/auto-templates/api.ts
  4. 0 47
      src/components/auto-templates/common.js
  5. 0 0
      src/components/auto-templates/neg-keywords.vue
  6. 0 40
      src/components/auto-templates/search-term.vue
  7. 0 49
      src/components/auto-templates/select-tmpl.vue
  8. 0 269
      src/components/auto-templates/target-rule.vue
  9. 0 40
      src/components/auto-templates/target-select.vue
  10. 118 75
      src/components/conditionBuilder/condition-group2.vue
  11. 49 17
      src/components/conditionBuilder/index.vue
  12. 4 1
      src/components/conditionBuilder/type.d.ts
  13. 30 0
      src/components/conditionBuilder/utils.ts
  14. 15 15
      src/components/input-float/index.vue
  15. 30 0
      src/components/range-float/index.vue
  16. 78 0
      src/components/tags-input/index.vue
  17. 305 271
      src/types/views.d.ts
  18. 11 0
      src/views/adManage/sp/campaigns/campaignDetail/automation/api.ts
  19. 133 29
      src/views/adManage/sp/campaigns/campaignDetail/automation/index.vue
  20. 14 6
      src/views/adManage/sp/campaigns/campaignDetail/index.vue
  21. 9 0
      src/views/components/ad-group-select/api.ts
  22. 71 0
      src/views/components/ad-group-select/index.vue
  23. 22 0
      src/views/components/auto/auto-campaigns/api.ts
  24. 83 0
      src/views/components/auto/auto-campaigns/common.ts
  25. 69 0
      src/views/components/auto/auto-campaigns/neg-keyword.vue
  26. 64 0
      src/views/components/auto/auto-campaigns/search-term.vue
  27. 82 0
      src/views/components/auto/auto-campaigns/select-tmpl.vue
  28. 102 0
      src/views/components/auto/auto-campaigns/switch-campaign.vue
  29. 62 0
      src/views/components/auto/auto-campaigns/target-rule.vue
  30. 68 0
      src/views/components/auto/auto-campaigns/timer-bid.vue
  31. 69 0
      src/views/components/auto/auto-campaigns/timer-budget.vue
  32. 84 0
      src/views/components/auto/auto-templates/neg-keyword.vue
  33. 74 0
      src/views/components/auto/auto-templates/search-term.vue
  34. 12 22
      src/views/components/auto/auto-templates/switch-campaign.vue
  35. 63 0
      src/views/components/auto/auto-templates/target-rule.vue
  36. 17 30
      src/views/components/auto/auto-templates/timer-bid.vue
  37. 14 29
      src/views/components/auto/auto-templates/timer-budget.vue
  38. 0 0
      src/views/components/auto/enum.js
  39. 19 0
      src/views/components/auto/freq-setting.vue
  40. 88 0
      src/views/components/auto/search-term-add.vue
  41. 66 0
      src/views/components/auto/search-term-bid.vue
  42. 96 0
      src/views/components/auto/st-bid-input.vue
  43. 241 0
      src/views/components/auto/target-rule-setting.vue
  44. 52 0
      src/views/components/auto/target-select.vue
  45. 12 0
      src/views/components/campaign-select/api.ts
  46. 64 0
      src/views/components/campaign-select/index.vue
  47. 48 24
      src/views/demo/index.vue
  48. 28 28
      src/views/efTools/automation/api.ts
  49. 58 21
      src/views/efTools/automation/index.vue

+ 219 - 209
src/components/TimerBidTable/index.vue

@@ -1,72 +1,63 @@
 <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 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="{ background: info.selected ? '#ccdbff' : '' }"
-						>
-							<span class="cell-text"> {{ info.value ? info.value : '' }} </span>
-						</td>
-					</tr>
-				</template>
-				<tr>
-					<th colspan="28" class="clear-bar">
-						<span class="middle">可拖动鼠标选择时间段</span>
-						<span class="hover-link fr" @click="resetAllBid">全部重置</span>
-					</th>
-				</tr>
-			</tbody>
-		</table>
+  <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>
+    <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[][],
-} 
-const props = defineProps<Props>()
+  data: number[][]
+  disabled: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+})
 const bidData = ref(props.data)
 
 const mouseDown = ref(false)
@@ -74,209 +65,228 @@ const startRowIndex = ref(0)
 const startColIndex = ref(0)
 const items = ref([])
 const WeekMap = {
-	0: '星期一',
-	1: '星期二',
-	2: '星期三',
-	3: '星期四',
-	4: '星期五',
-	5: '星期六',
-	6: '星期日'
+  0: '星期一',
+  1: '星期二',
+  2: '星期三',
+  3: '星期四',
+  4: '星期五',
+  5: '星期六',
+  6: '星期日',
 }
 const dialogVisible = ref(false)
 const bid = ref(0)
 const selectedItems = ref({})
 
 onMounted(() => {
-	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)
-	}
-	// console.log(99, items.value)
+  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)
+  }
+  // console.log(99, items.value)
 })
 
+const getTdStyle = (info: any) => {
+  if (props.disabled) {
+    return { cursor: 'not-allowed', background: '#fff' }
+  }
+  return { background: info.selected ? '#ccdbff' : '' }
+}
+
 watch(
-	() => props.data,
-	() => {
-		// console.log(102, 'watch', props.data)
-		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
-	}
+  () => props.data,
+  () => {
+    // console.log(102, '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
+  }
 )
 
-const handleMouseDown = ( rowIndex: number, colIndex: number, event: any ) => {
-		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 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 (event.button === 0) {
-		mouseDown.value = false
-		startColIndex.value = 0
-		startRowIndex.value = 0
-		dialogVisible.value = true
-	}
+  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(mouseDown.value) {
-		selectCell(rowIndex, colIndex)
-	}
+  if (props.disabled) return
+  if (mouseDown.value) {
+    selectCell(rowIndex, colIndex)
+  }
 }
 
 const selectCell = (rowIndex: number, colIndex: number) => {
-	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)
+  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)
-		}
-	}
+  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
+  // 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
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
 }
 
 const clearCellBackgroundColor = () => {
-	for (const hoursList of items.value) {
-		for (const info of hoursList) {
-			info.selected = false
-		}
-	}
+  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] = []
-	}
+  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 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()
+  // console.log("closeDialog")
+  cancelBid()
+  done()
 }
-
 </script>
 
 <style lang="scss" scoped>
 .calendar {
-	background-color: #fff;
-	-webkit-user-select: none;
-	position: relative;
-	display: inline-block;
+  background-color: #fff;
+  -webkit-user-select: none;
+  position: relative;
+  display: inline-block;
 
-	.calendar-table {
-		border-collapse: collapse;
-	}
+  .calendar-table {
+    border-collapse: collapse;
+  }
 
-	.week-td {
-		width: 90px;
-	}
+  .week-td {
+    width: 90px;
+  }
 }
 
 table {
-	display: table;
-	border-collapse: separate;
-	box-sizing: border-box;
-	text-indent: initial;
-	border-spacing: 2px;
-	border-color: gray;
+  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;
-	}
+  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);
+  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;
-		}
+    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;
-			cursor: pointer;
+    td {
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      min-width: 40px;
 
-			&:hover {
-				background: #ccdbff;
-			}
+      &:hover {
+        background: #ccdbff;
+        cursor: pointer;
+      }
 
-			.cell-text {
-				color: black;
-			}
-		}
-	}
+      .cell-text {
+        color: black;
+      }
+    }
+  }
 }
 
 .clear-bar {
-	line-height: 32px;
-	padding: 0 12px;
+  line-height: 32px;
+  padding: 0 12px;
 
-	.hover-link {
-		color: #1c6bde;
-    cursor: pointer;
+  .hover-link {
+    color: #1c6bde;
     font-size: 13px;
-	}
-	.fr {
-		float: right;
-	}
-	.middle {
-		float: center;
-	}
+    margin-top: 7px;
+  }
+  .fr {
+    float: right;
+  }
+  .middle {
+    float: center;
+  }
 }
 </style>

+ 71 - 61
src/components/TimerBudgetTable/index.vue

@@ -56,7 +56,7 @@
                 @mousedown="handleMouseDown(week, hour, $event)"
                 @mouseover="handleMouseMove(week, hour)"
                 @mouseup="handleMouseUp"
-                :style="{ background: getCellBackgroundColor(info) }">
+                :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'" />
@@ -68,43 +68,42 @@
           <tr>
             <th colspan="28" class="clear-bar">
               <span class="middle">可拖动鼠标选择时间段</span>
-              <span class="hover-link fr" @click="resetAllBid">全部重置</span>
+              <el-button class="hover-link fr" link @click="resetAllBid" :disabled="disabled">全部重置</el-button>
             </th>
           </tr>
         </tbody>
       </table>
-
-      <el-dialog v-model="dialogVisible" :close-on-click-modal="false" title="编辑" width="30%" :before-close="closeDialog">
-        <vxe-form :data="formData" :rules="formRules" ref="formRef" @submit="submitBid" @reset="cancelBid">
-          <vxe-form-item field="type" :span="16">
-            <el-select v-model="formData.type" @change="changeKind" style="width: 100%">
-              <el-option v-for="item in KindEnum" :key="item.value" :label="item.label" :value="item.value" />
-            </el-select>
-          </vxe-form-item>
-          <vxe-form-item field="value" v-if="formData.type !== ''" :span="8">
-            <vxe-input v-model="formData.value" type="float" size="medium" :min="0" :step="formData.type === 'FixBudget' ? 1 : 0.01">
-              <template v-if="isNumber" #prefix>$</template>
-              <template v-if="!isNumber" #suffix>%</template>
-            </vxe-input>
-          </vxe-form-item>
-          <vxe-form-item align="right" span="24">
-            <template #default>
-              <vxe-button type="reset" content="取消"></vxe-button>
-              <vxe-button type="submit" status="primary" content="确认"></vxe-button>
-            </template>
-          </vxe-form-item>
-        </vxe-form>
-      </el-dialog>
     </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 { VxeFormInstance, VxeFormPropTypes } from 'vxe-table'
+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
@@ -139,33 +138,19 @@ const KindEnum = [
 
 const formData: Ref<FormData> = ref({ type: 'FixBudget', value: '1.00' })
 const isNumber = ref(true)
-const formRef = ref<VxeFormInstance>()
-const formRules = ref<VxeFormPropTypes.Rules>({
-  type: [
-    {
-      validator({ itemValue }) {
-        if (itemValue === undefined || itemValue === null) {
-          return new Error('必填项')
-        }
-      },
-    },
-  ],
-  value: [
-    {
-      validator({ itemValue }) {
-        if (formData.value.type === 'FixBudget') {
-          if (itemValue < 1) {
-            return new Error('固定预算必须大于1')
-          }
-        } else {
-          if (itemValue <= 0) {
-            return new Error('数值必须大于0')
-          }
-        }
-      },
-    },
-  ],
-})
+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')
+    }
+  }
+}
 
 onMounted(() => {
   for (let i = 0; i < 7; i++) {
@@ -184,10 +169,38 @@ onMounted(() => {
   }
 })
 
+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
@@ -207,12 +220,6 @@ const changeKind = () => {
   formRef.value.clearValidate('value')
 }
 
-const getCellBackgroundColor = (row: any) => {
-  if (row.selected) return '#ccdbff'
-  if (row.type) return KindEnum.find((item) => item.value === row.type).color
-  return ''
-}
-
 const getCellText = (row: any) => {
   if (row.type === 'DownPercent' || row.type === 'UpPercent') return row.value + '%'
   if (row.value) return '$' + row.value
@@ -220,6 +227,7 @@ const getCellText = (row: any) => {
 }
 
 const handleMouseDown = (rowIndex: number, colIndex: number, event: any) => {
+  if (props.disabled) return
   if (event.button === 0) {
     mouseDown.value = true
     startRowIndex.value = rowIndex
@@ -230,6 +238,7 @@ const handleMouseDown = (rowIndex: number, colIndex: number, event: any) => {
 }
 
 const handleMouseUp = (event: any) => {
+  if (props.disabled) return
   if (event.button === 0) {
     mouseDown.value = false
     startColIndex.value = 0
@@ -239,12 +248,14 @@ const handleMouseUp = (event: any) => {
 }
 
 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)
@@ -286,7 +297,6 @@ const cancelBid = () => {
 }
 
 const closeDialog = (done: Function) => {
-  // console.log("closeDialog")
   cancelBid()
   done()
 }
@@ -367,10 +377,10 @@ table {
       text-align: center;
       line-height: 32px;
       min-width: 40px;
-      cursor: pointer;
       color: #fff;
       &:hover {
         background: #ccdbff;
+        cursor: pointer;
       }
     }
   }
@@ -382,8 +392,8 @@ table {
 
   .hover-link {
     color: #1c6bde;
-    cursor: pointer;
     font-size: 13px;
+    margin-top: 7px;
   }
   .fr {
     float: right;

+ 0 - 43
src/components/auto-templates/api.ts

@@ -1,43 +0,0 @@
-import { request } from '/@/utils/service'
-
-const urlPrefix = "/api/ad_manage/auto_plan_template/"
-
-export function getCampaignRuleState (params: any) {
-  return request({
-    url: 'http://127.0.0.1:4523/m1/3337156-0-default/api/campaign-rules/get-state',
-    method: 'get',
-    params: params
-  })
-}
-export function getTemplateList (params: any) {
-  return request({
-    url: 'http://127.0.0.1:4523/m1/3337156-0-default/api/rule-template',
-    method: 'get',
-    params: params
-  })
-}
-
-export function getTemplate (templateId: number) {
-  return request({
-    url: urlPrefix + templateId,
-    method: 'get'
-  })
-}
-
-export function createTemplate (data: any) {
-  return request({
-    url: urlPrefix,
-    method: 'post',
-    data: data
-  })
-}
-
-export function updateTemplate (id: number, data: any) {
-  return request({
-    url: urlPrefix + id + '/',
-    method: 'put',
-    data: data
-  })
-}
-
-

+ 0 - 47
src/components/auto-templates/common.js

@@ -1,47 +0,0 @@
-import { ref } from 'vue'
-import * as api from './api'
-import XEUtils from 'xe-utils'
-
-export const useFromData = (props) => {
-  const formData = ref({
-    name: '',
-    rule: {
-      type: props.data.templateType,
-      campaignType: '',
-      campaignAd: [],
-      action: {},
-      activeModel: '',
-      // setTime: '',
-      weekdays: [],
-      conditions: [],
-    }
-  })
-
-  function setFormData() {
-    const { mode, data } = props
-    if (mode === 'edit') {
-      formData.value.name = data.templateName
-      formData.value.rule.type = data.templateType
-      formData.value.rule.campaignType = data.rule.campaignType
-      formData.value.rule.campaignAd = data.rule.campaignAd
-      if (data.rule.action) {
-        formData.value.rule.action.state = data.rule.action.state
-        formData.value.rule.action.setTime = data.rule.action.setTime
-      }
-      formData.value.rule.weekdays = data.rule.weekdays
-      formData.value.rule.conditions = data.rule.conditions
-    }
-  }
-
-  async function submitFormData() {
-    const { mode, data } = props
-    if (mode === 'add') {
-      await api.createTemplate(formData.value)
-    } else if (mode === 'edit') {
-      await api.updateTemplate(data.templateId, formData.value)
-    }
-  }
-
-  return { formData, setFormData, submitFormData }
-}
-

+ 0 - 0
src/components/auto-templates/neg-keywords.vue


+ 0 - 40
src/components/auto-templates/search-term.vue

@@ -1,40 +0,0 @@
-<template>
-  <div>
-    <div class="asj-h2">添加搜索词</div>
-    <SelectTmpl :templateType="5"/>
-    <div class="asj-h3">添加到</div>
-    <el-radio-group style="display: flex; flex-direction: column; align-items: flex-start;" v-model="formData.isSelf">
-      <el-radio :label="1">当前广告活动(所有广告组)</el-radio>
-      <el-radio :label="3">当前广告活动(当前广告组)</el-radio>
-      <div>
-        <el-radio :label="2">指定广告活动的广告组</el-radio>
-        <el-form v-show="formData.isSelf === 2">
-          <el-form-item>
-            <el-select>
-              <el-option></el-option>
-              <el-option></el-option>
-            </el-select>
-          </el-form-item>
-        </el-form>
-      </div>
-    </el-radio-group>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref } from 'vue'
-import SelectTmpl from './select-tmpl.vue'
-
-const formData = ref({
-  campaignAd: [
-    { campaignType: '', campaignId: '', campaignName: '', adGroupId: '', adGroupName: '' },
-  ],  // 指定广告活动的广告组
-  isSelf: 2,
-
-})
-
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 49
src/components/auto-templates/select-tmpl.vue

@@ -1,49 +0,0 @@
-<template>
-  <div class="asj-h3">选择模板</div>
-  <el-radio-group v-model="rule">
-    <div style="display: flex; justify-content: flex-start; flex-direction:column">
-      <el-radio label="custom">自定义规则</el-radio>
-      <div style="display: flex; align-items: center;">
-        <el-radio label="tmpl">使用已有模板</el-radio>
-        <el-select v-show="rule === 'tmpl'" @change="changeTmpl" v-model="tmplId">
-          <el-option v-for="info in tmplList" :label="info.templateName" :value="info.id" :key="info.id">
-          </el-option>
-        </el-select>
-      </div>
-    </div>
-  </el-radio-group>
-</template>
-
-<script lang="ts" setup>
-import { ref, onMounted } from 'vue'
-import * as api from './api'
-
-interface Props {
-  templateType: number
-}
-const props = defineProps<Props>()
-const rule = ref("tmpl")
-const tmplId = ref('')
-const tmplList = ref([])
-
-const getTmplList = async () => {
-  const resp = await api.getTemplateList({
-    page: 1,
-    pageSize: 1000,
-    templateType: props.templateType
-  })
-  tmplList.value = resp.data
-}
-const changeTmpl = () => {
-
-}
-
-onMounted(async () => {
-  await getTmplList()
-})
-
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 269
src/components/auto-templates/target-rule.vue

@@ -1,269 +0,0 @@
-<template>
-  <div>
-    <div class="asj-h2">定向规则</div>
-    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
-      <el-form-item prop="name" v-if="mode !== 'auto'" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
-        <template #label>
-          <span class="asj-h3">模板名称</span>
-        </template>
-        <el-input v-model="formData.name" style="width: 30%"></el-input>
-      </el-form-item>
-
-      <el-form-item>
-        <template #label>
-          <span class="asj-h3">生效对象</span>
-        </template>
-        <TargetSelect></TargetSelect>
-      </el-form-item>
-    </el-form>
-  </div>
-  <div class="asj-h3">规则设置</div>
-  <div class="rule-setting-container">
-    <div v-for="info in formData.rule.conditions" class="rule-setting-item">
-      <div class="rule-setting-title">
-        <div class="asj-h3">{{ RuleNameMap[info.actionType] }} - 规则{{ info.ordering }}</div>
-        <div class="rule-setting-opration">
-          <el-tooltip content="添加规则" placement="top" effect="dark">
-            <el-icon @click="addRule(info.actionType)"><Plus /></el-icon>
-          </el-tooltip>
-          <el-tooltip content="删除规则" placement="top" effect="dark">
-            <el-icon @click="delRule(info.actionType)"><DeleteFilled /></el-icon>
-          </el-tooltip>
-          <el-tooltip content="收起/展开规则" placement="top" effect="dark">
-            <el-icon @click="info.show = !info.show"><ArrowDown /></el-icon>
-          </el-tooltip>
-        </div>
-      </div>
-      <div v-show="info.show">
-        <el-form :inline="true" :model="info.action" ref="ruleFormRef">
-          <div v-if="info.actionType === 'increase' || info.actionType === 'decrease'">
-            <el-form-item>
-              <el-select v-model="actionModel" @change="changeAction(info.action)" style="width: 250px">
-                <el-option
-                  v-for="item in ActionList"
-                  :key="item.value"
-                  :value="item.value"
-                  :label="item.label.replace('_', info.actionType === 'increase' ? '升高' : '降低')">
-                </el-option>
-              </el-select>
-            </el-form-item>
-            <el-form-item prop="set" :rules="{ validator: checkFloat }">
-              <InputFloat
-                v-model="info.action.set"
-                :prefix="info.action.numType === 'num' ? '$' : ''"
-                :suffix="info.action.numType === 'ratio' ? '%' : ''"
-                style="width: 120px">
-              </InputFloat>
-            </el-form-item>
-            <el-form-item label="最大值" v-if="info.actionType === 'increase'">
-              <InputFloat v-model="info.action.max" style="width: 120px" prefix="$"></InputFloat>
-            </el-form-item>
-            <el-form-item label="最小值" v-else>
-              <InputFloat v-model="info.action.min" style="width: 120px" prefix="$"></InputFloat>
-            </el-form-item>
-          </div>
-          <el-form-item v-if="info.actionType === 'set'" prop="set">
-            <el-input v-model="info.action.set"></el-input>
-          </el-form-item>
-        </el-form>
-        <div class="asj-h3">条件</div>
-        <conditionBuilder :data="info.conditions" :candidate-fields="props.candidateFields" ref="condiBuilderRef" />
-      </div>
-    </div>
-    <el-popover placement="bottom-start" trigger="hover">
-      <template #reference>
-        <el-button :icon="Plus" type="warning">添加规则</el-button>
-      </template>
-      <div class="popver-content">
-        <span class="popver-content-item" v-for="[name, title] of Object.entries(RuleNameMap)" @click="addRule(name)">{{ title }}</span>
-      </div>
-    </el-popover>
-    <div class="asj-h3">频率设置</div>
-    <el-checkbox-group v-model="formData.rule.weekdays">
-      <el-checkbox v-for="i in [1, 2, 3, 4, 5, 6, 0]" :label="i" :key="i">{{ WeekMap[i] }}</el-checkbox>
-    </el-checkbox-group>
-
-    <div class="auto-page-foot">
-      <el-button style="width: 200px" @click="cancel">取消</el-button>
-      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
-    </div>
-    <p>{{ formData }}</p>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref, computed } from 'vue'
-import { Plus } from '@element-plus/icons-vue'
-import conditionBuilder from '/@/components/conditionBuilder/index.vue'
-import InputFloat from '/@/components/input-float/index.vue'
-import SelectTmpl from './select-tmpl.vue'
-import TargetSelect from './target-select.vue'
-import { WeekMap, ActionList, RuleNameMap } from './enum'
-import { useFromData } from './common'
-import XEUtils from 'xe-utils'
-
-interface Props {
-  mode: string
-  candidateFields: CandidateField[]
-  data: { [key: string]: any }
-}
-const props = withDefaults(defineProps<Props>(), {
-  candidateFields: () => {
-    return [
-      { label: '曝光量', value: 'impressions' },
-      { label: '点击量', value: 'clicks' },
-      { label: '转化率', value: 'cr', suffix: '%' },
-      { label: '单次点击费用', value: 'cpc', prefix: '$' },
-      { label: '关键词', value: 'keyword', symbols: ['in'] }
-    ]
-  },
-})
-const emits = defineEmits(['refresh'])
-const formRef = ref()
-const ruleFormRef = ref()
-const condiBuilderRef = ref()
-const { formData, setFormData, submitFormData } = useFromData(props)
-
-const maxRuleNumber = computed(() => {
-  const ret = {
-    increase: 0,
-    decrease: 0,
-    set: 0,
-    pause: 0,
-  }
-  for (const info of formData.value.rule.conditions) {
-    ret[info.actionType] = Math.max(ret[info.actionType] || 0, info.ordering)
-  }
-  return ret
-})
-
-const addRule = (actionType: string) => {
-  formData.value.rule.conditions.push({
-    action: { set: '', baseType: '', max: '', min: '', numType: 'ratio' },
-    actionType: actionType,
-    conditions: [
-      {
-        key: Math.random().toString(36).substring(2),
-        day: 1,
-        exceptDay: 0,
-        items: [
-          {
-            dataType: props.candidateFields[0].value,
-            dayType: 'sum',
-            symbol: 'gte',
-            num: '',
-          },
-        ],
-      },
-    ],
-    day: '1',
-    exceptDay: '',
-    ordering: maxRuleNumber.value[actionType] + 1,
-    show: true,
-  })
-}
-if (props.mode === 'add') {
-  addRule('increase')
-}
-
-const checkFloat = (rule: any, value: any, callback: any) => {
-  if (value === '0.00' || value === '') {
-    callback(new Error('请输入大于0的数值!'))
-  } else {
-    callback()
-  }
-}
-
-const actionModel = ref('bid-ratio')
-const changeAction = (action: any) => {
-  const [a, b] = actionModel.value.split('-')
-  action.baseType = a
-  action.numType = b
-  action.set = ''
-}
-
-const delRule = (actionType: string) => {
-  // const index = maxRuleNumber.value[actionType]
-  // if (index > -1) {
-  //   maxRuleNumber.value[actionType]--
-  //   rules.value[actionType].splice(index, 1)
-  // }
-}
-const validateConditionsForm = async () => {
-  const ret = []
-  for (const f of condiBuilderRef.value) {
-    ret.push(await f.validate())
-  }
-  for (const validList of ret) {
-    if (validList.includes(false)) {
-      return false
-    }
-  }
-  return true
-}
-const validateRuleForm = async () => {
-  const ret = []
-  for (const f of ruleFormRef.value) {
-    await f.validate((valid: boolean) => { ret.push(valid) })
-  }
-  return ret.includes(false)
-}
-const submitForm = async () => {
-  const valid1 = await validateRuleForm()
-  const valid2 = await validateConditionsForm()
-  formRef.value.validate(async (valid: boolean) => {
-    if (valid1 && valid2 && valid) {
-      await submitFormData()
-      emits('refresh')
-    } else {
-      console.log('验证失败')
-    }
-  })
-}
-const cancel = () => {
-  if (props.mode !== 'auto') {
-    emits('refresh')
-  }
-}
-</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;
-    }
-  }
-}
-
-.rule-setting-container {
-  .rule-setting-item {
-    border: 1px solid #e0e0e0;
-    margin: 10px auto;
-    padding: 0 12px;
-  }
-
-  .rule-setting-title {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-
-    .rule-setting-opration {
-      display: flex;
-      align-items: center;
-      gap: 20px;
-
-      .el-icon {
-        cursor: pointer;
-      }
-    }
-  }
-}
-</style>

+ 0 - 40
src/components/auto-templates/target-select.vue

@@ -1,40 +0,0 @@
-<template>
-  <div>
-    <!-- <div class="asj-h3">生效对象</div> -->
-    <el-radio-group v-model="target">
-      <div class="target-radio-group">
-        <el-radio label="campaign">当前广告活动(所有定向)</el-radio>
-        <div class="target-radio-group-item">
-          <el-radio label="adGroup">当前广告活动指定广告组(所有定向)</el-radio>
-          <el-select style="margin-left: 23px" v-show="target === 'adGroup'">
-            <el-option label="111"></el-option>
-          </el-select>
-        </div>
-        <div class="target-radio-group-item">
-          <el-radio label="specified">指定定向</el-radio>
-          <el-button v-show="target === 'specified'" style="margin-left: 20px; color: blue;" link>选择定向</el-button>
-        </div>
-      </div>
-    </el-radio-group>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref } from 'vue'
-
-const target = ref('campaign')
-</script>
-
-<style scoped>
-.target-radio-group {
-  display: flex;
-  justify-content: flex-start;
-  flex-direction: column;
-  .target-radio-group-item {
-    display: flex;
-    flex-direction: column;
-    justify-content: flex-start;
-    align-items: flex-start;
-  }
-}
-</style>

+ 118 - 75
src/components/conditionBuilder/condition-group2.vue

@@ -1,7 +1,7 @@
 <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 :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>
@@ -16,13 +16,13 @@
         <el-form-item
           v-for="(conditionInfo, index) of formData.items"
           :key="conditionInfo.dataType"
-          :prop="`items[${index}].num`"
-          :rules="{ validator: checkRight }">
+          :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">
+          <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"
@@ -32,7 +32,10 @@
                 :value="item.value">
               </el-option>
             </el-select>
-            <el-select v-model="conditionInfo.dayType" style="width: 80px" v-show="conditionInfo.symbol !== 'in'">
+            <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>
@@ -40,24 +43,22 @@
               <el-option v-for="info in getSymbolOptions(conditionInfo.dataType)" :label="info.label" :value="info.value" :key="info.value">
               </el-option>
             </el-select>
-            <div v-if="conditionInfo.symbol === 'in'">
-            
-            </div>
-            <div v-else-if="conditionInfo.symbol === 'between' || conditionInfo.symbol === 'not_between'">
-              <InputFloat
-                v-model="conditionInfo.num[0]"
-                :prefix="findPrefix(conditionInfo.dataType)"
-                :suffix="findSuffix(conditionInfo.dataType)"
-                style="width: 100px">
-              </InputFloat>
-              <span> ~ </span>
-              <InputFloat
-                v-model="conditionInfo.num[1]"
-                :prefix="findPrefix(conditionInfo.dataType)"
-                :suffix="findSuffix(conditionInfo.dataType)"
-                style="width: 100px">
-              </InputFloat>
-            </div>
+            <template v-if="conditionInfo.symbol === 'in' || conditionInfo.symbol === 'not_in'">
+              <el-select v-if="getFieldInfo(conditionInfo.dataType).options" v-model="conditionInfo.values" multiple>
+                <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"
@@ -69,8 +70,14 @@
           </div>
         </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
+        link
+        type="primary"
+        @click="addConditionItem"
+        :icon="Plus"
+        :style="{'margin-left': '100px', color: disabled ? '': 'blue'}"
+        v-show="showAddCondiBtn"
+        :disabled="disabled">
         添加条件
       </el-button>
     </div>
@@ -80,15 +87,19 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, computed } from 'vue'
+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 = [
@@ -107,31 +118,51 @@ const excludeEnum = [
   { label: '最近7天', value: 7 },
   { label: '最近14天', value: 14 },
 ]
-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 getSymbolOptions = (field: string) => {
-  const info = XEUtils.find(props.candidateFields, (item) => item.value === field)
-  if (info.symbols && info.symbols.length > 0) return XEUtils.filter(SymbolOptionsList, (item) => info.symbols.includes(item.value))
-  return SymbolOptionsList
-}
-const props = defineProps<Props>()
+
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+})
 const formRef = 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 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
@@ -141,36 +172,50 @@ const validate = async () => {
   return ret
 }
 const showAddCondiBtn = computed(() => {
-  return formData.items.length < props.candidateFields.length
+  return formData.value.items.length < props.candidateFields.length
 })
 const selectedFields = computed(() => {
   const ret = {}
-  for (const info of formData.items) {
+  for (const info of formData.value.items) {
     ret[info.dataType] = true
   }
   return ret
 })
-const addCondition = () => {
+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]) {
-      const symbol = getSymbolOptions(item.value)[0].value
-      formData.items.push({
-        dataType: item.value,
-        dayType: symbol === 'in' ? '' : 'sum',
-        symbol: symbol,
-        num: '',
-      })
+      formData.value.items.push(buildConditionItem(item.value))
       break
     }
   }
 }
-const changeDataType = (conditionInfo: any) => {
-  conditionInfo.num = ''
+// 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
-  if (conditionInfo.symbol === 'in') {
-    conditionInfo.num = ['', '']
-  }
+  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 ?? ''
 }
@@ -178,12 +223,20 @@ const findSuffix = (dataType: string) => {
   return props.candidateFields.find((item) => item.value === dataType).suffix ?? ''
 }
 const deleteCondition = (index: number) => {
-  formData.items.splice(index, 1)
+  formData.value.items.splice(index, 1)
 }
 const deleteConditionGroup = () => {
   emits('deleteGroup')
 }
 
+watch(
+  () => props.data,
+  () => {
+    formData.value = props.data
+  },
+  { deep: true }
+)
+
 defineExpose({ validate })
 </script>
 
@@ -196,16 +249,6 @@ defineExpose({ validate })
   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;

+ 49 - 17
src/components/conditionBuilder/index.vue

@@ -1,42 +1,73 @@
 <template>
-  <ConditionGroup
-    ref="condiGroupRef"
-    v-for="info of condiGroups"
-    :candidate-fields="candidateFields"
-    :data="info"
-    :key="info.key"
-    :show-del-group-btn="condiGroups.length > 1"
-    @delete-group="delGroup(info.key)" />
-  <el-button type="success" @click="addConditionGroup" style="margin: 5px auto; display: block">添加条件组</el-button>
+  <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 = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+  titleClass: 'asj-h3',
+  candidateFields: () => {
+    return [
+      { label: '曝光量', value: 'impressions' },
+      { label: '点击量', value: 'clicks' },
+      { label: '转化率', value: 'cr', suffix: '%' },
+      { label: '单次点击费用', value: 'cpc', prefix: '$' },
+    ]
+  },
+})
 const condiGroups = ref(props.data)
 const condiGroupRef = ref()
-const addConditionGroup = () => {
-  condiGroups.value.push({
+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: props.candidateFields[0].value,
-        dayType: 'sum',
-        symbol: 'gte',
+        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)
 }
@@ -54,7 +85,8 @@ watch(
   () => props.data,
   () => {
     condiGroups.value = props.data
-  }
+  },
+  { deep: true }
 )
 
 defineExpose({ validate, addConditionGroup })

+ 4 - 1
src/components/conditionBuilder/type.d.ts

@@ -3,6 +3,8 @@ declare interface ConditionItem {
   dayType: string,
   symbol: string,
   num: string,
+  values: string[],
+  ranges: string[]
 }
 
 declare interface ConditionGroupItem {
@@ -17,5 +19,6 @@ declare interface CandidateField {
   value: string,
   prefix?: string,
   suffix?: string,
-  symbols?: 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 }
+}

+ 15 - 15
src/components/input-float/index.vue

@@ -14,30 +14,31 @@ const props = defineProps({
   },
   precision: {
     type: Number,
-    default: 2
+    default: 2,
   },
   prefix: {
     type: String,
-    default: ""
+    default: '',
   },
   suffix: {
     type: String,
-    default: ""
+    default: '',
   }
 })
 const data = ref(props.modelValue)
-const emits = defineEmits(["update:modelValue", "blur"])
+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(".")
+  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) {
+    if (data.value.length - index - 1 > props.precision) {
       data.value = val.substring(0, index + props.precision + 1)
     }
   }
@@ -54,9 +55,8 @@ const onBlur = () => {
 
 watch(
   () => props.modelValue,
-  () => data.value = props.modelValue
+  () => (data.value = props.modelValue)
 )
-
 </script>
 
 <style lang="scss" scoped>
@@ -66,4 +66,4 @@ watch(
 ::v-deep(.el-input-group__prepend) {
   padding: 0 10px !important;
 }
-</style>
+</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>

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

+ 305 - 271
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
@@ -342,69 +344,101 @@ declare interface MetricOptions {
 }
 
 declare interface ShowMetric {
-	label: string,
-	metric: string,
-	color: string
+  label: string
+  metric: string
+  color: string
 }
 
 declare interface MetricData extends MetricOptions {
-  metricVal: string,
-  preVal?: any,
+  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,
-	portfolioName?: string
-	servingStatus?: string
+  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,
-	adGroupId?: string,
-	adGroupName?: string,
-	state?: string,
-	defaultBid?: string,
-	startDate?: string,
-	endDate?: string,
-	targetingType?: string
+  id?: number
+  adGroupId?: string
+  adGroupName?: string
+  state?: string
+  defaultBid?: string
+  startDate?: string
+  endDate?: string
+  targetingType?: string
 }
 
-declare interface AutoTemplate {
-	id: number;
-	campaignNumber: number;
-	campaignType: string;
-	conType: string;
-	createdTime: string;
-	founder: string;
-	templateName: string;
-	templateType: string;
-	type: number;
-	updatedPerson: string;
-	updateTime: 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 {
-	type: number,
-	campaignType: string,
-	campaignAd: string[],
-	action: string,
-	activeModel: string,
-	setTime: string,
-	weekdays: number[],
-	conditions: number[][]
+  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 }
 }

+ 11 - 0
src/views/adManage/sp/campaigns/campaignDetail/automation/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service'
+
+const urlPrefix = "/api/ad_manage/auto_plan_template/"
+
+export function getCampaignRuleInfo (params: any) {
+  return request({
+    url: '/api/ad_manage/campaign_rule/get_info',
+    method: 'get',
+    params: params
+  })
+}

+ 133 - 29
src/views/adManage/sp/campaigns/campaignDetail/automation/index.vue

@@ -1,43 +1,147 @@
 <template>
-  <el-tabs tab-position="left" style="height: 100%;">
-    <el-tab-pane label="分时调价">
-      <TimerBidTmpl mode="auto" :data="ruleData"/>
+  <el-tabs tab-position="left" v-model="activeTab" @tab-change="changeTab">
+    <el-tab-pane label="分时调价" :name="1">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['1']" :disabled="RuleStatusButton['1'] === '-'"></el-switch>
+          <span>分时调价</span>
+        </div>
+      </template>
+      <TimerBid :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(1)" v-if="activeTab === 1" />
     </el-tab-pane>
-    <el-tab-pane label="分时预算" :lazy="true">
-      <!-- <TimerBudgetTmpl mode="auto" /> -->
+    <el-tab-pane label="分时预算" :name="2">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['2']" :disabled="RuleStatusButton['2'] === '-'"></el-switch>
+          <span>分时预算</span>
+        </div>
+      </template>
+      <TimerBudget :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(2)" v-if="activeTab === 2" />
     </el-tab-pane>
-    <el-tab-pane label="广告活动" :lazy="true">
-      <!-- <CampaignTmpl mode="auto" /> -->
+    <el-tab-pane label="广告活动" :name="3">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['3']" :disabled="RuleStatusButton['3'] === '-'"></el-switch>
+          <span>广告活动</span>
+        </div>
+      </template>
+      <SwitchCampaign :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(3)" v-if="activeTab === 3" />
     </el-tab-pane>
-    <el-tab-pane label="添加否定词" :lazy="true">
+    <el-tab-pane label="定向规则" :name="4">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['4']" :disabled="RuleStatusButton['4'] === '-'"></el-switch>
+          <span>定向规则</span>
+        </div>
+      </template>
+      <TargetRule :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(4)" v-if="activeTab === 4"></TargetRule>
+    </el-tab-pane>
+    <el-tab-pane label="添加搜索词" :name="5">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['5']" :disabled="RuleStatusButton['5'] === '-'"></el-switch>
+          <span>添加搜索词</span>
+        </div>
+      </template>
+      <SearchTermRule :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(5)" v-if="activeTab === 5"></SearchTermRule>
+    </el-tab-pane>
+    <el-tab-pane label="添加否定词" :name="6">
+      <template #label>
+        <div class="tab-label">
+          <el-switch size="small" v-model="RuleStatusButton['6']" :disabled="RuleStatusButton['6'] === '-'"></el-switch>
+          <span>添加否定词</span>
+        </div>
+      </template>
+      <NegKeywordRule :data="baseData" :RuleStatusButton="RuleStatusButton" @refresh="refresh(6)" v-if="activeTab === 6"></NegKeywordRule>
     </el-tab-pane>
   </el-tabs>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue'
-import TimerBidTmpl from '/@/components/auto-templates/timer-bid.vue'
-import TimerBudgetTmpl from '/@/components/auto-templates/timer-budget.vue'
-import CampaignTmpl from '/@/components/auto-templates/campaign.vue'
+import { ref, Ref, computed, onMounted, nextTick } from 'vue'
+import TimerBid from '/@/views/components/auto/auto-campaigns/timer-bid.vue'
+import TimerBudget from '/@/views/components/auto/auto-campaigns/timer-budget.vue'
+import SwitchCampaign from '/@/views/components/auto/auto-campaigns/switch-campaign.vue'
+import TargetRule from '/@/views/components/auto/auto-campaigns/target-rule.vue'
+import SearchTermRule from '/@/views/components/auto/auto-campaigns/search-term.vue'
+import NegKeywordRule from '/@/views/components/auto/auto-campaigns/neg-keyword.vue'
+import * as api from './api'
+import XEUtils from 'xe-utils'
 
-const ruleData = ref({
-  templateType: 0,
-  templateId: 0,
-  templateName: '',
-  rule: {
-    type: 0,
-    campaignType: '', // sp|sb|sd
-    campaignAd: [],
-    action: '',
-    activeModel: '',
-    setTime: '',
-    weekdays: [],
-    conditions: [],
-  },
+interface Props {
+  campaignId: string
+  profileId: string
+}
+const props = defineProps<Props>()
+const campaignType = 'sp'
+const activeTab = ref(1)
+const RuleStatusButton = ref({
+  '1': '-',
+  '2': '-',
+  '3': '-',
+  '4': '-',
+  '5': '-',
+  '6': '-',
+})
+// const height = ref(800)
+// const campaignRuleStatus = ref({
+//   campaignId: props.campaignId,
+//   campaignType: campaignType,
+//   profileId: props.profileId,
+//   ruleType: activeTab.value,
+//   templateName: '',
+//   useTmpl: false,
+//   template: null,
+//   rule: {
+//     type: activeTab.value,
+//     campaignType: campaignType,
+//     campaignAd: [],
+//     action: {},
+//     activeModel: '',
+//     setTime: '',
+//     weekdays: [],
+//     conditions: [],
+//   },
+//   RuleStatusButton: RuleStatusButton,
+// })
+const baseData = ref({
+  campaignId: props.campaignId,
+  campaignType: campaignType,
+  profileId: props.profileId,
+  ruleType: activeTab,
 })
 
-</script>
+const initData = async () => {
+  const resp = await api.getCampaignRuleInfo(baseData.value)
+  const info = resp.data[0]
+  if (info) {
+    for (const i of ['1', '2', '3', '4', '5', '6']) {
+      RuleStatusButton.value[i] = info.RuleStatusButton[i]
+    }
+  }
+}
 
-<style scoped>
+const changeTab = async () => {
+  // await initData()
+}
+
+const refresh = async (tab: number) => {
+  activeTab.value = 0
+  nextTick(() => {
+    activeTab.value = tab
+  })
+}
+
+onMounted(async () => {
+  await initData()
+})
+</script>
 
-</style>
+<style lang="scss" scoped>
+.tab-label {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 120px;
+}
+</style>

+ 14 - 6
src/views/adManage/sp/campaigns/campaignDetail/index.vue

@@ -15,16 +15,16 @@
     </div>
     <el-tabs type="border-card" class="asj-detail-tabs" v-model="tabActiveName">
       <el-tab-pane label="广告组" name="adGroup">
-        <AdGroups :campaignId="route.query.campaignId" v-if="tabActiveName==='adGroup'"></AdGroups>
+        <AdGroups :campaignId="campaignId" v-if="tabActiveName==='adGroup'"></AdGroups>
       </el-tab-pane>
       <el-tab-pane label="预算" name="budget">
-        <Budget :campaignId="route.query.campaignId" v-if="tabActiveName==='budget'"></Budget>
+        <Budget :campaignId="campaignId" v-if="tabActiveName==='budget'"></Budget>
       </el-tab-pane>
       <el-tab-pane label="自动化" name="automation">
-        <Automation :campaignId="route.query.campaignId" v-if="tabActiveName==='automation'"></Automation>
+        <Automation :campaignId="campaignId" :profileId="profile.profile_id" v-if="tabActiveName==='automation'"></Automation>
       </el-tab-pane>
       <el-tab-pane label="广告位" name="placement">
-        <Placement :campaignId="route.query.campaignId" v-if="tabActiveName==='placement'"/>
+        <Placement :campaignId="campaignId" v-if="tabActiveName==='placement'"/>
       </el-tab-pane>
       <el-tab-pane label="否定投放" name="negative">
         否定投放
@@ -55,6 +55,8 @@ const route = useRoute()
 const campaignInfo: Ref<SpCampaign> = ref({})
 const tabActiveName = ref('adGroup')
 
+const campaignId = ref(route.query.campaignId)
+
 onMounted(async () => {
   const resp = await GetObj(route.query.campaignId)
   campaignInfo.value = resp.data
@@ -63,6 +65,12 @@ onMounted(async () => {
 
 </script>
 
-<style lang="scss">
-
+<style>
+/* .el-tabs--card {
+  height: calc(100vh - 80px);
+}
+.el-tab-pane {
+  height: calc(100vh - 80px);
+  overflow-y: auto;
+} */
 </style>

+ 9 - 0
src/views/components/ad-group-select/api.ts

@@ -0,0 +1,9 @@
+import { request } from '/@/utils/service'
+
+export const getAdGroupList = (params: any) => {
+  return request({
+    url: '/api/ad_manage/ad_tree/adgroup',
+    method: 'get',
+    params: params,
+  })
+}

+ 71 - 0
src/views/components/ad-group-select/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <el-select
+    v-model="adGroupId"
+    filterable
+    remote
+    :remote-method="searchAdGroupName"
+    :style="{ width }"
+    clearable
+    @change="changeAdGroup"
+    placeholder="请选择广告组">
+    <el-option v-for="info in adGroupList" :key="info.adGroupId" :label="info.adGroupName" :value="info.adGroupId"></el-option>
+  </el-select>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch } from 'vue'
+import { getAdGroupList } from './api'
+
+interface Props {
+  modelValue: string
+  query: {
+    profileId: string
+    campaignType: string
+    campaignId: string
+  }
+  width: string
+}
+const props = withDefaults(defineProps<Props>(), {
+  width: '',
+})
+const adGroupId = ref(props.modelValue)
+const adGroupList = ref([])
+const emits = defineEmits(['update:modelValue', 'change-ad-group'])
+
+const searchAdGroupName = async (adGroupName: string) => {
+  if (!adGroupName) {
+    return
+  }
+  const { profileId, campaignType, campaignId } = props.query
+  await setAdGroupList({ profileId, campaignType, campaignId, adGroupName })
+}
+const changeAdGroup = () => {
+  emits('update:modelValue', adGroupId.value)
+  emits('change-ad-group', adGroupId.value)
+}
+
+const setAdGroupList = async (params: any) => {
+  if(!params.campaignId) return
+  const resp = await getAdGroupList(params)
+  adGroupList.value = resp.data
+  if(adGroupList.value.length === 1) {
+    adGroupId.value = adGroupList.value[0].adGroupId
+    changeAdGroup()
+  }
+}
+
+watch(
+  [() => props.query.campaignType, () => props.query.campaignId],
+  async () => {
+    await setAdGroupList(props.query)
+  },
+  { immediate: true }
+)
+watch(
+  () => props.modelValue,
+  () => { adGroupId.value = props.modelValue }
+)
+
+</script>
+
+<style scoped></style>

+ 22 - 0
src/views/components/auto/auto-campaigns/api.ts

@@ -0,0 +1,22 @@
+import { request } from '/@/utils/service'
+import XEUtils from 'xe-utils'
+
+import { GetList } from '/@/views/efTools/automation/api'
+
+export const getCampaignRuleInfo = (params: any) => {
+  return request({
+    url: '/api/ad_manage/campaign_rule/get_info',
+    method: 'get',
+    params,
+  })
+}
+
+export const saveCampaignRule = (data: any) => {
+  return request({
+    url: '/api/ad_manage/campaign_rule/save',
+    method: 'post',
+    data,
+  })
+}
+
+export const getTemplateList = GetList

+ 83 - 0
src/views/components/auto/auto-campaigns/common.ts

@@ -0,0 +1,83 @@
+import { Ref, ref, watch, onMounted } from 'vue'
+import * as api from './api'
+
+interface Props {
+  data: {
+    campaignId: string
+    campaignType: string
+    profileId: string
+    ruleType: number
+  }
+  RuleStatusButton?: { [key: string]: any }
+}
+
+export const userFormData = (props: Props) => {
+  const formData: Ref<AutoCampaignRule> = ref({
+    campaignId: props.data.campaignId,
+    campaignType: props.data.campaignType,
+    profileId: props.data.profileId,
+    ruleType: props.data.ruleType,
+    templateName: '',
+    useTmpl: false,
+    rule: {
+      type: props.data.ruleType,
+      campaignType: props.data.campaignType,
+      campaignAd: [],
+      action: {},
+      activeModel: '',
+      setTime: '',
+      weekdays: [],
+      conditions: [],
+    },
+    RuleStatusButton: props.RuleStatusButton,
+  })
+
+  const initData = async () => {
+    const resp = await api.getCampaignRuleInfo(props.data)
+    const info = resp.data[0]
+    if (info) {
+      delete info['RuleStatusButton']
+      formData.value = info
+      if (info.template) {
+        formData.value.useTmpl = true
+        formData.value.rule = info.template.rule
+      } else {
+        formData.value.useTmpl = false
+      }
+    }
+  }
+
+  const submitFormData = async () => {
+    const body: AutoCampaignRule = {
+      campaignId: formData.value.campaignId,
+      campaignType: formData.value.campaignType,
+      profileId: formData.value.profileId,
+      ruleType: formData.value.ruleType,
+      RuleStatusButton: formData.value.RuleStatusButton,
+      rule: null,
+      templateName: formData.value.templateName,
+    }
+    if (formData.value.useTmpl) {
+      body.templateId = formData.value.template.id
+    } else {
+      body.rule = formData.value.rule
+      delete body.rule['modifier_name']
+      delete body.rule.id
+    }
+    await api.saveCampaignRule(body)
+  }
+
+  watch(
+    () => props.RuleStatusButton,
+    () => {
+      formData.value.RuleStatusButton = props.RuleStatusButton
+    },
+    { deep: true }
+  )
+
+  onMounted(async () => {
+    await initData()
+  })
+
+  return { formData, submitFormData }
+}

+ 69 - 0
src/views/components/auto/auto-campaigns/neg-keyword.vue

@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <div class="asj-h2">添加否定词</div>
+    <SelectTmpl :data="formData" />
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef" :disabled="formData.useTmpl">
+      <SearchTermAdd :rule="formData.rule" ref="searchTermAddRef" :disabled="formData.useTmpl"/>
+      <el-form-item prop="rule.action.matchType" required :rules="{ required: true, message: '请至少选择一种匹配方式', trigger: 'change' }">
+        <template #label>
+          <span class="asj-h3">匹配方式</span>
+        </template>
+        <el-checkbox-group v-model="formData.rule.action.matchType">
+          <el-checkbox label="EXECT">精准</el-checkbox>
+          <el-checkbox label="PHRASE">词组</el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
+    </el-form>
+    <conditionBuilder :data="formData.rule.conditions" ref="condiBuilderRef" :disabled="formData.useTmpl" />
+    <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl"/>
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import SelectTmpl from './select-tmpl.vue'
+import SearchTermAdd from '../search-term-add.vue'
+import FreqSetting from '../freq-setting.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import XEUtils from 'xe-utils'
+import { userFormData } from './common'
+
+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 searchTermAddRef = ref()
+const condiBuilderRef = ref()
+const { formData, submitFormData } = userFormData(props)
+
+const submitForm = async () => {
+  const valid2 = await searchTermAddRef.value.validate()
+  const valid3 = !XEUtils.includes(await condiBuilderRef.value.validate(), false)
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2 && valid3) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败:', [valid, valid2, valid3])
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+</script>
+
+<style scoped></style>

+ 64 - 0
src/views/components/auto/auto-campaigns/search-term.vue

@@ -0,0 +1,64 @@
+<template>
+  <div>
+    <div class="asj-h2">添加搜索词</div>
+    <SelectTmpl :data="formData" />
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef" :disabled="formData.useTmpl">
+      <SearchTermAdd :rule="formData.rule" :disabled="formData.useTmpl" ref="searchTermAddRef"/>
+      <SearchTermBid :action="formData.rule.action" ref="searchTermBidRef" />
+    </el-form>
+    <conditionBuilder :data="formData.rule.conditions" ref="condiBuilderRef" :disabled="formData.useTmpl" />
+    <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl" />
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import SearchTermAdd from '../search-term-add.vue'
+import SearchTermBid from '../search-term-bid.vue'
+import SelectTmpl from './select-tmpl.vue'
+import FreqSetting from '../freq-setting.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import { userFormData } from './common'
+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 searchTermAddRef = ref()
+const searchTermBidRef = ref()
+const condiBuilderRef = ref()
+const { formData, submitFormData } = userFormData(props)
+
+const submitForm = async () => {
+  const valid2 = await searchTermAddRef.value.validate()
+  const valid3 = searchTermBidRef.value.valid
+  const valid4 = !XEUtils.includes(await condiBuilderRef.value.validate(), false)
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2 && valid3 && valid4) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败:', [valid, valid2, valid3, valid4])
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+</script>
+
+<style scoped></style>

+ 82 - 0
src/views/components/auto/auto-campaigns/select-tmpl.vue

@@ -0,0 +1,82 @@
+<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>
+import { ref, onMounted, watch } from 'vue'
+import * as api from './api'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  data: AutoCampaignRule
+}
+const props = defineProps<Props>()
+const ruleUsage = ref('custom')
+const tmplId = ref(0)
+if (props.data.template) {
+  tmplId.value = props.data.template.id
+}
+
+const tmplList = ref([])
+// const emits = defineEmits(['change-tmpl'])
+
+const getTmplList = async () => {
+  const resp = await api.getTemplateList({
+    page: 1,
+    pageSize: 999,
+    type: props.data.ruleType,
+  })
+  tmplList.value = resp.data
+}
+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
+  // emits('change-tmpl', tmpl)
+}
+const changeRuleUsage = () => {
+  props.data.useTmpl = ruleUsage.value === 'tmpl'
+  if (props.data.useTmpl) {
+    if (!tmplId.value && tmplList.value.length > 0) {
+      tmplId.value = tmplList.value[0].id
+    }
+    changeTmpl()
+  }
+}
+
+onMounted(async () => {
+  await getTmplList()
+})
+
+watch(
+  () => props.data.useTmpl,
+  () => {
+    if (props.data.useTmpl) {
+      ruleUsage.value = 'tmpl'
+      tmplId.value = props.data.template.id
+    } else {
+      ruleUsage.value = 'custom'
+    }
+  }
+)
+
+defineExpose({ getTmplList })
+</script>
+
+<style scoped></style>

+ 102 - 0
src/views/components/auto/auto-campaigns/switch-campaign.vue

@@ -0,0 +1,102 @@
+<template>
+  <div>
+    <div class="asj-h2">广告活动</div>
+    <SelectTmpl :data="formData" />
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef" :disabled="formData.useTmpl">
+      <div class="asj-h3">操作</div>
+      <el-checkbox-group v-model="formData.rule.action.state" :disabled="formData.useTmpl">
+        <div>
+          <el-checkbox label="enabled">开始</el-checkbox>
+          <div v-show="formData.rule.action.state.includes('enabled')" style="display: block; margin: 10px 0">
+            <el-form-item
+              prop="rule.action.setTime"
+              :rules="[{ required: formData.rule.action.state.includes('enabled'), message: '必填项', trigger: 'blur' }]">
+              <el-date-picker
+                v-model="formData.rule.action.setTime"
+                type="datetime"
+                format="YYYY-MM-DD HH:mm"
+                time-format="HH:mm"
+                value-format="YYYY-MM-DD HH:mm" />
+            </el-form-item>
+          </div>
+        </div>
+        <el-checkbox label="paused" @click="addConditions">暂停</el-checkbox>
+      </el-checkbox-group>
+    </el-form>
+    <div v-show="formData.rule.action.state.includes('paused')">
+      <conditionBuilder :data="formData.rule.conditions" :candidate-fields="condidateFields" ref="condiBuilderRef" :disabled="formData.useTmpl" />
+    </div>
+
+    <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl"/>
+
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, Ref } from 'vue'
+import { WeekMap } from '../enum'
+import SelectTmpl from './select-tmpl.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import { userFormData } from './common'
+import FreqSetting from '../freq-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 condiBuilderRef = ref()
+const { formData, submitFormData } = userFormData(props)
+formData.value.rule.action.state = []
+formData.value.rule.action.setTime = ''
+
+const condidateFields = [
+  { label: '曝光量', value: 'impressions' },
+  { label: '点击量', value: 'clicks' },
+  { label: '转化率', value: 'cr', suffix: '%' },
+  { label: '单次点击费用', value: 'cpc', prefix: '$' },
+]
+const addConditions = () => {
+  if (formData.value.rule.conditions.length === 0) {
+    condiBuilderRef.value.addConditionGroup()
+  }
+}
+
+const validateConditionsForm = async () => {
+  const validList = await condiBuilderRef.value.validate()
+  if (validList.includes(false)) {
+    return false
+  }
+  return true
+}
+const submitForm = async () => {
+  const valid2 = await validateConditionsForm()
+  formRef.value.validate(async (valid: any) => {
+    if (valid && valid2) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+
+const cancel = () => {
+  emits('refresh')
+}
+
+</script>
+
+<style scoped></style>

+ 62 - 0
src/views/components/auto/auto-campaigns/target-rule.vue

@@ -0,0 +1,62 @@
+<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>
+        <template #label>
+          <span class="asj-h3">生效对象</span>
+        </template>
+        <TargetSelect mode="auto" :data="formData.rule" :useTmpl="formData.useTmpl"></TargetSelect>
+      </el-form-item>
+    </el-form>
+    <TargetRuleSetting :rule="formData.rule" ref="ruleSettingRef" :disabled="formData.useTmpl"/>
+    <FreqSetting :rule="formData.rule" :disabled="formData.useTmpl" />
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</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 }
+}
+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>

+ 68 - 0
src/views/components/auto/auto-campaigns/timer-bid.vue

@@ -0,0 +1,68 @@
+<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>
+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'
+
+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 { formData, submitFormData } = userFormData(props)
+
+const checkConditions = (rule: any, value: any, callback: any) => {
+  for (const bidList of formData.value.rule.conditions) {
+    for (const val of bidList) {
+      if (val > 0) {
+        return callback()
+      }
+    }
+  }
+  callback(new Error('请先设置竞价!'))
+}
+const submit = async () => {
+  formRef.value.validate(async (valid: any) => {
+    if (valid) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+
+const cancel = async () => {
+  emits('refresh')
+}
+
+</script>
+
+<style scoped></style>

+ 69 - 0
src/views/components/auto/auto-campaigns/timer-budget.vue

@@ -0,0 +1,69 @@
+<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>
+        <TimerBudgetTable :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="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, Ref } from 'vue'
+import SelectTmpl from './select-tmpl.vue'
+import TimerBudgetTable from '/@/components/TimerBudgetTable/index.vue'
+import XEUtils from 'xe-utils'
+import { userFormData } from './common'
+
+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 { formData, submitFormData } = userFormData(props)
+
+const checkConditions = (rule: any, value: any, callback: any) => {
+  for (const bidList of formData.value.rule.conditions) {
+    for (const info of bidList) {
+      if (info.value && XEUtils.toNumber(info.value) > 0) {
+        return callback()
+      }
+    }
+  }
+  callback(new Error('请先设置预算!'))
+}
+
+const submitForm = async () => {
+  formRef.value.validate(async (valid: any) => {
+    if (valid) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+
+</script>
+
+<style scoped></style>

+ 84 - 0
src/views/components/auto/auto-templates/neg-keyword.vue

@@ -0,0 +1,84 @@
+<template>
+  <div>
+    <div class="asj-h2">添加否定词</div>
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef" :disabled="false">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
+        <template #label>
+          <span class="asj-h3">模板名称</span>
+        </template>
+        <el-input v-model="formData.name" style="width: 30%"></el-input>
+      </el-form-item>
+
+      <SearchTermAdd :rule="formData.rule" ref="searchTermAddRef"/>
+
+      <el-form-item prop="rule.action.matchType" required :rules="{ required: true, message: '请至少选择一种匹配方式', trigger: 'change' }">
+        <template #label>
+          <span class="asj-h3">匹配方式</span>
+        </template>
+        <el-checkbox-group v-model="formData.rule.action.matchType">
+          <el-checkbox label="EXECT">精准</el-checkbox>
+          <el-checkbox label="PHRASE">词组</el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
+    </el-form>
+    <conditionBuilder :data="formData.rule.conditions" ref="condiBuilderRef" />
+    <FreqSetting :rule="formData.rule" />
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import SearchTermAdd from '../search-term-add.vue'
+import FreqSetting from '../freq-setting.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  mode: string
+  data: AutoTemplate
+  submitFormData: Function
+  candidateFields: any[]
+}
+const props = withDefaults(defineProps<Props>(), {
+  candidateFields: () => {
+    return [
+      { label: '曝光量', value: 'impressions' },
+      { label: '点击量', value: 'clicks' },
+      { label: '转化率', value: 'cr', suffix: '%' },
+      { label: '单次点击费用', value: 'cpc', prefix: '$' },
+      { label: '关键词', value: 'keyword', type: 'array', options: [{ label: '精确匹配', value: 'exect' }] },
+      { label: '否定词', value: 'neg_keyword', type: 'array' },
+    ]
+  },
+})
+const emits = defineEmits(['refresh'])
+const formRef = ref()
+const searchTermAddRef = ref()
+const condiBuilderRef = ref()
+const formData = ref(props.data)
+if (!formData.value.rule.action.matchType) {
+  formData.value.rule.action.matchType = []
+}
+
+const submitForm = async () => {
+  const valid2 = await searchTermAddRef.value.validate()
+  const valid3 = !XEUtils.includes(await condiBuilderRef.value.validate(), false)
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2 && valid3) {
+      await props.submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败:', [valid, valid2, valid3])
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+</script>
+
+<style scoped></style>

+ 74 - 0
src/views/components/auto/auto-templates/search-term.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+    <div class="asj-h2">添加搜索词</div>
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef" :disabled="false">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
+        <template #label>
+          <span class="asj-h3">模板名称</span>
+        </template>
+        <el-input v-model="formData.name" style="width: 30%"></el-input>
+      </el-form-item>
+
+      <SearchTermAdd :rule="formData.rule" ref="searchTermAddRef" />
+      <SearchTermBid :action="formData.rule.action" ref="searchTermBidRef" />
+    </el-form>
+    <conditionBuilder :data="formData.rule.conditions" ref="condiBuilderRef" />
+    <FreqSetting :rule="formData.rule" />
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import SearchTermAdd from '../search-term-add.vue'
+import FreqSetting from '../freq-setting.vue'
+import SearchTermBid from '../search-term-bid.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  mode: string
+  data: AutoTemplate
+  submitFormData: Function
+  candidateFields: any[]
+}
+const props = withDefaults(defineProps<Props>(), {
+  candidateFields: () => {
+    return [
+      { label: '曝光量', value: 'impressions' },
+      { label: '点击量', value: 'clicks' },
+      { label: '转化率', value: 'cr', suffix: '%' },
+      { label: '单次点击费用', value: 'cpc', prefix: '$' },
+      { label: '关键词', value: 'keyword', type: 'array', options: [{ label: '精确匹配', value: 'exect' }] },
+      { label: '否定词', value: 'neg_keyword', type: 'array' },
+    ]
+  },
+})
+const emits = defineEmits(['refresh'])
+const formRef = ref()
+const condiBuilderRef = ref()
+const searchTermBidRef = ref()
+const searchTermAddRef = ref()
+const formData = ref(props.data)
+
+const submitForm = async () => {
+  const valid3 = await searchTermAddRef.value.validate()
+  const valid2 = !XEUtils.includes(await condiBuilderRef.value.validate(), false)
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2 && valid3 && searchTermBidRef.value.valid) {
+      await props.submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败:', [valid, valid2, valid3, searchTermBidRef.value.valid])
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+</script>
+
+<style scoped></style>

+ 12 - 22
src/components/auto-templates/campaign.vue → src/views/components/auto/auto-templates/switch-campaign.vue

@@ -2,7 +2,7 @@
   <div>
     <div class="asj-h2">广告活动</div>
     <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
-      <el-form-item prop="name" v-if="mode !== 'auto'" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
         <template #label>
           <span class="asj-h3">模板名称</span>
         </template>
@@ -30,7 +30,6 @@
       </el-checkbox-group>
     </el-form>
     <div v-show="formData.rule.action.state.includes('paused')">
-      <span class="asj-h3">条件</span>
       <conditionBuilder :data="formData.rule.conditions" :candidate-fields="condidateFields" ref="condiBuilderRef" />
     </div>
   </div>
@@ -44,28 +43,29 @@
     <el-button style="width: 200px" @click="cancel">取消</el-button>
     <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
   </div>
+  <p>{{ formData }}</p>
 </template>
 
 <script lang="ts" setup>
 import { ref, onMounted } from 'vue'
-import * as api from './api'
-import { useFromData } from './common'
-import { WeekMap } from './enum'
+import { WeekMap } from '../enum'
 import conditionBuilder from '/@/components/conditionBuilder/index.vue'
 import XEUtils from 'xe-utils'
 
 interface Props {
   mode: string
-  data: { [key: string]: any }
+  data: AutoTemplate
+  submitFormData: Function
 }
 const props = defineProps<Props>()
 const emits = defineEmits(['refresh'])
 const formRef = ref()
 const condiBuilderRef = ref()
-
-const { formData, setFormData, submitFormData } = useFromData(props)
-formData.value.rule.action.state = []
-formData.value.rule.action.setTime = ''
+const formData = ref(props.data)
+if (props.mode === 'add') {
+  formData.value.rule.action.state = []
+  formData.value.rule.action.setTime = ''
+}
 
 const addConditions = () => {
   if (props.mode === 'add' && formData.value.rule.conditions.length === 0) {
@@ -90,7 +90,7 @@ const submitForm = async () => {
   const valid2 = await validateConditionsForm()
   formRef.value.validate(async (valid: any) => {
     if (valid && valid2) {
-      await submitFormData()
+      await props.submitFormData()
       emits('refresh')
     } else {
       console.log('验证失败')
@@ -98,18 +98,8 @@ const submitForm = async () => {
   })
 }
 const cancel = () => {
-  if (props.mode !== 'auto') {
-    emits('refresh')
-  }
+  emits('refresh')
 }
-
-const initData = async () => {
-  setFormData()
-}
-
-onMounted(async () => {
-  await initData()
-})
 </script>
 
 <style scoped></style>

+ 63 - 0
src/views/components/auto/auto-templates/target-rule.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <div class="asj-h2">定向规则</div>
+    <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
+        <template #label>
+          <span class="asj-h3">模板名称</span>
+        </template>
+        <el-input v-model="formData.name" style="width: 30%"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <template #label>
+          <span class="asj-h3">生效对象</span>
+        </template>
+        <TargetSelect :mode="mode" :data="formData.rule"></TargetSelect>
+      </el-form-item>
+    </el-form>
+
+    <TargetRuleSetting :rule="formData.rule" ref="ruleSettingRef" />
+    <FreqSetting :rule="formData.rule" />
+
+    <div class="auto-page-foot">
+      <el-button style="width: 200px" @click="cancel">取消</el-button>
+      <el-button style="width: 200px" type="primary" @click="submitForm">提交</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import TargetSelect from '../target-select.vue'
+import TargetRuleSetting from '../target-rule-setting.vue'
+import FreqSetting from '../freq-setting.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  mode: string
+  data: AutoTemplate
+  submitFormData: Function
+}
+const props = defineProps<Props>()
+const emits = defineEmits(['refresh'])
+const formRef = ref()
+const formData = ref(props.data)
+const ruleSettingRef = ref()
+
+const submitForm = async () => {
+  const valid2 = await ruleSettingRef.value.validateForm()
+  formRef.value.validate(async (valid: boolean) => {
+    if (valid && valid2) {
+      await props.submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败:', [valid, valid2])
+    }
+  })
+}
+const cancel = () => {
+  emits('refresh')
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 17 - 30
src/components/auto-templates/timer-bid.vue → src/views/components/auto/auto-templates/timer-bid.vue

@@ -2,13 +2,12 @@
   <div>
     <div class="asj-h2">分时调价</div>
     <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
-      <el-form-item prop="name" required v-if="mode !== 'auto'">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
         <template #label>
           <span class="asj-h3">模板名称</span>
         </template>
         <el-input v-model="formData.name" style="width: 30%"></el-input>
       </el-form-item>
-
       <el-form-item prop="rule.conditions" :rules="[{ validator: checkConditions, trigger: 'xxx' }]">
         <template #label>
           <span class="asj-h3">设置竞价</span>
@@ -25,31 +24,34 @@
 
 <script lang="ts" setup>
 import { ref, onMounted, Ref } from 'vue'
-import * as api from './api'
 import TimerBidTable from '/@/components/TimerBidTable/index.vue'
-import { useFromData } from './common'
 
 interface Props {
-  mode: string,
-  data: { [key: string]: any }
+  mode: string
+  data: AutoTemplate
+  submitFormData: Function
 }
 const props = defineProps<Props>()
 const emits = defineEmits(['refresh'])
 
 const formRef = ref()
-const { formData, setFormData, submitFormData } = useFromData(props)
-for (let i = 0; i < 7; i++) {
-  const tmp = []
-  for (let j = 0; j < 24; j++) {
-    tmp.push(0)
+const formData = ref(props.data)
+if (props.mode === 'add') {
+  for (let i = 0; i < 7; i++) {
+    const tmp = []
+    for (let j = 0; j < 24; j++) {
+      tmp.push(0)
+    }
+    formData.value.rule.conditions.push(tmp)
   }
-  formData.value.rule.conditions.push(tmp)
 }
 
 const checkConditions = (rule: any, value: any, callback: any) => {
   for (const bidList of formData.value.rule.conditions) {
     for (const val of bidList) {
-      if (val > 0) { return callback() }
+      if (val > 0) {
+        return callback()
+      }
     }
   }
   callback(new Error('请先设置竞价!'))
@@ -57,31 +59,16 @@ const checkConditions = (rule: any, value: any, callback: any) => {
 const submitForm = async () => {
   formRef.value.validate(async (valid: any) => {
     if (valid) {
-      await submitFormData()
+      await props.submitFormData()
       emits('refresh')
     } else {
       console.log('验证失败')
     }
   })
 }
-
 const cancel = () => {
-  if (props.mode !== 'auto') {
-    emits('refresh')
-  } else {
-    setFormData()
-  }
+  emits('refresh')
 }
-
-const initData = () => {
-  if (props.mode === 'edit') {
-    setFormData()
-  }
-}
-
-onMounted(() => {
-  initData()
-})
 </script>
 
 <style scoped></style>

+ 14 - 29
src/components/auto-templates/timer-budget.vue → src/views/components/auto/auto-templates/timer-budget.vue

@@ -2,7 +2,7 @@
   <div>
     <div class="asj-h2">分时预算</div>
     <el-form :model="formData" label-position="top" style="margin-top: 20px" ref="formRef">
-      <el-form-item prop="name" required v-if="mode !== 'auto'">
+      <el-form-item prop="name" :rules="{ required: true, message: '必填项', trigger: 'blur' }">
         <template #label>
           <span class="asj-h3">模板名称</span>
         </template>
@@ -26,27 +26,26 @@
 
 <script lang="ts" setup>
 import { ref, onMounted } from 'vue'
-import * as api from './api'
 import TimerBudgetTable from '/@/components/TimerBudgetTable/index.vue'
-import { useFromData } from './common'
 import XEUtils from 'xe-utils'
 
 interface Props {
   mode: string
-  data: { [key: string]: any }
+  data: AutoTemplate
+  submitFormData: Function
 }
 const props = defineProps<Props>()
 const emits = defineEmits(['refresh'])
-
 const formRef = ref()
-const { formData, setFormData, submitFormData } = useFromData(props)
-
-for (let i = 0; i < 7; i++) {
-  const tmp = []
-  for (let j = 0; j < 24; j++) {
-    tmp.push({ type: '', value: '' })
+const formData = ref(props.data)
+if (props.mode === 'add') {
+  for (let i = 0; i < 7; i++) {
+    const tmp = []
+    for (let j = 0; j < 24; j++) {
+      tmp.push({ type: '', value: '' })
+    }
+    formData.value.rule.conditions.push(tmp)
   }
-  formData.value.rule.conditions.push(tmp)
 }
 
 const checkConditions = (rule: any, value: any, callback: any) => {
@@ -61,8 +60,8 @@ const checkConditions = (rule: any, value: any, callback: any) => {
 }
 const submitForm = async () => {
   formRef.value.validate(async (valid: any) => {
-    if(valid) {
-      await submitFormData()
+    if (valid) {
+      await props.submitFormData()
       emits('refresh')
     } else {
       console.log('验证失败')
@@ -70,22 +69,8 @@ const submitForm = async () => {
   })
 }
 const cancel = () => {
-  if (props.mode !== 'auto') {
-    emits('refresh')
-  } else {
-    setFormData()
-  }
+  emits('refresh')
 }
-
-const initData = async() => {
-  if (props.mode === 'edit') {
-    setFormData()
-  }
-}
-
-onMounted(async () => {
-  await initData()
-})
 </script>
 
 <style scoped></style>

+ 0 - 0
src/components/auto-templates/enum.js → src/views/components/auto/enum.js


+ 19 - 0
src/views/components/auto/freq-setting.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="asj-h3">频率设置</div>
+  <el-checkbox-group v-model="rule.weekdays" :disabled="disabled">
+    <el-checkbox v-for="i in [1, 2, 3, 4, 5, 6, 0]" :label="i" :key="i">{{ WeekMap[i] }}</el-checkbox>
+  </el-checkbox-group>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { WeekMap } from './enum'
+
+interface Props {
+  rule: AutoRule,
+  disabled?: boolean
+}
+const props = defineProps<Props>()
+</script>
+
+<style scoped></style>

+ 88 - 0
src/views/components/auto/search-term-add.vue

@@ -0,0 +1,88 @@
+<template>
+  <div>
+    <div class="asj-h3">添加到</div>
+    <el-radio-group style="display: flex; flex-direction: column; align-items: flex-start; margin-bottom: 20px" v-model="rule.activeModel">
+      <el-radio label="all">当前广告活动(所有广告组)</el-radio>
+      <el-radio label="current">当前广告活动(当前广告组)</el-radio>
+      <el-radio label="specified">指定广告活动的广告组</el-radio>
+      <div style="margin-left: 23px" v-if="rule.activeModel === 'specified'">
+        <el-form :inline="true" :model="rule" :disabled="disabled" ref="formRef">
+          <el-form-item
+            v-for="(info, index) in rule.campaignAd"
+            style="margin: 10px auto"
+            :prop="`campaignAd[${index}].adGroupId`"
+            :rules="{ required: true, message: '广告组必填' }">
+            <div style="display: flex; align-items: center; gap: 10px">
+              <el-select v-model="info.campaignType" style="width: 100px" @change="changeCampaignType(info)">
+                <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>
+              <CampaignSelect
+                v-model="info.campaignId"
+                :query="{ profileId: '3006125408623189', campaignType: info.campaignType }"
+                @change="info.adGroupId = ''"
+                width="450px"></CampaignSelect>
+              <AdGroupSelect
+                v-model="info.adGroupId"
+                width="450px"
+                :query="{ profileId: '3006125408623189', campaignType: info.campaignType, campaignId: info.campaignId }">
+              </AdGroupSelect>
+              <el-button link :icon="Delete" type="danger" @click="delCampaignAd(index)" v-show="rule.campaignAd.length > 1"></el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+        <el-button link :icon="Plus" type="primary" @click="addCampaignAd">添加</el-button>
+      </div>
+    </el-radio-group>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { Plus, Delete } from '@element-plus/icons-vue'
+import CampaignSelect from '/@/views/components/campaign-select/index.vue'
+import AdGroupSelect from '/@/views/components/ad-group-select/index.vue'
+
+interface Props {
+  rule: AutoRule
+  disabled?: boolean
+}
+
+const props = defineProps<Props>()
+const formRef = ref()
+
+const addCampaignAd = () => {
+  props.rule.campaignAd.push({
+    campaignType: 'sp',
+    campaignId: '',
+    adGroupId: '',
+  })
+}
+if (props.rule.campaignAd.length === 0) {
+  addCampaignAd()
+}
+if (!props.rule.activeModel) {
+  props.rule.activeModel = 'all'
+}
+const delCampaignAd = (index: number) => {
+  props.rule.campaignAd.splice(index, 1)
+}
+
+const changeCampaignType = async (info: any) => {
+  info.campaignId = ''
+  info.adGroupId = ''
+}
+
+const validate = async () => {
+  let ret = true
+  if(!formRef.value) return ret
+  await formRef.value.validate(async (valid: boolean) => {
+    ret = valid
+  })
+  return ret
+}
+defineExpose({ validate })
+</script>
+
+<style scoped></style>

+ 66 - 0
src/views/components/auto/search-term-bid.vue

@@ -0,0 +1,66 @@
+<template>
+  <div>
+    <div class="asj-h3">设置竞价</div>
+    <div>关键词竞价:</div>
+    <div v-for="(info, index) in action.keywords" class="bid">
+      <el-checkbox v-model="info.use" :label="info.matchType">{{ MatchTypeMap[info.matchType] }}</el-checkbox>
+      <STBidInput :bid-data="info" :index="index" v-show="info.use"></STBidInput>
+    </div>
+    <div>商品定向竞价:</div>
+    <div class="bid target">
+      <el-checkbox label="target" v-model="action.target.use">竞价</el-checkbox>
+      <STBidInput :bid-data="action.target" v-show="action.target.use"></STBidInput>
+    </div>
+    <p v-show="!valid" style="font-size: small; color: #f56c6c">请至少设置一种竞价方式!</p>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onBeforeMount, Ref, computed } from 'vue'
+import STBidInput from './st-bid-input.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  action: RuleSearchTermAction
+}
+const props = defineProps<Props>()
+const MatchTypeMap = {
+  EXACT: '精准匹配',
+  BROAD: '广泛匹配',
+  PHRASE: '词组匹配',
+}
+if (!props.action.keywords) {
+  props.action.keywords = [
+    { bidType: '1', type: 'increase', numType: '1', defaultBidding: '1.00', min: '', max: '', matchType: 'EXACT', use: false },
+    { bidType: '1', type: 'increase', numType: '1', defaultBidding: '1.00', min: '', max: '', matchType: 'BROAD', use: false },
+    { bidType: '1', type: 'increase', numType: '1', defaultBidding: '1.00', min: '', max: '', matchType: 'PHRASE', use: false },
+  ]
+}
+if (!props.action.target) {
+  props.action.target = { bidType: '1', type: 'increase', numType: '1', defaultBidding: '1', min: '', max: '', matchType: 'EXACT', use: false }
+}
+
+const valid = computed(() => {
+  const index = XEUtils.findIndexOf(props.action.keywords, (item) => item.use)
+  if (index !== -1) return true
+  return props.action.target.use
+})
+
+defineExpose({ valid })
+</script>
+
+<style scoped>
+.bid {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 20px;
+  /* margin-bottom: 10px; */
+  margin-left: 23px;
+  height: 50px;
+}
+.target {
+  gap: 49px;
+}
+</style>

+ 96 - 0
src/views/components/auto/st-bid-input.vue

@@ -0,0 +1,96 @@
+<template>
+  <div class="st-bid-input-container">
+    <el-form-item>
+      <el-select v-model="bidData.bidType" style="width: 150px">
+        <el-option v-for="item in bidTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-select v-model="bidData.type" v-show="bidData.bidType !== '1'" style="width: 100px">
+        <el-option label="提高竞价" value="increase"></el-option>
+        <el-option label="降低竞价" value="decrease"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-select v-model="bidData.numType" v-show="bidData.bidType !== '1'" style="width: 100px">
+        <el-option label="数值" value="1"></el-option>
+        <el-option label="百分比" value="2"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      style="width: 150px"
+      :prop="index !== undefined ? `rule.action.keywords[${index}].defaultBidding` : `rule.action.target.defaultBidding`"
+      :rules="{ validator: checkFloat, trigger: 'blur' }">
+      <InputFloat
+        v-model="bidData.defaultBidding"
+        :prefix="bidData.numType === '1' || bidData.bidType === '1' ? '$' : ''"
+        :suffix="bidData.numType === '2' && bidData.bidType !== '1' ? '%' : ''">
+      </InputFloat>
+    </el-form-item>
+    <div style="display: flex; justify-content: flex-start; gap: 5px" v-show="bidData.bidType !== '1'">
+      <div style="margin-top: 7px">出价范围:</div>
+      <el-form-item
+        :prop="index !== undefined ? `rule.action.keywords[${index}].min` : `rule.action.target.min`"
+        :rules="{ validator: checkMin, trigger: 'blur' }">
+        <InputFloat v-model="bidData.min" prefix="$" style="width: 150px"></InputFloat>
+      </el-form-item>
+    </div>
+    <div style="display: flex; justify-content: flex-start; gap: 10px" v-show="bidData.bidType !== '1'">
+      <div style="margin-top: 7px">~</div>
+      <el-form-item
+        :prop="index !== undefined ? `rule.action.keywords[${index}].max` : `rule.action.target.max`"
+        :rules="{ validator: checkMax, trigger: 'blur' }">
+        <InputFloat v-model="bidData.max" prefix="$" style="width: 150px"></InputFloat>
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import InputFloat from '/@/components/input-float/index.vue'
+import XEUtils from 'xe-utils';
+
+interface Props {
+  bidData: SearchTermBid
+  index?: number
+}
+const props = defineProps<Props>()
+const bidTypeList = [
+  { label: '设置竞价', value: '1' },
+  { label: '基于7天CPC', value: '2' },
+  { label: '基于14天CPC', value: '3' },
+  { label: '基于30天CPC', value: '4' },
+]
+
+const checkFloat = (rule: any, value: string, callback: any) => {
+  if (XEUtils.toNumber(value) <= 0) {
+    callback(new Error('请输入大于0的数值!'))
+  }
+  callback()
+}
+
+const checkMin = (rule: any, value: string, callback: any) => {
+  if(value === '' || props.bidData.max === '') return callback()
+  if (XEUtils.toNumber(value) >= XEUtils.toNumber(props.bidData.max)) {
+    callback(new Error('最小值必须小于最大值!'))
+  }
+  callback()
+}
+const checkMax = (rule: any, value: string, callback: any) => {
+  if(value === '' || props.bidData.min === '') return callback()
+  if (XEUtils.toNumber(value) <= XEUtils.toNumber(props.bidData.min)) {
+    callback(new Error('最大值必须大于最小值!'))
+  }
+  callback()
+}
+</script>
+
+<style scoped>
+.st-bid-input-container {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding-top: 15px;
+}
+</style>

+ 241 - 0
src/views/components/auto/target-rule-setting.vue

@@ -0,0 +1,241 @@
+<template>
+  <div>
+    <div class="asj-h3">规则设置</div>
+    <div class="rule-setting-container">
+      <div v-for="(info, index) in rule.conditions" class="rule-setting-item" :key="info.actionType + info.ordering">
+        <div class="rule-setting-title">
+          <div class="asj-h3">{{ RuleNameMap[info.actionType] }} - 规则{{ info.ordering }}</div>
+          <div class="rule-setting-opration">
+            <el-tooltip content="添加规则" placement="top" effect="dark" v-if="!disabled">
+              <el-icon @click="addRule(info.actionType, index)" :disabled="disabled"><Plus /></el-icon>
+            </el-tooltip>
+            <el-tooltip content="删除规则" placement="top" effect="dark" v-if="rule.conditions.length !== 1 && !disabled">
+              <el-icon @click="delRule(index)"><DeleteFilled /></el-icon>
+            </el-tooltip>
+            <el-tooltip content="收起/展开规则" placement="top" effect="dark">
+              <el-icon @click="info.show = !info.show"><ArrowDown /></el-icon>
+            </el-tooltip>
+          </div>
+        </div>
+        <div v-show="info.show">
+          <el-form :inline="true" :model="info.action" ref="ruleFormRef" :disabled="disabled">
+            <div v-if="info.actionType === 'increase' || info.actionType === 'decrease'">
+              <el-form-item>
+                <el-select v-model="actionModel" @change="changeAction(info.action)" style="width: 250px">
+                  <el-option
+                    v-for="item in ActionList"
+                    :key="item.value"
+                    :value="item.value"
+                    :label="item.label.replace('_', info.actionType === 'increase' ? '升高' : '降低')">
+                  </el-option>
+                </el-select>
+              </el-form-item>
+              <el-form-item prop="set" :rules="{ validator: checkFloat }">
+                <InputFloat
+                  v-model="info.action.set"
+                  :prefix="info.action.numType === 'num' ? '$' : ''"
+                  :suffix="info.action.numType === 'ratio' ? '%' : ''"
+                  style="width: 120px">
+                </InputFloat>
+              </el-form-item>
+              <el-form-item label="最大值" v-if="info.actionType === 'increase'" prop="max" :rules="{ validator: checkMinMax }">
+                <InputFloat v-model="info.action.max" style="width: 120px" prefix="$"></InputFloat>
+              </el-form-item>
+              <el-form-item label="最小值" v-else prop="min" :rules="{ validator: checkMinMax }">
+                <InputFloat v-model="info.action.min" style="width: 120px" prefix="$"></InputFloat>
+              </el-form-item>
+            </div>
+            <el-form-item v-if="info.actionType === 'set'" prop="set">
+              <el-input v-model="info.action.set"></el-input>
+            </el-form-item>
+          </el-form>
+          <conditionBuilder :data="info.conditions" :candidate-fields="candidateFields" ref="condiBuilderRef" :disabled="disabled" />
+        </div>
+      </div>
+      <el-popover placement="bottom-start" trigger="hover" :disabled="disabled">
+        <template #reference>
+          <el-button :icon="Plus" type="warning" :disabled="disabled">添加规则</el-button>
+        </template>
+        <div class="popver-content">
+          <span class="popver-content-item" v-for="[name, title] of Object.entries(RuleNameMap)" @click="addRule(name)">{{ title }}</span>
+        </div>
+      </el-popover>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import InputFloat from '/@/components/input-float/index.vue'
+import { useSymbolOptions } from '/@/components/conditionBuilder/utils'
+import { ActionList, RuleNameMap } from './enum'
+
+import XEUtils from 'xe-utils'
+
+interface Props {
+  rule: AutoRule
+  disabled?: boolean
+}
+const props = defineProps<Props>()
+const candidateFields = [
+  { label: '曝光量', value: 'impressions' },
+  { label: '点击量', value: 'clicks' },
+  { label: '转化率', value: 'cr', suffix: '%' },
+  { label: '单次点击费用', value: 'cpc', prefix: '$' },
+  { label: '关键词', value: 'keyword', type: 'array', options: [{ label: '精确匹配', value: 'exect' }] },
+  { label: '否定词', value: 'neg_keyword', type: 'array' },
+]
+const { getSymbolOptions } = useSymbolOptions(candidateFields)
+const ruleFormRef = ref()
+const condiBuilderRef = ref()
+const checkFloat = (rule: any, value: any, callback: any) => {
+  if (value === '0.00' || value === '') {
+    callback(new Error('请输入大于0的数值!'))
+  } else {
+    callback()
+  }
+}
+const checkMinMax = (rule: any, value: any, callback: any) => {
+  if (value === '0.00') {
+    callback(new Error('请输入大于0的数值!'))
+  } else {
+    callback()
+  }
+}
+const maxRuleNumber = computed(() => {
+  const ret = {
+    increase: 0,
+    decrease: 0,
+    set: 0,
+    pause: 0,
+  }
+  for (const info of props.rule.conditions) {
+    ret[info.actionType] = Math.max(ret[info.actionType] || 0, info.ordering)
+  }
+  return ret
+})
+const addRule = (actionType: string, index: number = -1) => {
+  const field = candidateFields[0].value
+  const symbol = getSymbolOptions(field)[0].value
+  const rule = {
+    action: { set: '', baseType: '', max: '', min: '', numType: 'ratio' },
+    actionType: actionType,
+    conditions: [
+      {
+        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: [],
+          },
+        ],
+      },
+    ],
+    day: '1',
+    exceptDay: '',
+    ordering: maxRuleNumber.value[actionType] + 1,
+    show: true,
+  }
+  if (index === -1) {
+    props.rule.conditions.push(rule)
+  } else {
+    props.rule.conditions.splice(index + 1, 0, rule)
+  }
+}
+if (props.rule.conditions.length === 0) {
+  props.rule.campaignType = 'sp'
+  props.rule.activeModel = 'campaign'
+  addRule('increase', -1)
+}
+
+const delRule = (index: number) => {
+  props.rule.conditions.splice(index, 1)
+}
+
+const actionModel = ref('bid-ratio')
+const changeAction = (action: any) => {
+  const [a, b] = actionModel.value.split('-')
+  action.baseType = a
+  action.numType = b
+  action.set = ''
+}
+
+const validateConditionsForm = async () => {
+  const ret = []
+  for (const f of condiBuilderRef.value) {
+    ret.push(await f.validate())
+  }
+  for (const validList of ret) {
+    if (validList.includes(false)) {
+      return false
+    }
+  }
+  return true
+}
+const validateRuleForm = async () => {
+  const ret = []
+  for (const f of ruleFormRef.value) {
+    await f.validate((valid: boolean) => {
+      ret.push(valid)
+    })
+  }
+  return !ret.includes(false)
+}
+
+const validateForm = async () => {
+  const valid1 = await validateRuleForm()
+  const valid2 = await validateConditionsForm()
+  return valid1 && valid2
+}
+
+defineExpose({ validateForm })
+</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;
+    }
+  }
+}
+
+.rule-setting-container {
+  .rule-setting-item {
+    border: 1px solid #e0e0e0;
+    margin: 10px auto;
+    padding: 0 12px;
+  }
+
+  .rule-setting-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .rule-setting-opration {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+
+      .el-icon {
+        cursor: pointer;
+      }
+    }
+  }
+}
+</style>

+ 52 - 0
src/views/components/auto/target-select.vue

@@ -0,0 +1,52 @@
+<template>
+  <div style="display: flex; flex-direction: column; align-items: center">
+    <div style="margin: 5px auto">
+      <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>
+          <el-select style="margin-left: 23px" v-show="mode === 'auto' && data.activeModel === 'adGroup'" v-model="data.campaignAd">
+            <el-option label="111"></el-option>
+          </el-select>
+        </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>
+import { ref, watch } from 'vue'
+
+interface Props {
+  mode: string,
+  data: AutoRule,
+  useTmpl?: boolean
+}
+const props = defineProps<Props>()
+</script>
+
+<style lang="scss" scoped>
+.target-radio-group {
+  display: flex;
+  justify-content: flex-start;
+  flex-direction: column;
+  .target-radio-group-item {
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+  }
+}
+</style>

+ 12 - 0
src/views/components/campaign-select/api.ts

@@ -0,0 +1,12 @@
+import { request } from '/@/utils/service'
+
+
+interface Params {[key: string]: any}
+
+export function getCampaignList(params: Params) {
+  return request({
+    url: '/api/ad_manage/ad_tree/campaign',
+    method: 'get',
+    params: params,
+  })
+}

+ 64 - 0
src/views/components/campaign-select/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <el-select
+    v-model="campaignId"
+    filterable
+    remote
+    :remote-method="searchCampaignName"
+    :style="{ width }"
+    clearable
+    @change="changeCampaign"
+    placeholder="请选择广告活动">
+    <el-option v-for="info in campaignList" :key="info.campaignId" :label="info.campaignName" :value="info.campaignId"></el-option>
+  </el-select>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch } from 'vue'
+import { getCampaignList } from './api'
+
+interface Props {
+  modelValue: string
+  query: {
+    profileId: string
+    campaignType: string
+  }
+  width: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  width: '',
+})
+const campaignId = ref(props.modelValue)
+const emits = defineEmits(['update:modelValue', 'change'])
+const campaignList = ref([])
+
+const setCampaignList = async (params: any) => {
+  const resp = await getCampaignList(params)
+  campaignList.value = resp.data
+}
+
+const searchCampaignName = async (campaignName: string) => {
+  if (!campaignName) return
+  await setCampaignList({ profileId: props.query.profileId, campaignType: props.query.campaignType, campaignName })
+}
+
+const changeCampaign = () => {
+  emits('update:modelValue', campaignId.value)
+  emits('change', campaignId.value)
+}
+
+
+watch(
+  () => props.query.campaignType,
+  async () => {
+    await setCampaignList(props.query)
+  },
+  { immediate: true }
+)
+watch(
+  () => props.modelValue,
+  () => { campaignId.value = props.modelValue }
+)
+</script>
+
+<style scoped></style>

+ 48 - 24
src/views/demo/index.vue

@@ -13,13 +13,17 @@
     <!-- <conditionBuilder :candidate-fields="condidateFields" :data="formData" ref="condiBuilder"></conditionBuilder>
     <p>{{ formData }}</p>
     <el-button @click="save">保存</el-button> -->
-    
+
     <!-- <TimerBid /> -->
     <!-- <RuleCampaign /> -->
     <!-- <RuleTarget :candidate-fields="condidateFields"/> -->
-    <RuleSearchTerm></RuleSearchTerm>
+    <!-- <RuleSearchTerm></RuleSearchTerm> -->
+    <!-- <TagsInput :data="tags"></TagsInput> -->
+    <!-- <CampaignSelect v-model="campaignId" :query="{ profileId: '3006125408623189', campaignType: 'sp' }"></CampaignSelect> -->
+    <!-- <AdGroupSelect v-model="campaignId" :query="{ profileId: '3006125408623189', campaignType: 'sp', campaignId: '25915710734770' }" width="500px" /> -->
+    <p>---{{ campaignId }}</p>
+    <!-- <STBidInput :bid-data="stBidData"></STBidInput> -->
   </div>
-
 </template>
 
 <script lang="ts" setup>
@@ -35,13 +39,31 @@ import RuleTimerBudget from '/@/components/auto-templates/timer-budget.vue'
 import RuleCampaign from '/@/components/auto-templates/campaign.vue'
 import RuleTarget from '/@/components/auto-templates/target-rule.vue'
 import RuleSearchTerm from '/@/components/auto-templates/search-term.vue'
+import TagsInput from '/@/components/tags-input/index.vue'
+import CampaignSelect from '/@/views/components/campaign-select/index.vue'
+import AdGroupSelect from '/@/views/components/ad-group-select/index.vue'
+import STBidInput from '/@/views/components/auto/st-bid-input.vue'
+
+const stBidData = ref({
+  bidType: '1',
+  type: 'increase',
+  defaultBidding: '1',
+  min: '',
+  max: '',
+  matchType: '',
+  numType: '1',
+})
+
+const campaignId = ref('')
+
+const tags = ref(['aaa'])
 
 const condiBuilder = ref()
 const condidateFields = [
-  { label: '曝光量', value: 'impressions'},
-  { label: '点击量', value: 'clicks'},
-  { label: '转化率', value: 'cr', suffix: '%'},
-  { label: '单次点击费用', value: 'cpc', prefix: '$'},
+  { label: '曝光量', value: 'impressions' },
+  { label: '点击量', value: 'clicks' },
+  { label: '转化率', value: 'cr', suffix: '%' },
+  { label: '单次点击费用', value: 'cpc', prefix: '$' },
 ]
 const formData = reactive([
   {
@@ -53,9 +75,9 @@ const formData = reactive([
         dataType: 'impressions',
         dayType: 'sum',
         symbol: 'gte',
-        num: '1000'
-      }
-    ]
+        num: '1000',
+      },
+    ],
   },
   {
     key: '2',
@@ -66,16 +88,16 @@ const formData = reactive([
         dataType: 'clicks',
         dayType: 'sum',
         symbol: 'lte',
-        num: '100'
-      }
-    ]
-  }
+        num: '100',
+      },
+    ],
+  },
 ])
 
 const validateForm = async () => {
   const validList = await condiBuilder.value.validate()
   // console.log(validList)
-  for(const items of validList) {
+  for (const items of validList) {
     if (items.includes(false)) {
       return false
     }
@@ -90,17 +112,17 @@ const save = async () => {
   }
 }
 
-const prefix = ref("")
-const suffix = ref("")
+const prefix = ref('')
+const suffix = ref('')
 const b = ref(false)
 const change = () => {
   b.value = !b.value
   if (b.value) {
-    prefix.value = "$"
-    suffix.value = ""
+    prefix.value = '$'
+    suffix.value = ''
   } else {
-    prefix.value = ""
-    suffix.value = "%"
+    prefix.value = ''
+    suffix.value = '%'
   }
   num.value += '1'
 }
@@ -118,7 +140,7 @@ const initData = () => {
     const tmp2 = []
     for (let j = 0; j < 24; j++) {
       tmp.push(1)
-      tmp2.push({type: '', value: ''})
+      tmp2.push({ type: '', value: '' })
     }
     bidData.value.push(tmp)
     budgetData.value.push(tmp2)
@@ -126,13 +148,15 @@ const initData = () => {
 }
 
 const reqData = () => {
-  setTimeout(() => { initData();console.log('----req') }, 2000)
+  setTimeout(() => {
+    initData()
+    console.log('----req')
+  }, 2000)
 }
 
 const refreshTimerBid = () => {
   console.log('refreshTimerBid')
 }
-
 </script>
 
 <style scoped>

+ 28 - 28
src/views/efTools/automation/api.ts

@@ -1,42 +1,42 @@
-import { request } from '/@/utils/service';
-import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
-import XEUtils from 'xe-utils';
+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/auto_plan_template/';
+export const apiPrefix = '/api/ad_manage/auto_plan_template/'
 export function GetList(query: UserPageQuery) {
-    return request({
-        url: apiPrefix,
-        method: 'get',
-        params: query,
-    })
+  return request({
+    url: apiPrefix,
+    method: 'get',
+    params: query,
+  })
 }
 export function GetObj(id: number) {
-    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 },
+  })
 }

+ 58 - 21
src/views/efTools/automation/index.vue

@@ -7,42 +7,50 @@
     </fs-crud>
   </fs-page>
 
-  <el-drawer v-model="showDrawer" :title="mode === 'add' ? '新建模板' : '编辑模板'" size="60%" :destroy-on-close="true" :close-on-click-modal="false">
+  <el-drawer v-model="showDrawer" :title="mode === 'add' ? '新建模板' : '编辑模板'" size="70%" :destroy-on-close="true" :close-on-click-modal="false">
     <div style="padding: 0 15px">
-      <component :is="dyComponents[ruleData.templateType]" :mode="mode" :data="ruleData" @refresh="refreshTable"></component>
+      <component
+        :is="dyComponents[formData.rule.type]"
+        :mode="mode"
+        :data="formData"
+        @refresh="refreshTable"
+        :submitFormData="submitFormData"></component>
     </div>
   </el-drawer>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, watch } from 'vue'
+import { onMounted, ref, Ref, watch } from 'vue'
 import { FsPage, useFs } from '@fast-crud/fast-crud'
-import { CirclePlusFilled } from '@element-plus/icons-vue'
 import { createCrudOptions } from './crud'
+import * as api from './api'
 import { TemplateType } from '../utils/enum'
 import SelectBotton from '/@/components/select-button/index.vue'
-import TimerBidTmpl from '/@/components/auto-templates/timer-bid.vue'
-import TimerBudgetTmpl from '/@/components/auto-templates/timer-budget.vue'
-import CampaignTmpl from '/@/components/auto-templates/campaign.vue'
-import TargetRuleTmpl from '/@/components/auto-templates/target-rule.vue'
+import TimerBidTmpl from '/@/views/components/auto/auto-templates/timer-bid.vue'
+import TimerBudgetTmpl from '/@/views/components/auto/auto-templates/timer-budget.vue'
+import SwitchCampaignTmpl from '/@/views/components/auto/auto-templates/switch-campaign.vue'
+import TargetRuleTmpl from '/@/views/components/auto/auto-templates/target-rule.vue'
+import SearchTermTmpl from '/@/views/components/auto/auto-templates/search-term.vue'
+import NegKeywordTmpl from '/@/views/components/auto/auto-templates/neg-keyword.vue'
 
 const dyComponents = {
   1: TimerBidTmpl,
   2: TimerBudgetTmpl,
-  3: CampaignTmpl,
-  4: TargetRuleTmpl
+  3: SwitchCampaignTmpl,
+  4: TargetRuleTmpl,
+  5: SearchTermTmpl,
+  6: NegKeywordTmpl
 }
 const mode = ref('')
 const showDrawer = ref(false)
-const ruleData = ref({
-  templateType: 0,
-  templateId: 0,
-  templateName: '',
+
+const formData: Ref<AutoTemplate> = ref({
+  name: '',
   rule: {
     type: 0,
-    campaignType: '', // sp|sb|sd
+    campaignType: '',
     campaignAd: [],
-    action: '',
+    action: {},
     activeModel: '',
     setTime: '',
     weekdays: [],
@@ -52,21 +60,50 @@ const ruleData = ref({
 
 const createTmpl = (val: number) => {
   mode.value = 'add'
-  ruleData.value.templateType = val
+
+  delete formData.value.id
+  formData.value.name = ''
+  formData.value.rule = {
+    type: val,
+    campaignType: '',
+    campaignAd: [],
+    action: {},
+    activeModel: '',
+    setTime: '',
+    weekdays: [],
+    conditions: [],
+  }
+  console.log(formData.value)
   showDrawer.value = true
 }
 const editTmpl = (row: any) => {
   mode.value = 'edit'
   // console.log(50, row)
+  formData.value.id = row.id
+  formData.value.name = row.name
+  formData.value.rule = row.rule
 
-  ruleData.value.templateType = row.rule.type
-  ruleData.value.templateId = row.rule.id
-  ruleData.value.templateName = row.name
-  ruleData.value.rule = row.rule
+  // formData.value.rule.id = row.rule.id
+  // formData.value.rule.type = row.rule.type
+  // formData.value.rule.campaignType = row.rule.campaignType
+  // formData.value.rule.campaignAd = row.rule.campaignAd
+  // formData.value.rule.action = row.rule.action
+  // formData.value.rule.activeModel = row.rule.activeModel
+  // formData.value.rule.setTime = row.rule.setTime
+  // formData.value.rule.weekdays = row.rule.weekdays
+  // formData.value.rule.conditions = row.rule.conditions
 
   showDrawer.value = true
 }
 
+const submitFormData = async () => {
+  if (mode.value === 'add') {
+    await api.AddObj(formData.value)
+  } else if (mode.value === 'edit') {
+    await api.UpdateObj(formData.value)
+  }
+}
+
 const refreshTable = () => {
   showDrawer.value = false
   crudExpose.doRefresh()