feat: 商品分类和商品基础信息升级改造

This commit is contained in:
郝先瑞 2022-01-07 23:58:25 +08:00
parent c741d89b15
commit d465fd6b47
7 changed files with 574 additions and 120 deletions

View File

@ -503,7 +503,7 @@ vite-plugin-svg-icons 使用说明https://github.com/anncwb/vite-plugin-svg-i
**安装**
```
npm i vite-plugin-svg-icons -D
npm i -D vite-plugin-svg-icons
```
@ -550,4 +550,20 @@ router.afterEach(() => {
NProgress.done()
})
```
```
## TinyMCE 富文本编辑器
**官网:** http://tinymce.ax-z.cn/integrations/integrate-index.php
**安装**
```
npm i -S tinymce
npm i -S @tinymce/tinymce-vue
npm i -D @types/tinymce
```

View File

@ -8,6 +8,7 @@
},
"dependencies": {
"@element-plus/icons": "0.0.11",
"@tinymce/tinymce-vue": "^4.0.5",
"axios": "^0.24.0",
"element-plus": "^1.2.0-beta.6",
"nprogress": "^0.2.0",
@ -15,6 +16,7 @@
"path-to-regexp": "^6.2.0",
"pinia": "^2.0.9",
"screenfull": "^6.0.0",
"tinymce": "^5.10.2",
"vue": "^3.2.16",
"vue-router": "^4.0.12"
},
@ -22,6 +24,7 @@
"@types/node": "^16.11.7",
"@types/nprogress": "^0.2.0",
"@types/path-browserify": "^1.0.0",
"@types/tinymce": "^4.6.4",
"@vitejs/plugin-vue": "^1.9.3",
"sass": "^1.43.4",
"typescript": "^4.4.3",

View File

@ -0,0 +1,255 @@
<template>
<div
:class="{fullscreen: fullscreen}"
class="tinymce-container"
:style="{width: containerWidth}"
>
<TinymceEditor
:id="id"
v-model:value="tinymceContent"
:init="initOptions"
/>
<div class="editor-custom-btn-container">
<EditorImageUpload
:color="uploadButtonColor"
class="editor-upload-btn"
@success-callback="imageSuccessCBK"
/>
</div>
</div>
</template>
<script lang="ts">
// Docs: https://www.tiny.cloud/docs/advanced/usage-with-module-loaders/
// Import TinyMCE
import 'tinymce'
// Default icons are required for TinyMCE 5.3 or above
import 'tinymce/icons/default'
// Import themes
import 'tinymce/themes/silver'
import 'tinymce/themes/mobile'
// Any plugins you want to use has to be imported
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autoresize'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/emoticons'
import 'tinymce/plugins/fullpage'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/help'
import 'tinymce/plugins/hr'
import 'tinymce/plugins/image'
import 'tinymce/plugins/imagetools'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/noneditable'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/print'
import 'tinymce/plugins/save'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/spellchecker'
import 'tinymce/plugins/tabfocus'
import 'tinymce/plugins/table'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'
import TinymceEditor from '@tinymce/tinymce-vue' // TinyMCE vue wrapper
import EditorImageUpload, { UploadObject } from './components/EditorImage.vue'
import { plugins, toolbar } from './config'
import {
defineComponent,
reactive,
toRefs,
watch,
nextTick,
ref,
computed
} from 'vue'
import { useStore } from '@/store'
const defaultId = () =>
'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
export default defineComponent({
components: {
TinymceEditor,
EditorImageUpload
},
props: {
value: {
type: String,
default: ''
},
id: {
type: String,
default: defaultId
},
toolbar: {
type: Array,
default: () => {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: String || Number,
default: '360px'
},
width: {
type: String || Number,
default: 'auto'
}
},
emits: ['input'],
setup(props, ctx) {
const store = useStore()
const dataMap = reactive({
hasChange: false,
hasInit: false,
fullscreen: true,
getlanguage: () => {
return store.state.app.language
},
uploadButtonColor: () => {
return store.state.settings.theme
},
tinymceContent: computed(() => {
return props.value
}),
containerWidth: () => {
const width = props.width
// Test matches `100`, `'100'`
if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) {
return `${width}px`
}
return width
}
})
const initOptions = ref(
{
selector: `#${props.id}`,
height: props.height,
// eslint-disable-next-line @typescript-eslint/camelcase
body_class: 'panel-body',
// eslint-disable-next-line @typescript-eslint/camelcase
object_resizing: false,
toolbar: props.toolbar.length > 0 ? props.toolbar : toolbar,
menubar: props.menubar,
plugins: plugins,
// eslint-disable-next-line @typescript-eslint/camelcase
language_url: store.state.app.language === 'en' ? '' : `${process.env.BASE_URL}tinymce/langs/${store.state.app.language}.js`,
language: 'zh_CN',
// eslint-disable-next-line @typescript-eslint/camelcase
// eslint-disable-next-line @typescript-eslint/camelcase
skin_url: `${process.env.BASE_URL}tinymce/skins/`,
// eslint-disable-next-line @typescript-eslint/camelcase
emoticons_database_url: `${process.env.BASE_URL}tinymce/emojis.min.js`,
// eslint-disable-next-line @typescript-eslint/camelcase
end_container_on_empty_block: true,
// eslint-disable-next-line @typescript-eslint/camelcase
powerpaste_word_import: 'clean',
// eslint-disable-next-line @typescript-eslint/camelcase
code_dialog_height: 450,
// eslint-disable-next-line @typescript-eslint/camelcase
code_dialog_width: 1000,
// eslint-disable-next-line @typescript-eslint/camelcase
advlist_bullet_styles: 'square',
// eslint-disable-next-line @typescript-eslint/camelcase
advlist_number_styles: 'default',
// eslint-disable-next-line @typescript-eslint/camelcase
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
// eslint-disable-next-line @typescript-eslint/camelcase
default_link_target: '_blank',
// eslint-disable-next-line @typescript-eslint/camelcase
link_title: false,
// inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
// eslint-disable-next-line @typescript-eslint/camelcase
nonbreaking_force_tab: true,
// https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
// https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
// eslint-disable-next-line @typescript-eslint/camelcase
convert_urls: false,
// eslint-disable-next-line @typescript-eslint/camelcase
init_instance_callback: (editor: any) => {
if (props.value) {
editor.setContent(props.value)
}
dataMap.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
dataMap.hasChange = true
ctx.emit('input', editor.getContent())
})
},
setup: (editor: any) => {
editor.on('FullscreenStateChanged', (e: any) => {
dataMap.fullscreen = e.state
})
}
}
)
watch(() => store.state.app.language, () => {
const tinymceManager = (window as any).tinymce
const tinymceInstance = tinymceManager.get(props.id)
if (dataMap.fullscreen) {
tinymceInstance.execCommand('mceFullScreen')
}
if (tinymceInstance) {
tinymceInstance.destroy()
}
nextTick(() => {
tinymceManager.init(initOptions)
})
})
watch(() => dataMap.tinymceContent, (value) => {
console.log(value)
})
const imageSuccessCBK = (arr: UploadObject[]) => {
const tinymce = (window as any).tinymce.get(props.id)
arr.forEach((v) => {
tinymce.insertContent(`<img class="wscnph" src="${v.url}" >`)
})
}
return { ...toRefs(dataMap), imageSuccessCBK, initOptions }
}
})
</script>
<style lang="scss" scoped>
.tinymce-container {
position: relative;
line-height: normal;
.mce-fullscreen {
z-index: 10000;
}
}
.editor-custom-btn-container {
position: absolute !important;
right: 6px;
top: 6px;
z-index: 1002;
}
.editor-upload-btn {
display: inline-block;
}
textarea {
visibility: hidden;
z-index: -1;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<div class="upload-container">
<el-button
:style="{background: color, borderColor: color}"
icon="el-icon-upload"
size="mini"
type="primary"
@click="dialogVisible = true"
>
上传
</el-button>
<el-dialog
v-model="dialogVisible"
:modal-append-to-body="false"
>
<el-upload
:multiple="true"
:file-list="defaultFileList"
:show-file-list="true"
:on-remove="handleRemove"
:on-success="handleSuccess"
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card"
>
<el-button
size="small"
type="primary"
>
Click upload
</el-button>
</el-upload>
<el-button @click="dialogVisible = false">
Cancel
</el-button>
<el-button
type="primary"
@click="handleSubmit"
>
Confirm
</el-button>
</el-dialog>
</div>
</template>
<script lang="ts">
import { reactive, defineComponent, toRefs } from 'vue'
import { ElMessage } from 'element-plus'
export interface UploadObject {
hasSuccess: boolean
uid: number
url: string
width: number
height: number
}
export default defineComponent({
props: {
color: {
type: String,
default: ''
}
},
emits: ['success-callback'],
setup(_, ctx) {
let listObj: { [key: string]: UploadObject } = {}
const dataMap = reactive({
dialogVisible: false,
defaultFileList: [],
checkAllSuccess: () => {
return Object.keys(listObj).every(item => listObj[item].hasSuccess)
},
handleSubmi: () => {
const arr = Object.keys(listObj).map(v => listObj[v])
if (!dataMap.checkAllSuccess()) {
ElMessage.success('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
}
ctx.emit('success-callback', arr)
listObj = {}
dataMap.defaultFileList = []
dataMap.dialogVisible = false
},
handleSuccess: (response: any, file: any) => {
const uid = file.uid
const objKeyArr = Object.keys(listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (listObj[objKeyArr[i]].uid === uid) {
listObj[objKeyArr[i]].url = response.files.file
listObj[objKeyArr[i]].hasSuccess = true
return
}
}
},
handleRemove: (file: any) => {
const uid = file.uid
const objKeyArr = Object.keys(listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (listObj[objKeyArr[i]].uid === uid) {
delete listObj[objKeyArr[i]]
return
}
}
},
beforeUpload: (file: any) => {
const fileName = file.uid
const img = new Image()
img.src = window.URL.createObjectURL(file)
img.onload = () => {
listObj[fileName] = {
hasSuccess: false,
uid: file.uid,
url: '',
width: img.width,
height: img.height
}
}
},
handleSubmit() {
const arr = Object.keys(listObj).map(v => listObj[v])
if (!dataMap.checkAllSuccess()) {
ElMessage.warning('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
return
}
ctx.emit('success-callback', arr)
listObj = {}
dataMap.defaultFileList = []
dataMap.dialogVisible = false
}
})
return { ...toRefs(dataMap), listObj }
}
})
</script>
<style lang="scss">
.editor-slide-upload {
.el-upload--picture-card {
width: 100%;
}
}
</style>
<style lang="scss" scoped>
.editor-slide-upload {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,8 @@
// Import plugins that you want to use
// Detail plugins list see: https://www.tiny.cloud/apps/#core-plugins
// Custom builds see: https://www.tiny.cloud/get-tiny/custom-builds/
export const plugins = ['advlist anchor autolink autoresize autosave charmap code codesample directionality emoticons fullpage fullscreen help hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount']
// Here is the list of toolbar control components
// Details see: https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
export const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample help', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons charmap forecolor backcolor fullpage fullscreen']

View File

@ -20,7 +20,7 @@
</div>
<div class="components-container__footer">
<el-button type="primary" @click="onNextStepClick">下一步填写商品信息</el-button>
<el-button type="primary" @click="handleNext">下一步填写商品信息</el-button>
</div>
</div>
</template>
@ -32,8 +32,10 @@ import {ElCascaderPanel, ElMessage} from "element-plus";
const emit = defineEmits(['next'])
const props = defineProps({
goodsId: Number,
categoryId: Number
modelValue: {
type: Object,
default:{ }
}
})
const state = reactive({
@ -44,15 +46,11 @@ const state = reactive({
const {categoryOptions, pathLabels, categoryId} = toRefs(state)
onMounted(() => {
loadData()
})
function loadData() {
listCascadeCategories({}).then(response => {
state.categoryOptions = response.data
if (props.goodsId) {
state.categoryId = props.categoryId as any
if (props.modelValue.id) {
state.categoryId = props.modelValue.categoryId
nextTick(() => {
handleCategoryChange()
})
@ -67,8 +65,7 @@ function handleCategoryChange() {
state.categoryId = checkNode.value
}
function onNextStepClick() {
function handleNext() {
if (!state.categoryId) {
ElMessage.warning('请选择商品分类')
return false
@ -76,6 +73,11 @@ function onNextStepClick() {
emit('next' )
}
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>

View File

@ -1,49 +1,62 @@
<!--
<template>
<div class="components-container">
<div class="components-container__main">
<el-form
ref="goodsForm"
:rules="rules"
:model="value"
label-width="150px">
ref="goodsForm"
:rules="rules"
:model="modelValue"
label-width="120px"
>
<el-form-item label="商品名称" prop="name">
<el-input style="width: 400px" v-model="value.name"/>
<el-input style="width: 400px" v-model="modelValue.name"/>
</el-form-item>
<el-form-item label="原价" prop="originPrice">
<el-input style="width: 400px" v-model="value.originPrice"/>
<el-input style="width: 400px" v-model="modelValue.originPrice"/>
</el-form-item>
<el-form-item label="现价" prop="price">
<el-input style="width: 400px" v-model="value.price"/>
<el-input style="width: 400px" v-model="modelValue.price"/>
</el-form-item>
<el-form-item label="商品品牌" prop="brandId">
<el-select
v-model="value.brandId"
clearable
style="width:400px">
<el-option v-for="item in brandOptions" :key="item.id" :label="item.name" :value="item.id"/>
v-model="modelValue.brandId"
style="width:400px"
clearable
>
<el-option
v-for="item in brandOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品简介">
<el-input type="textarea" style="width: 400px" v-model="value.description"/>
<el-input
type="textarea"
v-model="modelValue.description"
style="width: 400px"
/>
</el-form-item>
<el-form-item label="商品相册">
<el-row :gutter="10">
<el-col style="width: 180px" v-for="(item,index) in pictures">
<el-card :body-style="{ padding: '10px' }">
<single-upload v-model="item.url"></single-upload>
<single-upload v-model="item.url"/>
<div class="bottom" v-if="item.url">
<el-button type="text" class="button" v-if="item.main==true" style="color:#ff4d51">商品主图</el-button>
<el-button type="text" class="button" v-else @click="setMainPicture(index)">设为主图</el-button>
<el-button type="text" class="button" @click="handlePictureRemove(index)">删除图片</el-button>
<el-button type="text" class="button" v-else @click="changeMainPicture(index)">设为主图</el-button>
<el-button type="text" class="button" @click="removePicture(index)">删除图片</el-button>
</div>
<div class="bottom" v-else>
<el-button type="text" class="button"></el-button>
<el-button type="text" class="button"/>
</div>
</el-card>
</el-col>
@ -51,7 +64,7 @@
</el-form-item>
<el-form-item label="商品详情" prop="detail">
<tinymce v-model="value.detail" :height="400"/>
<tinymce v-model="modelValue.detail" :height="400"/>
</el-form-item>
</el-form>
@ -62,101 +75,107 @@
</div>
</div>
</template>
<script>
<script setup lang="ts">
import {listBrands} from "@/api/pms/brand"
import SingleUpload from '@/components/Upload/SingleUpload.vue'
import Tinymce from '@/components/Tinymce/index.vue'
import {onMounted, reactive, ref, toRefs, unref} from "vue"
import {ElForm} from "element-plus"
import {list as listBrand} from "@/api/pms/brand"
import SingleUpload from '@/components/Upload/SingleUpload'
import Tinymce from '@/components/Tinymce'
const emit = defineEmits(['prev', 'next'])
const dataForm = ref(ElForm)
export default {
name: "GoodsInfo",
components: {SingleUpload, Tinymce},
props: {
value: Object
},
data() {
return {
brandOptions: [],
pictures: [],
rules: {
name: [{required: true, message: '请填写商品名称', trigger: 'blur'}],
originPrice: [{required: true, message: '请填写原价', trigger: 'blur'}],
price: [{required: true, message: '请填写现价', trigger: 'blur'}],
brandId: [{required: true, message: '请选择商品品牌', trigger: 'blur'}],
}
const props = defineProps({
modelValue: {
type: Object,
default: {}
}
})
const state = reactive({
brandOptions: [],
//
pictures: [] as Array<any>,
rules: {
name: [{required: true, message: '请填写商品名称', trigger: 'blur'}],
originPrice: [{required: true, message: '请填写原价', trigger: 'blur'}],
price: [{required: true, message: '请填写现价', trigger: 'blur'}],
brandId: [{required: true, message: '请选择商品品牌', trigger: 'blur'}],
}
})
const {brandOptions, pictures, rules} = toRefs(state)
function loadData() {
listBrands({}).then(response => {
state.brandOptions = response.data
})
const goodsId = props.modelValue.id
if (goodsId) {
const mainPicUrl = props.modelValue.picUrl
if (mainPicUrl) {
state.pictures.filter(item => item.main)[0].url = mainPicUrl
}
},
created() {
this.loadData()
},
methods: {
async loadData() {
this.handleFormReset()
await listBrand().then(response => {
this.brandOptions = response.data
})
const goodsId = this.value.id
if (goodsId) {
const mainPicUrl = this.value.picUrl
if (mainPicUrl) {
this.pictures.filter(item => item.main == true)[0].url = mainPicUrl
}
const subPicUrls = this.value.subPicUrls
if (subPicUrls && subPicUrls.length > 0) {
for (let i = 1; i <= subPicUrls.length; i++) {
this.pictures[i].url = subPicUrls[i - 1]
}
}
const subPicUrls = props.modelValue.subPicUrls
if (subPicUrls && subPicUrls.length > 0) {
for (let i = 1; i <= subPicUrls.length; i++) {
state.pictures[i].url = subPicUrls[i - 1]
}
},
//
setMainPicture(changeIndex) {
const mainPicture = JSON.parse(JSON.stringify( this.pictures[0]))
const changePicture = JSON.parse(JSON.stringify( this.pictures[changeIndex]))
console.log(changeIndex,changePicture.url,mainPicture.url)
this.pictures[0].url = changePicture.url
this.pictures[changeIndex].url = mainPicture.url
},
handlePictureRemove(index) {
this.pictures[index].url = undefined
},
handleFormReset: function () {
this.pictures = [
{url: undefined, main: true},
{url: undefined, main: false},
{url: undefined, main: false},
{url: undefined, main: false},
{url: undefined, main: false},
]
},
handlePrev: function () {
this.$emit('prev')
},
handleNext: function () {
this.$refs["goodsForm"].validate((valid) => {
if (valid) {
//
const tempMainPicUrl = this.pictures.filter(item => item.main == true && item.url).map(item => item.url)
if (tempMainPicUrl && tempMainPicUrl.length > 0) {
this.value.picUrl = tempMainPicUrl[0]
}
const tempSubPicUrl = this.pictures.filter(item => item.main == false && item.url).map(item => item.url)
if (tempSubPicUrl && tempSubPicUrl.length > 0) {
this.value.subPicUrls = tempSubPicUrl
}
this.$emit('next')
}
})
}
}
}
function resetForm() {
state.pictures = [
{url: undefined, main: true},
{url: undefined, main: false},
{url: undefined, main: false},
{url: undefined, main: false},
{url: undefined, main: false},
]
}
/**
* 切换主图
*/
function changeMainPicture(changeIndex: number) {
const currMainPicture = JSON.parse(JSON.stringify(state.pictures[0]))
const nextMainPicture = JSON.parse(JSON.stringify(state.pictures[changeIndex]))
state.pictures[0].url = nextMainPicture.url
state.pictures[changeIndex].url = currMainPicture.url
}
function removePicture(index: number) {
state.pictures[index].url = undefined
}
function handlePrev() {
emit('prev')
}
function handleNext() {
const form = unref(dataForm)
form.validate((valid: any) => {
if (valid) {
//
const mainPicUrl = state.pictures.filter(item => item.main == true && item.url).map(item => item.url)
if (mainPicUrl && mainPicUrl.length > 0) {
props.modelValue.picUrl = mainPicUrl[0]
}
const subPicUrl = state.pictures.filter(item => item.main == false && item.url).map(item => item.url)
if (subPicUrl && subPicUrl.length > 0) {
props.modelValue.subPicUrls = subPicUrl
}
emit('next')
}
})
}
onMounted(() => {
loadData()
resetForm()
})
</script>
<style lang="scss" scoped>
@ -177,4 +196,3 @@ export default {
}
}
</style>
-->