From b6e34e48b20c02446c1ada2b2617b800f529898a Mon Sep 17 00:00:00 2001
From: cloudroam <cloudroam>
Date: 星期五, 23 五月 2025 17:12:05 +0800
Subject: [PATCH] 111

---
 components/base-menu-item.vue              |  108 +
 components/base-sidebar.vue                |  216 ++
 components/el-table-print.vue              |   25 
 components/sms/select-all-user.vue         |  246 +++
 components/base-link.vue                   |  103 +
 components/sms/template-download.vue       |   80 +
 components/warehouse/select-order.vue      |   68 
 components/coupon/select-shop-user.vue     |  188 ++
 components/order/level-down-list.vue       |   49 
 components/cascader-filter.vue             |  224 ++
 config/default-test.json5                  |    4 
 components/base-menu-icon.vue              |   98 +
 components/tags-view/index.vue             |  299 +++
 components/order/after-sale-table.vue      |  146 +
 components/tags-view/tag-item.vue          |   15 
 components/input-select.vue                |  129 +
 components/order/evaluation-table.vue      |  146 +
 components/order/video-list.vue            |   30 
 components/base-nav.vue                    |  300 ++++
 components/coupon/member-rule.vue          |  126 +
 components/el-bus-breadcrumb.vue           |   91 +
 components/goods/goods-params.vue          |   59 
 components/base-image-info.vue             |  115 +
 components/custom-date-range.vue           |  140 +
 config/default-dev.json5                   |    2 
 components/order/print-list.vue            |  137 +
 components/tags-view/scroll-pane.vue       |  119 +
 components/base-editor.vue                 |  140 +
 components/sms/copy-textarea.vue           |   92 +
 components/order/check-abnormal-list.vue   |   73 
 components/order/goods-table-item-list.vue |   41 
 components/simple-text.vue                 |   33 
 components/order/after-sale-items.vue      |  126 +
 components/warehouse/location-item.vue     |  199 ++
 components/content-wrapper.vue             |   36 
 components/base-role-permission-tree.vue   |   85 +
 pages/content/filmset.vue                  |  225 ++
 components/area-select.vue                 |  172 ++
 38 files changed, 4,452 insertions(+), 33 deletions(-)

diff --git a/components/area-select.vue b/components/area-select.vue
new file mode 100644
index 0000000..55ea053
--- /dev/null
+++ b/components/area-select.vue
@@ -0,0 +1,172 @@
+<template>
+  <div class="area-select">
+    <el-button v-if="!disabled" class="mb-10" type="primary" @click="openDialog"
+      >选择</el-button
+    >
+    <div class="tag-list">
+      <el-tag
+        v-for="item in value"
+        :key="item.city"
+        type="primary"
+        :closable="!disabled"
+        @close="onTagClose(item)"
+        >{{ item.province }}-{{ item.city }}</el-tag
+      >
+    </div>
+    <el-dialog
+      title="选择地区"
+      :visible.sync="dialogVisible"
+      append-to-body
+      :close-on-click-modal="false"
+    >
+      <div
+        v-for="item in districtList"
+        :key="item.code"
+        class="mb-10 p-10 border-dashed border-[#eee]"
+      >
+        <el-bus-checkbox
+          v-model="item.selected"
+          has-select-all
+          :from-dict="false"
+          :options="item.children"
+          :props="{ label: 'name', value: 'code', selectAllLabel: item.name }"
+        ></el-bus-checkbox>
+      </div>
+      <div slot="footer" class="flex items-center justify-between">
+        <el-checkbox v-model="allChecked" @change="onCheckedChange"
+          >全选</el-checkbox
+        >
+        <div>
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确定</el-button>
+        </div>
+      </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 {
+      list: [],
+      districtList: [],
+      dialogVisible: false,
+      allChecked: false,
+    }
+  },
+  mounted() {
+    this.getDistrictList()
+  },
+  methods: {
+    async getDistrictList() {
+      if (this.districtList.length === 0) {
+        const { code, data } = await this.$services.base.getAreaJson()
+        if (code === 0) {
+          const list = JSON.parse(data)
+          this.deleteRegion(list)
+          this.districtList = list
+        }
+      }
+    },
+    deleteRegion(list) {
+      list.forEach((province) => {
+        if (Array.isArray(province.children)) {
+          province.children.forEach((city) => {
+            city.parentName = province.name
+            if ('children' in city) {
+              delete city.children
+            }
+          })
+        }
+      })
+    },
+    getAreaStatus() {
+      return new Promise((resolve) => {
+        if (this.districtList.length > 0) {
+          resolve()
+        } else {
+          const timer = setInterval(() => {
+            if (this.districtList.length > 0) {
+              resolve()
+              clearTimeout(timer)
+            }
+          }, 100)
+        }
+      })
+    },
+    async openDialog() {
+      await this.getAreaStatus()
+      this.setSelectedCity()
+      this.dialogVisible = true
+    },
+    onCheckedChange(e) {
+      if (e) {
+        this.districtList.forEach((province) => {
+          if (Array.isArray(province.children)) {
+            province.selected = province.children.map((i) => i.code)
+          }
+        })
+      } else {
+        this.districtList.forEach((province) => {
+          province.selected = []
+        })
+      }
+    },
+    // 根据当前value选中弹出框中的城市
+    setSelectedCity() {
+      this.districtList.forEach((province) => {
+        const selectedCity = this.value
+          .filter((i) => i.province === province.code)
+          .map((i) => i.city)
+        province.selected = selectedCity
+      })
+      this.districtList = cloneDeep(this.districtList)
+    },
+    onConfirm() {
+      const value = this.districtList.reduce((total, current) => {
+        if (Array.isArray(current.selected) && current.selected.length > 0) {
+          total = total.concat(
+            current.selected.reduce((t, c) => {
+              t.push({ province: current.code, city: c })
+              return t
+            }, [])
+          )
+        }
+        return total
+      }, [])
+      this.$emit('input', value)
+      this.dialogVisible = false
+    },
+    onTagClose(item) {
+      const value = this.value.filter(
+        (i) => i.province !== item.province || i.city !== item.city
+      )
+      this.$emit('input', value)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.area-select {
+  width: 100%;
+  .tag-list {
+    .el-tag {
+      margin-right: 6px;
+      margin-bottom: 6px;
+    }
+  }
+}
+</style>
diff --git a/components/base-editor.vue b/components/base-editor.vue
new file mode 100644
index 0000000..56ca01f
--- /dev/null
+++ b/components/base-editor.vue
@@ -0,0 +1,140 @@
+<template>
+  <Editor
+    ref="editorRef"
+    v-model="currentValue"
+    :tinymce-script-src="`${baseUrl}tinymce/tinymce.min.js`"
+    :plugins="plugins"
+    :toolbar="toolbar"
+    :init="init"
+    output-format="html"
+    class="el-ext-editor"
+  />
+</template>
+
+<script>
+// eslint-disable-next-line
+import tinymce from 'tinymce/tinymce'
+import Editor from '@tinymce/tinymce-vue'
+import 'tinymce/icons/default/icons'
+import 'tinymce/plugins/image'
+import 'tinymce/plugins/table'
+import 'tinymce/plugins/lists' // 列表插件
+import 'tinymce/plugins/wordcount' // 文字计数
+import 'tinymce/plugins/preview' // 预览
+import 'tinymce/plugins/emoticons' // emoji表情
+import 'tinymce/plugins/emoticons/js/emojis.js' // 必须引入这个文件才有表情图库
+import 'tinymce/plugins/code' // 编辑源码
+import 'tinymce/plugins/link' // 链接插件
+import 'tinymce/plugins/advlist' // 高级列表
+import 'tinymce/plugins/autoresize' // 自动调整编辑器大小
+import 'tinymce/plugins/searchreplace' // 查找替换
+import 'tinymce/plugins/autolink' // 自动链接
+import 'tinymce/plugins/visualblocks' // 显示元素范围
+import 'tinymce/plugins/visualchars' // 显示不可见字符
+import 'tinymce/plugins/charmap' // 特殊符号
+import 'tinymce/plugins/importcss'
+import 'tinymce/plugins/nonbreaking' // 插入不间断空格
+import 'tinymce/plugins/anchor'
+import 'tinymce/plugins/codesample'
+import 'tinymce/plugins/fullscreen'
+import 'tinymce/plugins/paste'
+export default {
+  components: {
+    Editor,
+  },
+  props: {
+    initOptions: {
+      type: Object,
+      default: () => ({}),
+    },
+    value: {
+      type: String,
+      default: '',
+    },
+    plugins: {
+      type: [String, Array],
+      default:
+        'importcss autoresize searchreplace autolink code visualblocks visualchars fullscreen image link codesample table charmap nonbreaking anchor advlist lists wordcount charmap emoticons indent2em paste',
+    },
+    toolbar: {
+      type: [String, Array],
+      default: () => [
+        'code undo redo | bold italic underline strikethrough ltr rtl  | align numlist bullist | link image | table | lineheight outdent indent indent2em | charmap emoticons | anchor',
+        'fontselect fontsizeselect | forecolor backcolor removeformat',
+      ],
+    },
+    baseUrl: {
+      type: String,
+      default() {
+        return this.$config.baseUrl || '/'
+      },
+    },
+  },
+  data() {
+    return {
+      currentValue: '',
+    }
+  },
+  computed: {
+    init() {
+      return {
+        base_url: `${this.baseUrl}tinymce/`,
+        width: '100%',
+        min_height: 400,
+        max_height: 700,
+        language: 'zh_CN',
+        language_url: `${this.baseUrl}tinymce/langs/zh_CN.js`,
+        branding: false,
+        promotion: false,
+        convert_urls: false,
+        paste_preprocess: (plugin, args) => {
+          if (args.wordContent) {
+            this.$message.warning(
+              '检测到可能是从word中复制的内容,如果存在图片请通过编辑器的图片上传功能上传'
+            )
+          }
+        },
+        paste_data_images: true,
+        font_formats:
+          'Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;',
+        fontsize_formats:
+          '10px 11px 12px 14px 16px 18px 20px 22px 24px 36px 48px 64px 72px',
+        images_upload_handler: async (blobInfo, success) => {
+          const formData = new FormData()
+          formData.append('file', blobInfo.blob())
+          const { code, data } = await this.$elBusHttp.request(
+            'flower/api/upload/oss/file',
+            {
+              method: 'post',
+              data: formData,
+              contentType: 'multipart/form-data',
+            }
+          )
+          if (code === 0) {
+            success(data[0]?.url)
+          }
+        },
+        ...this.initOptions,
+      }
+    },
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = value || ''
+      },
+    },
+    currentValue(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    },
+  },
+}
+</script>
+
+<style>
+.tox-tinymce-aux {
+  z-index: 10000 !important;
+}
+</style>
diff --git a/components/base-image-info.vue b/components/base-image-info.vue
new file mode 100644
index 0000000..656148a
--- /dev/null
+++ b/components/base-image-info.vue
@@ -0,0 +1,115 @@
+<template>
+  <div class="base-image-info">
+    <div class="base-image-info__list">
+      <div
+        v-for="(item, index) in list"
+        :key="index"
+        class="base-image-info__item"
+      >
+        <el-ext-upload
+          v-model="item.imageUrl"
+          list-type="picture-card"
+          value-type="string"
+          :limit="1"
+          @change="onInputChange"
+        ></el-ext-upload>
+        <el-form label-width="auto" class="base-image-info__main">
+          <el-form-item label="标题">
+            <el-input v-model="item.title" @change="onInputChange"></el-input>
+          </el-form-item>
+          <el-form-item label="链接">
+            <el-input v-model="item.link" @change="onInputChange"></el-input>
+          </el-form-item>
+          <el-form-item label="描述">
+            <el-input
+              v-model="item.desc"
+              type="textarea"
+              :rows="3"
+              @change="onInputChange"
+            ></el-input>
+          </el-form-item>
+        </el-form>
+        <div class="base-image-info__delete" @click="deleteItem(index)">
+          <i class="el-icon-delete"></i>
+        </div>
+      </div>
+    </div>
+    <div class="base-image-info__add">
+      <el-button type="primary" icon="el-icon-plus" @click="add"
+        >添加</el-button
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'BaseImageInfo',
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      list: [],
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.list = value || []
+      },
+    },
+  },
+  methods: {
+    add() {
+      this.list.push({
+        imageUrl: '',
+        title: '',
+        desc: '',
+        link: '',
+      })
+      this.onInputChange()
+    },
+    deleteItem(index) {
+      this.list.splice(index, 1)
+      this.onInputChange()
+    },
+    onInputChange() {
+      this.$emit('input', this.list)
+      this.$emit('change', this.list)
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.base-image-info {
+  &__list {
+  }
+  &__item {
+    display: flex;
+  }
+  &__main {
+    margin-left: 15px;
+    flex: 1;
+    .el-form-item {
+      margin-bottom: 5px;
+    }
+  }
+  &__delete {
+    cursor: pointer;
+    font-size: 20px;
+    margin-left: 15px;
+    color: $tip-color;
+    display: flex;
+    align-items: center;
+  }
+  &__add {
+    margin-top: 5px;
+  }
+}
+</style>
diff --git a/components/base-link.vue b/components/base-link.vue
new file mode 100644
index 0000000..b5231d4
--- /dev/null
+++ b/components/base-link.vue
@@ -0,0 +1,103 @@
+<template>
+  <div class="base-link">
+    <div v-for="(item, index) in linkList" :key="index" class="base-link__item">
+      <el-input
+        v-model="item.name"
+        placeholder="请输入名称"
+        clearable
+        @change="onInputChange"
+      />
+      <i class="base-link__separator">-</i>
+      <el-input
+        v-model="item.link"
+        placeholder="请输入链接"
+        clearable
+        @change="onInputChange"
+      />
+      <div class="base-link__delete" @click="deletePoject(index)">
+        <i class="el-icon-delete"></i>
+      </div>
+    </div>
+    <div class="base-link__add">
+      <el-button type="primary" icon="el-icon-plus" @click="addPoject"
+        >继续添加</el-button
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'BaseLink',
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      codeForm: {},
+      linkList: [],
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.linkList = value || []
+      },
+    },
+  },
+  methods: {
+    addPoject() {
+      this.linkList.push({
+        name: '',
+        link: '',
+      })
+      this.onInputChange()
+    },
+    deletePoject(item) {
+      this.linkList.splice(item, 1)
+      this.onInputChange()
+    },
+    onInputChange() {
+      this.$emit('input', this.linkList)
+      this.$emit('change', this.linkList)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.base-link {
+  height: 100%;
+  &__item {
+    position: relative;
+    display: flex;
+    align-items: center;
+    .el-input {
+      width: 45% !important;
+    }
+    &:not(:last-child) {
+      margin-bottom: 8px;
+    }
+  }
+  &__separator {
+    margin: 0 6px;
+  }
+  &__delete {
+    cursor: pointer;
+    font-size: 20px;
+    margin-left: 15px;
+    color: $tip-color;
+  }
+  &__add {
+    margin-top: 5px;
+  }
+}
+</style>
diff --git a/components/base-menu-icon.vue b/components/base-menu-icon.vue
new file mode 100644
index 0000000..2d17b5a
--- /dev/null
+++ b/components/base-menu-icon.vue
@@ -0,0 +1,98 @@
+<template>
+  <div>
+    <el-popover
+      ref="popover"
+      width="800"
+      placement="bottom-start"
+      trigger="click"
+      popper-class="mod-menu__icon-popover"
+    >
+      <el-tabs v-model="activeTab">
+        <el-tab-pane
+          v-for="tab in tabs"
+          :key="tab.title"
+          :label="tab.title"
+          :name="tab.title"
+        >
+          <div class="mod-menu__icon-list">
+            <el-button
+              v-for="(item, index) in tab.icons"
+              :key="index"
+              :class="{ 'is-active': item === currentValue }"
+              @click="onIconChange(item)"
+            >
+              <i :class="item" aria-hidden="true" />
+            </el-button>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-popover>
+    <el-input
+      v-popover:popover
+      v-bind="$attrs"
+      :value="currentValue"
+      placeholder="图标"
+      @input="onInput"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  FILE_ICONS,
+  EDITOR_ICONS,
+  CHART_ICONS,
+  COMMON_ICONS,
+} from '@/plugins/icons'
+export default {
+  inheritAttrs: false,
+  props: {
+    value: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      tabs: [
+        { title: '文件类图标', icons: FILE_ICONS },
+        { title: '文本编辑类图标', icons: EDITOR_ICONS },
+        { title: '数据类图标', icons: CHART_ICONS },
+        { title: '通用类', icons: COMMON_ICONS },
+      ],
+      activeTab: '文件类图标',
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = value
+      },
+    },
+  },
+  methods: {
+    onIconChange(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    },
+    onInput(e) {
+      this.$emit('input', e)
+      this.$emit('change', e)
+      // this.onValidate()
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+.mod-menu__icon-popover {
+  .mod-menu__icon-list {
+    height: 300px;
+    overflow: auto;
+    .el-button {
+      margin: 0 10px 10px 0;
+    }
+  }
+}
+</style>
diff --git a/components/base-menu-item.vue b/components/base-menu-item.vue
new file mode 100644
index 0000000..63c6a8f
--- /dev/null
+++ b/components/base-menu-item.vue
@@ -0,0 +1,108 @@
+<template>
+  <div class="base-menu-item-comp">
+    <el-submenu
+      v-if="hasChildren()"
+      :index="item.menuHref"
+      :class="`level-${level}`"
+    >
+      <template slot="title">
+        <i v-if="item.menuIcon" :class="item.menuIcon"></i>
+        <span class="base-menu-item-comp__title">{{ item.menuName }}</span>
+      </template>
+      <base-menu-item
+        v-for="(data, index) in item.children"
+        :key="index"
+        :item="data"
+        :super-path="resolvePath(item.menuHref)"
+        :level="level + 1"
+      ></base-menu-item>
+    </el-submenu>
+    <el-menu-item v-else :index="resolvePath(item.menuHref)" @click="goRoute">
+      <i v-if="item.menuIcon" :class="item.menuIcon"></i>
+      <span class="title">{{ item.menuName }}</span>
+    </el-menu-item>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+
+export default {
+  name: 'BaseMenuItem',
+  props: {
+    item: {
+      type: Object,
+      required: true,
+    },
+    superPath: {
+      type: String,
+      default: '/',
+    },
+    level: {
+      type: Number,
+      default: 0,
+    },
+  },
+  methods: {
+    hasChildren() {
+      if (this.item.children && this.item.children.length > 0) {
+        return true
+      }
+      return false
+    },
+    goRoute() {
+      if (this.item.menuHref.includes('http')) {
+        window.open(this.item.menuHref, '_blank')
+      } else if (this.item.menuHref.startsWith('/')) {
+        this.$router.push(this.item.menuHref)
+      } else {
+        this.$router.push(this.resolvePath(this.item.menuHref))
+      }
+    },
+    resolvePath(route) {
+      if (route.includes('http') || route.startsWith('/')) {
+        return route
+      }
+      return path.join(this.superPath, route).replace(/\\/g, '/')
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.base-menu-item-comp {
+  i {
+    color: #fff;
+    font-size: 14px;
+    margin-right: 16px;
+  }
+  ::v-deep .el-submenu__icon-arrow {
+    color: #fff;
+  }
+  .el-submenu {
+    @for $i from 1 through 4 {
+      .el-menu-item {
+        padding-left: 64px !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-item {
+          @if $i==0 {
+            padding-left: 64px !important;
+          } @else {
+            padding-left: 32px * ($i + 1) + 10px !important;
+          }
+        }
+      }
+    }
+  }
+}
+</style>
diff --git a/components/base-nav.vue b/components/base-nav.vue
new file mode 100644
index 0000000..121c8ec
--- /dev/null
+++ b/components/base-nav.vue
@@ -0,0 +1,300 @@
+<template>
+  <div class="base-nav-comp">
+    <div class="base-nav-comp__toggle" @click="toggleMenu">
+      <svg
+        :class="{ 'is-active': !menuShrink }"
+        class="base-nav-comp__toggle__svg"
+        viewBox="0 0 1024 1024"
+        xmlns="http://www.w3.org/2000/svg"
+        width="64"
+        height="64"
+      >
+        <path
+          d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
+        />
+      </svg>
+    </div>
+    <!--    <div class="base-nav-comp__breadcrumb">-->
+    <!--      <el-ext-breadcrumb :menus="menus"></el-ext-breadcrumb>-->
+    <!--    </div>-->
+    <div class="base-nav-comp__right">
+      <!--      <el-tooltip content="主题色" effect="dark" placement="bottom">-->
+      <!--        <div class="base-nav-comp__right__action">-->
+      <!--          <base-theme-picker />-->
+      <!--        </div>-->
+      <!--      </el-tooltip>-->
+      <el-tooltip
+        :content="$t('nav.layoutSize')"
+        effect="dark"
+        placement="bottom"
+      >
+        <el-dropdown trigger="click" @command="handleSize">
+          <div class="base-nav-comp__right__action">
+            <i class="fa fa-text-height"></i>
+          </div>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item
+              v-for="item in sizeList"
+              :key="item.value"
+              :disabled="size === item.value"
+              :command="item.value"
+            >
+              {{ item.label }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+      </el-tooltip>
+      <!--      <el-tooltip-->
+      <!--        :content="$t('nav.language')"-->
+      <!--        effect="dark"-->
+      <!--        placement="bottom"-->
+      <!--      >-->
+      <!--        <el-dropdown trigger="click" @command="handleLanguage">-->
+      <!--          <div class="base-nav-comp__right__action">-->
+      <!--            <i class="fa fa-language"></i>-->
+      <!--          </div>-->
+      <!--          <el-dropdown-menu slot="dropdown">-->
+      <!--            <el-dropdown-item-->
+      <!--              v-for="item in languageList"-->
+      <!--              :key="item.value"-->
+      <!--              :disabled="$i18n.locale === item.value"-->
+      <!--              :command="item.value"-->
+      <!--            >-->
+      <!--              {{ item.label }}-->
+      <!--            </el-dropdown-item>-->
+      <!--          </el-dropdown-menu>-->
+      <!--        </el-dropdown>-->
+      <!--      </el-tooltip>-->
+      <el-dropdown trigger="click" @command="handleUser">
+        <div class="base-nav-comp__right__user base-nav-comp__right__action">
+          <img
+            v-if="avatar"
+            :src="avatar"
+            class="base-nav-comp__right__user__avatar"
+          />
+          <img
+            src="~static/images/avatar.png"
+            class="base-nav-comp__right__user__avatar"
+          />
+          <div class="base-nav-comp__right__user__nickname">
+            {{ nickName || loginName }}
+          </div>
+        </div>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="changePassword">{{
+            $t('nav.changePassword')
+          }}</el-dropdown-item>
+          <el-dropdown-item command="logout">{{
+            $t('nav.logout')
+          }}</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+    <extra-dialog
+      ref="passwordDialog"
+      :title="$t('nav.changePassword')"
+      :dialog-attrs="{ appendToBody: true }"
+      :form-attrs="{ labelWidth: 'auto' }"
+      :form="passwordForm"
+      :at-confirm="onPasswordFormConfirm"
+      @formUpdate="onPasswordFormUpdate"
+    ></extra-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, mapGetters } from 'vuex'
+import { joinLocaleText } from 'el-business'
+
+export default {
+  name: 'BaseNav',
+  data() {
+    return {
+      sizeList: [
+        { label: 'Medium', value: 'medium' },
+        { label: 'Small', value: 'small' },
+        { label: 'Mini', value: 'mini' },
+      ],
+      languageList: [
+        { label: '中文', value: 'zh-CN' },
+        { label: 'English', value: 'en' },
+      ],
+      passwordFormInfo: {},
+      passwordForm: [
+        {
+          label: this.$t('nav.newPassword'),
+          id: 'password',
+          type: 'input',
+          el: { type: 'password' },
+          rules: [
+            {
+              required: true,
+              message: joinLocaleText(
+                this.$t('elBus.common.pleaseEnter'),
+                this.$t('nav.newPassword')
+              ),
+              trigger: 'blur',
+            },
+            {
+              pattern:
+                /^(?:(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[-_!@#$%^&*? ]))\S{8,20}$/,
+              message: this.$t('nav.passwordPattern'),
+              trigger: 'blur',
+            },
+          ],
+        },
+        {
+          label: this.$t('nav.confirmPassword'),
+          id: 'confirmPassword',
+          type: 'input',
+          el: { type: 'password' },
+          rules: [
+            {
+              required: true,
+              message: joinLocaleText(
+                this.$t('elBus.common.pleaseEnter'),
+                this.$t('nav.confirmPassword')
+              ),
+              trigger: 'blur',
+            },
+            {
+              validator: (rule, value, callback) => {
+                if (value !== this.passwordFormInfo.password) {
+                  callback(new Error(this.$t('nav.passwordInconsistent')))
+                } else {
+                  callback()
+                }
+              },
+              trigger: 'blur',
+            },
+          ],
+        },
+      ],
+    }
+  },
+  computed: {
+    ...mapState({
+      menuShrink: (state) => state.app.menuShrink,
+      size: (state) => state.app.size,
+    }),
+    ...mapGetters({
+      nickName: 'auth/nickName',
+      loginName: 'auth/loginName',
+      avatar: 'auth/avatar',
+      tel: 'auth/tel',
+      menus: 'permission/menus',
+    }),
+  },
+  methods: {
+    toggleMenu() {
+      this.$store.commit('app/SET_MENU_SHRINK', !this.menuShrink)
+    },
+    handleSize(size) {
+      this.$store.commit('app/SET_SIZE', size)
+      window.location.reload()
+    },
+    handleLanguage(language) {
+      const url = this.switchLocalePath(language)
+      window.location.href = url
+    },
+    handleUser(command) {
+      this[command]()
+    },
+    logout() {
+      this.$store.dispatch('auth/clearLoginInfo')
+      this.$router.replace('/login')
+    },
+    changePassword() {
+      this.$refs.passwordDialog.show()
+    },
+    onPasswordFormUpdate(e) {
+      this.passwordFormInfo = { ...e }
+    },
+    async onPasswordFormConfirm(e) {
+      const { code } = await this.$services.auth.changePassword(e)
+      if (code === 0) {
+        this.$message.success('密码修改成功')
+        return true
+      }
+      return false
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.base-nav-comp {
+  height: 50px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  display: flex;
+  align-items: center;
+  &__toggle {
+    padding: 0 15px;
+    line-height: 46px;
+    height: 100%;
+    float: left;
+    cursor: pointer;
+    transition: background 0.3s;
+    -webkit-tap-highlight-color: transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.025);
+    }
+    &__svg {
+      display: inline-block;
+      vertical-align: middle;
+      width: 20px;
+      height: 20px;
+      &.is-active {
+        transform: rotate(180deg);
+      }
+    }
+  }
+  &__breadcrumb {
+    margin-left: 8px;
+  }
+  &__right {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    padding: 0 20px;
+    height: 100%;
+    .el-dropdown {
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    &__user {
+      &__avatar {
+        width: 30px;
+        height: 30px;
+        border-radius: 50%;
+      }
+      &__nickname {
+        font-size: 14px;
+        color: $main-title-color;
+        margin-left: 10px;
+      }
+    }
+    &__action {
+      padding: 0 15px;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+      height: 100%;
+      i {
+        font-size: 16px;
+        color: $main-title-color;
+      }
+      &:hover {
+        background-color: rgba(0, 0, 0, 0.025);
+      }
+    }
+  }
+}
+</style>
diff --git a/components/base-role-permission-tree.vue b/components/base-role-permission-tree.vue
new file mode 100644
index 0000000..2349468
--- /dev/null
+++ b/components/base-role-permission-tree.vue
@@ -0,0 +1,85 @@
+<template>
+  <el-tree
+    ref="tree"
+    :check-strictly="false"
+    :data="list"
+    :props="defaultProps"
+    node-key="id"
+    show-checkbox
+    @check-change="onCheckChange"
+  />
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  watch: {
+    value: {
+      immediate: true,
+      async handler(value) {
+        if (this.list.length === 0) {
+          await this.getList()
+        }
+        this.$nextTick(() => {
+          if (Array.isArray(value)) {
+            value.forEach((i) => {
+              this.$refs.tree.setChecked(i, true, false)
+            })
+          }
+        })
+      },
+    },
+    disabled(value) {
+      this.setDisabled(this.list, value)
+    },
+  },
+  data() {
+    return {
+      defaultProps: {
+        children: 'children',
+        label: 'menuName',
+      },
+      list: [],
+    }
+  },
+  methods: {
+    onCheckChange() {
+      const checkedNodes = this.$refs.tree.getCheckedNodes(false, true)
+      const checkedKeys = checkedNodes.map((i) => i.id)
+      this.$emit('input', checkedKeys)
+      this.$emit('change', checkedKeys)
+    },
+    async getList() {
+      const { code, data } = await this.$elBusHttp.request(
+        'flower/api/menu/list'
+      )
+      if (code === 0) {
+        const list = data || []
+        if (this.disabled) {
+          this.setDisabled(list)
+        }
+        this.list = list
+      }
+    },
+    setDisabled(list, disabled = true) {
+      list.forEach((item) => {
+        item.disabled = disabled
+        if (Array.isArray(item.children) && item.children.length > 0) {
+          this.setDisabled(item.children)
+        }
+      })
+    },
+  },
+}
+</script>
+
+<style scoped></style>
diff --git a/components/base-sidebar.vue b/components/base-sidebar.vue
new file mode 100644
index 0000000..fd736c8
--- /dev/null
+++ b/components/base-sidebar.vue
@@ -0,0 +1,216 @@
+<template>
+  <div
+    class="cubebase-sidebar-comp"
+    :class="{ 'is-hide': menuShrink, 'is-mobile': isMobile }"
+  >
+    <nuxt-link class="cubebase-sidebar-comp__top" to="/">
+      <transition name="nameFade">
+        <div v-if="!menuShrink" class="cubebase-sidebar-comp__top__title">
+          {{ platformName }}
+        </div>
+      </transition>
+    </nuxt-link>
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-bus-menu
+        :content="menus"
+        :default-active="activeRoute"
+        :collapse="menuShrink"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :unique-opened="false"
+        :active-text-color="variables.menuActiveText"
+        :collapse-transition="false"
+        mode="vertical"
+        :props="{ index: 'fullPath' }"
+        @menuItemSelect="onMenuItemClick"
+      />
+    </el-scrollbar>
+  </div>
+</template>
+
+<script>
+import { mapState, mapGetters } from 'vuex'
+import variables from '@/assets/variable.scss'
+
+export default {
+  name: 'BaseSidebar',
+  data() {
+    return {
+      platformName: this.$config.platformName,
+    }
+  },
+  computed: {
+    variables() {
+      return variables
+    },
+    ...mapState({
+      menuShrink: (state) => state.app.menuShrink,
+      isMobile: (state) => state.app.isMobile,
+      activeRoute: (state) => state.app.activeRoute,
+    }),
+    ...mapGetters({
+      menus: 'permission/menus',
+      leafMenus: 'permission/leafMenus',
+    }),
+  },
+  watch: {
+    $route() {
+      this.setActiveRoute()
+    },
+  },
+  mounted() {
+    this.setActiveRoute()
+  },
+  methods: {
+    setActiveRoute() {
+      const currentRoute = this.$route.fullPath
+      if (Array.isArray(this.leafMenus)) {
+        const matchRoute = this.leafMenus.find((item) =>
+          currentRoute.includes(item.fullPath)
+        )
+        if (matchRoute) {
+          this.$store.commit('app/SET_ACTIVE_ROUTE', matchRoute.fullPath)
+        }
+      }
+    },
+    onMenuItemClick(item) {
+      if (item.fullPath.includes('http')) {
+        window.open(item.fullPath, '_blank')
+      } else {
+        this.$router.push(item.fullPath)
+      }
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.nameFade-enter-active {
+  opacity: 0;
+  white-space: nowrap;
+  transition: all 0.28s;
+}
+
+.cubebase-sidebar-comp {
+  display: flex;
+  flex-direction: column;
+  transition: width 0.28s;
+  width: $sideBarWidth !important;
+  background-color: $menuBg;
+  height: 100%;
+  position: relative;
+  z-index: 1001;
+  overflow: hidden;
+  &__top {
+    position: relative;
+    width: 100%;
+    padding: 25px 10px;
+    background: #2b2f3a;
+    text-align: center;
+    overflow: hidden;
+    display: block;
+    &__logo {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      vertical-align: middle;
+      margin-bottom: 15px;
+    }
+    &__title {
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      font-size: 14px;
+      overflow: hidden;
+    }
+  }
+  .el-scrollbar {
+    flex: 1;
+    ::v-deep .el-menu {
+      border: none;
+      height: 100%;
+      width: 100% !important;
+      .submenu-title-noDropdown,
+      .el-submenu__title {
+        &:hover {
+          background-color: $menuHover !important;
+        }
+      }
+
+      .is-active > .el-submenu__title {
+        color: $subMenuActiveText !important;
+      }
+
+      .nest-menu .el-submenu > .el-submenu__title,
+      .el-submenu .el-menu-item {
+        min-width: $sideBarWidth !important;
+        background-color: $subMenuBg !important;
+
+        &:hover {
+          background-color: $subMenuHover !important;
+        }
+      }
+    }
+    ::v-deep {
+      .el-bus-menu {
+        i {
+          color: #fff;
+          font-size: 14px;
+          margin-right: 16px;
+          width: 14px;
+        }
+        ::v-deep .el-submenu__icon-arrow {
+          color: #fff;
+        }
+        .el-menu-item {
+          @for $i from 2 through 4 {
+            &.el-bus-level-#{$i} {
+              padding-left: 20px * $i + 24px !important;
+            }
+          }
+        }
+        .el-menu {
+          @for $i from 2 through 4 {
+            &:has(.el-submenu.el-bus-level-#{$i}) {
+              .el-menu-item {
+                &.el-bus-level-#{$i} {
+                  padding-left: 20px * $i !important;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    ::v-deep .scrollbar-wrapper {
+      overflow-x: hidden !important;
+    }
+  }
+  &.is-hide {
+    width: 54px !important;
+    ::v-deep .el-menu {
+      .el-submenu {
+        overflow: hidden;
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+      }
+    }
+    .cubebase-sidebar-comp__top {
+      &__logo {
+        margin-bottom: 0;
+      }
+    }
+  }
+  &.is-mobile {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    &.is-hide {
+      width: 0 !important;
+    }
+  }
+}
+</style>
diff --git a/components/cascader-filter.vue b/components/cascader-filter.vue
new file mode 100644
index 0000000..b4a6f83
--- /dev/null
+++ b/components/cascader-filter.vue
@@ -0,0 +1,224 @@
+<template>
+  <div class="cascader-filter">
+    <div v-for="(item, index) in radioList" :key="index">
+      <el-bus-radio
+        :from-dict="false"
+        :options="item"
+        :props="mProps"
+        :value="currentValue[index] || ''"
+        v-bind="$attrs"
+        @change="onRadioChange($event, index)"
+      ></el-bus-radio>
+    </div>
+  </div>
+</template>
+
+<script>
+import cacheStorage from 'el-business-cache-utils'
+import utils from 'el-business-utils'
+import _get from 'lodash.get'
+
+export default {
+  inheritAttrs: false,
+  props: {
+    value: {
+      type: [Array, String, Number],
+      default() {
+        return this.emitPath ? [] : null
+      },
+    },
+    emitPath: {
+      type: Boolean,
+      default: true,
+    },
+    props: Object,
+    interfaceUri: {
+      type: String,
+      default: '',
+    },
+    otherInterfaceUri: {
+      type: String,
+      default: '',
+    },
+    extraQuery: {
+      type: Object,
+      default: null,
+    },
+    code: {
+      type: String,
+    },
+    codeOnPath: {
+      type: Boolean,
+      default() {
+        return utils.isTrueEmpty(this?.$ELBUSINESS?.dictCodeOnPath)
+          ? false
+          : this?.$ELBUSINESS?.dictCodeOnPath
+      },
+    },
+    codeRequestKey: {
+      type: String,
+      default() {
+        return this?.$ELBUSINESS?.dictCodeRequestKey || 'type'
+      },
+    },
+  },
+  data() {
+    return {
+      options: [],
+    }
+  },
+  computed: {
+    mProps() {
+      const DEFAULT_PROPS = {
+        label: 'label',
+        value: 'value',
+        allLabel: '不限',
+        allValue: '',
+        dataPath: '',
+      }
+      return Object.assign(
+        {},
+        DEFAULT_PROPS,
+        this.$ELBUSINESS?.radioProps,
+        this.props
+      )
+    },
+    mInterfaceUri() {
+      return (
+        this.interfaceUri ||
+        this.$ELBUSINESS?.dictInterfaceUri ||
+        'wxkj/code/value'
+      )
+    },
+    currentValue() {
+      return this.emitPath ? this.value : this.getArrayValue(this.value)
+    },
+    radioList() {
+      if (Array.isArray(this.options) && this.options.length > 0) {
+        const list = [this.options]
+        let mList = this.options
+        if (Array.isArray(this.currentValue) && this.currentValue.length > 0) {
+          this.currentValue.forEach((item) => {
+            const child = mList.find((i) => i[this.mProps.value] === item)
+            if (
+              child &&
+              Array.isArray(child.children) &&
+              child.children.length > 0
+            ) {
+              list.push(child.children)
+              mList = child.children
+            }
+          })
+        }
+        return list
+      }
+      return []
+    },
+  },
+  mounted() {
+    if (this.otherInterfaceUri) {
+      this.getOtherOptions()
+    } else if (this.code) {
+      this.getOptionList()
+    }
+  },
+  methods: {
+    async getOptionList() {
+      if (this.$ELBUSINESS?.enableCache) {
+        const list = cacheStorage.getItem(this.code)
+        if (Array.isArray(list)) {
+          this.options = list
+          this.$emit('optionsChange', this.options)
+          return
+        }
+      }
+      const requestUrl = this.codeOnPath
+        ? utils.joinPath(this.mInterfaceUri, this.code)
+        : this.mInterfaceUri
+      const { code, data } = await this.$elBusHttp.request(requestUrl, {
+        params: this.codeOnPath
+          ? undefined
+          : { [this.codeRequestKey]: this.code },
+      })
+      if (code === 0) {
+        if (this.$ELBUSINESS?.enableCache) {
+          if (Array.isArray(data)) {
+            cacheStorage.setItem(this.code, data)
+          }
+        }
+        this.options = data || []
+        this.$emit('optionsChange', this.options)
+      } else {
+        console.warn('can not get option list')
+      }
+    },
+    async getOtherOptions() {
+      const params = Object.assign({}, this.extraQuery)
+      const { code, data } = await this.$elBusHttp.request(
+        this.otherInterfaceUri,
+        {
+          params,
+        }
+      )
+      if (code === 0) {
+        if (this.mProps.dataPath) {
+          this.options = _get(data || [], this.mProps.dataPath)
+        } else {
+          this.options = data || []
+        }
+        this.$emit('optionsChange', this.options)
+      }
+    },
+    getArrayValue(value) {
+      const list = this.tree2List(this.options)
+      const deepValue = (v) => {
+        if (v) {
+          const current = list.find((i) => i[this.mProps.value] === v)
+          return [].concat(v, deepValue(current.parentId))
+        }
+        return []
+      }
+      if (Array.isArray(this.options) && this.options.length > 0) {
+        return deepValue(value).reverse()
+      }
+      return []
+    },
+    tree2List(tree) {
+      const toList = (arr, parentId = null) => {
+        if (Array.isArray(arr))
+          return Array.prototype.concat.apply(
+            [],
+            arr.map((i) => toList(i))
+          )
+        else if (Reflect.has(arr, 'children'))
+          return [
+            arr,
+            ...toList(
+              arr.children.map((i) => ({
+                ...i,
+                parentId: arr[this.mProps.value],
+              }))
+            ),
+          ]
+        return [arr]
+      }
+      return toList(tree)
+    },
+    onRadioChange(e, index) {
+      const value = this.currentValue.slice(0, index)
+      if (e !== this.mProps.allValue) {
+        value.push(e)
+      }
+      const emitPath = this.emitPath
+      if (emitPath) {
+        this.$emit('input', value)
+        this.$emit('change', value)
+      } else {
+        const emitValue = value.length > 0 ? value.pop() : ''
+        this.$emit('input', emitValue)
+        this.$emit('change', emitValue)
+      }
+    },
+  },
+}
+</script>
diff --git a/components/content-wrapper.vue b/components/content-wrapper.vue
new file mode 100644
index 0000000..8f1782d
--- /dev/null
+++ b/components/content-wrapper.vue
@@ -0,0 +1,36 @@
+<template>
+  <el-card class="content-wrapper">
+    <div slot="header" class="content-wrapper__header">
+      <div class="content-wrapper__header__title">{{ title }}</div>
+      <slot name="header-right"></slot>
+    </div>
+    <slot></slot>
+  </el-card>
+</template>
+
+<script>
+export default {
+  props: {
+    title: {
+      type: String,
+      default: '',
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.content-wrapper {
+  &__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    &__title {
+      font-size: $font-size-lg;
+      color: $main-title-color;
+      font-weight: 500;
+      line-height: 2;
+    }
+  }
+}
+</style>
diff --git a/components/coupon/member-rule.vue b/components/coupon/member-rule.vue
new file mode 100644
index 0000000..4d1376b
--- /dev/null
+++ b/components/coupon/member-rule.vue
@@ -0,0 +1,126 @@
+<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: 1,
+        max: 99999999,
+        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..243d2d7
--- /dev/null
+++ b/components/coupon/select-shop-user.vue
@@ -0,0 +1,188 @@
+<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,
+        hasNew: 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/custom-date-range.vue b/components/custom-date-range.vue
new file mode 100644
index 0000000..ec899b8
--- /dev/null
+++ b/components/custom-date-range.vue
@@ -0,0 +1,140 @@
+<script>
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+dayjs.locale('zh-cn')
+export default {
+  inputFormat(row, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 3
+    ) {
+      const dateType = item.commonFormatProps[0]
+      const startDate = item.commonFormatProps[1]
+      const endDate = item.commonFormatProps[2]
+      if (dateType in row || startDate in row || endDate in row) {
+        const dateValue =
+          !row[startDate] && !row[endDate] ? [] : [row[startDate], row[endDate]]
+        return !row[dateType] && dateValue.length === 0
+          ? []
+          : [row[dateType], dateValue]
+      }
+    }
+  },
+  outputFormat(val, item) {
+    if (
+      Array.isArray(item.commonFormatProps) &&
+      item.commonFormatProps.length === 3
+    ) {
+      const dateValue = val?.[1] || []
+      const radioValue = val?.[0] || ''
+      return {
+        [item.commonFormatProps[0]]: radioValue,
+        [item.commonFormatProps[1]]: dateValue?.[0] || '',
+        [item.commonFormatProps[2]]: dateValue?.[1] || '',
+      }
+    }
+  },
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    options: {
+      type: Array,
+      default: () => [],
+    },
+    radioAttrs: {
+      type: Object,
+      default: () => ({}),
+    },
+    dateRangeAttrs: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      radioValue: '',
+      dateRangeValue: [],
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.radioValue = value?.[0] || ''
+        this.dateRangeValue = value?.[1] || []
+      },
+    },
+  },
+  methods: {
+    onRadioChange(e) {
+      if (!e) {
+        this.emitValue([])
+      } else if (e === 'custom') {
+        this.emitValue(['custom'])
+      } else if (typeof e === 'string') {
+        if (!e.includes(',')) {
+          if (e === 'yesterday') {
+            const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD')
+            this.emitValue([e, [yesterday, yesterday]])
+          } else {
+            const startDate = dayjs().startOf(e).format('YYYY-MM-DD')
+            const endDate = dayjs().format('YYYY-MM-DD')
+            this.emitValue([e, [startDate, endDate]])
+          }
+        } else {
+          const arr = e.split(',')
+          const num = arr[0]
+          let funcName = ''
+          if (e > 0) {
+            funcName = 'add'
+          } else {
+            funcName = 'subtract'
+          }
+          const startDate = dayjs()
+            [funcName](Math.abs(num), arr[1])
+            .format('YYYY-MM-DD')
+          const endDate = dayjs().format('YYYY-MM-DD')
+          this.emitValue([e, [startDate, endDate]])
+        }
+      }
+    },
+    onDateRangeChange(e) {
+      this.emitValue(['custom', e])
+    },
+    emitValue(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    },
+  },
+}
+</script>
+
+<template>
+  <div class="custom-date-range">
+    <el-bus-radio
+      v-bind="{ childType: 'el-radio-button', hasAll: true, ...radioAttrs }"
+      v-model="radioValue"
+      :from-dict="false"
+      :options="options"
+      @change="onRadioChange"
+    />
+    <el-bus-date-range
+      v-bind="{ canOverToday: false, ...dateRangeAttrs }"
+      v-model="dateRangeValue"
+      @change="onDateRangeChange"
+    />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.custom-date-range {
+  display: flex;
+  align-items: center;
+  .el-bus-date-range {
+    margin-bottom: 10px;
+    margin-left: 15px;
+  }
+}
+</style>
diff --git a/components/el-bus-breadcrumb.vue b/components/el-bus-breadcrumb.vue
new file mode 100644
index 0000000..6dfa2b2
--- /dev/null
+++ b/components/el-bus-breadcrumb.vue
@@ -0,0 +1,91 @@
+<template>
+  <el-breadcrumb class="el-ext-breadcrumb" v-bind="$attrs">
+    <el-breadcrumb-item v-for="(item, index) in levelList" :key="index">
+      <span v-if="index == levelList.length - 1">{{ item.title }}</span>
+      <a v-else @click.prevent="handleLink(item)">{{ item.title }}</a>
+    </el-breadcrumb-item>
+  </el-breadcrumb>
+</template>
+
+<script>
+const { compile } = require('path-to-regexp')
+
+export default {
+  name: 'ElExtBreadcrumb',
+  props: {
+    menus: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      levelList: [],
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    },
+  },
+  mounted() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      const timer = setTimeout(() => {
+        const levelList = []
+        const { params } = this.$route
+        this.$route.matched.forEach((route) => {
+          if (
+            route.instances.default &&
+            route.instances.default.$metaInfo &&
+            route.instances.default.$metaInfo.title
+          ) {
+            const toPath = compile(route.path)
+            levelList.push({
+              title: route.instances.default.$metaInfo.title,
+              path: toPath(params),
+            })
+          }
+        })
+        this.levelList = levelList
+        clearTimeout(timer)
+      }, 100)
+    },
+    handleLink(item) {
+      const mPath = item.path
+      const firstChild = this.findFirstChild(mPath, this.menus)
+      this.$router.push(firstChild)
+    },
+    findFirstChild(mPath, routes) {
+      let firstChild = mPath
+      if (!routes) {
+        return firstChild
+      }
+      for (const route of routes) {
+        if (route.fullPath === mPath) {
+          if (route.children && route.children.length > 0) {
+            return this.getLeafChild(route.children)
+          } else {
+            return mPath
+          }
+        } else if (route.children && route.children.length > 0) {
+          firstChild = this.findFirstChild(mPath, route.children)
+        }
+      }
+      return firstChild
+    },
+    getLeafChild(array) {
+      const first = array[0]
+      if (first.children && first.children.length > 0) {
+        return this.getLeafChild(first.children)
+      } else {
+        return first.fullPath
+      }
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/components/el-table-print.vue b/components/el-table-print.vue
new file mode 100644
index 0000000..f6755bf
--- /dev/null
+++ b/components/el-table-print.vue
@@ -0,0 +1,25 @@
+<script>
+import { Table } from 'element-ui'
+export default {
+  extends: Table,
+  mounted() {
+    this.$nextTick(function () {
+      const thead = this.$el.querySelector('.el-table__header-wrapper thead')
+      const theadNew = thead.cloneNode(true)
+      this.$el
+        .querySelector('.el-table__body-wrapper table')
+        .appendChild(theadNew)
+    })
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-table {
+    &__body-wrapper thead {
+      display: none;
+    }
+  }
+}
+</style>
diff --git a/components/goods/goods-params.vue b/components/goods/goods-params.vue
new file mode 100644
index 0000000..e6f9066
--- /dev/null
+++ b/components/goods/goods-params.vue
@@ -0,0 +1,59 @@
+<template>
+  <el-table :data="currentValue" border>
+    <el-table-column
+      label="参数名称"
+      prop="name"
+      width="120px"
+    ></el-table-column>
+    <el-table-column label="参数名称">
+      <template #default="{ row }">
+        <div v-if="disabled">{{ row.value }}</div>
+        <el-input v-else v-model="row.value" @change="onInputChange"></el-input>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      currentValue: [],
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = (value || []).map((item) => {
+          if (!item.value) {
+            item.value = (item.values || []).join(',')
+          }
+          return item
+        })
+      },
+    },
+  },
+  methods: {
+    onInputChange() {
+      this.$emit('input', this.currentValue)
+      this.$emit('change', this.currentValue)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.goods-params {
+}
+</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/components/order/after-sale-items.vue b/components/order/after-sale-items.vue
new file mode 100644
index 0000000..4019006
--- /dev/null
+++ b/components/order/after-sale-items.vue
@@ -0,0 +1,126 @@
+<template>
+  <el-table :data="currentValue">
+    <el-table-column
+      label="商品名称"
+      prop="flowerName"
+      min-width="100"
+      fixed="left"
+    ></el-table-column>
+    <el-table-column
+      label="商品分类"
+      prop="flowerCategory"
+      min-width="150"
+    ></el-table-column>
+    <el-table-column
+      label="级别"
+      prop="flowerLevelStr"
+      min-width="80"
+    ></el-table-column>
+    <el-table-column
+      label="颜色"
+      prop="flowerColor"
+      min-width="80"
+    ></el-table-column>
+    <el-table-column
+      label="规格"
+      prop="flowerUnit"
+      min-width="100"
+    ></el-table-column>
+    <el-table-column label="数量" prop="num" min-width="100"></el-table-column>
+    <el-table-column
+      label="单价"
+      prop="price"
+      min-width="100"
+    ></el-table-column>
+    <el-table-column
+      label="供应商"
+      prop="supplierName"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="判断责任方"
+      prop="personInCharge"
+      min-width="120"
+      fixed="right"
+    >
+      <template #default="{ row }">
+        <el-bus-select-dict
+          v-if="!disabled"
+          v-model="row.personInCharge"
+          code="CHARGE_PERSON"
+          clearable
+          @change="onFormUpdate"
+        ></el-bus-select-dict>
+        <div v-else>{{ row.personInChargeStr }}</div>
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="赔付金额(元)"
+      prop="amount"
+      min-width="120"
+      fixed="right"
+    >
+      <template #default="{ row }">
+        <el-input-number
+          v-if="!disabled"
+          v-model="row.amount"
+          :precision="2"
+          :min="0"
+          :controls="false"
+          style="width: 100%"
+          @change="onFormUpdate"
+        ></el-input-number>
+        <div v-else>{{ row.amount }}</div>
+      </template>
+    </el-table-column>
+    <el-table-column label="备注" prop="remarks" min-width="250" fixed="right">
+      <template #default="{ row }">
+        <el-input
+          v-if="!disabled"
+          v-model="row.remarks"
+          @change="onFormUpdate"
+        ></el-input>
+        <div v-else>{{ row.remarks }}</div>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      currentValue: [],
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = value || []
+      },
+    },
+  },
+  methods: {
+    onFormUpdate() {
+      this.$emit('input', this.currentValue)
+      this.$emit('change', this.currentValue)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.after-sale-items {
+}
+</style>
diff --git a/components/order/after-sale-table.vue b/components/order/after-sale-table.vue
new file mode 100644
index 0000000..b8cafbb
--- /dev/null
+++ b/components/order/after-sale-table.vue
@@ -0,0 +1,146 @@
+<template>
+  <div class="after-sale-table">
+    <div class="table-header">
+      <div class="table-th">商品信息</div>
+      <div class="table-th">合计详情</div>
+      <div class="table-th !flex-none w-120">供应商信息</div>
+      <div class="table-th">收货人信息</div>
+      <div class="table-th !flex-none w-180">操作</div>
+    </div>
+    <div v-for="item in list" :key="item.id" class="table-item">
+      <div class="table-item__title">
+        <span class="font-bold">订单号:{{ item.orderNo }}</span>
+        <span class="font-bold">售后单号:{{ item.salesNo }}</span>
+        <span>申请时间:{{ item.createTime }}</span>
+        <span
+          >售后状态:<span
+            :class="{ 'text-primary': item.status === 'PENDING' }"
+            >{{ item.statusStr }}</span
+          ></span
+        >
+        <el-tag v-if="item.title" type="danger" size="mini" class="ml-4"
+          >第二次售后</el-tag
+        >
+      </div>
+      <div class="table-body">
+        <div class="table-td">
+          <div class="flex">
+            <el-bus-image :src="item.flowerCover" class="w-60 h-60 mr-8" />
+            <div class="leading-20">
+              <div class="text-14 font-bold">
+                {{ item.flowerName }} × {{ item.flowerNum }}
+              </div>
+              <div class="leading-20">
+                <span>等级:{{ item.flowerLevelStr }}</span>
+                <span class="ml-8">颜色:{{ item.flowerColor }}</span>
+              </div>
+              <div class="leading-20">
+                <span>单价:¥{{ item.price }}</span>
+                <span class="ml-8">订单总额:¥{{ item.total }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="table-td">
+          <div class="leading-20">申请数量:{{ item.num }}</div>
+          <div class="leading-20">实际退款:{{ item.totalFee }}</div>
+          <div class="leading-20 flex">
+            售后类别:
+            <div class="flex-1 text-overflow-2 w-0 break-all">
+              {{ item.salesTypeStr }}
+            </div>
+          </div>
+        </div>
+        <div class="table-td !flex-none w-120 flex items-center">
+          {{ item.supplierName }}
+        </div>
+        <div class="table-td">
+          <div class="leading-20">姓名:{{ item.customer }}</div>
+          <div class="leading-20">联系方式:{{ item.customerTel }}</div>
+          <div class="leading-20 flex">
+            用户地址:
+            <div class="flex-1 w-0">
+              {{ item.customerProvince }}{{ item.customerCity
+              }}{{ item.customerRegion }}{{ item.customerAddress }}
+            </div>
+          </div>
+        </div>
+        <div class="table-td !flex-none w-180 flex items-center">
+          <el-button type="text" @click="onDetail(item)">查看详情</el-button>
+          <el-button
+            v-if="item.status === 'PENDING'"
+            type="text"
+            @click="onHandle(item)"
+            >售后处理</el-button
+          >
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    list: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  methods: {
+    onDetail(item) {
+      this.$emit('detail', item)
+    },
+    onHandle(item) {
+      this.$emit('handle', item)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.after-sale-table {
+  .table-header {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: $main-title-color;
+    background-color: #f4f4f5;
+    .table-th {
+      flex: 1;
+      height: 45px;
+      line-height: 45px;
+      padding: 0 10px;
+      font-weight: bold;
+    }
+  }
+  .table-item {
+    margin-top: 10px;
+    border-bottom: 1px solid #eee;
+    &__title {
+      height: 35px;
+      line-height: 35px;
+      background-color: #f4f4f5;
+      font-size: 14px;
+      color: $main-title-color;
+      padding: 0 10px;
+      & > span {
+        margin-right: 10px;
+      }
+    }
+    .table-body {
+      display: flex;
+      align-items: stretch;
+      font-size: 12px;
+      color: $main-title-color;
+      .table-td {
+        flex: 1;
+        padding: 15px 10px;
+        &:not(:last-child) {
+          border-right: 1px solid #eee;
+        }
+      }
+    }
+  }
+}
+</style>
diff --git a/components/order/check-abnormal-list.vue b/components/order/check-abnormal-list.vue
new file mode 100644
index 0000000..0ce2650
--- /dev/null
+++ b/components/order/check-abnormal-list.vue
@@ -0,0 +1,73 @@
+<template>
+  <el-table :data="value">
+    <el-table-column
+      label="商品名称"
+      prop="flowerName"
+      min-width="150"
+      fixed="left"
+    ></el-table-column>
+    <el-table-column
+      label="级别"
+      prop="flowerLevelStr"
+      min-width="100"
+    ></el-table-column>
+    <el-table-column
+      label="规格"
+      prop="flowerUnit"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="单价(元)"
+      prop="price"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column label="数量" prop="num" min-width="120"></el-table-column>
+    <el-table-column
+      label="花材底价(元)"
+      prop="supplierPrice"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="补货数量"
+      prop="replaceNum"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="降级数量"
+      prop="reduceNum"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="降级退款(元)"
+      prop="reduceAmount"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="缺货数量"
+      prop="lackNum"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="缺货退款(元)"
+      prop="lackAmount"
+      min-width="120"
+    ></el-table-column>
+    <el-table-column
+      label="退款总计(元)"
+      prop="deductAmount"
+      min-width="150"
+      fixed="right"
+    ></el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+  },
+}
+</script>
diff --git a/components/order/evaluation-table.vue b/components/order/evaluation-table.vue
new file mode 100644
index 0000000..2c22745
--- /dev/null
+++ b/components/order/evaluation-table.vue
@@ -0,0 +1,146 @@
+<template>
+  <div class="after-sale-table">
+    <div class="table-header">
+      <div class="table-th">商品名称</div>
+      <div class="table-th !flex-none w-220">订单编号</div>
+      <div class="table-th !flex-none w-120">评价星级</div>
+      <div class="table-th">供应商信息</div>
+      <div class="table-th">评价内容</div>
+      <div class="table-th !flex-none w-220">买家</div>
+      <div class="table-th !flex-none w-220">评价时间</div>
+      <div class="table-th !flex-none w-180">操作</div>
+    </div>
+    <div v-for="item in list" :key="item.id" class="table-item">
+      <div class="table-body">
+        <div class="table-td">
+          <div class="flex">
+            <el-bus-image :src="item.flowerCover" class="w-60 h-60 mr-8" />
+            <div class="leading-20">
+              <div class="text-14 font-bold">
+                {{ item.flowerName }}
+              </div>
+              <div class="leading-20">
+                <span>规格:{{ item.flowerUnit }}</span>
+              </div>
+              <div class="leading-20">
+                <span>等级:{{ item.flowerLevel }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="table-td !flex-none w-220 flex items-center">
+          <div class="leading-20">{{ item.orderNo }}</div>
+        </div>
+        <div class="table-td !flex-none w-120 flex items-center">
+          <div class="leading-20" style="color: #3598db">{{item.commentGrade }}星</div>
+        </div>
+        <div class="table-td">
+          <div class="leading-20">{{ item.supplierName }}[ID:{{ item.supplierId }}]</div>
+        </div>
+
+        <div class="table-td">
+          <div class="leading-20">{{ item.comment }}</div>
+        </div>
+
+        <div class="table-td !flex-none w-220 flex items-center">
+          <div class="leading-20">
+            <div class="leading-20">
+              <span>UID: {{ item.customerId }}</span>
+            </div>
+            <div class="leading-20">
+              <span>昵称: {{ item.customerName }}</span>
+            </div>
+
+          </div>
+        </div>
+
+        <div class="table-td !flex-none w-220 flex items-center">
+          <div class="leading-20">{{ item.createTime }}</div>
+        </div>
+
+        <div class="table-td !flex-none w-180 flex items-center">
+          <el-button type="text" @click="onDetail(item)">查看</el-button>
+          <el-button  v-if="item.showFlag == '1'" type="text" @click="onShow(item)" >显示</el-button>
+          <el-button  v-if="item.showFlag == '0'" type="text" @click="onHide(item)" >隐藏</el-button>
+          <el-button  type="text" @click="onHandle(item)" >回复</el-button>
+          <el-button  type="text" @click="onDelete(item)" >删除</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    list: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  methods: {
+    onDetail(item) {
+      this.$emit('detail', item)
+    },
+    onHandle(item) {
+      this.$emit('handle', item)
+    },
+    onDelete(item) {
+      this.$emit('delete', item)
+    },
+    onShow(item) {
+      this.$emit('show', item)
+    },
+    onHide(item) {
+      this.$emit('hide', item)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.after-sale-table {
+  .table-header {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: $main-title-color;
+    background-color: #f4f4f5;
+    .table-th {
+      flex: 1;
+      height: 45px;
+      line-height: 45px;
+      padding: 0 10px;
+      font-weight: bold;
+    }
+  }
+  .table-item {
+    margin-top: 10px;
+    border-bottom: 1px solid #eee;
+    &__title {
+      height: 35px;
+      line-height: 35px;
+      background-color: #f4f4f5;
+      font-size: 14px;
+      color: $main-title-color;
+      padding: 0 10px;
+      & > span {
+        margin-right: 10px;
+      }
+    }
+    .table-body {
+      display: flex;
+      align-items: stretch;
+      font-size: 12px;
+      color: $main-title-color;
+      .table-td {
+        flex: 1;
+        padding: 15px 10px;
+        &:not(:last-child) {
+          border-right: 1px solid #eee;
+        }
+      }
+    }
+  }
+}
+</style>
diff --git a/components/order/goods-table-item-list.vue b/components/order/goods-table-item-list.vue
new file mode 100644
index 0000000..3ba7893
--- /dev/null
+++ b/components/order/goods-table-item-list.vue
@@ -0,0 +1,41 @@
+<template>
+  <div class="goods-table-item-list">
+    <div v-for="(item, index) in showItems" :key="index">{{ item.str }}</div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  computed: {
+    showItems() {
+      const list = this.items || []
+      return list.map((item) => {
+        const str = [
+          `${item.flowerName} · ${item.flowerColor}`,
+          item.flowerLevelStr,
+          item.flowerCategory,
+          item.flowerUnit,
+          item.supplierName,
+          `¥${item.price} × ${item.num}扎`,
+        ]
+          .filter((i) => !!i)
+          .join('|')
+        return {
+          str,
+        }
+      })
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.goods-table-item-list {
+}
+</style>
diff --git a/components/order/level-down-list.vue b/components/order/level-down-list.vue
new file mode 100644
index 0000000..edf8f61
--- /dev/null
+++ b/components/order/level-down-list.vue
@@ -0,0 +1,49 @@
+<template>
+  <el-table :data="list">
+    <el-table-column label="商品名称" prop="flowerName"></el-table-column>
+    <el-table-column label="商品等级" prop="flowerLevelStr"></el-table-column>
+    <el-table-column label="商品数量" prop="num"></el-table-column>
+    <el-table-column label="单价" prop="price"></el-table-column>
+    <el-table-column label="供应商名称" prop="supplierName"></el-table-column>
+    <el-table-column label="质检备注" prop="checkRemarks"></el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  props: {
+    orderId: {
+      type: String,
+      default: null,
+    },
+  },
+  data() {
+    return {
+      list: [],
+    }
+  },
+  watch: {
+    orderId(value) {
+      if (value) {
+        this.getOrderItemList(value)
+      }
+    },
+  },
+  methods: {
+    async getOrderItemList(id) {
+      const { code, data } = await this.$elBusHttp.request(
+        'flower/api/order/item/list',
+        { params: { id } }
+      )
+      if (code === 0) {
+        this.list = (data || []).filter((i) => i.status === 'reduce')
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.level-down-list {
+}
+</style>
diff --git a/components/order/print-list.vue b/components/order/print-list.vue
new file mode 100644
index 0000000..2e270b4
--- /dev/null
+++ b/components/order/print-list.vue
@@ -0,0 +1,137 @@
+<template>
+  <div class="print-list">
+    <div v-for="(item, i) in orderList" :key="i" class="break-page">
+      <div ref="orderTable" class="print-item">
+        <el-row :gutter="10" class="mb-15">
+          <el-col :span="24">
+            <div class="area-text">
+              {{ item.warehouseName || '' }}/{{
+                item.warehouseLocationCode || ''
+              }}
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="desc-info">
+              <div>姓名:</div>
+              <div class="desc-info__value">{{ item.customer }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="desc-info">
+              <div>电话:</div>
+              <div class="desc-info__value">{{ item.customerTel }}</div>
+            </div>
+          </el-col>
+          <el-col :span="24">
+            <div class="desc-info">
+              <div>收货地址:</div>
+              <div class="desc-info__value">
+                {{ item.customerProvince }}{{ item.customerCity
+                }}{{ item.customerRegion }}{{ item.customerAddress }}
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+        <el-table-print
+          :data="item.items"
+          :summary-method="getSummaries.bind(this, item.totalAmount)"
+          show-summary
+          border
+          style="width: 100%"
+        >
+          <el-table-column label="序号" type="index"></el-table-column>
+          <el-table-column
+            prop="orderNo"
+            label="订单号"
+            align="center"
+          ></el-table-column>
+          <el-table-column
+            label="下单品种"
+            :formatter="
+              (row) => `${row.flowerName || ''} · ${row.flowerColor || ''}`
+            "
+            align="center"
+          >
+          </el-table-column>
+          <el-table-column prop="flowerLevelStr" label="等级" align="center">
+          </el-table-column>
+          <el-table-column prop="num" label="数量" align="center">
+          </el-table-column>
+          <el-table-column prop="flowerUnit" label="规格" align="center">
+          </el-table-column>
+          <el-table-column
+            prop="supplierName"
+            label="供应商名称"
+            align="center"
+          >
+          </el-table-column>
+          <el-table-column prop="stationName" label="所属集货站" align="center">
+          </el-table-column>
+        </el-table-print>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+// import groupBy from 'lodash.groupby'
+export default {
+  props: {
+    orderList: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  computed: {
+    // groupList() {
+    //   const sanhuList = this.orderList.filter((i) => !i.partnerId)
+    //   const partnerList = this.orderList.filter((i) => !!i.partnerId)
+    //   const sList = groupBy(sanhuList, (i) => i.createBy)
+    //   const pList = groupBy(partnerList, (i) => i.partnerId)
+    //   return [...Object.values(pList), ...Object.values(sList)]
+    // },
+  },
+  methods: {
+    objectSpanMethod(len, { rowIndex, columnIndex }) {
+      if (columnIndex === 0) {
+        if (rowIndex === 0) {
+          return {
+            rowspan: len,
+            colspan: 1,
+          }
+        } else {
+          return {
+            rowspan: 0,
+            colspan: 0,
+          }
+        }
+      }
+    },
+    getSummaries(totalAmount, param) {
+      const { columns, data } = param
+      const sums = []
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = `总扎数合计:${data.reduce((total, current) => {
+            total += current.num
+            return total
+          }, 0)}                总金额:¥${totalAmount}`
+        } else {
+          sums[index] = ''
+        }
+        // if (index === 1) {
+        //   sums[index] = '总扎数合计'
+        // } else if (index === 2) {
+        //   sums[index] = data.reduce((total, current) => {
+        //     total += current.num
+        //     return total
+        //   }, 0)
+        // } else {
+        //   sums[index] = ''
+        // }
+      })
+      return sums
+    },
+  },
+}
+</script>
diff --git a/components/order/video-list.vue b/components/order/video-list.vue
new file mode 100644
index 0000000..1266e54
--- /dev/null
+++ b/components/order/video-list.vue
@@ -0,0 +1,30 @@
+<template>
+  <div class="video-list">
+    <video
+      v-for="(item, index) in value"
+      :key="index"
+      controls
+      width="300px"
+      height="200"
+      class="mr-20 mb-15"
+    >
+      <source :src="item" />
+    </video>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: Array,
+      default: () => [],
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.video-list {
+}
+</style>
diff --git a/components/simple-text.vue b/components/simple-text.vue
new file mode 100644
index 0000000..5366caa
--- /dev/null
+++ b/components/simple-text.vue
@@ -0,0 +1,33 @@
+<template>
+  <div class="simple-text" :class="{ 'is-primary': type === 'primary' }">
+    {{ text || value }}
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: [String, Number],
+      default: null,
+    },
+    type: {
+      type: String,
+      default: '',
+    },
+    text: {
+      type: [String, Number],
+      default: null,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.simple-text {
+  display: inline-block;
+  &.is-primary {
+    color: $primary-color;
+  }
+}
+</style>
diff --git a/components/sms/copy-textarea.vue b/components/sms/copy-textarea.vue
new file mode 100644
index 0000000..16e3b90
--- /dev/null
+++ b/components/sms/copy-textarea.vue
@@ -0,0 +1,92 @@
+<template>
+  <div class="copy-textarea">
+    <div>
+      <span style="color:red;">手动输入最多支持100个号码,大批量号码建议通过文件导入形式提交</span>
+      <el-button type="text" @click="clearVal">点击清空</el-button></div>
+    <el-input type="textarea" :rows="5" v-model="currentValue"
+      placeholder="提示:一行输入一个号码,多个手机号请换行隔开。"
+      width="80%"
+      @change="handlerInputChange"
+    ></el-input>
+    <div>
+      <span style="color:gray;">提示:一行输入一个号码,多个手机号请换行隔开。</span>
+    </div>
+   
+  </div>
+</template>
+
+<script>
+import cloneDeep from 'lodash.clonedeep'
+export default {
+  props: {
+    value: {
+      type: String,
+      default:'',
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      currentValue: '',
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = value 
+      },
+    },
+  },
+  methods: {
+    clearVal(){
+      this.$elBusUtil
+        .confirm('确定要清空吗?')
+        .then(() => {
+          this.currentValue = ''
+          this.$emit('input', '')
+          this.$emit('change', '')
+        })
+        .catch(() => {})
+    },
+    handlerInputChange(){
+      this.$emit('input', this.currentValue)
+      this.$emit('change', this.currentValue)
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.copy-textarea {
+}
+</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/sms/select-all-user.vue b/components/sms/select-all-user.vue
new file mode 100644
index 0000000..524604d
--- /dev/null
+++ b/components/sms/select-all-user.vue
@@ -0,0 +1,246 @@
+<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="loginName"></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.loginName
+            }}</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/user/user/list',
+        saveQuery: false,
+        hasNew: false,
+        hasEdit: false,
+        hasDelete: false,
+        hasView: false,
+        columns: [
+          { type: 'selection' },
+          { label: 'id', prop: 'id' },
+          { label: '名称', prop: 'loginName' },
+          { label: '注册手机号', prop: 'tel' },
+        ],
+        extraButtons: [
+          {
+            text: '选择',
+            show: (row) =>
+              !this.currentValue.find((item) => item.id === row.id),
+            atClick: (row) => {
+              this.currentValue.push({
+                id: row.id,
+                userId: row.userId,
+                loginName: row.loginName,
+                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
+            },
+          },
+        ],
+        headerButtons: [
+          {
+            text: '批量选择',
+            type: 'primary',
+            disabled: (selected) => selected.length === 0,
+            atClick: (selected) => {
+              console.log(selected)
+              selected.forEach(item => {
+                // 检查 selected 数组的每个元素是否已在 currentValue 中
+                if (!this.currentValue.some(currentItem => currentItem.id === item.id)) {
+                  this.currentValue.push({
+                    id: item.id,
+                    userId: item.userId,
+                    loginName: item.loginName,
+                    tel: item.tel,
+                  })
+                }
+              });
+              return true
+            },
+          },
+          {
+            text: '批量取消',
+            type: 'primary',
+            disabled: (selected) => selected.length === 0,
+            atClick: (selected) => {
+              selected.forEach(item => {
+                // 检查 selected 数组的每个元素是否存在于 currentValue 中
+                const index = this.currentValue.findIndex(currentItem => currentItem.id === item.id);
+                if (index !== -1) {
+                  this.currentValue.splice(index, 1); // 如果存在,则移除
+                }
+              });
+              return true
+            },
+          },
+        ],
+
+        searchFormAttrs: {
+          labelWidth: 'auto',
+        },
+        searchForm: [
+          {
+            type: 'row',
+            span: 12,
+            items: [
+              {
+                label: '列表类型:',
+                id: 'userType',
+                type: 'bus-select-dict',
+                el: {
+                  code: 'USER_TYPE',
+                  multiple: false,
+                  style: 'width:100%',
+                },
+                default: 'customer',
+                searchImmediately: true,
+                on: {
+                change: (e, updateForm, obj) => {
+                  console.log(e[0])
+                  // if (e[0] === 'supplier') {
+                  //   this.tableConfig.url = 'flower/api/supplier/page'
+                  // } else if (e[0] === 'partner') {
+                  //   this.tableConfig.url = 'flower/api/partner/page'
+                  // }else if(e[0]==='customer'){
+                  //   this.tableConfig.url = 'flower/api/customer/page'
+                  // }
+                },
+              },
+
+              },
+
+              { label: 'id:', id: 'id', type: 'input' },
+              { label: '名称:', id: 'loginName', type: 'input' },
+              { label: '注册手机号:', id: 'tel', type: 'input' },
+            ],
+          },
+        ],
+      },
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = cloneDeep(value || [])
+      },
+    },
+  },
+  methods: {
+    handleSelectionChange(rows) {
+      console.log(rows)
+      alert("全选")
+    },
+    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/sms/template-download.vue b/components/sms/template-download.vue
new file mode 100644
index 0000000..5a631a5
--- /dev/null
+++ b/components/sms/template-download.vue
@@ -0,0 +1,80 @@
+<template>
+  <div class="copy-textarea">
+    <div>
+      <el-link href="https://hmy-flower.oss-cn-shanghai.aliyuncs.com/ea/ea5ed90664d345768245f20682e9564bsample.xlsx" download="template_phone.xlsx"> 
+        <span style="color:#5FA7EE;">点击下载模板</span>
+      </el-link>
+
+      <!-- <el-link @click="downloadTemplate">点击下载模板a</el-link> -->
+    </div>
+   
+  </div>
+</template>
+
+<script>
+import cloneDeep from 'lodash.clonedeep'
+export default {
+  props: {
+    value: {
+      type: String,
+      default:'',
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      currentValue: '',
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(value) {
+        this.currentValue = value 
+      },
+    },
+  },
+  methods: {
+    downloadTemplate() {
+      const link = document.createElement('a');
+      link.href = 'https://hmy-flower.oss-cn-shanghai.aliyuncs.com/a5/a57ec65b165148e5a669e7766743e489template_phone.xlsx';
+      link.download = 'template_phone.xlsx';
+      link.click();
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.copy-textarea {
+}
+</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/tags-view/index.vue b/components/tags-view/index.vue
new file mode 100644
index 0000000..364f853
--- /dev/null
+++ b/components/tags-view/index.vue
@@ -0,0 +1,299 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane
+      ref="scrollPane"
+      class="tags-view-wrapper"
+      @scroll="handleScroll"
+    >
+      <tag-item
+        v-for="tag in visitedViews"
+        ref="tag"
+        :key="tag.path"
+        :class="isActive(tag) ? 'active' : ''"
+        :to="{
+          ...tag,
+        }"
+        tag="span"
+        class="tags-view-item"
+        @click.native.prevent="toCurrentTag(tag)"
+        @click.middle.native="closeSelectedTag(tag)"
+        @contextmenu.prevent.native="openMenu(tag, $event)"
+      >
+        {{ tag.title }}
+        <span
+          v-if="!isOnlyOne"
+          class="el-icon-close"
+          @click.prevent.stop="closeSelectedTag(tag)"
+        />
+      </tag-item>
+    </scroll-pane>
+    <ul
+      v-show="visible"
+      :style="{ left: left + 'px', top: top + 'px' }"
+      class="contextmenu"
+    >
+      <li
+        :class="{ 'is-disabled': isOnlyOne }"
+        @click="closeSelectedTag(selectedTag)"
+      >
+        关闭
+      </li>
+      <li :class="{ 'is-disabled': isOnlyOne }" @click="closeOthersTags">
+        关闭其他标签页
+      </li>
+      <li
+        :class="{ 'is-disabled': isLastView }"
+        @click="closeRight(selectedTag)"
+      >
+        关闭右侧标签页
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import ScrollPane from './scroll-pane'
+import TagItem from './tag-item'
+
+export default {
+  components: { ScrollPane, TagItem },
+  data() {
+    return {
+      visible: false,
+      top: 0,
+      left: 0,
+      selectedTag: {},
+    }
+  },
+  computed: {
+    ...mapState({
+      visitedViews: (state) => state.tagsView.visitedViews,
+    }),
+    isLastView() {
+      const latestView = this.visitedViews.slice(-1)[0]
+      if (latestView?.name && latestView.name === this.selectedTag.name) {
+        return true
+      }
+      return false
+    },
+    isOnlyOne() {
+      return this.visitedViews.length <= 1
+    },
+  },
+  watch: {
+    $route() {
+      const timer = setTimeout(() => {
+        this.addTags()
+        this.moveToCurrentTag()
+        clearTimeout(timer)
+      }, 500)
+    },
+    visible(value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    },
+  },
+  mounted() {
+    this.addTags()
+  },
+  methods: {
+    isActive(route) {
+      return route.name === this.$route.name
+    },
+    toCurrentTag(tag) {
+      if (!this.isActive(tag)) {
+        this.$router.push(tag.fullPath)
+      }
+    },
+    addTags() {
+      const { name } = this.$route
+      if (name) {
+        this.$store.dispatch('tagsView/addVisitedView', this.$route)
+      }
+      return false
+    },
+    moveToCurrentTag() {
+      const tags = this.$refs.tag
+      if (tags) {
+        this.$nextTick(() => {
+          for (const tag of tags) {
+            if (tag.to.name === this.$route.name) {
+              this.$refs.scrollPane.moveToTarget(tag, this.visitedViews)
+              break
+            }
+          }
+        })
+      }
+    },
+    closeSelectedTag(view) {
+      if (!this.isOnlyOne) {
+        this.$store
+          .dispatch('tagsView/delVisitedView', view)
+          .then((visitedViews) => {
+            if (this.isActive(view)) {
+              this.toLastView(visitedViews)
+              // this.moveToCurrentTag()
+            }
+          })
+      }
+    },
+    closeOthersTags() {
+      if (!this.isOnlyOne) {
+        this.$router.push(this.selectedTag)
+        this.$store
+          .dispatch('tagsView/delOthersVisitedViews', this.selectedTag)
+          .then(() => {
+            // this.moveToCurrentTag()
+          })
+      }
+    },
+    closeRight() {
+      if (!this.isLastView) {
+        this.$store
+          .dispatch('tagsView/delRightVisitedViews', this.selectedTag)
+          .then((visitedViews) => {
+            if (!visitedViews.find((item) => item.name === this.$route.name)) {
+              if (
+                this.$route.matched.slice(-1)[0]?.instances?.default?.$metaInfo
+                  ?.title
+              ) {
+                this.toLastView(visitedViews)
+              }
+            }
+          })
+      }
+    },
+    toLastView(visitedViews) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      }
+    },
+    openMenu(tag, e) {
+      const menuMinWidth = 105
+      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+      const offsetWidth = this.$el.offsetWidth // container width
+      const maxLeft = offsetWidth - menuMinWidth // left boundary
+      const left = e.clientX - offsetLeft + 15 // 15: margin right
+
+      if (left > maxLeft) {
+        this.left = maxLeft
+      } else {
+        this.left = left
+      }
+
+      this.top = e.clientY
+      this.visible = true
+      this.selectedTag = tag
+    },
+    closeMenu() {
+      this.visible = false
+    },
+    handleScroll() {
+      this.closeMenu()
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+  height: 34px;
+  width: 100%;
+  background: #fff;
+  border-bottom: 1px solid #d8dce5;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 26px;
+      line-height: 26px;
+      border: 1px solid #d8dce5;
+      color: #495060;
+      background: #fff;
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      margin-top: 4px;
+      &:first-of-type {
+        margin-left: 15px;
+      }
+      &:last-of-type {
+        margin-right: 15px;
+      }
+      &.active {
+        background-color: #42b983;
+        color: #fff;
+        border-color: #42b983;
+        &::before {
+          content: '';
+          background: #fff;
+          display: inline-block;
+          width: 8px;
+          height: 8px;
+          border-radius: 50%;
+          position: relative;
+          margin-right: 2px;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &.is-disabled {
+        color: #999;
+        cursor: not-allowed;
+      }
+      &:not(.is-disabled):hover {
+        background: #eee;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+  .tags-view-item {
+    .el-icon-close {
+      width: 16px;
+      height: 16px;
+      vertical-align: 2px;
+      border-radius: 50%;
+      text-align: center;
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      transform-origin: 100% 50%;
+      &:before {
+        transform: scale(0.6);
+        display: inline-block;
+        vertical-align: -3px;
+      }
+      &:hover {
+        background-color: #b4bccc;
+        color: #fff;
+      }
+    }
+  }
+}
+</style>
diff --git a/components/tags-view/scroll-pane.vue b/components/tags-view/scroll-pane.vue
new file mode 100644
index 0000000..3402077
--- /dev/null
+++ b/components/tags-view/scroll-pane.vue
@@ -0,0 +1,119 @@
+<template>
+  <el-scrollbar
+    ref="scrollContainer"
+    :vertical="false"
+    class="scroll-container"
+    @wheel.native.prevent="handleScroll"
+  >
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script>
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0,
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.scrollContainer.$refs.wrap
+    },
+  },
+  mounted() {
+    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
+  },
+  beforeDestroy() {
+    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    emitScroll() {
+      this.$emit('scroll')
+    },
+    moveToTarget(currentTag, visitedViews) {
+      const $container = this.$refs.scrollContainer.$el
+      const $containerWidth = $container.offsetWidth
+      const $scrollWrapper = this.scrollWrapper
+      // 这边的tagList的顺序不能保证和实际源数组的顺序保持一致
+      // 比如源数组中在某个位置删除一项,然后在相同位置加入一项,那么加入的这一项是在tagList末尾
+      // 所以这边不能直接根据顺序去取第一项和最后一项
+      const tagList = this.$parent.$refs.tag
+
+      let firstTag = null
+      let lastTag = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList.find(
+          (item) => item?.to?.name === visitedViews[0].name
+        )
+        lastTag = tagList.find(
+          (item) =>
+            item?.to?.name === visitedViews[visitedViews.length - 1].name
+        )
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex = visitedViews.findIndex(
+          (item) => item.name === currentTag?.to?.name
+        )
+        const prevTag = tagList.find(
+          (item) => item?.to?.name === visitedViews[currentIndex - 1].name
+        )
+        const nextTag = tagList.find(
+          (item) => item?.to?.name === visitedViews[currentIndex + 1].name
+        )
+
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft =
+          nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft =
+          prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (
+          afterNextTagOffsetLeft >
+          $scrollWrapper.scrollLeft + $containerWidth
+        ) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  ::v-deep {
+    .el-scrollbar__bar {
+      bottom: 0px;
+    }
+    .el-scrollbar__wrap {
+      height: 49px;
+      overflow-x: auto !important;
+    }
+  }
+}
+</style>
diff --git a/components/tags-view/tag-item.vue b/components/tags-view/tag-item.vue
new file mode 100644
index 0000000..03888a5
--- /dev/null
+++ b/components/tags-view/tag-item.vue
@@ -0,0 +1,15 @@
+<template>
+  <span><slot></slot></span>
+</template>
+
+<script>
+// 原来用router-link跳转时有问题,为了较少改动增加一个没有实际意义的组件
+export default {
+  name: 'TagItem',
+  props: {
+    to: Object,
+  },
+}
+</script>
+
+<style scoped></style>
diff --git a/components/warehouse/location-item.vue b/components/warehouse/location-item.vue
new file mode 100644
index 0000000..55c4987
--- /dev/null
+++ b/components/warehouse/location-item.vue
@@ -0,0 +1,199 @@
+<template>
+  <el-card class="location-item">
+    <div class="location-item__main">
+      <div class="location-item__title text-overflow-1">
+        <div>{{ info.code }}</div>
+        <div
+          v-if="info.used && info.orderDTO && info.orderDTO.length > 0"
+          class="h-130 py-10"
+        >
+          <div class="flex items-center">
+            <div class="text-12 text-primary flex-1 text-overflow-1">
+              {{ info.orderDTO[0].orderNo }}
+            </div>
+            <el-popover placement="bottom" trigger="hover">
+              <el-table :data="info.orderDTO">
+                <el-table-column
+                  prop="orderNo"
+                  label="订单号"
+                  min-width="150"
+                ></el-table-column>
+                <el-table-column
+                  label="订单金额(元)"
+                  prop="totalAmount"
+                  min-width="120"
+                ></el-table-column>
+                <el-table-column
+                  label="下单时间"
+                  prop="createTime"
+                  min-width="180"
+                ></el-table-column>
+                <el-table-column
+                  label="合伙人"
+                  prop="partnerName"
+                  min-width="120"
+                ></el-table-column>
+              </el-table>
+              <el-button slot="reference" type="text" class="p-0 ml-4"
+                >查看更多</el-button
+              >
+            </el-popover>
+          </div>
+          <div
+            v-if="info.goodsItems && info.goodsItems.length > 0"
+            class="flex items-center mt-10"
+          >
+            <div class="text-subTitle text-12 flex-1 text-overflow-1">
+              {{ info.goodsItems[0].flowerName
+              }}<span class="ml-8">{{ info.goodsItems[0].flowerLevelStr }}</span
+              ><span class="ml-8">{{ info.goodsItems[0].flowerColor }}</span
+              ><span class="ml-8">{{ info.goodsItems[0].flowerUnit }}</span
+              >×{{ info.goodsItems[0].num }}
+            </div>
+            <el-popover placement="bottom" trigger="hover">
+              <el-table :data="info.goodsItems">
+                <el-table-column
+                  prop="flowerName"
+                  label="商品名称"
+                ></el-table-column>
+                <el-table-column
+                  prop="flowerLevelStr"
+                  label="级别"
+                ></el-table-column>
+                <el-table-column
+                  prop="flowerColor"
+                  label="颜色"
+                ></el-table-column>
+                <el-table-column
+                  property="flowerUnit"
+                  label="规格"
+                ></el-table-column>
+                <el-table-column
+                  property="supplierName"
+                  label="供应商名称"
+                ></el-table-column>
+                <el-table-column
+                  property="orderNo"
+                  label="订单号"
+                  min-width="150"
+                ></el-table-column>
+              </el-table>
+              <el-button slot="reference" type="text" class="p-0 ml-4"
+                >查看更多</el-button
+              >
+            </el-popover>
+          </div>
+          <div class="text-subTitle text-12 mt-10 text-overflow-1">
+            {{ info.orderDTO[0].customer
+            }}<span class="ml-8">{{ info.orderDTO[0].customerTel }}</span>
+          </div>
+          <el-tooltip
+            v-if="info.orderDTO[0].customerAddress"
+            class="item"
+            effect="dark"
+            :content="`${info.orderDTO[0].customerProvince || ''}${
+              info.orderDTO[0].customerCity || ''
+            }${info.orderDTO[0].customerRegion || ''}${
+              info.orderDTO[0].customerAddress || ''
+            }`"
+            placement="top-start"
+          >
+            <div class="text-subTitle text-12 mt-10 text-overflow-1">
+              {{ info.orderDTO[0].customerProvince
+              }}{{ info.orderDTO[0].customerCity
+              }}{{ info.orderDTO[0].customerRegion
+              }}{{ info.orderDTO[0].customerAddress }}
+            </div>
+          </el-tooltip>
+        </div>
+        <div
+          v-else
+          class="h-130 flex items-center justify-center text-primary cursor-pointer"
+          @click="onAddOrder"
+        >
+          <i class="el-icon-circle-plus-outline mr-6"></i>
+          添加订单
+        </div>
+      </div>
+    </div>
+    <div class="location-item__bottom">
+      <div class="location-item__area">
+        {{ info.warehouseName }}
+      </div>
+      <div>
+        <el-tooltip class="item" effect="dark" content="编辑">
+          <i class="el-icon-edit" @click="editItem"></i>
+        </el-tooltip>
+        <el-tooltip class="item" effect="dark" content="删除">
+          <i class="el-icon-delete is-delete" @click="deleteItem"></i>
+        </el-tooltip>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script>
+export default {
+  props: {
+    info: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  methods: {
+    editItem() {
+      this.$emit('edit')
+    },
+    deleteItem() {
+      this.$emit('delete')
+    },
+    onAddOrder() {
+      this.$emit('addOrder')
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.location-item {
+  position: relative;
+  margin-bottom: 20px;
+  &__main {
+    padding-top: 20px;
+  }
+  &__title {
+    font-size: 16px;
+    color: $main-title-color;
+    font-weight: bold;
+  }
+  &__bottom {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border-top: 1px solid #eee;
+    height: 50px;
+    i {
+      font-size: 18px;
+      font-weight: normal;
+      padding: 0 6px;
+      cursor: pointer;
+      margin-left: 8px;
+      &.is-delete {
+        color: $danger-color;
+      }
+    }
+  }
+  &__area {
+    font-size: 14px;
+    color: $main-title-color;
+  }
+}
+::v-deep {
+  .el-card {
+    &__body {
+      padding-top: 0;
+      padding-bottom: 0;
+    }
+  }
+}
+</style>
diff --git a/components/warehouse/select-order.vue b/components/warehouse/select-order.vue
new file mode 100644
index 0000000..c3c4869
--- /dev/null
+++ b/components/warehouse/select-order.vue
@@ -0,0 +1,68 @@
+<template>
+  <el-bus-crud ref="crud" v-bind="tableConfig"></el-bus-crud>
+</template>
+
+<script>
+import { getPartnerListConfig } from '@/utils/form-item-config'
+export default {
+  props: {
+    value: {
+      type: String,
+      default: null,
+    },
+  },
+  data() {
+    return {
+      tableConfig: {
+        url: 'flower/api/warehouse/location/list/orders',
+        hasNew: false,
+        hasEdit: false,
+        hasDelete: false,
+        hasView: false,
+        saveQuery: false,
+        columns: [
+          {
+            label: '序号',
+            type: 'index',
+          },
+          { label: '订单号', prop: 'orderNo' },
+          { label: '下单人姓名', prop: 'customer' },
+          { label: '下单人联系电话', prop: 'customerTel' },
+          {
+            label: '下单人地址',
+            formatter: (row) =>
+              `${row.customerProvince || ''}${row.customerCity || ''}${
+                row.customerRegion || ''
+              }${row.customerAddress || ''}`,
+          },
+          { label: '所属合伙人', prop: 'partnerName' },
+        ],
+        searchForm: [
+          {
+            type: 'row',
+            items: [
+              { label: '订单号', id: 'orderNo', type: 'input' },
+              { ...getPartnerListConfig() },
+            ],
+          },
+        ],
+        extraButtons: [
+          {
+            text: '选择',
+            show: (row) => this.value !== row.id,
+            atClick: (row) => {
+              this.$emit('input', row.id)
+              return false
+            },
+          },
+          {
+            text: '已选择',
+            disabled: () => true,
+            show: (row) => this.value === row.id,
+          },
+        ],
+      },
+    }
+  },
+}
+</script>
diff --git a/config/default-dev.json5 b/config/default-dev.json5
index 36520fe..28f1e82 100644
--- a/config/default-dev.json5
+++ b/config/default-dev.json5
@@ -1,3 +1,3 @@
 {
-  httpBaseUri: 'http://localhost:8080',
+  httpBaseUri: 'http://192.168.1.213:8080',
 }
diff --git a/config/default-test.json5 b/config/default-test.json5
index 473ad38..513a785 100644
--- a/config/default-test.json5
+++ b/config/default-test.json5
@@ -1,6 +1,6 @@
 {
-  //httpBaseUri: 'http://localhost:8080',
-  httpBaseUri: 'http://14.103.144.28',
+  httpBaseUri: 'http://192.168.1.213:8080',
+//  httpBaseUri: 'http://14.103.144.28',
   baseUrl: '/platform/'
 }
 
diff --git a/pages/content/filmset.vue b/pages/content/filmset.vue
index 54edd61..35ff877 100644
--- a/pages/content/filmset.vue
+++ b/pages/content/filmset.vue
@@ -7,21 +7,42 @@
   data() {
     return {
       tableConfig: {
-        url: 'flower/api/filmset/page',
+        url: 'flower/api/filmWorks/list',
+        newUrl: 'flower/api/filmWorks/new',
+        editUrl: 'flower/api/filmWorks/edit',
+        deleteUrl: 'flower/api/filmWorks/delete',
         dialogNeedRequest: true,
         persistSelection: true,
         columns: [
           { type: 'selection' },
-          { label: '标题', prop: 'title' },
-          { label: '发布日期', prop: 'publishDate' },
-          { label: '编辑日期', prop: 'updateTime' },
-          { label: '状态', prop: 'statusStr' },
+          { label: '中文名称', prop: 'nameCn', minWidth: 120  },
+          { label: '英文名称', prop: 'nameEn', minWidth: 120  },
+          { label: '作品类型', prop: 'typeStr' , minWidth: 150 },
+          { label: '上映年份', prop: 'releaseYear' },
+          { label: '导演', prop: 'director', minWidth: 150  },
+          { label: '制片方', prop: 'producer', minWidth: 150  },
+          { label: '主要演员', prop: 'actors', minWidth: 300  },
+          { label: '剧情关键词', prop: 'keywords' },
+          { label: '剧情简介', prop: 'synopsis' , minWidth: 400 },
+          { label: '封面图片' ,formatter: this.formatterImage, minWidth: 200 },
+          { label: '封面图片描述', prop: 'coverAlt', minWidth: 120  },
+          { label: '用户类型', prop: 'userTypeStr' },
+          { label: '置顶权重', prop: 'stickyWeight' , minWidth: 80 },
+          { label: '状态', prop: 'statusStr' , minWidth: 80 },
+          { label: '收藏量', prop: 'collectCount' , minWidth: 80 },
+          { label: '点赞量', prop: 'likeCount', minWidth: 80  },
+          { label: '评论量', prop: 'commentCount', minWidth: 80  },
+          { label: '分享量', prop: 'shareCount', minWidth: 80  },
+          { label: '创建日期', prop: 'createDate', minWidth: 80  },
         ],
         searchForm: [
           {
             type: 'row',
             items: [
-              { label: '标题', id: 'title', type: 'input' },
+              { label: '中文名称', id: 'nameCn', type: 'input' },
+              { label: '英文名称', id: 'nameEn', type: 'input' },
+              { label: '作品类型', id: 'type', type: 'input' },
+              { label: '上映年份', id: 'releaseYear', type: 'input' },
               {
                 label: '创建日期',
                 id: 'createDateBeginStr',
@@ -35,8 +56,8 @@
         ],
         form: [
           {
-            label: '标题:',
-            id: 'title',
+            label: '中文名称:',
+            id: 'nameCn',
             type: 'input',
             rules: {
               required: true,
@@ -45,7 +66,17 @@
             },
           },
           {
-            label: '片场内容类型:',
+            label: '英文名称:',
+            id: 'nameEn',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入标题',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '作品类型:',
             id: 'type',
             type: 'bus-select-dict',
             el: {
@@ -55,29 +86,153 @@
             },
             rules: {
               required: true,
-              message: '请选择片场内容类型',
+              message: '请选择影视作品类型',
             },
           },
           {
-            label: '内容:',
-            id: 'content',
+            label: '上映年份:',
+            id: 'releaseYear',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入上映年份',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '导演:',
+            id: 'director',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入导演,多个导演,分割',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '制片方:',
+            id: 'producer',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入制片方,多个制片方,分割',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '主要演员:',
+            id: 'actors',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入主要演员,多个主要演员,分割',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '剧情关键词:',
+            id: 'keywords',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入剧情关键词',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '剧情简介:',
+            id: 'synopsis',
             component: 'base-editor',
             richText: true,
-            rules: { required: true, message: '请输入内容', trigger: 'blur' },
+            rules: { required: true, message: '请输入剧情简介', trigger: 'blur' },
           },
-          // {
-          //   label: '封面:',
-          //   id: 'cover',
-          //   type: 'bus-upload',
-          //   el: {
-          //     listType: 'picture-card',
-          //     limitSize: 2,
-          //     limit: 1,
-          //     tipText: '大小不超过2M',
-          //     valueType: 'string',
-          //   },
-          //   forceDisabled: true,
-          // },
+          {
+            label: '封面图片:',
+            id: 'coverUrl',
+            type: 'bus-upload',
+            el: {
+              listType: 'picture-card',
+              limitSize: 2,
+              limit: 1,
+              tipText: '大小不超过2M',
+              valueType: 'string',
+            },
+            forceDisabled: true,
+          },
+          {
+            label: '封面图片描述:',
+            id: 'coverAlt',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入封面图片描述',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '用户类型:',
+            id: 'userType',
+            type: 'bus-select-dict',
+            el: {
+              code: 'FILMSET_CREATE_TYPE',
+              style: 'width:100%',
+              clearable: true,
+            },
+            rules: {
+              required: true,
+              message: '请选择用户类型',
+            },
+          },
+          {
+            label: '置顶权重:',
+            id: 'stickyWeight',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入置顶权重描述',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '收藏量:',
+            id: 'collectCount',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入收藏量',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '点赞量:',
+            id: 'likeCount',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入点赞量',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '评论量:',
+            id: 'commentCount',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入评论量',
+              trigger: 'blur',
+            },
+          },
+          {
+            label: '分享量:',
+            id: 'shareCount',
+            type: 'input',
+            rules: {
+              required: true,
+              message: '请输入分享量',
+              trigger: 'blur',
+            },
+          },
         ],
         extraButtons: [
           {
@@ -87,7 +242,7 @@
               try {
                 await this.$elBusUtil.confirm(`确定要${action}吗?`)
                 const { code } = await this.$elBusHttp.request(
-                  'flower/api/filmset/page/changeStatus',
+                  'flower/api/filmWorks/changeStatus',
                   { params: { id: row.id } }
                 )
                 if (code === 0) {
@@ -115,7 +270,7 @@
                   `确定要批量发布这${selectedNotice.length}个片场内容吗?`
                 )
                 const { code } = await this.$elBusHttp.request(
-                  'flower/api/filmset/page/publish/batch',
+                  'flower/api/filmWorks/publish/batch',
                   {
                     method: 'post',
                     data: {
@@ -142,7 +297,7 @@
                   `确定要批量删除这${selected.length}个片场内容吗?`
                 )
                 const { code } = await this.$elBusHttp.request(
-                  'flower/api/filmset/page/delete/batch',
+                  'flower/api/filmWorks/delete/batch',
                   {
                     method: 'post',
                     data: {
@@ -165,7 +320,17 @@
   },
   head() {
     return {
-      title: '片场内容管理',
+      title: '影视作品内容管理',
+    }
+  },
+  methods: {
+    formatterImage(row) {
+      if (row.coverUrl) {
+        // 使用第三方镜像服务(示例)
+        const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(row.coverUrl)}`;
+        return <el-bus-image src={proxyUrl} preview-src-list={[proxyUrl]} style="width:150px" />
+      }
+      return '无封面';
     }
   },
 }

--
Gitblit v1.9.3