feat: 商品管理+首页商品合并

This commit is contained in:
zc 2025-11-06 16:50:31 +08:00
parent 701c093695
commit 954a9d49f0
8 changed files with 249 additions and 128 deletions

View File

@ -16,7 +16,7 @@ export default [
modifyTime: '2024-07-15 15:45:58',
tag: null,
childList: [
{
/* {
id: 38,
resourceName: '首页商品',
resourceType: 1,
@ -33,7 +33,7 @@ export default [
modifyTime: '2024-06-19 17:56:01',
tag: null,
childList: []
},
}, */
{
id: 39,
resourceName: '商品管理',

View File

@ -152,8 +152,13 @@ const onClickChooseResourceBtn = (data: Data) => {
curFileData.value = data
showFileExplorer.value = true
}
const handleChooseResourceFileCallback = (files: FileItem[], curFileData: Data) => {
curFileData.imageUrl = files[0].resourceUrl
const handleChooseResourceFileCallback = async (files: FileItem[], curFileData: Data) => {
const imgUrl = files[0].resourceUrl
await api.commodity.updateAppCategory.post!({
id: curFileData.id,
imageUrl: imgUrl
})
curFileData.imageUrl = imgUrl
}
</script>

View File

@ -19,9 +19,9 @@ export const initConfig = () => {
isNew: { label: '新品', slot: 'isNew' },
isFlash: { label: '限时秒杀', slot: 'isFlash' },
btn: {
types: ['primary', 'info', 'warning', 'success', 'danger'],
names: ['编辑', '复制', '加入首页', '上下架', '删除'],
width: 230
types: ['primary', 'info', 'danger'],
names: ['编辑', '复制', '删除'],
width: 150
}
}
})

View File

@ -9,7 +9,7 @@
<Editor
style="height: 500px; overflow-y: hidden"
v-model="model"
:defaultConfig="{ placeholder: '请输入内容...' }"
:defaultConfig="editorConfig"
mode="default"
@onCreated="
(editor: any) => {
@ -23,15 +23,44 @@
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { ElMessage } from 'element-plus'
const model = defineModel()
// ajax
/* onMounted(() => {
setTimeout(() => {
// model.value = '<p> Ajax </p>'
}, 1500)
}) */
//
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
//
async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
try {
// FormData
const form = new FormData()
form.append('files', file)
//
const res = await api.resource.uploadFile.post!<any>(form)
// URL
const imageUrl = res.data[0]?.url
if (imageUrl) {
//
insertFn(imageUrl, file.name, imageUrl)
} else {
ElMessage.error('图片上传失败')
}
} catch (error) {
console.error('图片上传失败:', error)
ElMessage.error('图片上传失败')
}
}
}
}
}
//
const toolbarConfig = {}
// shallowRef
const editorRef = shallowRef()

View File

@ -1,6 +1,6 @@
<template>
<div>
<search-module :search="search" :table="table">
<search-module :search="search" :table="table" @search="onSearch">
<template #btn>
<el-button @click="onAddOrEdit('add', null)" type="success">新增</el-button>
</template>
@ -24,34 +24,60 @@
</el-tree-select>
</template>
</search-module>
<table-module :table="table">
<template #headerBtn>
<el-radio-group v-model="commodityType" @change="onChangeCommodityType">
<el-radio-button
v-for="item in commodityTypeOptions"
:key="item.value"
:value="item.value"
>{{ item.label }}</el-radio-button
>
</el-radio-group>
</template>
<div class="bg-white mt-2.5 pt-2.5 pl-2.5">
<el-radio-group v-model="commodityType" @change="onChangeCommodityType">
<el-radio-button
v-for="item in commodityTypeOptions"
:key="item.value"
:value="item.value"
>{{ item.label }}</el-radio-button
>
</el-radio-group>
</div>
<table-module :table="table" v-show="commodityType !== 'frontPage'" class="!mt-0">
<template #status="{ row }">
<el-icon v-if="row.status === 'online'" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.status"
:active-value="'online'"
:inactive-value="'down'"
size="small"
@change="onChangeStatus(row)"
/>
</template>
<template #isNew="{ row }">
<el-icon v-if="row.isNew" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.isNew"
:active-value="1"
:inactive-value="0"
size="small"
@change="onChangeCommodityOtherStatus(row, 'isNew')"
/>
</template>
<template #isFlash="{ row }">
<el-icon v-if="row.isFlash" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.isFlash"
:active-value="1"
:inactive-value="0"
size="small"
@change="onChangeCommodityOtherStatus(row, 'isFlash')"
/>
</template>
<template #frontPage="{ row }">
<el-icon v-if="row.frontPage" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.frontPage"
:active-value="1"
:inactive-value="0"
size="small"
@change="onChangeCommodityOtherStatus(row, 'frontPage')"
/>
</template>
</table-module>
<home-goods
v-show="commodityType === 'frontPage'"
:rondomNumber="rondomNumber"
:searchData="search.$data"
class="!mt-0"
></home-goods>
</div>
</template>
@ -59,8 +85,7 @@
import { ElMessage } from 'element-plus'
import { initConfig } from './config'
import { useCommodityType, handleLoadNode } from './use-method'
import { Check } from '@element-plus/icons-vue'
import { Close } from '@element-plus/icons-vue'
import HomeGoods from '../home-goods/index.vue'
const router = useRouter()
/** 新增&编辑 */
@ -76,18 +101,7 @@ const onAddOrEdit = (type: string, row: any) => {
const onCopy = async (row: any) => {
await api.commodity.copyCommodity.post!({ productId: row.id })
ElMessage.success('复制成功')
table.value.$onGetData(table.value)
}
//
const onAddHome = (row: any) => {
handleMessageBox({
msg: `是否加入首页?`,
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, frontPage: 1 }
}).then(() => {
table.value.$onGetData(table.value)
})
table.value.$onGetData(table.value, 1, search)
}
//
@ -97,32 +111,54 @@ const onDelete = (row: any) => {
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, isDelete: 1 }
}).then(() => {
table.value.$onGetData(table.value)
table.value.$onGetData(table.value, 1, search)
})
}
//
const onChangeStatus = (row: any) => {
const type = row.status === 'online' ? '下架' : '上架'
const type = row.status === 'online' ? '上架' : '下架'
handleMessageBox({
msg: `是否确认${type}吗?`,
success: api.commodity.changeCommodityInfo.post!,
data: {
id: row.id,
status: row.status === 'online' ? 'down' : 'online'
status: row.status
},
fail: () => {
row.status = row.status === 'online' ? 'down' : 'online'
}
}).then(() => {
table.value.$onGetData(table.value)
})
.then(() => {
table.value.$onGetData(table.value, 1, search)
})
.catch(() => {
row.status = row.status === 'online' ? 'down' : 'online'
})
}
/** 改变其他状态 */
const onChangeCommodityOtherStatus = (row: any, status: string) => {
const type = { isNew: '新品', isFlash: '限时秒杀', frontPage: '加入首页' }[status]
handleMessageBox({
msg: `是否将此商品${row[status] === 1 ? '设置' : '取消'}${type}吗?`,
success: api.commodity.changeCommodityInfo.post!,
data: {
id: row.id,
[status]: row[status]
},
fail: () => {
row[status] = row[status] === 1 ? 0 : 1
}
}).catch(() => {
row[status] = row[status] === 1 ? 0 : 1
})
}
/** 初始化页面 */
const ConfigData = initConfig()
const { search, table } = handleInit(ConfigData, api.commodity.getCommodityList.post, [
onAddOrEdit.bind(null, 'edit'),
onCopy,
onAddHome,
onChangeStatus,
onDelete
])
const { commodityType, onChangeCommodityType, commodityTypeOptions } = useCommodityType(
@ -131,6 +167,15 @@ const { commodityType, onChangeCommodityType, commodityTypeOptions } = useCommod
)
onChangeCommodityType(commodityType.value)
const rondomNumber = ref(NaN)
const onSearch = (val: any) => {
if (search.value.$data.status === 'frontPage') {
rondomNumber.value = Math.random()
} else {
table.value.$onGetData(table.value, 1, val)
}
}
/** 页面缓存 */
defineOptions({ name: 'Commodity' })
</script>

View File

@ -1,32 +1,35 @@
type TCommodityType = 'all' | 'online' | 'down' | 'draft' | 'delete'
type TCommodityType = 'all' | 'online' | 'down' | 'draft' | 'delete' | 'frontPage'
export const useCommodityType = (search: any, table: any) => {
let beforeValue = 'online'
const commodityType = ref<TCommodityType>('online')
const commodityTypeOptions = [
{ label: '在线商品', value: 'online' },
{ label: '首页商品', value: 'frontPage' },
{ label: '所有商品', value: 'all' },
{ label: '下架商品', value: 'down' },
{ label: '草稿', value: 'draft' },
{ label: '已删商品', value: 'delete' }
]
const cacheTableData = { all: [], online: [], down: [], draft: [], delete: [] } // 缓存表单数据
const cacheTablePage = { all: {}, online: {}, down: {}, draft: {}, delete: {} } // 缓存表单页面数据
const onChangeCommodityType = (type: 'all' | 'online' | 'down' | 'draft' | 'delete') => {
const cacheTableData = { all: [], online: [], down: [], draft: [], delete: [], frontPage: [] } // 缓存表单数据
const cacheTablePage = { all: {}, online: {}, down: {}, draft: {}, delete: {}, frontPage: {} } // 缓存表单页面数据
const onChangeCommodityType = (
type: 'all' | 'online' | 'down' | 'draft' | 'delete' | 'frontPage'
) => {
cacheTableData[beforeValue as typeof type] = toRaw(table.value.$data)
cacheTablePage[beforeValue as typeof type] = toRaw(table.value.$pages)
beforeValue = type
search.value.$default = { status: type }
search.value.$data = { status: type }
handleSetTableConfig(table, type)
search.value.$default.status = type
search.value.$data.status = type
// handleSetTableConfig(table, type)
if (cacheTableData[type].length) {
table.value.$data = cacheTableData[type]
table.value.$pages = cacheTablePage[type]
} else {
if (type === 'all') {
search.value.$data = { status: '' }
search.value.$data.status = ''
table.value.$onGetData(table.value, 1, search)
} else {
} else if (type !== 'frontPage') {
table.value.$onGetData(table.value, 1, search)
}
}

View File

@ -1,10 +1,6 @@
const configData = ref()
export const initConfig = () => {
configData.value = pageConfig({
search: {
comTitle: { label: '标题', clearable: true },
adminCategoryId: { label: '类目', slot: 'adminCategoryId' }
},
table: {
id: { label: '产品ID' },
title: { label: '标题' },
@ -15,12 +11,13 @@ export const initConfig = () => {
label: '在线',
slot: 'status'
},
frontPage: { label: '加入首页', slot: 'frontPage' },
isNew: { label: '新品', slot: 'isNew' },
isFlash: { label: '限时秒杀', slot: 'isFlash' },
btn: {
types: ['primary', 'warning', 'success', 'danger'],
names: ['编辑', '移除首页', '上架', '下架'],
width: 200
types: ['primary', 'warning', 'danger'],
names: ['编辑', '复制', '删除'],
width: 150
}
}
})

View File

@ -1,39 +1,42 @@
<template>
<div>
<search-module :search="search" :table="table">
<template #adminCategoryId>
<el-tree-select
show-checkbox
v-model="search.$data.adminCategoryId"
:props="{
label: 'label',
children: 'children',
disabled: 'disabled',
isLeaf: (data: any) => {
return !data.hasChild
}
}"
:render-after-expand="false"
:load="handleLoadNode"
lazy
style="width: 240px"
>
</el-tree-select>
</template>
</search-module>
<div id="table-drag-wrap">
<table-module :table="table">
<template #status="{ row }">
<el-icon v-if="row.status === 'online'" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.status"
:active-value="'online'"
:inactive-value="'down'"
size="small"
@change="onChangeStatus(row)"
/>
</template>
<template #frontPage="{ row }">
<el-switch
v-model="row.frontPage"
:active-value="1"
:inactive-value="0"
@change="onChangeCommodityOtherStatus(row, 'frontPage')"
size="small"
/>
</template>
<template #isNew="{ row }">
<el-icon v-if="row.isNew" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.isNew"
:active-value="1"
:inactive-value="0"
@change="onChangeCommodityOtherStatus(row, 'isNew')"
size="small"
></el-switch>
</template>
<template #isFlash="{ row }">
<el-icon v-if="row.isFlash" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
<el-switch
v-model="row.isFlash"
:active-value="1"
:inactive-value="0"
@change="onChangeCommodityOtherStatus(row, 'isFlash')"
size="small"
></el-switch>
</template>
</table-module>
</div>
@ -43,46 +46,65 @@
<script lang="ts" setup>
import { useDraggable } from 'vue-draggable-plus'
import { initConfig } from './config'
import { Check } from '@element-plus/icons-vue'
import { Close } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
//
const onRemoveHome = (row: any) => {
handleMessageBox({
msg: `是否加入首页?`,
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, frontPage: 0 }
}).then(() => {
table.value.$onGetData(table.value)
})
const props = defineProps<{ searchData: any; rondomNumber: number }>()
/** 复制 */
const onCopy = async (row: any) => {
await api.commodity.copyCommodity.post!({ productId: row.id })
ElMessage.success('复制成功')
table.value.$onGetData(table.value, 1, { ...props.searchData, frontPage: 1, status: undefined })
}
//
const onDown = (row: any) => {
//
const onChangeStatus = (row: any) => {
const type = row.status === 'online' ? '上架' : '下架'
handleMessageBox({
msg: `是否确认下架该商品?`,
msg: `是否确认${type}?`,
success: api.commodity.changeCommodityInfo.post!,
data: {
id: row.id,
status: 'down'
status: row.status
},
fail: () => {
row.status = row.status === 'online' ? 'down' : 'online'
}
})
.then(() => {
table.value.$onGetData(table.value, 1, {
...props.searchData,
frontPage: 1,
status: undefined
})
})
.catch(() => {
row.status = row.status === 'online' ? 'down' : 'online'
})
}
//
const onDelete = (row: any) => {
handleMessageBox({
msg: `是否确认删除吗?`,
success: api.commodity.changeCommodityInfo.post!,
data: { id: row.id, isDelete: 1 }
}).then(() => {
table.value.$onGetData(table.value)
table.value.$onGetData(table.value, 1, { ...props.searchData, frontPage: 1, status: undefined })
})
}
//
const onUp = (row: any) => {
const onChangeCommodityOtherStatus = (row: any, status: string) => {
const type = { isNew: '新品', isFlash: '限时秒杀' }[status]
handleMessageBox({
msg: `是否确认上架该商品?`,
msg: `是否将此商品${row[status] === 1 ? '设置' : '取消'}${type}?`,
success: api.commodity.changeCommodityInfo.post!,
data: {
id: row.id,
status: 'online'
data: { id: row.id, [status]: row[status] },
fail: () => {
row[status] = row[status] === 1 ? 0 : 1
}
}).then(() => {
table.value.$onGetData(table.value)
}).catch(() => {
row[status] = row[status] === 1 ? 0 : 1
})
}
@ -93,15 +115,13 @@ const onEdit = (row: any) => {
/** 初始化页面 */
const ConfigData = initConfig()
const { search, table } = handleInit(ConfigData, api.commodity.getCommodityList.post, [
const { table } = handleInit(ConfigData, api.commodity.getCommodityList.post, [
onEdit,
onRemoveHome,
onUp,
onDown
onCopy,
onDelete
])
search.value.$default = { frontPage: 1 }
table.value.$pages.pageSize = 1000
table.value.$onGetData(table.value, 1, search)
table.value.$onGetData(table.value, 1, { frontPage: 1 })
//
let draggableInstance: any = null
@ -151,6 +171,28 @@ watch(
{ deep: true }
)
watch(
() => props.searchData,
(val) => {
if (val.status === 'frontPage' && !table.value.$data.length) {
table.value.$onGetData(table.value, 1, { ...val, frontPage: 1, status: undefined })
}
}
)
watch(
() => props.rondomNumber,
(val) => {
if (val) {
table.value.$onGetData(table.value, 1, {
...props.searchData,
frontPage: 1,
status: undefined
})
}
}
)
//
const handleLoadNode = (node: any, resolve: any) => {
api.commodity.getCategoryList.post!<any>({ parentId: node.data.value || 0 }).then((res) => {