Forráskód Böngészése

完成广告指标卡片出版

guojing_wu 1 éve
szülő
commit
ae0f90e7a4

+ 114 - 0
src/components/MetricsCards/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="metrics-cards">
+    <MCard
+     v-model="info.metric"
+     :metric-items="allMetricItems"
+     :color="info.color"
+     v-for="info in displayMetrics"
+     @change-metric="changedMetric"
+     @click="clickCard(info.metric)"/>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, withDefaults, Ref, onBeforeMount, watch, computed,ComputedRef } from 'vue'
+import MCard from './mCard.vue'
+
+interface ModelData {
+  metric: string,
+  color: string
+}
+interface Props {
+  modelValue: ModelData[],
+  metricItems: MetricData[],
+  colors?: string[]
+}
+const colorsMap: { [key: string]: boolean } = {}
+const props = withDefaults(defineProps<Props>(), { colors: () => ["aqua", "orange", "blue"] })
+const emits = defineEmits(['change', 'update:modelValue'])
+const allMetricItems = ref(props.metricItems)
+const selectedMetric = ref(props.modelValue)
+const displayMetrics: Ref<{metric:string, color?: string}[]> = ref([])
+
+onBeforeMount(()=> {
+  for (const color of props.colors) {
+    colorsMap[color] = false
+  }
+  const tmp:{[key: string]: boolean} = {}
+  for (const info of selectedMetric.value) {
+    displayMetrics.value.push({ metric: info.metric, color: info.color })
+    tmp[info.metric] = true
+  }
+  for (const info of allMetricItems.value) {
+    if (info.disabled && !tmp[info.value]) { displayMetrics.value.push({ metric: info.value }) }
+  }
+})
+
+const getColor = () => {
+  for (const [k,v] of Object.entries(colorsMap)) {
+    if (!v) return k
+  }
+  return ""
+}
+const changedMetric = (oldVal: string, newVal: string) => {
+  for (const info of allMetricItems.value) {
+    if (info.value === newVal) {
+      info.disabled = true 
+    } else if (info.value === oldVal) {
+      info.disabled = false
+    }
+  }
+  const index = selectedMetric.value.findIndex( info => info.metric === oldVal)
+  if (index > -1) {
+    selectedMetric.value[index].metric = newVal
+    emits('update:modelValue', selectedMetric.value)
+    emits('change', selectedMetric.value)
+  }
+}
+const clickCard = (metric: string) => {
+  const index = selectedMetric.value.findIndex( info => info.metric === metric)
+  if (index > -1) {  // 已存在则删除
+    if (selectedMetric.value.length <= 1 ) return
+    const tmp = selectedMetric.value[index]
+    selectedMetric.value.splice(index, 1)
+    colorsMap[tmp.color] = false
+    emits('update:modelValue', selectedMetric.value)
+    emits('change', selectedMetric.value)
+  } else {  // 不存在则添加
+    if (selectedMetric.value.length === 3) { 
+      selectedMetric.value[2].metric = metric
+    } else {
+      const color = getColor()
+      colorsMap[color] = true
+      selectedMetric.value.push({ metric: metric, color: color})
+    }
+    emits('update:modelValue', selectedMetric.value)
+    emits('change', selectedMetric.value)
+  }
+}
+watch(selectedMetric.value, () => {
+  const cache:{ [key: string]: string } = {}
+  for (const info of selectedMetric.value) {
+    cache[info.metric] = info.color
+  }
+  for (const info of displayMetrics.value) {
+    const color = cache[info.metric]
+    if (color) {
+      info.color = color
+    } else {
+      info.color = undefined
+    }
+  }
+})
+
+</script>
+
+<style scoped>
+.metrics-cards {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 12px;
+  width: 100%;
+}
+</style>

+ 96 - 0
src/components/MetricsCards/mCard.vue

@@ -0,0 +1,96 @@
+<template>
+  <el-card class="metric-card">
+    <div class="metric-card__color" :style="boardTopStyle"></div>
+    <TextSelector v-model="metric" :options="props.metricItems" @change="changeMetric"></TextSelector>
+    <div class="metric-value">{{ selectedData?.metricVal }}</div>
+    <div class="metric-pre">
+      <span>{{ selectedData?.preVal }}&nbsp;&nbsp;</span>
+      <el-icon>
+        <!-- <Bottom class="green"/> -->
+        <Top class="green"/>
+      </el-icon>
+      <span class="green">{{ selectedData?.gapVal }}</span>
+    </div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import TextSelector from '/@/components/TextSelector/index.vue'
+
+defineOptions({
+  name: 'MCard'
+})
+
+interface Props {
+  modelValue: string,
+  metricItems: MetricData[],
+  color?: string,
+}
+const props = defineProps<Props>()
+const emits = defineEmits(["update:modelValue", "change-metric"])
+const metric = ref(props.modelValue)
+const changeMetric = (oldVal: string, newVal: string) => {
+  emits('update:modelValue', newVal)
+  emits('change-metric', oldVal, newVal)
+}
+const selectedData = computed(():MetricData|null => {
+  const info = props.metricItems.find(item => item.value === metric.value)
+  if(!info) return null
+  return info
+})
+const boardTopStyle = computed(() => {
+  const style_ = { "border-top-color": "rgb(232, 244, 255)" }
+  if (props.color) { style_["border-top-color"] = props.color }
+  return style_
+})
+
+</script>
+
+<style scoped>
+:deep(.el-card__body) {
+  padding: 0;
+}
+.metric-card {
+  padding: 12px 8px;
+  height: 100px;
+
+  position: relative;
+  min-width: 150px;
+  overflow-y: hidden;
+  line-height: 1.4;
+  background-color: #fff;
+  border-radius: 10px;
+  box-shadow: 0 0 12px rgba(51,89,181,.1607843137254902);
+  cursor: pointer;
+  flex-grow: 1;
+}
+.metric-card__color {
+  position: absolute;
+  top: 0;
+  left: 8px;
+  width: calc(100% - 16px);
+  height: 0;
+  border-top: 4px solid #86909c;
+  border-left: 2px solid transparent;
+  border-right: 2px solid transparent;
+}
+.metric-value {
+  padding: 8px 0;
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 25px;
+}
+
+.metric-pre {
+  color: #6b7785;
+  font-size: 12px;
+  white-space: nowrap;
+}
+.red {
+  color: red;
+}
+.green {
+  color: #1cbc0e;
+}
+</style>

+ 49 - 0
src/components/TextSelector/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <el-dropdown class="el-dropdown-link" @command="handleCommand" trigger="click">
+    <span @click.stop>
+      {{ displayLabel }}
+      <el-icon>
+        <arrow-down />
+      </el-icon>
+    </span>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item v-for="info in options" :command="info.value" :disabled="info.disabled"> {{ info.label }}</el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+
+interface Props {
+  modelValue: string,
+  options: MetricOptions[]
+}
+const props = defineProps<Props>()
+const data = ref(props.modelValue)
+const emits = defineEmits(['update:modelValue', 'change'])
+const displayLabel = computed(() => {
+  const info = props.options.find(item => item.value === data.value)
+  if(!info) return ''
+  return info.label ? info.label: data.value
+})
+const handleCommand = (command: string) => {
+  if (command === data.value) return
+  const oldVal = data.value
+  data.value = command
+  emits('update:modelValue', command)
+  emits('change', oldVal, command)
+}
+
+</script>
+
+<style scoped>
+.el-dropdown-link {
+  cursor: pointer;
+  color: var(--el-color-primary);
+  /* display: flex;
+  align-items: center; */
+}
+</style>

+ 2 - 2
src/theme/fastCrud.scss

@@ -1,5 +1,5 @@
 .fs-page {
   background-color: rgb(255, 255, 255);
-    border-radius: 10px;
-    height: 88vh !important;
+  border-radius: 10px;
+  height: 88vh !important;
 }

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

@@ -334,3 +334,15 @@ declare type TableDemoState = {
 		param: EmptyObjectType;
 	};
 };
+
+declare interface MetricOptions {
+  label: string
+  value: string
+  disabled?: boolean
+}
+
+declare interface MetricData extends MetricOptions {
+  metricVal: string,
+  preVal?: string,
+  gapVal?: string
+}

+ 23 - 31
src/views/demo/index.vue

@@ -1,40 +1,32 @@
 <template>
-  <div>
-    <DateRangePicker
-      v-model="dateRange"
-      timezone="America/Los_Angeles"
-      @change="changedValue">
-    </DateRangePicker>
-     <p>{{ dateRange }}</p>
-     <!-- <tableSelector v-model="selectedData" :tableConfig="tableConfig"></tableSelector> -->
-     <!-- {{ selectedData }} -->
-     <shopInfo></shopInfo>
+  <div class="test">
+    <MetricsCards v-model="selectedVal" :metric-items="options"></MetricsCards>
+    <p>{{ selectedVal }}</p>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref } from 'vue'
-import DateRangePicker from '/@/components/DateRangePicker/index.vue'
-import shopInfo from '/@/components/shopSelector/index.vue'
-// import tableSelector from '/@/components/tableSelector/index.vue'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import MCard from '/@/components/MetricsCards/mCard.vue'
 
-const dateRange = ref([])
-function changedValue(newVal: string[]) {
-  // dateRange.value = value
-  console.log(newVal)
-}
-
-// 测试表格选择器
-// const selectedData = ref("")
-// const tableConfig = ref({
-//   url: '/api/ad_manage/portfolios/',
-//   label: 'name',
-//   value: 'portfolioId',
-//   // data: [],
-//   columns: [
-//     { prop:'portfolioId',  label: '广告组合ID' },
-//     { prop:'name',  label: '广告组合' },
-//   ]
-// })
+const selectedVal = ref([{metric: 'ACOS', color: 'blue'}])
+const options = ref([
+  {label: 'ACOS', value: 'ACOS', metricVal: "18.00%", preVal: '20.15%', gapVal: '-2.00%', disabled:true},
+  {label: '点击量', value: 'clicks', metricVal: "19.00%", preVal: '20.15%', gapVal: '-1.00%', disabled:true},
+  {label: '曝光量', value: 'impression', metricVal: "20.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率1', value: 'rate1', metricVal: "1.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率2', value: 'rate2', metricVal: "2.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率3', value: 'rate3', metricVal: "3.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率4', value: 'rate4', metricVal: "4.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率5', value: 'rate5', metricVal: "5.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率6', value: 'rate6', metricVal: "6.00%", preVal: '15.00%', gapVal: '5.00%'},
+])
 
 </script>
+
+<style scoped>
+.test {
+  padding: 10px;
+}
+</style>