111
cloudroam
2025-05-23 b6e34e48b20c02446c1ada2b2617b800f529898a
111
已修改3个文件
已添加35个文件
4485 ■■■■■ 文件已修改
components/area-select.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-editor.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-image-info.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-link.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-menu-icon.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-menu-item.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-nav.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-role-permission-tree.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/base-sidebar.vue 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/cascader-filter.vue 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/content-wrapper.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/member-rule.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/coupon/select-shop-user.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/custom-date-range.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/el-bus-breadcrumb.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/el-table-print.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/goods/goods-params.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/input-select.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/after-sale-items.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/after-sale-table.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/check-abnormal-list.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/evaluation-table.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/goods-table-item-list.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/level-down-list.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/print-list.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/order/video-list.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/simple-text.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/sms/copy-textarea.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/sms/select-all-user.vue 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/sms/template-download.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/tags-view/index.vue 299 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/tags-view/scroll-pane.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/tags-view/tag-item.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/warehouse/location-item.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/warehouse/select-order.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/default-dev.json5 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/default-test.json5 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/content/filmset.vue 225 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
config/default-dev.json5
@@ -1,3 +1,3 @@
{
  httpBaseUri: 'http://localhost:8080',
  httpBaseUri: 'http://192.168.1.213:8080',
}
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/'
}
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 '无封面';
    }
  },
}