Bladeren bron

add auto-templates

guojing_wu 1 jaar geleden
bovenliggende
commit
49bd75dc10
31 gewijzigde bestanden met toevoegingen van 2346 en 447 verwijderingen
  1. 25 18
      src/components/TimerBidTable/index.vue
  2. 0 414
      src/components/TimerBudget/index.vue
  3. 399 0
      src/components/TimerBudgetTable/index.vue
  4. 43 0
      src/components/auto-templates/api.ts
  5. 115 0
      src/components/auto-templates/campaign.vue
  6. 47 0
      src/components/auto-templates/common.js
  7. 34 0
      src/components/auto-templates/enum.js
  8. 0 0
      src/components/auto-templates/neg-keywords.vue
  9. 40 0
      src/components/auto-templates/search-term.vue
  10. 49 0
      src/components/auto-templates/select-tmpl.vue
  11. 269 0
      src/components/auto-templates/target-rule.vue
  12. 40 0
      src/components/auto-templates/target-select.vue
  13. 87 0
      src/components/auto-templates/timer-bid.vue
  14. 91 0
      src/components/auto-templates/timer-budget.vue
  15. 201 0
      src/components/conditionBuilder/condition-group.vue
  16. 222 0
      src/components/conditionBuilder/condition-group2.vue
  17. 63 0
      src/components/conditionBuilder/index.vue
  18. 21 0
      src/components/conditionBuilder/type.d.ts
  19. 69 0
      src/components/input-float/index.vue
  20. 53 0
      src/components/select-button/index.vue
  21. 28 0
      src/theme/app.scss
  22. 3 3
      src/theme/element.scss
  23. 25 0
      src/types/views.d.ts
  24. 43 0
      src/views/adManage/sp/campaigns/campaignDetail/automation/index.vue
  25. 2 1
      src/views/adManage/sp/campaigns/campaignDetail/index.vue
  26. 4 4
      src/views/demo/api.ts
  27. 105 7
      src/views/demo/index.vue
  28. 42 0
      src/views/efTools/automation/api.ts
  29. 138 0
      src/views/efTools/automation/crud.tsx
  30. 80 0
      src/views/efTools/automation/index.vue
  31. 8 0
      src/views/efTools/utils/enum.ts

+ 25 - 18
src/components/TimerBid/index.vue → src/components/TimerBidTable/index.vue

@@ -17,9 +17,9 @@
 					<tr>
 						<th class="td-normal">{{ WeekMap[week] }}</th>
 						<td 
-							class="un-selected" v-for="info in hoursList" :key="info.hour"  
-							@mousedown="handleMouseDown(week, info.hour, $event)"  
-							@mouseover="handleMouseMove(week, info.hour)" 
+							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' : '' }"
 						>
@@ -61,14 +61,13 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, watch } from 'vue'
+import { ref, onMounted, watch, reactive } from 'vue'
 
 interface Props {
 	data: number[][],
-}
+} 
 const props = defineProps<Props>()
-const emits = defineEmits(["cancelBid"])
-// const data = ref(props.data)
+const bidData = ref(props.data)
 
 const mouseDown = ref(false)
 const startRowIndex = ref(0)
@@ -92,20 +91,25 @@ onMounted(() => {
 		selectedItems.value[i] = []
 		const tmp = []
 		for (let j = 0; j < 24; j++) {
-			tmp.push({ hour: j, value: props.data.length === 0? 0: props.data[i][j], selected: false })
+			tmp.push({ value: bidData.value.length === 0? 0: bidData.value[i][j], selected: false })
 		}
 		items.value.push(tmp)
 	}
+	// console.log(99, items.value)
 })
 
-watch(props.data, () => {
-	console.log('	watch-------')
-	for (let i = 0; i < 7; i++) {
-		for (let j = 0; j < 24; j++) {
-			items.value[i][j].value = props.data[i][j]
+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
 	}
-})
+)
 
 const handleMouseDown = ( rowIndex: number, colIndex: number, event: any ) => {
 		if (event.button === 0) {
@@ -147,10 +151,11 @@ const selectCell = (rowIndex: number, colIndex: number) => {
 }
 
 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
-			props.data[row][col] = bid.value
+			items.value[row][col].value = bid.value
+			bidData.value[row][col] = bid.value
 		}
 	}
 	clearSelectedItems()
@@ -162,7 +167,6 @@ const cancelBid = () => {
 	clearSelectedItems()
 	clearCellBackgroundColor()
 	dialogVisible.value = false
-	emits('cancelBid')
 }
 
 const clearCellBackgroundColor = () => {
@@ -182,7 +186,7 @@ const resetAllBid = () => {
 		for (let j = 0; j < 24; j++) {
 			items.value[i][j].value = 0
 			items.value[i][j].selected = false
-			props.data[i][j] = 0
+			bidData.value[i][j] = 0
 		}
 	}
 }
@@ -252,6 +256,9 @@ table {
 				background: #ccdbff;
 			}
 
+			.cell-text {
+				color: black;
+			}
 		}
 	}
 }

+ 0 - 414
src/components/TimerBudget/index.vue

@@ -1,414 +0,0 @@
-<template>
-	<el-row>
-		<el-col :span="3">
-			<span style="background-color: #11acf5; color: #fff;">
-				<el-icon style="display: inline-block; padding-top: 2.5px;" ><Bottom /></el-icon>
-			</span>
-			<span>基于原始预算降低(百分比)</span>
-		</el-col>
-		<el-col :span="3">
-			<span style="background-color: #11acf5; color: #fff;">
-				<el-icon style="display: inline-block; padding-top: 2.5px;"><Top /></el-icon>
-			</span>
-			<span>基于原始预算升高(百分比)</span>
-		</el-col>
-		<el-col :span="3">
-			<span style="background-color: #3fd4cf; color: #fff;">
-				<el-icon style="display: inline-block; padding-top: 2.5px;"><Bottom /></el-icon>
-			</span>
-			<span>基于原始预算降低(数值)</span>
-		</el-col>
-		<el-col :span="3">
-			<span style="background-color: #3fd4cf; color: #fff;">
-				<el-icon style="display: inline-block; padding-top: 2.5px;"><Top /></el-icon>
-			</span>
-			<span>基于原始预算升高(数值)</span>
-		</el-col>
-		<el-col :span="3">
-			<span style="background-color: #3359b5; color: #fff;">
-				<el-icon style="display: inline-block; padding-top: 2.5px;"></el-icon>
-			</span>
-			<span>固定预算</span>
-		</el-col>
-	</el-row>
-	<div class="calendar">
-		<table class="calendar-table calendar-table-hour">
-			<thead class="calendar-head">
-				<tr>
-					<th rowspan="8" class="week-td">星期 / 时间</th>
-					<th colspan="12">00:00 - 12:00</th>
-					<th colspan="12">12:00 - 24:00</th>
-					<th colspan="4" rowspan="2" class="week-td" style="display: none">小时</th>
-				</tr>
-				<tr>
-					<th colspan="1" v-for="(_, i) in 24" :key="i">{{ i }}</th>
-				</tr>
-			</thead>
-			<tbody class="calendar-body">
-				<template v-for="(hoursList, week) in items">
-					<tr>
-						<th class="td-normal">{{ WeekMap[week] }}</th>
-						<td 
-							class="un-selected" v-for="(info, hour) in hoursList" :key="hour"  
-							@mousedown="handleMouseDown(week, hour, $event)"  
-							@mouseover="handleMouseMove(week, hour)" 
-							@mouseup="handleMouseUp"
-							:style="{ background: getCellBackgroundColor(info) }"
-						>
-							<!-- <div class="cell-text">
-								
-							</div> -->
-							{{ getCellText(info) }}
-								<el-icon v-if="info.kind !=='FixBudget'" style="display: inline-block; padding-top: 2px;">
-									<Top v-if="info.kind==='UpPercent'|| info.kind==='UpNumber'"/>
-									<Bottom v-if="info.kind==='DownPercent' || info.kind==='DownNumber'"/>
-								</el-icon>
-						</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>
-
-		<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="kind" :span="16">
-					<el-select v-model="formData.kind" @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.kind !== ''" :span="8">
-					<vxe-input v-model="formData.value" type="float" size="medium" :min="0" :step="formData.kind==='FixBudget'?1:0.01" v-if="isNumber">
-						<template #prefix>$</template>
-					</vxe-input>
-					<vxe-input v-model="formData.value" type="float" size="medium" :min="0" v-else>
-						<template #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>
-</template>
-
-<script lang="ts" setup>
-import { ref, onMounted, watch, Ref } from 'vue'
-import { VXETable, VxeFormInstance, VxeFormPropTypes, VxeFormEvents } from 'vxe-table'
-
-interface Props {
-	data: {value: string, kind: string}[][],
-}
-interface FormData {
-	kind: string,
-	value: string
-}
-const props = defineProps<Props>()
-const emits = defineEmits(["cancelBid"])
-
-const mouseDown = ref(false)
-const startRowIndex = ref(0)
-const startColIndex = ref(0)
-const items = ref([])
-const WeekMap = {
-	0: '星期一',
-	1: '星期二',
-	2: '星期三',
-	3: '星期四',
-	4: '星期五',
-	5: '星期六',
-	6: '星期日'
-}
-const dialogVisible = ref(false)
-const selectedItems = ref({})
-
-const KindEnum = [
-	{ label: '无需调整预算', value: '' },
-	{ label: '固定预算', value: 'FixBudget', color: '#3359b5' },
-	{ label: '基于原始预算降低(百分比)', value: 'DownPercent', color: '#11acf5' },
-	{ label: '基于原始预算升高(百分比)', value: 'UpPercent', color: '#11acf5' },
-	{ label: '基于原始预算降低(数值)', value: 'DownNumber', color: '#3fd4cf' },
-	{ label: '基于原始预算升高(数值)', value: 'UpNumber', color: '#3fd4cf' }
-]
-
-const formData:Ref<FormData> = ref({ kind: 'FixBudget', value: '1.00' })
-const isNumber = ref(true)
-const formRef = ref<VxeFormInstance>()
-const formRules = ref<VxeFormPropTypes.Rules>({
-	kind: [
-		{ 
-			validator({ itemValue }){
-				if (itemValue === undefined || itemValue === null) {
-					return new Error('必填项')
-				}
-			}
-		}
-	],
-	value: [
-		{ 
-			validator({ itemValue }) {
-				if (formData.value.kind === 'FixBudget') {
-					if (itemValue < 1) {
-						return new Error('固定预算必须大于1')
-					}
-				} else {
-					if (itemValue <= 0) {
-						return new Error('数值必须大于0')
-					}
-				}
-			} 
-		}
-	]
-})
-
-
-onMounted(() => {
-	for (let i = 0; i < 7; i++) {
-		selectedItems.value[i] = []
-		const tmp = []
-		for (let j = 0; j < 24; j++) {
-			let kind = ''
-			let value = ''
-			if (props.data.length !== 0) {
-				kind = props.data[i][j].kind	
-				value = props.data[i][j].value
-			}
-			tmp.push({ kind: kind, value: value })
-		}
-		items.value.push(tmp)
-	}
-})
-
-watch(props.data, () => {
-	for (let i = 0; i < 7; i++) {
-		for (let j = 0; j < 24; j++) {
-			items.value[i][j].kind = props.data[i][j].kind
-			items.value[i][j].value = props.data[i][j].value
-		}
-	}
-})
-
-
-
-const changeKind = () => {
-	if (formData.value.kind === 'DownPercent' || formData.value.kind === 'UpPercent') {
-		isNumber.value = false
-	} else {
-		isNumber.value = true
-	}
-	formRef.value.clearValidate('value')
-}
-
-const getCellBackgroundColor = (row: any) => {
-	if (row.selected) return '#ccdbff'
-	if (row.kind) return KindEnum.find(item => item.value===row.kind).color
-	return ''
-}
-
-const getCellText = (row: any) => {
-	if (row.kind === 'DownPercent' || row.kind === 'UpPercent') return row.value + '%'
-	if (row.value) return '$' + row.value
-	return ''
-}
-
-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 handleMouseUp = (event: any) => {
-	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)
-	}
-}
-
-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)
-
-	for (let i = rowStart; i <= rowEnd; i++) {
-		for (let j = cellStart; j <= cellEnd; j++) {
-			items.value[i][j].selected = true
-			selectedItems.value[i].push(j)
-		}
-	}
-}
-
-const submitBid = () => {
-	if (formData.value.kind === '') {
-		for (const row of Object.keys(selectedItems.value)) {
-			for (const col of selectedItems.value[row]) {
-				props.data[row][col].kind = ''
-				props.data[row][col].value = ''
-			}
-		}
-	} else {
-		for (const row of Object.keys(selectedItems.value)) {
-			for (const col of selectedItems.value[row]) {
-				props.data[row][col].kind = formData.value.kind
-				props.data[row][col].value = formData.value.value
-			}
-		}
-	}
-	clearSelectedItems()
-	clearCellBackgroundColor()
-	dialogVisible.value = false
-}
-
-const cancelBid = () => {
-	clearSelectedItems()
-	clearCellBackgroundColor()
-	dialogVisible.value = false
-	emits('cancelBid')
-}
-
-const closeDialog = (done: Function) => {
-	// console.log("closeDialog")
-	cancelBid()
-	done()
-}
-
-const clearCellBackgroundColor = () => {
-	for (const hoursList of items.value) {
-		for (const info of hoursList) {
-			info.selected = false
-		}
-	}
-}
-const clearSelectedItems = () => {
-	for (var i = 0; i < 7; i++) {
-		selectedItems.value[i] = []
-	}
-}
-const resetAllBid = () => {	
-	for (let i = 0; i < 7; i++) {
-		for (let j = 0; j < 24; j++) {
-			items.value[i][j].value = 0
-			items.value[i][j].selected = false
-			props.data[i][j].kind = ''
-			props.data[i][j].value = ''
-		}
-	}
-}
-
-</script>
-
-<style lang="scss" scoped>
-.calendar {
-	background-color: #fff;
-	-webkit-user-select: none;
-	position: relative;
-	display: inline-block;
-
-	.calendar-table {
-		border-collapse: collapse;
-	}
-
-	.week-td {
-		width: 90px;
-	}
-}
-
-table {
-	display: table;
-	border-collapse: separate;
-	box-sizing: border-box;
-	text-indent: initial;
-	border-spacing: 2px;
-	border-color: gray;
-
-	thead {
-		display: table-header-group;
-		vertical-align: middle;
-		border-color: inherit;
-	}
-
-	tr {
-		border: 1px solid #e0e5f4;
-		font-size: 12px;
-		text-align: center;
-		line-height: 32px;
-		color: rgba(0, 0, 0, 0.5);
-
-		th {
-			min-width: 40px;
-			border: 1px solid #e0e5f4;
-			font-size: 12px;
-			text-align: center;
-			line-height: 32px;
-			background: #f7f8fa;
-		}
-
-		td {
-			border: 1px solid #e0e5f4;
-			font-size: 12px;
-			text-align: center;
-			line-height: 32px;
-			min-width: 40px;
-			cursor: pointer;
-			color: #fff;
-			&:hover {
-				background: #ccdbff;
-			}
-
-		}
-	}
-}
-
-.clear-bar {
-	line-height: 32px;
-	padding: 0 12px;
-
-	.hover-link {
-		color: #1c6bde;
-    cursor: pointer;
-    font-size: 13px;
-	}
-	.fr {
-		float: right;
-	}
-	.middle {
-		float: center;
-	}
-}
-
-.cell-text {
-	color: #fff;
-}
-</style>

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

@@ -0,0 +1,399 @@
+<template>
+  <div style="width: 100%">
+    <el-row justify-content="start">
+      <el-col :span="4">
+        <span style="background-color: #11acf5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Bottom /></el-icon>
+        </span>
+        <span>基于原始预算降低(百分比)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #11acf5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Top /></el-icon>
+        </span>
+        <span>基于原始预算升高(百分比)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3fd4cf; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Bottom /></el-icon>
+        </span>
+        <span>基于原始预算降低(数值)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3fd4cf; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"><Top /></el-icon>
+        </span>
+        <span>基于原始预算升高(数值)</span>
+      </el-col>
+      <el-col :span="4">
+        <span style="background-color: #3359b5; color: #fff">
+          <el-icon style="display: inline-block; padding-top: 2.5px"></el-icon>
+        </span>
+        <span>固定预算</span>
+      </el-col>
+    </el-row>
+    <div class="calendar">
+      <table class="calendar-table calendar-table-hour">
+        <thead class="calendar-head">
+          <tr>
+            <th rowspan="8" class="week-td">星期 / 时间</th>
+            <th colspan="12">00:00 - 12:00</th>
+            <th colspan="12">12:00 - 24:00</th>
+            <th colspan="4" rowspan="2" class="week-td" style="display: none">小时</th>
+          </tr>
+          <tr>
+            <th colspan="1" v-for="(_, i) in 24" :key="i">{{ i }}</th>
+          </tr>
+        </thead>
+        <tbody class="calendar-body">
+          <template v-for="(hoursList, week) in items">
+            <tr>
+              <th class="td-normal">{{ WeekMap[week] }}</th>
+              <td
+                class="un-selected"
+                v-for="(info, hour) in hoursList"
+                :key="hour"
+                @mousedown="handleMouseDown(week, hour, $event)"
+                @mouseover="handleMouseMove(week, hour)"
+                @mouseup="handleMouseUp"
+                :style="{ background: getCellBackgroundColor(info) }">
+                {{ getCellText(info) }}
+                <el-icon v-if="info.type !== 'FixBudget'" style="display: inline-block; padding-top: 2px">
+                  <Top v-if="info.type === 'UpPercent' || info.type === 'UpNumber'" />
+                  <Bottom v-if="info.type === 'DownPercent' || info.type === 'DownNumber'" />
+                </el-icon>
+              </td>
+            </tr>
+          </template>
+          <tr>
+            <th colspan="28" class="clear-bar">
+              <span class="middle">可拖动鼠标选择时间段</span>
+              <span class="hover-link fr" @click="resetAllBid">全部重置</span>
+            </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>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, Ref } from 'vue'
+import { VxeFormInstance, VxeFormPropTypes } from 'vxe-table'
+
+interface Props {
+  data: { value: string; type: string }[][]
+}
+interface FormData {
+  type: string
+  value: string
+}
+const props = defineProps<Props>()
+
+const mouseDown = ref(false)
+const startRowIndex = ref(0)
+const startColIndex = ref(0)
+const items = ref([])
+const WeekMap = {
+  0: '星期一',
+  1: '星期二',
+  2: '星期三',
+  3: '星期四',
+  4: '星期五',
+  5: '星期六',
+  6: '星期日',
+}
+const dialogVisible = ref(false)
+const selectedItems = ref({})
+
+const KindEnum = [
+  { label: '无需调整预算', value: '' },
+  { label: '固定预算', value: 'FixBudget', color: '#3359b5' },
+  { label: '基于原始预算降低(百分比)', value: 'DownPercent', color: '#11acf5' },
+  { label: '基于原始预算升高(百分比)', value: 'UpPercent', color: '#11acf5' },
+  { label: '基于原始预算降低(数值)', value: 'DownNumber', color: '#3fd4cf' },
+  { label: '基于原始预算升高(数值)', value: 'UpNumber', color: '#3fd4cf' },
+]
+
+const formData: Ref<FormData> = ref({ type: 'FixBudget', value: '1.00' })
+const isNumber = ref(true)
+const formRef = ref<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')
+          }
+        }
+      },
+    },
+  ],
+})
+
+onMounted(() => {
+  for (let i = 0; i < 7; i++) {
+    selectedItems.value[i] = []
+    const tmp = []
+    for (let j = 0; j < 24; j++) {
+      let type = ''
+      let value = ''
+      if (props.data.length !== 0) {
+        type = props.data[i][j].type
+        value = props.data[i][j].value
+      }
+      tmp.push({ type, value })
+    }
+    items.value.push(tmp)
+  }
+})
+
+watch(
+  () => props.data,
+  () => {
+    // console.log(191, props.data)
+    for (let i = 0; i < 7; i++) {
+      for (let j = 0; j < 24; j++) {
+        items.value[i][j].type = props.data[i][j].type
+        items.value[i][j].value = props.data[i][j].value
+      }
+    }
+  },
+  { deep: true }
+)
+
+const changeKind = () => {
+  if (formData.value.type === 'DownPercent' || formData.value.type === 'UpPercent') {
+    isNumber.value = false
+  } else {
+    isNumber.value = true
+  }
+  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
+  return ''
+}
+
+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 handleMouseUp = (event: any) => {
+  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)
+  }
+}
+
+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)
+
+  for (let i = rowStart; i <= rowEnd; i++) {
+    for (let j = cellStart; j <= cellEnd; j++) {
+      items.value[i][j].selected = true
+      selectedItems.value[i].push(j)
+    }
+  }
+}
+
+const submitBid = () => {
+  if (formData.value.type === '') {
+    for (const row of Object.keys(selectedItems.value)) {
+      for (const col of selectedItems.value[row]) {
+        props.data[row][col].type = ''
+        props.data[row][col].value = ''
+      }
+    }
+  } else {
+    for (const row of Object.keys(selectedItems.value)) {
+      for (const col of selectedItems.value[row]) {
+        props.data[row][col].type = formData.value.type
+        props.data[row][col].value = formData.value.value
+      }
+    }
+  }
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const cancelBid = () => {
+  clearSelectedItems()
+  clearCellBackgroundColor()
+  dialogVisible.value = false
+}
+
+const closeDialog = (done: Function) => {
+  // console.log("closeDialog")
+  cancelBid()
+  done()
+}
+
+const clearCellBackgroundColor = () => {
+  for (const hoursList of items.value) {
+    for (const info of hoursList) {
+      info.selected = false
+    }
+  }
+}
+const clearSelectedItems = () => {
+  for (var i = 0; i < 7; i++) {
+    selectedItems.value[i] = []
+  }
+}
+const resetAllBid = () => {
+  for (let i = 0; i < 7; i++) {
+    for (let j = 0; j < 24; j++) {
+      items.value[i][j].value = 0
+      items.value[i][j].selected = false
+      props.data[i][j].type = ''
+      props.data[i][j].value = ''
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.calendar {
+  background-color: #fff;
+  -webkit-user-select: none;
+  position: relative;
+  display: inline-block;
+
+  .calendar-table {
+    border-collapse: collapse;
+  }
+
+  .week-td {
+    width: 90px;
+  }
+}
+
+table {
+  display: table;
+  border-collapse: separate;
+  box-sizing: border-box;
+  text-indent: initial;
+  border-spacing: 2px;
+  border-color: gray;
+
+  thead {
+    display: table-header-group;
+    vertical-align: middle;
+    border-color: inherit;
+  }
+
+  tr {
+    border: 1px solid #e0e5f4;
+    font-size: 12px;
+    text-align: center;
+    line-height: 32px;
+    color: rgba(0, 0, 0, 0.5);
+
+    th {
+      min-width: 40px;
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      background: #f7f8fa;
+    }
+
+    td {
+      border: 1px solid #e0e5f4;
+      font-size: 12px;
+      text-align: center;
+      line-height: 32px;
+      min-width: 40px;
+      cursor: pointer;
+      color: #fff;
+      &:hover {
+        background: #ccdbff;
+      }
+    }
+  }
+}
+
+.clear-bar {
+  line-height: 32px;
+  padding: 0 12px;
+
+  .hover-link {
+    color: #1c6bde;
+    cursor: pointer;
+    font-size: 13px;
+  }
+  .fr {
+    float: right;
+  }
+  .middle {
+    float: center;
+  }
+}
+
+.cell-text {
+  color: #fff;
+}
+</style>

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

@@ -0,0 +1,43 @@
+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
+  })
+}
+
+

+ 115 - 0
src/components/auto-templates/campaign.vue

@@ -0,0 +1,115 @@
+<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>
+
+      <div class="asj-h3">操作</div>
+      <el-checkbox-group v-model="formData.rule.action.state">
+        <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')">
+      <span class="asj-h3">条件</span>
+      <conditionBuilder :data="formData.rule.conditions" :candidate-fields="condidateFields" ref="condiBuilderRef" />
+    </div>
+  </div>
+
+  <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>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import * as api from './api'
+import { useFromData } from './common'
+import { WeekMap } from './enum'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  mode: string
+  data: { [key: string]: any }
+}
+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 addConditions = () => {
+  if (props.mode === 'add' && formData.value.rule.conditions.length === 0) {
+    condiBuilderRef.value.addConditionGroup()
+  }
+}
+
+const condidateFields = [
+  { label: '曝光量', value: 'impressions' },
+  { label: '点击量', value: 'clicks' },
+  { label: '转化率', value: 'cr', suffix: '%' },
+  { label: '单次点击费用', value: 'cpc', prefix: '$' },
+]
+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 = () => {
+  if (props.mode !== 'auto') {
+    emits('refresh')
+  }
+}
+
+const initData = async () => {
+  setFormData()
+}
+
+onMounted(async () => {
+  await initData()
+})
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,47 @@
+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 }
+}
+

+ 34 - 0
src/components/auto-templates/enum.js

@@ -0,0 +1,34 @@
+export const WeekMap = {
+  0: '星期日',
+  1: '星期一',
+  2: '星期二',
+  3: '星期三',
+  4: '星期四',
+  5: '星期五',
+  6: '星期六',
+}
+
+export const ActionList = [
+  {
+    label: '基于当前竞价_(百分比)',
+    value: 'bid-ratio',
+  },
+  {
+    label: '基于当前竞价_(数值)',
+    value: 'bid-num',
+  },
+  {
+    label: '基于数据周期CPC_(百分比)',
+    value: 'cpc-ratio',
+  },
+  {
+    label: '基于数据周期CPC_(数值)',
+    value: 'cpc-num',
+  },
+]
+export const RuleNameMap = {
+  increase: '升高竞价',
+  decrease: '降低竞价',
+  set: '设置竞价',
+  pause: '暂停定向',
+}

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


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

@@ -0,0 +1,40 @@
+<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>

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

@@ -0,0 +1,49 @@
+<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>

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

@@ -0,0 +1,269 @@
+<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>

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

@@ -0,0 +1,40 @@
+<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>

+ 87 - 0
src/components/auto-templates/timer-bid.vue

@@ -0,0 +1,87 @@
+<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" required v-if="mode !== 'auto'">
+        <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>
+        </template>
+        <TimerBidTable :data="formData.rule.conditions" @click="formRef.clearValidate('rule.conditions')" />
+      </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, 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 }
+}
+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)
+  }
+  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() }
+    }
+  }
+  callback(new Error('请先设置竞价!'))
+}
+const submitForm = async () => {
+  formRef.value.validate(async (valid: any) => {
+    if (valid) {
+      await submitFormData()
+      emits('refresh')
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+
+const cancel = () => {
+  if (props.mode !== 'auto') {
+    emits('refresh')
+  } else {
+    setFormData()
+  }
+}
+
+const initData = () => {
+  if (props.mode === 'edit') {
+    setFormData()
+  }
+}
+
+onMounted(() => {
+  initData()
+})
+</script>
+
+<style scoped></style>

+ 91 - 0
src/components/auto-templates/timer-budget.vue

@@ -0,0 +1,91 @@
+<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" required v-if="mode !== 'auto'">
+        <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>
+        </template>
+        <TimerBudgetTable :data="formData.rule.conditions" @click="formRef.clearValidate('rule.conditions')" />
+      </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 } 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 }
+}
+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: '' })
+  }
+  formData.value.rule.conditions.push(tmp)
+}
+
+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 = () => {
+  if (props.mode !== 'auto') {
+    emits('refresh')
+  } else {
+    setFormData()
+  }
+}
+
+const initData = async() => {
+  if (props.mode === 'edit') {
+    setFormData()
+  }
+}
+
+onMounted(async () => {
+  await initData()
+})
+</script>
+
+<style scoped></style>

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

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

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

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

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

@@ -0,0 +1,63 @@
+<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>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import ConditionGroup from './condition-group2.vue'
+import XEUtils from 'xe-utils'
+
+interface Props {
+  candidateFields: CandidateField[]
+  data: ConditionGroupItem[]
+}
+const props = defineProps<Props>()
+const condiGroups = ref(props.data)
+const condiGroupRef = ref()
+const addConditionGroup = () => {
+  condiGroups.value.push({
+    key: Math.random().toString(36).substring(2),
+    day: 1,
+    exceptDay: 0,
+    items: [
+      {
+        dataType: props.candidateFields[0].value,
+        dayType: 'sum',
+        symbol: 'gte',
+        num: '',
+      },
+    ],
+  })
+}
+const delGroup = (key: string) => {
+  XEUtils.remove(condiGroups.value, (item) => item.key === key)
+}
+const validate = async () => {
+  const ret = []
+  if (!condiGroupRef.value) return ret
+  for (const info of condiGroupRef.value) {
+    ret.push(await info.validate())
+  }
+  // console.log(52, ret)
+  return ret
+}
+
+watch(
+  () => props.data,
+  () => {
+    condiGroups.value = props.data
+  }
+)
+
+defineExpose({ validate, addConditionGroup })
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,21 @@
+declare interface ConditionItem {
+  dataType: string,
+  dayType: string,
+  symbol: string,
+  num: string,
+}
+
+declare interface ConditionGroupItem {
+  key: string,
+  day: number,
+  exceptDay: number,
+  items: ConditionItem[]
+}
+
+declare interface CandidateField {
+  label: string,
+  value: string,
+  prefix?: string,
+  suffix?: string,
+  symbols?: string[]
+}

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

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

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

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

+ 28 - 0
src/theme/app.scss

@@ -597,4 +597,32 @@ body,
   background-color: #fef0f0 !important;
   color: #f56c6c !important;
   border-color: #fab6b6 !important;
+}
+
+.asj-h2 {
+  height: 60px;
+  line-height: 60px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.asj-h3 {
+  font-size: 15px;
+  font-weight: 750;
+  color: #333;
+  line-height: 22px;
+  padding: 12px 0;
+  position: relative;
+}
+
+.auto-page-foot {
+  background-color: #fff;
+  padding: 24px;
+  position: sticky;
+  bottom: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: center;
 }

+ 3 - 3
src/theme/element.scss

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

+ 25 - 0
src/types/views.d.ts

@@ -383,3 +383,28 @@ declare type SpAdGroup = {
 	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 AutoRule {
+	type: number,
+	campaignType: string,
+	campaignAd: string[],
+	action: string,
+	activeModel: string,
+	setTime: string,
+	weekdays: number[],
+	conditions: number[][]
+}

+ 43 - 0
src/views/adManage/sp/campaigns/campaignDetail/automation/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-tabs tab-position="left" style="height: 100%;">
+    <el-tab-pane label="分时调价">
+      <TimerBidTmpl mode="auto" :data="ruleData"/>
+    </el-tab-pane>
+    <el-tab-pane label="分时预算" :lazy="true">
+      <!-- <TimerBudgetTmpl mode="auto" /> -->
+    </el-tab-pane>
+    <el-tab-pane label="广告活动" :lazy="true">
+      <!-- <CampaignTmpl mode="auto" /> -->
+    </el-tab-pane>
+    <el-tab-pane label="添加否定词" :lazy="true">
+    </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'
+
+const ruleData = ref({
+  templateType: 0,
+  templateId: 0,
+  templateName: '',
+  rule: {
+    type: 0,
+    campaignType: '', // sp|sb|sd
+    campaignAd: [],
+    action: '',
+    activeModel: '',
+    setTime: '',
+    weekdays: [],
+    conditions: [],
+  },
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 2 - 1
src/views/adManage/sp/campaigns/campaignDetail/index.vue

@@ -21,7 +21,7 @@
         <Budget :campaignId="route.query.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>
       </el-tab-pane>
       <el-tab-pane label="广告位" name="placement">
         <Placement :campaignId="route.query.campaignId" v-if="tabActiveName==='placement'"/>
@@ -40,6 +40,7 @@ import {useRoute} from 'vue-router'
 import AdGroups from './adGroups/index.vue'
 import Placement from './placement/index.vue'
 import Budget from './budget/index.vue'
+import Automation from './automation/index.vue'
 import {getEnumLabel} from '/@/views/adManage/utils/tools.js'
 import {dynBidStrategyEnum} from '/@/views/adManage/utils/enum.js'
 import {useShopInfo} from '/@/stores/shopInfo'

+ 4 - 4
src/views/demo/api.ts

@@ -1,16 +1,16 @@
 import { request } from '/@/utils/service';
-import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
 import XEUtils from 'xe-utils';
 
 export const apiPrefix = '/api/ad_manage/';
-export function GetList(query: PageQuery) {
+export function GetList(query: UserPageQuery) {
     return request({
         url: apiPrefix,
         method: 'get',
         params: query,
     })
 }
-export function GetObj(id: InfoReq) {
+export function GetObj(id: number) {
     return request({
         url: apiPrefix + id,
         method: 'get',
@@ -33,7 +33,7 @@ export function UpdateObj(obj: EditReq) {
     });
 }
 
-export function DelObj(id: DelReq) {
+export function DelObj(id: number) {
     return request({
         url: apiPrefix + id + '/',
         method: 'delete',

+ 105 - 7
src/views/demo/index.vue

@@ -1,19 +1,113 @@
 <template>
   <div class="test">
-    <TimerBid :data="bidData" @cancel-bid="refreshTimerBid"></TimerBid>
-    <p>{{ bidData }}</p>
-    <TimerBudget :data="budgetData"></TimerBudget>
-    <p>{{ budgetData }}</p>
+    <!-- <TimerBidTable :data="bidData" @cancel-bid="refreshTimerBid"></TimerBidTable>
+    <p>{{ bidData }}</p> -->
+    <!-- <TimerBudgetTable :data="budgetData"></TimerBudgetTable>
+    <p>{{ budgetData }}</p> -->
+
+    <!-- <InputFloat v-model="num" :prefix="prefix" :suffix="suffix" style="width: 200px;"></InputFloat>
+    <p>{{ num }}</p>
+    <el-button @click="change">切换</el-button> -->
+
+    <!-- <conditionGroup :candidate-fields="condidateFields" v-model="formData"></conditionGroup> -->
+    <!-- <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>
   </div>
+
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue'
-import TimerBid from '/@/components/TimerBid/index.vue'
-import TimerBudget from '/@/components/TimerBudget/index.vue'
+import { onMounted, ref, Ref, reactive } from 'vue'
+import TimerBidTable from '/@/components/TimerBidTable/index.vue'
+import TimerBudgetTable from '/@/components/TimerBudgetTable/index.vue'
+import InputFloat from '/@/components/input-float/index.vue'
+import conditionGroup from '/@/components/conditionBuilder/condition-group.vue'
+import conditionBuilder from '/@/components/conditionBuilder/index.vue'
+
+import RuleTimerBid from '/@/components/auto-templates/timer-bid.vue'
+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'
+
+const condiBuilder = ref()
+const condidateFields = [
+  { label: '曝光量', value: 'impressions'},
+  { label: '点击量', value: 'clicks'},
+  { label: '转化率', value: 'cr', suffix: '%'},
+  { label: '单次点击费用', value: 'cpc', prefix: '$'},
+]
+const formData = reactive([
+  {
+    key: '1',
+    day: 1,
+    exceptDay: 0,
+    items: [
+      {
+        dataType: 'impressions',
+        dayType: 'sum',
+        symbol: 'gte',
+        num: '1000'
+      }
+    ]
+  },
+  {
+    key: '2',
+    day: 7,
+    exceptDay: 0,
+    items: [
+      {
+        dataType: 'clicks',
+        dayType: 'sum',
+        symbol: 'lte',
+        num: '100'
+      }
+    ]
+  }
+])
+
+const validateForm = async () => {
+  const validList = await condiBuilder.value.validate()
+  // console.log(validList)
+  for(const items of validList) {
+    if (items.includes(false)) {
+      return false
+    }
+  }
+  return true
+}
+const save = async () => {
+  if (await validateForm()) {
+    console.log('save')
+  } else {
+    console.log('not save')
+  }
+}
+
+const prefix = ref("")
+const suffix = ref("")
+const b = ref(false)
+const change = () => {
+  b.value = !b.value
+  if (b.value) {
+    prefix.value = "$"
+    suffix.value = ""
+  } else {
+    prefix.value = ""
+    suffix.value = "%"
+  }
+  num.value += '1'
+}
 
 const bidData = ref([])
 const budgetData = ref([])
+const num = ref('1')
 
 onMounted(() => {
   reqData()
@@ -21,10 +115,13 @@ onMounted(() => {
 const initData = () => {
   for (let i = 0; i < 7; i++) {
     const tmp = []
+    const tmp2 = []
     for (let j = 0; j < 24; j++) {
       tmp.push(1)
+      tmp2.push({type: '', value: ''})
     }
     bidData.value.push(tmp)
+    budgetData.value.push(tmp2)
   }
 }
 
@@ -41,5 +138,6 @@ const refreshTimerBid = () => {
 <style scoped>
 .test {
   padding: 10px;
+  background-color: #fff;
 }
 </style>

+ 42 - 0
src/views/efTools/automation/api.ts

@@ -0,0 +1,42 @@
+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 function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+        params: query,
+    })
+}
+export function GetObj(id: number) {
+    return request({
+        url: apiPrefix + id,
+        method: 'get',
+    });
+}
+
+export function AddObj(obj: AddReq) {
+    return request({
+        url: apiPrefix,
+        method: 'post',
+        data: obj,
+    });
+}
+
+export function UpdateObj(obj: EditReq) {
+    return request({
+        url: apiPrefix + obj.id + '/',
+        method: 'put',
+        data: obj,
+    });
+}
+
+export function DelObj(id: DelReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'delete',
+        data: { id },
+    });
+}

+ 138 - 0
src/views/efTools/automation/crud.tsx

@@ -0,0 +1,138 @@
+import * as api from './api'
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject, nextTick, ref } from 'vue'
+import { TemplateType } from '../utils/enum'
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+
+  const { editTmpl } = context
+
+  const pageRequest = async (query: UserPageQuery) => {
+    return await api.GetList(query)
+  }
+  const editRequest = async ({ form, row }: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({ row }: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({ form }: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        size: 'small',
+      },
+      container: {
+        fixedHeight: false,
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        fixed: 'right',
+        width: 100,
+        buttons: {
+          view: {
+            show: false,
+          },
+          edit: {
+          	text: null,
+            iconRight: 'Edit',
+          	type: 'text',
+          	// show: hasPermissions('dictionary:Update'),
+            order: 1,
+          	tooltip: {
+          		placement: 'top',
+          		content: '编辑',
+          	},
+            click: (ctx: any) => {
+              editTmpl(ctx.row)
+            }
+          },
+          custom: {
+            text: null,
+            iconRight: 'SetUp',
+            type: 'text',
+            order: 2,
+            tooltip: {
+          		placement: 'top',
+          		content: '关联广告活动',
+          	}
+          },
+          remove: {
+            iconRight: 'Delete',
+            type: 'text',
+            text: null,
+            order: 3,
+            tooltip: {
+          		placement: 'top',
+          		content: '删除',
+          	}
+            // show: hasPermissions('dictionary:Delete'),
+          },
+        },
+      },
+      actionbar: {
+        show: true,
+        buttons: {
+          add: {
+            show: false,
+          }
+        }
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            width: 100,
+            align: 'center',
+          },
+        },
+        name: {
+          title: '模板名称',
+          column: {
+            width: '150px',
+          },
+          search: {
+            show: false
+          }
+        },
+        "rule.type": {
+          title: '模板类型',
+          type: 'dict-select',
+          dict: dict({
+            data: TemplateType
+          }),
+          search: {
+            show: true,
+            component: {
+              props: {
+                clearable: true,
+              },
+            },
+          }
+        },
+        campaignNumber: {
+          title: '广告活动数量'
+        },
+        creator_username: {
+          title: '创建人'
+        },
+        modifier_username: {
+          title: '修改人'
+        }
+      }
+    }
+  }
+}

+ 80 - 0
src/views/efTools/automation/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #actionbar-right>
+        <SelectBotton btn-title="新建模板" :options="TemplateType" @click="createTmpl"></SelectBotton>
+      </template>
+    </fs-crud>
+  </fs-page>
+
+  <el-drawer v-model="showDrawer" :title="mode === 'add' ? '新建模板' : '编辑模板'" size="60%" :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>
+    </div>
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, ref, watch } from 'vue'
+import { FsPage, useFs } from '@fast-crud/fast-crud'
+import { CirclePlusFilled } from '@element-plus/icons-vue'
+import { createCrudOptions } from './crud'
+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'
+
+const dyComponents = {
+  1: TimerBidTmpl,
+  2: TimerBudgetTmpl,
+  3: CampaignTmpl,
+  4: TargetRuleTmpl
+}
+const mode = ref('')
+const showDrawer = ref(false)
+const ruleData = ref({
+  templateType: 0,
+  templateId: 0,
+  templateName: '',
+  rule: {
+    type: 0,
+    campaignType: '', // sp|sb|sd
+    campaignAd: [],
+    action: '',
+    activeModel: '',
+    setTime: '',
+    weekdays: [],
+    conditions: [],
+  },
+})
+
+const createTmpl = (val: number) => {
+  mode.value = 'add'
+  ruleData.value.templateType = val
+  showDrawer.value = true
+}
+const editTmpl = (row: any) => {
+  mode.value = 'edit'
+  // console.log(50, row)
+
+  ruleData.value.templateType = row.rule.type
+  ruleData.value.templateId = row.rule.id
+  ruleData.value.templateName = row.name
+  ruleData.value.rule = row.rule
+
+  showDrawer.value = true
+}
+
+const refreshTable = () => {
+  showDrawer.value = false
+  crudExpose.doRefresh()
+}
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { editTmpl } })
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+</script>
+
+<style scoped></style>

+ 8 - 0
src/views/efTools/utils/enum.ts

@@ -0,0 +1,8 @@
+export const TemplateType = [
+  { label: '分时调价', value: 1 },
+  { label: '分时预算', value: 2 },
+  { label: '广告活动', value: 3 },
+  { label: '定向规则', value: 4 },
+  { label: '添加搜索词', value: 5 },
+  { label: '添加否定词', value: 6 },
+]