From 3a68c4380090ce1ded8941ef30d22a8a576ca6a3 Mon Sep 17 00:00:00 2001
From: mayf <m13160102112@163.com>
Date: 星期二, 03 九月 2024 17:34:07 +0800
Subject: [PATCH] 优惠券,会员等级,积分商城dev

---
 components/coupon/member-rule.vue                 |  125 +++
 package-lock.json                                 |   14 
 pages/marketing/coupon/member/index.vue           |   80 ++
 components/base-sidebar.vue                       |   29 
 pages/marketing/coupon/activity/index.vue         |  259 ++++++
 pages/marketing/member-level.vue                  |  146 +++
 pages/marketing/point-mall/point-distribution.vue |  224 +++++
 config/default-dev.json5                          |    2 
 utils/coupon-form.js                              |  173 ++++
 pages/marketing/point-mall/goods.vue              |  242 ++++++
 pages/marketing/coupon/activity/_id.vue           |   72 +
 assets/coupon/detail.scss                         |   11 
 components/coupon/select-shop-user.vue            |  187 ++++
 pages/marketing/coupon/member/_id.vue             |   66 +
 pages/marketing/point-mall/coupon/index.vue       |  238 ++++++
 pages/marketing/coupon/user/_id.vue               |   65 +
 package.json                                      |    4 
 pages/marketing/coupon/user/index.vue             |  138 +++
 pages/marketing/point-mall/coupon/_id.vue         |   64 +
 components/input-select.vue                       |  129 +++
 plugins/mixins/coupon-detail.vue                  |   27 
 21 files changed, 2,267 insertions(+), 28 deletions(-)

diff --git a/assets/coupon/detail.scss b/assets/coupon/detail.scss
new file mode 100644
index 0000000..6d60d03
--- /dev/null
+++ b/assets/coupon/detail.scss
@@ -0,0 +1,11 @@
+.coupon-detail {
+  border-radius: 0;
+  .el-bus-title {
+    margin-bottom: 10px;
+  }
+  ::v-deep {
+    .el-form-item {
+      margin-bottom: 0;
+    }
+  }
+}
diff --git a/components/base-sidebar.vue b/components/base-sidebar.vue
index f528164..fd736c8 100644
--- a/components/base-sidebar.vue
+++ b/components/base-sidebar.vue
@@ -162,26 +162,19 @@
         ::v-deep .el-submenu__icon-arrow {
           color: #fff;
         }
-        .el-submenu {
-          @for $i from 1 through 4 {
-            .el-menu-item {
-              padding-left: 64px !important;
+        .el-menu-item {
+          @for $i from 2 through 4 {
+            &.el-bus-level-#{$i} {
+              padding-left: 20px * $i + 24px !important;
             }
-            .el-submenu.level-#{$i} {
-              ::v-deep {
-                .el-submenu__title {
-                  @if $i==0 {
-                    padding-left: 20px !important;
-                  } @else {
-                    padding-left: 32px * ($i + 1) !important;
-                  }
-                }
-              }
+          }
+        }
+        .el-menu {
+          @for $i from 2 through 4 {
+            &:has(.el-submenu.el-bus-level-#{$i}) {
               .el-menu-item {
-                @if $i==0 {
-                  padding-left: 64px !important;
-                } @else {
-                  padding-left: 32px * ($i + 1) + 10px !important;
+                &.el-bus-level-#{$i} {
+                  padding-left: 20px * $i !important;
                 }
               }
             }
diff --git a/components/coupon/member-rule.vue b/components/coupon/member-rule.vue
new file mode 100644
index 0000000..948e2d6
--- /dev/null
+++ b/components/coupon/member-rule.vue
@@ -0,0 +1,125 @@
+<template>
+  <div class="member-rule">
+    <div class="mr-8">消费</div>
+    <el-input-number
+      v-model="amountValue"
+      v-bind="inputAttrs"
+      @input="onAmountChange"
+    ></el-input-number>
+    <div class="mx-8">元等于</div>
+    <el-input-number
+      v-model="growthValue"
+      v-bind="inputAttrs"
+      @input="onGrowthChange"
+    ></el-input-number>
+    <div class="ml-8">成长值</div>
+  </div>
+</template>
+
+<script>
+import { t } from 'el-business'
+import utils from 'el-business-utils'
+
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  rules(item) {
+    const errorMsg = `${t('elBus.common.pleaseEnter')}${
+      item.label ? item.label.replace(/:/g, '').replace(/:/g, '') : ''
+    }`
+    return [
+      {
+        required: true,
+        message: errorMsg,
+      },
+      {
+        validator: (rule, value, callback) => {
+          if (
+            Array.isArray(value) &&
+            value.filter((i) => !utils.isTrueEmpty(i)).length === 2
+          ) {
+            callback()
+          } else {
+            callback(new Error(errorMsg))
+          }
+        },
+      },
+    ]
+  },
+  inputFormat(row, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 2
+    ) {
+      const amount = item.commonFormatProps[0]
+      const growth = item.commonFormatProps[1]
+      if (amount in row || growth in row) {
+        return [row[amount], row[growth]]
+      }
+    } else {
+      console.warn('please set commonFormatProps')
+    }
+  },
+  outputFormat(val, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 2
+    ) {
+      const amount = item.commonFormatProps[0]
+      const growth = item.commonFormatProps[1]
+      return {
+        [amount]: !utils.isTrueEmpty(val?.[0]) ? val[0] : null,
+        [growth]: !utils.isTrueEmpty(val?.[1]) ? val[1] : null,
+      }
+    } else {
+      console.warn('please set commonFormatProps')
+    }
+  },
+  data() {
+    return {
+      amountValue: null,
+      growthValue: null,
+      inputAttrs: {
+        precision: 0,
+        min: 0,
+        controls: false,
+      },
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        value = value || []
+        this.amountValue = value?.[0] || undefined
+        this.growthValue = value?.[1] || undefined
+      },
+    },
+  },
+  methods: {
+    onAmountChange(e) {
+      const value = [e, this.growthValue]
+      this.emitValue(value)
+    },
+    onGrowthChange(e) {
+      const value = [this.amountValue, e]
+      this.emitValue(value)
+    },
+    emitValue(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.member-rule {
+  display: flex;
+  align-items: center;
+}
+</style>
diff --git a/components/coupon/select-shop-user.vue b/components/coupon/select-shop-user.vue
new file mode 100644
index 0000000..c3da491
--- /dev/null
+++ b/components/coupon/select-shop-user.vue
@@ -0,0 +1,187 @@
+<template>
+  <div class="select-shop-user">
+    <el-button v-if="!disabled" type="primary" @click="chooseUser"
+      >指定用户</el-button
+    >
+    <el-table v-if="value && value.length > 0" :data="value">
+      <el-table-column label="id" prop="id"></el-table-column>
+      <el-table-column label="店铺名称" prop="name"></el-table-column>
+      <el-table-column label="联系方式" prop="tel"></el-table-column>
+      <el-table-column v-if="!disabled" label="操作">
+        <template #default="{ $index }">
+          <text-button type="danger" @click="deleteUser($index)"
+            >删除</text-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-dialog
+      :visible.sync="dialogVisible"
+      title="指定用户"
+      append-to-body
+      :close-on-click-modal="false"
+      custom-class="shop-user-dialog"
+      width="80%"
+    >
+      <div class="dialog-container">
+        <div class="dialog-container__list">
+          <el-bus-crud v-bind="tableConfig" />
+        </div>
+        <div class="dialog-container__selected">
+          <el-bus-title title="已添加用户" size="mini" />
+          <el-tag
+            v-for="(tag, i) in currentValue"
+            :key="tag.id"
+            closable
+            @close="deleteCurrentUser(i)"
+            >{{ tag.name }}</el-tag
+          >
+        </div>
+      </div>
+      <div slot="footer">
+        <el-button @click="cancel">取消</el-button>
+        <el-button type="primary" @click="confirm">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import cloneDeep from 'lodash.clonedeep'
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      currentValue: [],
+      tableConfig: {
+        url: 'flower/api/customer/page',
+        saveQuery: false,
+        hasEdit: false,
+        hasDelete: false,
+        hasView: false,
+        columns: [
+          { label: 'id', prop: 'id' },
+          { label: '店铺名称', prop: 'name' },
+          { label: '联系方式', prop: 'tel' },
+        ],
+        extraButtons: [
+          {
+            text: '选择',
+            show: (row) =>
+              !this.currentValue.find((item) => item.id === row.id),
+            atClick: (row) => {
+              this.currentValue.push({
+                id: row.id,
+                name: row.name,
+                tel: row.tel,
+              })
+              return false
+            },
+          },
+          {
+            text: '取消选择',
+            show: (row) => this.currentValue.find((item) => item.id === row.id),
+            atClick: (row) => {
+              const index = this.currentValue.findIndex(
+                (item) => item.id === row.id
+              )
+              this.currentValue.splice(index, 1)
+              return false
+            },
+          },
+        ],
+        searchFormAttrs: {
+          labelWidth: 'auto',
+        },
+        searchForm: [
+          {
+            type: 'row',
+            span: 12,
+            items: [
+              { label: 'id:', id: 'id', type: 'input' },
+              { label: '店铺名称:', id: 'name', type: 'input' },
+              { label: '联系方式:', id: 'tel', type: 'input' },
+            ],
+          },
+        ],
+      },
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = cloneDeep(value || [])
+      },
+    },
+  },
+  methods: {
+    chooseUser() {
+      this.currentValue = cloneDeep(this.value || [])
+      this.dialogVisible = true
+    },
+    deleteCurrentUser(i) {
+      this.currentValue.splice(i, 1)
+    },
+    deleteUser(i) {
+      this.$elBusUtil
+        .confirm('确定要删除吗?')
+        .then(() => {
+          const userList = cloneDeep(this.value || [])
+          userList.splice(i, 1)
+          this.$emit('input', userList)
+          this.$emit('change', userList)
+        })
+        .catch(() => {})
+    },
+    confirm() {
+      this.$emit('input', this.currentValue)
+      this.$emit('change', this.currentValue)
+      this.dialogVisible = false
+    },
+    cancel() {
+      this.dialogVisible = false
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.select-shop-user {
+}
+</style>
+<style lang="scss">
+.shop-user-dialog {
+  .dialog-container {
+    display: flex;
+    align-items: flex-start;
+    &__list {
+      flex: 1;
+      border-right: 1px solid #eee;
+      height: 100%;
+    }
+    &__selected {
+      width: 40%;
+      height: 100%;
+      padding: 24px;
+      .el-bus-title {
+        margin-bottom: 15px;
+      }
+      .el-tag {
+        margin-right: 6px;
+        margin-bottom: 6px;
+      }
+    }
+  }
+}
+</style>
diff --git a/components/input-select.vue b/components/input-select.vue
new file mode 100644
index 0000000..c9aff80
--- /dev/null
+++ b/components/input-select.vue
@@ -0,0 +1,129 @@
+<template>
+  <div class="input-select">
+    <el-input-number
+      v-model="inputValue"
+      v-bind="inputAttrs"
+      class="input-select__input"
+      @input="onInputChange"
+    />
+    <el-bus-select-dict
+      v-model="selectValue"
+      v-bind="selectAttrs"
+      class="input-select__select"
+      @change="onSelectChange"
+    />
+  </div>
+</template>
+
+<script>
+import { t } from 'el-business'
+import utils from 'el-business-utils'
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    inputAttrs: {
+      type: Object,
+      default: () => ({}),
+    },
+    selectAttrs: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  rules(item) {
+    const errorMsg = `${t('elBus.common.pleaseEnter')}${
+      item.label ? item.label.replace(/:/g, '').replace(/:/g, '') : ''
+    }`
+    return [
+      {
+        required: true,
+        message: errorMsg,
+      },
+      {
+        validator: (rule, value, callback) => {
+          if (
+            Array.isArray(value) &&
+            value.filter((i) => !utils.isTrueEmpty(i)).length === 2
+          ) {
+            callback()
+          } else {
+            callback(new Error(errorMsg))
+          }
+        },
+      },
+    ]
+  },
+  inputFormat(row, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 2
+    ) {
+      const input = item.commonFormatProps[0]
+      const select = item.commonFormatProps[1]
+      if (select in row || input in row) {
+        return [row[input], row[select]]
+      }
+    } else {
+      console.warn('please set commonFormatProps')
+    }
+  },
+  outputFormat(val, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 2
+    ) {
+      const input = item.commonFormatProps[0]
+      const select = item.commonFormatProps[1]
+      return {
+        [input]: !utils.isTrueEmpty(val?.[0]) ? val[0] : null,
+        [select]: !utils.isTrueEmpty(val?.[1]) ? val[1] : null,
+      }
+    } else {
+      console.warn('please set commonFormatProps')
+    }
+  },
+  data() {
+    return {
+      inputValue: null,
+      selectValue: null,
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        value = value || []
+        this.inputValue = value?.[0] || undefined
+        this.selectValue = value?.[1] || null
+      },
+    },
+  },
+  methods: {
+    onInputChange(e) {
+      const value = [e, this.selectValue]
+      this.emitValue(value)
+    },
+    onSelectChange(e) {
+      const value = [this.inputValue, e]
+      this.emitValue(value)
+    },
+    emitValue(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.input-select {
+  display: flex;
+  align-items: center;
+  &__input {
+    margin-right: 8px;
+  }
+}
+</style>
diff --git a/config/default-dev.json5 b/config/default-dev.json5
index 6dfcbf5..36520fe 100644
--- a/config/default-dev.json5
+++ b/config/default-dev.json5
@@ -1,3 +1,3 @@
 {
-  httpBaseUri: 'http://106.14.123.210:8080',
+  httpBaseUri: 'http://localhost:8080',
 }
diff --git a/package-lock.json b/package-lock.json
index c46d66c..8e00304 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
         "css-vars-ponyfill": "^2.4.8",
         "dayjs": "^1.11.10",
         "echarts": "^5.5.0",
-        "el-business": "^1.1.21",
+        "el-business": "^1.1.22",
         "el-business-cache-utils": "^1.0.0",
         "el-business-cookie": "^1.0.0",
         "el-business-http": "^1.1.4",
@@ -9371,9 +9371,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/el-business": {
-      "version": "1.1.21",
-      "resolved": "https://registry.npmjs.org/el-business/-/el-business-1.1.21.tgz",
-      "integrity": "sha512-rtMLZ1o0M/hMPYi1/mNPgb5LI0DYOSY7jhJwIueocuHw3+yt3G8gPmZ8eP383AsRbvouOMBwq7EwoO9u4b9nFA==",
+      "version": "1.1.22",
+      "resolved": "https://registry.npmjs.org/el-business/-/el-business-1.1.22.tgz",
+      "integrity": "sha512-l5IUsX6HKgAZCusej+pybry+XQCqWwGYhtKGzU8D+hjBp3/46rti+ajeO2ffDyTMydl1w9FqE3tgPHuNRHeLXA==",
       "dependencies": {
         "axios": "^0.19.2",
         "cookie-universal": "^2.1.4",
@@ -30591,9 +30591,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "el-business": {
-      "version": "1.1.21",
-      "resolved": "https://registry.npmjs.org/el-business/-/el-business-1.1.21.tgz",
-      "integrity": "sha512-rtMLZ1o0M/hMPYi1/mNPgb5LI0DYOSY7jhJwIueocuHw3+yt3G8gPmZ8eP383AsRbvouOMBwq7EwoO9u4b9nFA==",
+      "version": "1.1.22",
+      "resolved": "https://registry.npmjs.org/el-business/-/el-business-1.1.22.tgz",
+      "integrity": "sha512-l5IUsX6HKgAZCusej+pybry+XQCqWwGYhtKGzU8D+hjBp3/46rti+ajeO2ffDyTMydl1w9FqE3tgPHuNRHeLXA==",
       "requires": {
         "axios": "^0.19.2",
         "cookie-universal": "^2.1.4",
diff --git a/package.json b/package.json
index 1468c89..d0d17c3 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "scripts": {
-    "start": "cross-env NODE_APP_INSTANCE=test NODE_ENV=development nuxt",
+    "start": "cross-env NODE_APP_INSTANCE=dev NODE_ENV=development nuxt",
     "build:dev": "cross-env NODE_APP_INSTANCE=dev NODE_ENV=production nuxt build",
     "build:prod": "cross-env NODE_APP_INSTANCE=prod NODE_ENV=production nuxt build",
     "launch": "nuxt start",
@@ -25,7 +25,7 @@
     "css-vars-ponyfill": "^2.4.8",
     "dayjs": "^1.11.10",
     "echarts": "^5.5.0",
-    "el-business": "^1.1.21",
+    "el-business": "^1.1.22",
     "el-business-cache-utils": "^1.0.0",
     "el-business-cookie": "^1.0.0",
     "el-business-http": "^1.1.4",
diff --git a/pages/marketing/coupon/activity/_id.vue b/pages/marketing/coupon/activity/_id.vue
new file mode 100644
index 0000000..864d8d9
--- /dev/null
+++ b/pages/marketing/coupon/activity/_id.vue
@@ -0,0 +1,72 @@
+<template>
+  <div class="base-page-wrapper coupon-detail">
+    <el-bus-title title="优惠券信息" size="small" />
+    <el-bus-form
+      ref="form"
+      label-width="auto"
+      :content="formContent"
+      readonly
+    />
+    <div class="base-page-wrapper__line"></div>
+    <el-bus-title title="领取记录" size="small" />
+    <el-bus-crud v-bind="recordTableConfig" />
+    <div class="text-center mt-20">
+      <el-button class="min-w-100" @click="goBack">返回</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  couponForm,
+  couponRecordColumn,
+  recordTableConfig,
+  getActivityReceiveTime,
+  getActivityEffectiveTime,
+} from '@/utils/coupon-form'
+import CouponDetail from '@/plugins/mixins/coupon-detail.vue'
+export default {
+  mixins: [CouponDetail],
+  data() {
+    return {
+      detailUrl: `flower/api/v2/coupon/avtivy`,
+      formContent: [
+        {
+          type: 'row',
+          items: [
+            ...couponForm(),
+            { label: '总数:', id: 'couponAmount', type: 'input' },
+            { label: '已领取:', id: 'getNum', type: 'input' },
+            { label: '剩余:', id: 'unGetNum', type: 'input' },
+            {
+              label: '领取时间:',
+              id: 'getStartDate',
+              inputFormat: (row) => getActivityReceiveTime(row),
+              span: 24,
+            },
+            {
+              label: '有效期:',
+              id: 'usageStartDate',
+              inputFormat: (row) => getActivityEffectiveTime(row),
+              span: 24,
+            },
+          ],
+        },
+      ],
+      recordTableConfig: {
+        ...recordTableConfig(this.$route.params.id),
+        columns: [...couponRecordColumn()],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '活动优惠券详情',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/assets/coupon/detail.scss';
+</style>
diff --git a/pages/marketing/coupon/activity/index.vue b/pages/marketing/coupon/activity/index.vue
new file mode 100644
index 0000000..1dd3278
--- /dev/null
+++ b/pages/marketing/coupon/activity/index.vue
@@ -0,0 +1,259 @@
+<template>
+  <el-bus-crud v-bind="tableConfig" />
+</template>
+
+<script>
+import InputSelect from '@/components/input-select'
+import {
+  couponForm,
+  couponSearchForm,
+  getActivityEffectiveTime,
+  getActivityReceiveTime,
+  couponColumn,
+} from '@/utils/coupon-form'
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/v2/coupon/avtivy/page',
+        newUrl: 'flower/api/v2/coupon/avtivy',
+        viewUrl: 'flower/api/v2/coupon/avtivy',
+        viewOnPath: true,
+        editUrl: 'flower/api/v2/coupon/avtivy',
+        editMethodType: 'put',
+        editOnPath: true,
+        deleteUrl: 'flower/api/v2/coupon/avtivy',
+        deleteMethodType: 'delete',
+        deleteOnPath: true,
+        canEdit: (row) => row.status === 'inactive' || row.status === 'expired',
+        canDelete: (row) =>
+          row.status === 'inactive' || row.status === 'expired',
+        operationAttrs: {
+          width: 180,
+          fixed: 'right',
+        },
+        onResetView: (row) => {
+          this.$router.push(`${this.$route.path}/${row.id}`)
+        },
+        beforeOpen: (row, isNew) => {
+          if (!isNew) {
+            row.usageTypeStr = getActivityEffectiveTime(row)
+            row.getStartDateStr = getActivityReceiveTime(row)
+          }
+        },
+        columns: [
+          ...couponColumn(),
+          {
+            label: '领取时间',
+            formatter: getActivityReceiveTime,
+            minWidth: 320,
+          },
+          {
+            label: '有效期',
+            formatter: getActivityEffectiveTime,
+            minWidth: 320,
+          },
+          { label: '已领取总数', prop: 'getNum', minWidth: 150 },
+          { label: '剩余未领取总数', prop: 'unGetNum', minWidth: 150 },
+          { label: '状态', prop: 'statusName', minWidth: 120 },
+          { label: '操作人', prop: 'createByName', minWidth: 120 },
+        ],
+        // 页面上要到分,后端要传到秒
+        beforeConfirm: (data) => {
+          if (data.getStartDate) {
+            data.getStartDate = this.$elBusUtil.formatDate(
+              data.getStartDate,
+              'YYYY-MM-DD HH:mm:ss'
+            )
+          }
+          if (data.getEndDate) {
+            data.getEndDate = this.$elBusUtil.formatDate(
+              data.getEndDate,
+              'YYYY-MM-DD HH:mm:ss'
+            )
+          }
+          if (data.usageStartDate) {
+            data.usageStartDate = this.$elBusUtil.formatDate(
+              data.usageStartDate,
+              'YYYY-MM-DD HH:mm:ss'
+            )
+          }
+          if (data.usageEndDate) {
+            data.usageEndDate = this.$elBusUtil.formatDate(
+              data.usageEndDate,
+              'YYYY-MM-DD HH:mm:ss'
+            )
+          }
+        },
+        searchForm: [
+          {
+            type: 'row',
+            items: [...couponSearchForm()],
+          },
+        ],
+        form: [
+          ...couponForm(),
+          {
+            label: '领取渠道:',
+            id: 'getType',
+            type: 'bus-select-dict',
+            el: {
+              code: 'COUPON_GET_TYPE',
+              style: 'width:100%',
+            },
+            str: true,
+            strKey: 'getTypeName',
+            rules: { required: true, message: '请选择领取渠道' },
+          },
+          {
+            label: '领取时间:',
+            id: 'getStartDate',
+            component: 'el-bus-date-range',
+            el: {
+              type: 'datetime',
+              format: 'yyyy-MM-dd HH:mm',
+              valueFormat: 'yyyy-MM-dd HH:mm',
+            },
+            str: true,
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['getStartDate', 'getEndDate'],
+          },
+          {
+            label: '使用时间:',
+            id: 'usageType',
+            type: 'bus-select-dict',
+            el: {
+              code: 'COUPON_USAGE_TYPE',
+              style: 'width:100%',
+            },
+            str: true,
+            rules: { required: true, message: '请选择使用时间' },
+            on: {
+              change: (e, updateForm) => {
+                updateForm({
+                  usageStartDate: null,
+                  usageEndDate: null,
+                  usageTimeNum: undefined,
+                  usageTimeType: null,
+                })
+              },
+            },
+          },
+          {
+            label: '使用固定时间:',
+            id: 'usageStartDate',
+            component: 'el-bus-date-range',
+            el: {
+              type: 'datetime',
+              format: 'yyyy-MM-dd HH:mm',
+              valueFormat: 'yyyy-MM-dd HH:mm',
+            },
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['usageStartDate', 'usageEndDate'],
+            hidden: (row, item, mode) =>
+              row.usageType !== 'fixed' || mode === 'view',
+          },
+          {
+            label: '领取后有效时间:',
+            id: 'usageTimeNum',
+            component: InputSelect,
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['usageTimeNum', 'usageTimeType'],
+            el: {
+              inputAttrs: {
+                min: 1,
+                precision: 0,
+                controls: false,
+              },
+              selectAttrs: {
+                code: 'COUPON_USAGE_TIME_TYPE',
+              },
+            },
+            hidden: (row, item, mode) =>
+              row.usageType !== 'get_after_time' || mode === 'view',
+          },
+          {
+            label: '发放数量:',
+            id: 'couponAmount',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 1,
+              controls: false,
+            },
+            unit: '张',
+            rules: {
+              required: true,
+              message: '请输入发放数量',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '每人限领:',
+            id: 'getLimit',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 1,
+              controls: false,
+            },
+            unit: '张',
+            rules: {
+              required: true,
+              message: '请输入每人限领',
+              trigger: 'blur',
+            },
+          },
+        ],
+        extraButtons: [
+          {
+            text: '发布',
+            show: (row) =>
+              row.status === 'inactive' || row.status === 'expired',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要发布吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/avtivy/active/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('发布成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '下架',
+            show: (row) => row.status === 'active',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要下架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/avtivy/expire/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '活动优惠券',
+    }
+  },
+}
+</script>
diff --git a/pages/marketing/coupon/member/_id.vue b/pages/marketing/coupon/member/_id.vue
new file mode 100644
index 0000000..4084dd5
--- /dev/null
+++ b/pages/marketing/coupon/member/_id.vue
@@ -0,0 +1,66 @@
+<template>
+  <div class="base-page-wrapper coupon-detail">
+    <el-bus-title title="优惠券信息" size="small" />
+    <el-bus-form
+      ref="form"
+      label-width="auto"
+      :content="formContent"
+      readonly
+    />
+    <div class="base-page-wrapper__line"></div>
+    <el-bus-title title="发放记录" size="small" />
+    <el-bus-crud v-bind="recordTableConfig" />
+    <div class="text-center mt-20">
+      <el-button class="min-w-100" @click="goBack">返回</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  couponForm,
+  couponRecordColumn,
+  recordTableConfig,
+} from '@/utils/coupon-form'
+import CouponDetail from '@/plugins/mixins/coupon-detail.vue'
+export default {
+  mixins: [CouponDetail],
+  data() {
+    return {
+      detailUrl: `flower/api/v2/coupon/vip`,
+      formContent: [
+        {
+          type: 'row',
+          items: [
+            ...couponForm(),
+            { label: '已发放总数:', id: 'todo' },
+            {
+              label: '有效期:',
+              id: 'usageStartDate',
+              inputFormat: (row) => {
+                return row.usageStartDate
+                  ? `${row.usageStartDate} ~ ${row.usageEndDate || ''}`
+                  : ''
+              },
+              span: 24,
+            },
+          ],
+        },
+      ],
+      recordTableConfig: {
+        ...recordTableConfig(this.$route.params.id),
+        columns: [...couponRecordColumn()],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '会员优惠券详情',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/assets/coupon/detail.scss';
+</style>
diff --git a/pages/marketing/coupon/member/index.vue b/pages/marketing/coupon/member/index.vue
new file mode 100644
index 0000000..f11da18
--- /dev/null
+++ b/pages/marketing/coupon/member/index.vue
@@ -0,0 +1,80 @@
+<template>
+  <el-bus-crud v-bind="tableConfig" />
+</template>
+
+<script>
+import { couponForm, couponSearchForm, couponColumn } from '@/utils/coupon-form'
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/v2/coupon/vip/page',
+        newUrl: 'flower/api/v2/coupon/vip',
+        viewUrl: 'flower/api/v2/coupon/vip',
+        viewOnPath: true,
+        editUrl: 'flower/api/v2/coupon/vip',
+        editMethodType: 'put',
+        editOnPath: true,
+        deleteUrl: 'flower/api/v2/coupon/vip',
+        deleteMethodType: 'delete',
+        deleteOnPath: true,
+        onResetView: (row) => {
+          this.$router.push(`${this.$route.path}/${row.id}`)
+        },
+        operationAttrs: {
+          width: 140,
+          fixed: 'right',
+        },
+        columns: [
+          ...couponColumn(),
+          { label: '会员等级', prop: 'memberName' },
+          {
+            label: '有效期',
+            formatter: (row) =>
+              row.usageStartDate
+                ? `${row.usageStartDate || ''} ~ ${row.usageEndDate || ''}`
+                : '',
+            minWidth: 320,
+          },
+          { label: '已发放数量', prop: 'getNum', minWidth: 120 },
+          { label: '操作人', prop: 'createByName', minWidth: 120 },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [...couponSearchForm()],
+          },
+        ],
+        form: [
+          {
+            label: '会员等级:',
+            id: 'memberId',
+            type: 'bus-select',
+            el: {
+              interfaceUri: 'flower/api/member/list',
+              props: {
+                label: 'name',
+                value: 'id',
+                dataPath: 'records',
+              },
+              extraQuery: {
+                current: 1,
+                size: 2000,
+              },
+              filterable: true,
+              style: 'width:100%',
+            },
+            rules: { required: true, message: '请选择会员等级' },
+          },
+          ...couponForm(),
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '会员优惠券',
+    }
+  },
+}
+</script>
diff --git a/pages/marketing/coupon/user/_id.vue b/pages/marketing/coupon/user/_id.vue
new file mode 100644
index 0000000..a660b02
--- /dev/null
+++ b/pages/marketing/coupon/user/_id.vue
@@ -0,0 +1,65 @@
+<template>
+  <div class="base-page-wrapper coupon-detail">
+    <el-bus-title title="优惠券信息" size="small" />
+    <el-bus-form
+      ref="form"
+      label-width="auto"
+      :content="formContent"
+      readonly
+    />
+    <div class="base-page-wrapper__line"></div>
+    <el-bus-title title="发放记录" size="small" />
+    <el-bus-crud v-bind="recordTableConfig" />
+    <div class="text-center mt-20">
+      <el-button class="min-w-100" @click="goBack">返回</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  couponForm,
+  couponRecordColumn,
+  recordTableConfig,
+} from '@/utils/coupon-form'
+import CouponDetail from '@/plugins/mixins/coupon-detail.vue'
+export default {
+  mixins: [CouponDetail],
+  data() {
+    return {
+      detailUrl: `flower/api/v2/coupon/user`,
+      formContent: [
+        {
+          type: 'row',
+          items: [
+            ...couponForm(),
+            {
+              label: '有效期:',
+              id: 'usageStartDate',
+              inputFormat: (row) => {
+                return row.usageStartDate
+                  ? `${row.usageStartDate} ~ ${row.usageEndDate || ''}`
+                  : ''
+              },
+              span: 24,
+            },
+          ],
+        },
+      ],
+      recordTableConfig: {
+        ...recordTableConfig(this.$route.params.id),
+        columns: [...couponRecordColumn()],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '用户优惠券详情',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/assets/coupon/detail.scss';
+</style>
diff --git a/pages/marketing/coupon/user/index.vue b/pages/marketing/coupon/user/index.vue
new file mode 100644
index 0000000..ce68f1e
--- /dev/null
+++ b/pages/marketing/coupon/user/index.vue
@@ -0,0 +1,138 @@
+<template>
+  <el-bus-crud v-bind="tableConfig" />
+</template>
+
+<script>
+import InputSelect from '@/components/input-select'
+import SelectShopUser from '@/components/coupon/select-shop-user'
+import { couponForm, couponSearchForm, couponColumn } from '@/utils/coupon-form'
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/v2/coupon/user/page',
+        newUrl: 'flower/api/v2/coupon/user',
+        viewUrl: 'flower/api/v2/coupon/user',
+        viewOnPath: true,
+        editUrl: 'flower/api/v2/coupon/user',
+        editMethodType: 'put',
+        editOnPath: true,
+        deleteUrl: 'flower/api/v2/coupon/user',
+        deleteMethodType: 'delete',
+        deleteOnPath: true,
+        dialogNeedRequest: true,
+        canEdit: (row) => row.status === 'inactive' || row.status === 'expired',
+        canDelete: (row) =>
+          row.status === 'inactive' || row.status === 'expired',
+        onResetView: (row) => {
+          this.$router.push(`${this.$route.path}/${row.id}`)
+        },
+        operationAttrs: {
+          width: 200,
+          fixed: 'right',
+        },
+        columns: [
+          ...couponColumn(),
+          { label: '发放时间', prop: 'usageStartDate', minWidth: 180 },
+          {
+            label: '有效期',
+            formatter: (row) =>
+              row.usageStartDate
+                ? `${row.usageStartDate} ~ ${row.usageEndDate || ''}`
+                : '',
+            minWidth: 400,
+          },
+          { label: '状态', prop: 'statusName', minWidth: 120 },
+          { label: '操作人', prop: 'createByName', minWidth: 120 },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [...couponSearchForm()],
+          },
+        ],
+        form: [
+          ...couponForm(),
+          {
+            label: '发放后有效期:',
+            id: 'usageTimeNum',
+            component: InputSelect,
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['usageTimeNum', 'usageTimeType'],
+            el: {
+              inputAttrs: {
+                min: 1,
+                precision: 0,
+                controls: false,
+              },
+              selectAttrs: {
+                code: 'COUPON_USAGE_TIME_TYPE',
+              },
+            },
+          },
+          {
+            label: '领取用户:',
+            id: 'pointCostomIdList',
+            component: SelectShopUser,
+            rules: { required: true, message: '请选择领取用户' },
+            inputFormat: (row) => {
+              if ('customerList' in row) {
+                return row.customerList
+              }
+            },
+            outputFormat: (val) => {
+              return val?.length ? val.map((i) => i.id) : []
+            },
+            forceDisabled: true,
+          },
+        ],
+        extraButtons: [
+          {
+            text: '发布',
+            show: (row) =>
+              row.status === 'inactive' || row.status === 'expired',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要发布吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/user/active/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('发布成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '下架',
+            show: (row) => row.status === 'active',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要下架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/user/expire/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '用户优惠券',
+    }
+  },
+}
+</script>
diff --git a/pages/marketing/member-level.vue b/pages/marketing/member-level.vue
new file mode 100644
index 0000000..8502649
--- /dev/null
+++ b/pages/marketing/member-level.vue
@@ -0,0 +1,146 @@
+<template>
+  <el-bus-crud v-bind="tableConfig" />
+</template>
+
+<script>
+import MemberRule from '@/components/coupon/member-rule.vue'
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/member/list',
+        newUrl: 'flower/api/member/new',
+        editUrl: 'flower/api/member/edit',
+        deleteUrl: 'flower/api/member/delete',
+        columns: [
+          { label: '序号', type: 'index' },
+          { label: '等级名称', prop: 'name' },
+          { label: '成长值', prop: 'startPoint' },
+          { label: '等级折扣', prop: 'discountTypeStr' },
+          { label: '操作人', prop: 'createName' },
+        ],
+        beforeOpen: (row, isNew) => {
+          if (!isNew) {
+            row.consumptionAmountStr = `消费${row.consumptionAmount}元等于${row.growthValue}成长值`
+            row.startPointStr = `${
+              this.$elBusUtil.isTrueEmpty(row.startPoint) ? '' : row.startPoint
+            } ~ ${
+              this.$elBusUtil.isTrueEmpty(row.endPoint) ? '' : row.endPoint
+            }`
+          }
+        },
+        searchForm: [
+          {
+            type: 'row',
+            items: [{ label: '等级名称:', id: 'name', type: 'input' }],
+          },
+        ],
+        form: [
+          {
+            label: '会员等级名称:',
+            id: 'name',
+            type: 'input',
+            rules: { required: true, message: '请输入会员等级名称' },
+          },
+          {
+            label: '成长值范围:',
+            id: 'startPoint',
+            component: 'el-bus-number-range',
+            el: {
+              unit: '',
+              separator: '<= 成长值范围 <',
+              inputAttrs: {
+                controls: false,
+              },
+            },
+            commonFormat: true,
+            commonFormatProps: ['startPoint', 'endPoint'],
+            commonRules: true,
+            commonRulesLevel: 1,
+            str: true,
+          },
+          {
+            label: '折扣类型:',
+            id: 'discountType',
+            type: 'bus-select-dict',
+            el: {
+              code: 'DISCOUNT_TYPE',
+              style: 'width:100%',
+            },
+            str: true,
+            rules: { required: true, message: '请选择折扣类型' },
+            on: {
+              change: (e, updateForm) => {
+                updateForm({
+                  discountRatio: undefined,
+                  discountAmount: undefined,
+                })
+              },
+            },
+          },
+          {
+            label: '会员折扣:',
+            id: 'discountRatio',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 0,
+              max: 100,
+              controls: false,
+            },
+            unit: '%',
+            rules: {
+              required: true,
+              message: '请输入会员折扣',
+              trigger: 'blur',
+            },
+            hidden: (row) => row.discountType !== 'ratio',
+          },
+          {
+            label: '会员优惠:',
+            id: 'discountAmount',
+            type: 'input-number',
+            el: {
+              precision: 2,
+              min: 0,
+              controls: false,
+            },
+            unit: '元',
+            rules: {
+              required: true,
+              message: '请输入会员优惠',
+              trigger: 'blur',
+            },
+            hidden: (row) => row.discountType !== 'amount',
+          },
+          {
+            label: '会员规则:',
+            id: 'consumptionAmount',
+            component: MemberRule,
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['consumptionAmount', 'growthValue'],
+            str: true,
+          },
+          {
+            label: '降级规则:',
+            id: 'downgradeValue',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 0,
+              controls: false,
+            },
+            unit: '成长值',
+            rules: {
+              required: true,
+              message: '请输入降级规则',
+              trigger: 'blur',
+            },
+          },
+        ],
+      },
+    }
+  },
+}
+</script>
diff --git a/pages/marketing/point-mall/coupon/_id.vue b/pages/marketing/point-mall/coupon/_id.vue
new file mode 100644
index 0000000..3fe87a6
--- /dev/null
+++ b/pages/marketing/point-mall/coupon/_id.vue
@@ -0,0 +1,64 @@
+<template>
+  <div class="base-page-wrapper coupon-detail">
+    <el-bus-title title="优惠券信息" size="small" />
+    <el-bus-form
+      ref="form"
+      label-width="auto"
+      :content="formContent"
+      readonly
+    />
+    <div class="base-page-wrapper__line"></div>
+    <el-bus-title title="兑换记录" size="small" />
+    <el-bus-crud v-bind="recordTableConfig" />
+    <div class="text-center mt-20">
+      <el-button class="min-w-100" @click="goBack">返回</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  couponForm,
+  couponRecordColumn,
+  recordTableConfig,
+} from '@/utils/coupon-form'
+import CouponDetail from '@/plugins/mixins/coupon-detail.vue'
+export default {
+  mixins: [CouponDetail],
+  data() {
+    return {
+      detailUrl: `flower/api/v2/coupon/point`,
+      formContent: [
+        {
+          type: 'row',
+          items: [
+            ...couponForm(),
+            {
+              label: '领取后有效时间:',
+              id: 'usageTimeNum',
+              inputFormat: (row) => {
+                return `${row.usageTimeNum}${row.usageTimeTypeName}`
+              },
+            },
+            { label: '库存:', id: 'couponAmount' },
+            { label: '积分数量:', id: 'point' },
+          ],
+        },
+      ],
+      recordTableConfig: {
+        ...recordTableConfig(this.$route.params.id),
+        columns: [...couponRecordColumn()],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '会员优惠券详情',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/assets/coupon/detail.scss';
+</style>
diff --git a/pages/marketing/point-mall/coupon/index.vue b/pages/marketing/point-mall/coupon/index.vue
new file mode 100644
index 0000000..d445c10
--- /dev/null
+++ b/pages/marketing/point-mall/coupon/index.vue
@@ -0,0 +1,238 @@
+<template>
+  <el-bus-crud v-bind="tableConfig" />
+</template>
+
+<script>
+import { couponForm, couponSearchForm, couponColumn } from '@/utils/coupon-form'
+import InputSelect from '@/components/input-select'
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/v2/coupon/point/page',
+        newUrl: 'flower/api/v2/coupon/point',
+        viewUrl: 'flower/api/v2/coupon/point',
+        viewOnPath: true,
+        editUrl: 'flower/api/v2/coupon/point',
+        editMethodType: 'put',
+        editOnPath: true,
+        deleteUrl: 'flower/api/v2/coupon/point',
+        deleteMethodType: 'delete',
+        deleteOnPath: true,
+        canEdit: (row) => row.status === 'inactive' || row.status === 'expired',
+        canDelete: (row) =>
+          row.status === 'inactive' || row.status === 'expired',
+        onResetView: (row) => {
+          this.$router.push(`${this.$route.path}/${row.id}`)
+        },
+        persistSelection: true,
+        operationAttrs: {
+          width: 160,
+          fixed: 'right',
+        },
+        columns: [
+          {
+            label: '',
+            type: 'selection',
+            minWidth: 60,
+          },
+          ...couponColumn(),
+          { label: '状态', prop: 'statusName', minWidth: 120 },
+          { label: '库存', prop: 'couponAmount', minWidth: 120 },
+          { label: '所需积分', prop: 'point', minWidth: 120 },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [...couponSearchForm()],
+          },
+        ],
+        form: [
+          ...couponForm(),
+          {
+            label: '领取后有效时间:',
+            id: 'usageTimeNum',
+            component: InputSelect,
+            commonRules: true,
+            commonFormat: true,
+            commonFormatProps: ['usageTimeNum', 'usageTimeType'],
+            el: {
+              inputAttrs: {
+                min: 1,
+                precision: 0,
+                controls: false,
+              },
+              selectAttrs: {
+                code: 'COUPON_USAGE_TIME_TYPE',
+              },
+            },
+          },
+          {
+            label: '库存:',
+            id: 'couponAmount',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 0,
+              controls: false,
+            },
+            rules: { required: true, message: '请输入库存', trigger: 'blur' },
+          },
+          {
+            label: '积分数量:',
+            id: 'point',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 1,
+              controls: false,
+            },
+            rules: {
+              required: true,
+              message: '请输入积分数量',
+              trigger: 'blur',
+            },
+          },
+        ],
+        extraButtons: [
+          {
+            text: '上架',
+            show: (row) =>
+              row.status === 'inactive' || row.status === 'expired',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要上架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/point/active/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('上架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '下架',
+            show: (row) => row.status === 'active',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要下架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  `flower/api/v2/coupon/point/expire/${row.id}`,
+                  { method: 'put' }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+        headerButtons: [
+          {
+            text: '批量上架',
+            type: 'primary',
+            disabled: (selected) =>
+              selected.filter(
+                (i) => i.status === 'inactive' || i.status === 'expired'
+              ).length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter(
+                  (i) => i.status === 'inactive' || i.status === 'expired'
+                )
+                await this.$elBusUtil.confirm(
+                  `确定要上架这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/v2/coupon/point/batch/active',
+                  {
+                    data: { idList: items.map((i) => i.id) },
+                    method: 'post',
+                  }
+                )
+                if (code === 0) {
+                  this.$message.success('上架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '批量下架',
+            type: 'primary',
+            disabled: (selected) =>
+              selected.filter((i) => i.status === 'active').length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter((i) => i.status === 'active')
+                await this.$elBusUtil.confirm(
+                  `确定要下架这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/v2/coupon/point/batch/expire',
+                  {
+                    data: { idList: items.map((i) => i.id) },
+                    method: 'post',
+                  }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '批量删除',
+            type: 'danger',
+            disabled: (selected) =>
+              selected.filter(
+                (i) => i.status === 'inactive' || i.status === 'expired'
+              ).length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter(
+                  (i) => i.status === 'inactive' || i.status === 'expired'
+                )
+                await this.$elBusUtil.confirm(
+                  `确定要删除这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/v2/coupon/point/batch/del',
+                  {
+                    data: { idList: items.map((i) => i.id) },
+                    method: 'post',
+                  }
+                )
+                if (code === 0) {
+                  this.$message.success('删除成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '积分优惠券',
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.index {
+}
+</style>
diff --git a/pages/marketing/point-mall/goods.vue b/pages/marketing/point-mall/goods.vue
new file mode 100644
index 0000000..0821523
--- /dev/null
+++ b/pages/marketing/point-mall/goods.vue
@@ -0,0 +1,242 @@
+<template>
+  <el-bus-crud ref="crud" v-bind="tableConfig" />
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/point/goods/list',
+        canEdit: (row) => row.status === 'I',
+        canDelete: (row) => row.status === 'I',
+        columns: [
+          { label: '', type: 'selection' },
+          { label: '序号', type: 'index' },
+          {
+            label: '商品图片',
+            formatter: (row) =>
+              row.cover ? (
+                <el-bus-image
+                  src={row.cover}
+                  lazy={true}
+                  style="width:50px;height:50px"
+                ></el-bus-image>
+              ) : null,
+          },
+          { label: '商品名称', prop: 'name' },
+          { label: '状态', prop: 'statusStr' },
+          { label: '库存', prop: 'stock' },
+          { label: '所需积分', prop: 'point' },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [
+              {
+                label: '商品名称',
+                id: 'name',
+                type: 'input',
+              },
+              {
+                label: '状态',
+                id: 'status',
+                type: 'bus-select-dict',
+                el: {
+                  code: 'POINT_GOODS_STATUS',
+                  style: 'width:100%',
+                  clearable: true,
+                },
+              },
+            ],
+          },
+        ],
+        form: [
+          {
+            label: '商品名称:',
+            id: 'name',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入商品名称',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '商品规格信息:',
+            id: 'description',
+            type: 'input',
+            el: {
+              type: 'textarea',
+              rows: 4,
+            },
+            rules: {
+              required: true,
+              message: '请输入商品规格信息',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '库存:',
+            id: 'stock',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 0,
+              controls: false,
+            },
+            rules: { required: true, message: '请输入库存', trigger: 'blur' },
+          },
+          {
+            label: '商品图片:',
+            id: 'cover',
+            type: 'bus-upload',
+            el: {
+              listType: 'picture-card',
+              limit: 1,
+              limitSize: 2,
+              tipText: '大小不超过2M',
+              valueType: 'string',
+            },
+            forceDisabled: true,
+            rules: {
+              required: true,
+              message: '请上传商品图片',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '积分数量:',
+            id: 'point',
+            type: 'input-number',
+            el: {
+              precision: 0,
+              min: 1,
+              controls: false,
+            },
+            rules: {
+              required: true,
+              message: '请输入积分数量',
+              trigger: 'blur',
+            },
+          },
+        ],
+        extraButtons: [
+          {
+            text: '上架',
+            show: (row) => row.status === 'I',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要上架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/point/goods/list/on',
+                  { params: { id: row.id } }
+                )
+                if (code === 0) {
+                  this.$message.success('上架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '下架',
+            show: (row) => row.status === 'A',
+            atClick: async (row) => {
+              try {
+                await this.$elBusUtil.confirm('确定要下架吗?')
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/point/goods/list/off',
+                  { params: { id: row.id } }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+        headerButtons: [
+          {
+            text: '批量上架',
+            type: 'primary',
+            disabled: (selected) =>
+              selected.filter((i) => i.status === 'I').length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter((i) => i.status === 'I')
+                await this.$elBusUtil.confirm(
+                  `确定要上架这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/point/goods/list/on',
+                  { params: { id: items.map((i) => i.id).join(',') } }
+                )
+                if (code === 0) {
+                  this.$message.success('上架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '批量下架',
+            type: 'primary',
+            disabled: (selected) =>
+              selected.filter((i) => i.status === 'A').length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter((i) => i.status === 'A')
+                await this.$elBusUtil.confirm(
+                  `确定要下架这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/point/goods/list/off',
+                  { params: { id: items.map((i) => i.id).join(',') } }
+                )
+                if (code === 0) {
+                  this.$message.success('下架成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+          {
+            text: '批量删除',
+            type: 'danger',
+            disabled: (selected) =>
+              selected.filter((i) => i.status === 'I').length === 0,
+            atClick: async (selected) => {
+              try {
+                const items = selected.filter((i) => i.status === 'I')
+                await this.$elBusUtil.confirm(
+                  `确定要删除这${items.length}个商品吗?`
+                )
+                const { code } = await this.$elBusHttp.request(
+                  'flower/api/point/goods/list/delete',
+                  { params: { id: items.map((i) => i.id).join(',') } }
+                )
+                if (code === 0) {
+                  this.$message.success('删除成功')
+                }
+              } catch (e) {
+                return false
+              }
+            },
+          },
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '积分商品',
+    }
+  },
+}
+</script>
diff --git a/pages/marketing/point-mall/point-distribution.vue b/pages/marketing/point-mall/point-distribution.vue
new file mode 100644
index 0000000..640f3d9
--- /dev/null
+++ b/pages/marketing/point-mall/point-distribution.vue
@@ -0,0 +1,224 @@
+<template>
+  <div>
+    <el-bus-crud ref="crud" v-bind="tableConfig" />
+    <el-dialog title="积分变动记录" :visible.sync="dialogVisible" width="80%">
+      <el-bus-crud
+        v-if="customerId"
+        :key="customerId"
+        :extra-query="{ customerId }"
+        v-bind="recordTableConfig"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      dialogVisible: false,
+      customerId: null,
+      tableConfig: {
+        url: 'flower/api/customer/point/page',
+        hasNew: false,
+        hasEdit: false,
+        hasDelete: false,
+        hasView: false,
+        columns: [
+          { label: '序号', type: 'index' },
+          { label: '用户信息', prop: 'customerName' },
+          { label: '总积分', prop: 'totalPoint' },
+          { label: '已使用积分', prop: 'usedPoint' },
+          {
+            label: '剩余积分',
+            formatter: (row) =>
+              parseInt(row.totalPoint) -
+              parseInt(row.usedPoint) -
+              parseInt(row.expiredPoint),
+          },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [{ label: '用户名称', id: 'customerName', type: 'input' }],
+          },
+        ],
+        extraDialogs: [
+          {
+            title: '积分赠送',
+            form: [
+              {
+                id: 'customerId',
+                type: 'input',
+                hidden: () => true,
+              },
+              {
+                label: '积分赠送:',
+                id: 'point',
+                type: 'input-number',
+                el: {
+                  precision: 0,
+                  min: 1,
+                  controls: false,
+                },
+                rules: {
+                  required: true,
+                  message: '请输入积分赠送',
+                  trigger: 'blur',
+                },
+              },
+              {
+                label: '原因:',
+                id: 'remarks',
+                type: 'input',
+                el: {
+                  type: 'textarea',
+                  rows: 6,
+                },
+                rules: {
+                  required: true,
+                  message: '请输入原因',
+                  trigger: 'blur',
+                },
+              },
+            ],
+            atConfirm: async (val) => {
+              const { code } = await this.$elBusHttp.request(
+                'flower/api/customer/point/giveaway',
+                {
+                  method: 'post',
+                  data: val,
+                }
+              )
+              if (code === 0) {
+                this.$message.success('赠送成功')
+              }
+            },
+          },
+          {
+            title: '积分扣减',
+            form: [
+              {
+                id: 'customerId',
+                type: 'input',
+                hidden: () => true,
+              },
+              {
+                label: '积分扣减:',
+                id: 'point',
+                type: 'input-number',
+                el: {
+                  precision: 0,
+                  min: 1,
+                  controls: false,
+                },
+                rules: {
+                  required: true,
+                  message: '请输入积分扣减',
+                  trigger: 'blur',
+                },
+              },
+              {
+                label: '原因:',
+                id: 'remarks',
+                type: 'input',
+                el: {
+                  type: 'textarea',
+                  rows: 6,
+                },
+                rules: {
+                  required: true,
+                  message: '请输入原因',
+                  trigger: 'blur',
+                },
+              },
+            ],
+            atConfirm: async (val) => {
+              const { code } = await this.$elBusHttp.request(
+                'flower/api/customer/point/deduction',
+                {
+                  method: 'post',
+                  data: val,
+                }
+              )
+              if (code === 0) {
+                this.$message.success('扣减成功')
+              }
+            },
+          },
+        ],
+        extraButtons: [
+          {
+            text: '积分变动记录',
+            atClick: (row) => {
+              this.customerId = row.customerId
+              this.dialogVisible = true
+            },
+          },
+          {
+            text: '积分赠送',
+            atClick: (row) => {
+              this.$refs.crud.$refs.extraDialog[0].show(row)
+              return false
+            },
+          },
+          {
+            text: '积分扣减',
+            atClick: (row) => {
+              this.$refs.crud.$refs.extraDialog[1].show(row)
+              return false
+            },
+          },
+        ],
+      },
+      recordTableConfig: {
+        url: 'flower/api/customer/point/page/list',
+        saveQuery: false,
+        hasNew: false,
+        hasOperation: false,
+        columns: [
+          { label: '序号', type: 'index' },
+          { label: '变动类型', prop: 'changeTypeStr' },
+          { label: '变动积分', prop: 'point' },
+          { label: '变动原因', prop: 'typeStr' },
+          { label: '变动时间', prop: 'recordDate' },
+          { label: '备注', prop: 'remarks' },
+        ],
+        searchFormAttrs: {
+          labelWidth: 'auto',
+        },
+        searchForm: [
+          {
+            type: 'row',
+            items: [
+              {
+                label: '变动原因:',
+                id: 'type',
+                type: 'bus-radio',
+                el: {
+                  code: 'point_type',
+                  childType: 'el-radio-button',
+                  hasAll: true,
+                },
+                default: '',
+                searchImmediately: true,
+                span: 24,
+              },
+              {
+                label: '备注:',
+                id: 'remarks',
+                type: 'input',
+              },
+            ],
+          },
+        ],
+      },
+    }
+  },
+  head() {
+    return {
+      title: '积分发放',
+    }
+  },
+}
+</script>
diff --git a/plugins/mixins/coupon-detail.vue b/plugins/mixins/coupon-detail.vue
new file mode 100644
index 0000000..89194b7
--- /dev/null
+++ b/plugins/mixins/coupon-detail.vue
@@ -0,0 +1,27 @@
+<script>
+export default {
+  data() {
+    return {
+      detailUrl: '',
+    }
+  },
+  mounted() {
+    this.getCouponDetail()
+  },
+  methods: {
+    async getCouponDetail() {
+      const { code, data } = await this.$elBusHttp.request(
+        `${this.detailUrl}/${this.$route.params.id}`
+      )
+      if (code === 0) {
+        if (this.$refs.form) {
+          this.$refs.form.updateForm(data)
+        }
+      }
+    },
+    goBack() {
+      this.$router.back()
+    },
+  },
+}
+</script>
diff --git a/utils/coupon-form.js b/utils/coupon-form.js
new file mode 100644
index 0000000..cf3e885
--- /dev/null
+++ b/utils/coupon-form.js
@@ -0,0 +1,173 @@
+import utils from 'el-business-utils'
+
+// 优惠券表单公共字段
+export const couponForm = () => {
+  return [
+    {
+      label: '优惠券名称:',
+      id: 'couponName',
+      type: 'input',
+      rules: {
+        required: true,
+        message: '请输入优惠券名称',
+        trigger: 'blur',
+      },
+    },
+    {
+      label: '优惠券类型:',
+      id: 'couponDiscountType',
+      type: 'bus-select-dict',
+      el: {
+        code: 'COUPON_TYPE',
+        style: 'width:100%',
+      },
+      on: {
+        change: (e, updateForm, obj) => {
+          if (e[0] === 'zero') {
+            updateForm({ minOrderAmount: 0 })
+            obj.elBusForm
+              .getComponentById('minOrderAmount')
+              .$parent.clearValidate()
+          }
+        },
+      },
+      rules: { required: true, message: '请选择优惠券类型' },
+      str: true,
+      strKey: 'couponDiscountTypeName',
+    },
+    {
+      label: '使用规则:',
+      id: 'couponDescription',
+      type: 'input',
+      el: {
+        type: 'textarea',
+        rows: 6,
+      },
+      rules: {
+        required: true,
+        message: '请输入使用规则',
+        trigger: 'blur',
+      },
+    },
+    {
+      label: '优惠券使用条件:',
+      id: 'minOrderAmount',
+      type: 'input-number',
+      el: {
+        min: 0,
+        precision: 2,
+        controls: false,
+      },
+      prefix: '满',
+      unit: '元',
+      rules: {
+        required: true,
+        message: '请输入优惠券使用条件',
+        trigger: 'blur',
+      },
+      disabled: (row) => row.couponDiscountType === 'zero',
+    },
+    {
+      label: '优惠券面值:',
+      id: 'couponDiscountValue',
+      type: 'input-number',
+      el: {
+        min: 0.01,
+        precision: 2,
+        controls: false,
+      },
+      unit: '元',
+      rules: {
+        required: true,
+        message: '请输入优惠券面值',
+        trigger: 'blur',
+      },
+    },
+  ]
+}
+
+export const couponSearchForm = () => {
+  return [
+    {
+      label: '优惠券类型',
+      id: 'couponDiscountType',
+      type: 'bus-select-dict',
+      el: {
+        code: 'COUPON_TYPE',
+        style: 'width:100%',
+        clearable: true,
+      },
+    },
+    {
+      label: '优惠券名称',
+      id: 'name',
+      type: 'input',
+    },
+  ]
+}
+
+export const getActivityEffectiveTime = (row) => {
+  if (row.usageType === 'get') {
+    return `${utils.formatDate(row.getStartDate, 'YYYY-MM-DD HH:mm') || ''} ~ ${
+      utils.formatDate(row.getEndDate, 'YYYY-MM-DD HH:mm') || ''
+    }`
+  } else if (row.usageType === 'fixed') {
+    return `${
+      utils.formatDate(row.usageStartDate, 'YYYY-MM-DD HH:mm') || ''
+    } ~ ${utils.formatDate(row.usageEndDate, 'YYYY-MM-DD HH:mm') || ''}`
+  } else if (row.usageType === 'get_after_time') {
+    return `领取后${row.usageTimeNum}${row.usageTimeTypeName || ''}`
+  }
+  return ''
+}
+
+export const getActivityReceiveTime = (row) => {
+  return `${utils.formatDate(
+    row.getStartDate,
+    'YYYY-MM-DD HH:mm'
+  )} ~ ${utils.formatDate(row.getEndDate, 'YYYY-MM-DD HH:mm')}`
+}
+
+// 优惠券列表公共字段
+export const couponColumn = () => {
+  return [
+    { label: '序号', type: 'index', minWidth: 60, fixed: 'left' },
+    { label: '优惠券名称', prop: 'couponName', minWidth: 150, fixed: 'left' },
+    { label: '优惠券类型', prop: 'couponDiscountTypeName', minWidth: 120 },
+    {
+      label: '使用条件',
+      formatter: (row) => `满${row.minOrderAmount}`,
+      minWidth: 120,
+    },
+    { label: '优惠券面值', prop: 'couponDiscountValue', minWidth: 120 },
+  ]
+}
+
+// 优惠券领取/发放记录
+export const couponRecordColumn = () => {
+  return [
+    { label: '序号', type: 'index' },
+    { label: '店铺名称', prop: 'customerName' },
+    { label: '优惠券类型', prop: 'couponDiscountTypeName' },
+    {
+      label: '使用条件',
+      formatter: (row) => `满${row.minOrderAmount}`,
+    },
+    { label: '优惠券面值', prop: 'couponDiscountValue' },
+    { label: '状态', prop: 'statusName' },
+    { label: '使用时间', prop: 'index' },
+    { label: '订单号', prop: 'orderNo' },
+  ]
+}
+
+export const recordTableConfig = (couponId) => {
+  return {
+    url: 'flower/v2/coupon-record/page',
+    hasNew: false,
+    hasOperation: false,
+    saveQuery: false,
+    extraQuery: {
+      couponId,
+    },
+  }
+}

--
Gitblit v1.9.3