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