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