!152 合并最新的 Vue3 重构

Merge pull request !152 from 芋道源码/dev
This commit is contained in:
芋道源码 2023-05-15 15:00:12 +00:00 committed by Gitee
commit 47f20e6d4c
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
47 changed files with 2541 additions and 567 deletions

View File

@ -17,17 +17,17 @@ export interface CategoryVO {
*/ */
name: string name: string
/** /**
* *
*/ */
picUrl: string picUrl: string
/**
* PC
*/
bigPicUrl?: string
/** /**
* *
*/ */
sort?: number sort: number
/**
*
*/
description?: string
/** /**
* *
*/ */

View File

@ -0,0 +1,39 @@
import request from '@/config/axios'
import type { SpuType } from './type/spuType' // TODO @puhui999: type 和 api 一起放,简单一点哈~
// TODO @puhui999中英文之间有空格
// 获得spu列表 TODO @puhui999这个是 getSpuPage 哈
export const getSpuList = (params: PageParam) => {
return request.get({ url: '/product/spu/page', params })
}
// 获得spu列表tabsCount
export const getTabsCount = () => {
return request.get({ url: '/product/spu/tabsCount' })
}
// 创建商品spu
export const createSpu = (data: SpuType) => {
return request.post({ url: '/product/spu/create', data })
}
// 更新商品spu
export const updateSpu = (data: SpuType) => {
return request.put({ url: '/product/spu/update', data })
}
// 更新商品spu status
export const updateStatus = (data: { id: number; status: number }) => {
return request.put({ url: '/product/spu/updateStatus', data })
}
// 获得商品 spu
export const getSpu = (id: number) => {
return request.get({ url: `/product/spu/get-detail?id=${id}` })
}
// 删除商品Spu
export const deleteSpu = (id: number) => {
return request.delete({ url: `/product/spu/delete?id=${id}` })
}

View File

@ -0,0 +1,79 @@
export interface Property {
/**
*
*
* {@link ProductPropertyDO#getId()}
*/
propertyId?: number
/**
*
*
* {@link ProductPropertyValueDO#getId()}
*/
valueId?: number
/**
*
*/
valueName?: string
}
export interface SkuType {
/**
* SKU
*/
id?: number
/**
* SPU
*/
spuId?: number
/**
* JSON
*/
properties?: Property[]
/**
*
*/
price?: number
/**
*
*/
marketPrice?: number
/**
*
*/
costPrice?: number
/**
*
*/
barCode?: string
/**
*
*/
picUrl?: string
/**
*
*/
stock?: number
/**
* kg
*/
weight?: number
/**
* m^3
*/
volume?: number
/**
*
*/
subCommissionFirstPrice?: number
/**
*
*/
subCommissionSecondPrice?: number
/**
*
*/
salesCount?: number
}

View File

@ -0,0 +1,25 @@
import { SkuType } from './skuType'
export interface SpuType {
id?: number
name?: string // 商品名称
categoryId?: number | null // 商品分类
keyword?: string // 关键字
unit?: number | null // 单位
picUrl?: string // 商品封面图
sliderPicUrls?: string[] // 商品轮播图
introduction?: string // 商品简介
deliveryTemplateId?: number // 运费模版
specType?: boolean // 商品规格
subCommissionType?: boolean // 分销类型
skus: SkuType[] // sku数组
description?: string // 商品详情
sort?: string // 商品排序
giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量
recommendHot?: boolean // 是否热卖
recommendBenefit?: boolean // 是否优惠
recommendBest?: boolean // 是否精品
recommendNew?: boolean // 是否新品
recommendGood?: boolean // 是否优品
}

View File

@ -71,8 +71,8 @@ export const getPropertyList = (params: any) => {
} }
// 获得属性项列表 // 获得属性项列表
export const getPropertyListAndValue = (params: any) => { export const getPropertyListAndValue = (data: any) => {
return request.get({ url: '/product/property/get-value-list', params }) return request.post({ url: '/product/property/get-value-list', data })
} }
// ------------------------ 属性值 ------------------- // ------------------------ 属性值 -------------------

View File

@ -1,7 +1,7 @@
import request from '@/config/axios' import request from '@/config/axios'
export interface AccountVO { export interface AccountVO {
id?: number id: number
name: string name: string
} }

View File

@ -1,6 +1,6 @@
import type { Slots } from 'vue' import type { Slots } from 'vue'
import { getSlot } from '@/utils/tsxHelper' import { getSlot } from '@/utils/tsxHelper'
import { PlaceholderMoel } from './types' import { PlaceholderModel } from './types'
import { FormSchema } from '@/types/form' import { FormSchema } from '@/types/form'
import { ColProps } from '@/types/components' import { ColProps } from '@/types/components'
@ -10,7 +10,7 @@ import { ColProps } from '@/types/components'
* @returns * @returns
* @description placeholder * @description placeholder
*/ */
export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => { export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
const { t } = useI18n() const { t } = useI18n()
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword'] const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect'] const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
@ -108,8 +108,8 @@ export const setItemComponentSlots = (
/** /**
* *
* @param schema Form表单结构化数组 * @param schema Form表单结构化数组
* @param formModel FormMoel * @param formModel FormModel
* @returns FormMoel * @returns FormModel
* @description formModel * @description formModel
*/ */
export const initModel = (schema: FormSchema[], formModel: Recordable) => { export const initModel = (schema: FormSchema[], formModel: Recordable) => {

View File

@ -1,6 +1,6 @@
import { FormSchema } from '@/types/form' import { FormSchema } from '@/types/form'
export interface PlaceholderMoel { export interface PlaceholderModel {
placeholder?: string placeholder?: string
startPlaceholder?: string startPlaceholder?: string
endPlaceholder?: string endPlaceholder?: string

View File

@ -2,9 +2,9 @@ import { Layout } from '@/utils/routerHelper'
const { t } = useI18n() const { t } = useI18n()
/** /**
* redirect: noredirect noredirect * redirect: noredirect noredirect
* name:'router-name' 使<keep-alive> * name:'router-name' 使<keep-alive>
* meta : { * meta : {
hidden: true true 404login等页面( false) hidden: true true 404login等页面( false)
alwaysShow: true children 1 alwaysShow: true children 1
@ -31,7 +31,7 @@ const { t } = useI18n()
canTo: true true即使hidden为true( false) canTo: true true即使hidden为true( false)
} }
**/ **/
const remainingRouter: AppRouteRecordRaw[] = [ const remainingRouter: AppRouteRecordRaw[] = [
{ {
path: '/redirect', path: '/redirect',
@ -345,6 +345,29 @@ const remainingRouter: AppRouteRecordRaw[] = [
meta: { title: '商品属性值', icon: '', activeMenu: '/product/property' } meta: { title: '商品属性值', icon: '', activeMenu: '/product/property' }
} }
] ]
},
{
path: '/product',
component: Layout,
name: 'ProductManagementEdit',
meta: {
hidden: true
},
children: [
{
path: 'productManagementAdd', // TODO @puhui999最好拆成 add 和 edit 两个路由;添加商品;修改商品
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'ProductManagementAdd',
meta: {
noCache: true,
hidden: true,
canTo: true,
icon: 'ep:edit',
title: '添加商品',
activeMenu: '/product/product-management'
}
}
]
} }
] ]

View File

@ -10,6 +10,12 @@
width: 100% !important; width: 100% !important;
} }
// 解决表格内容超过表格总宽度后横向滚动条前端顶不到表格边缘的问题
.el-scrollbar__bar {
display: flex;
justify-content: flex-start;
}
/* nprogress 适配 element-plus 的主题色 */ /* nprogress 适配 element-plus 的主题色 */
#nprogress { #nprogress {
& .bar { & .bar {

View File

@ -220,3 +220,21 @@ export const PayRefundStatusEnum = {
name: '退款关闭' name: '退款关闭'
} }
} }
/**
* SPU枚举类
*/
export const ProductSpuStatusEnum = {
RECYCLE: {
status: -1,
name: '回收站'
},
DISABLE: {
status: 0,
name: '下架'
},
ENABLE: {
status: 1,
name: '上架'
}
}

View File

@ -144,5 +144,9 @@ export enum DICT_TYPE {
// ========== MP 模块 ========== // ========== MP 模块 ==========
MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
MP_MESSAGE_TYPE = 'mp_message_type' // 消息类型 MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
// ========== MALL 模块 ==========
PRODUCT_UNIT = 'product_unit', // 商品单位
PRODUCT_SPU_STATUS = 'product_spu_status' //商品状态
} }

18
src/utils/object.ts Normal file
View File

@ -0,0 +1,18 @@
// TODO @puhui999这个方法可以考虑放到 index.js
/**
* target: {a:1} source:{a:2,b:3} {a:2}
* @param target
* @param source
*/
export const copyValueToTarget = (target, source) => {
const newObj = Object.assign({}, target, source)
// 删除多余属性
Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除
if (Object.keys(target).indexOf(key) === -1) {
delete newObj[key]
}
})
// 更新目标对象值
Object.assign(target, newObj)
}

View File

@ -1,7 +1,6 @@
<template> <template>
<doc-alert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" /> <doc-alert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" />
<doc-alert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" /> <doc-alert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" />
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)"> <el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
<el-row> <el-row>
<!-- 基本信息 --> <!-- 基本信息 -->
@ -51,127 +50,224 @@
<!-- 命令统计 --> <!-- 命令统计 -->
<el-col :span="12" class="mt-3"> <el-col :span="12" class="mt-3">
<el-card :gutter="12" shadow="hover"> <el-card :gutter="12" shadow="hover">
<div ref="commandStatsRef" class="h-88"></div> <Echart :options="commandStatsRefChika" :height="420" />
</el-card> </el-card>
</el-col> </el-col>
<!-- 内存使用量统计 --> <!-- 内存使用量统计 -->
<el-col :span="12" class="mt-3"> <el-col :span="12" class="mt-3">
<el-card class="ml-3" :gutter="12" shadow="hover"> <el-card class="ml-3" :gutter="12" shadow="hover">
<div ref="usedmemory" class="h-88"></div> <Echart :options="usedmemoryEchartChika" :height="420" />
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
</el-scrollbar> </el-scrollbar>
</template> </template>
<script setup lang="ts" name="InfraRedis"> <script setup lang="ts">
import * as echarts from 'echarts' import echarts from '@/plugins/echarts'
import { GaugeChart } from 'echarts/charts'
import { ToolboxComponent } from 'echarts/components'
import * as RedisApi from '@/api/infra/redis' import * as RedisApi from '@/api/infra/redis'
import { RedisMonitorInfoVO } from '@/api/infra/redis/types' import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
const cache = ref<RedisMonitorInfoVO>() const cache = ref<RedisMonitorInfoVO>()
// //
const readRedisInfo = async () => { const readRedisInfo = async () => {
const data = await RedisApi.getCache() const data = await RedisApi.getCache()
cache.value = data cache.value = data
loadEchartOptions(data.commandStats)
} }
//
const commandStatsRef = ref<HTMLElement>()
const usedmemory = ref<HTMLDivElement>()
const loadEchartOptions = (stats) => { // 使
const commandStats = [] as any[] const usedmemoryEchartChika = reactive({
const nameList = [] as string[] title: {
stats.forEach((row) => { //
commandStats.push({ text: '内存使用情况',
name: row.command, left: 'center',
value: row.calls show: true, // , true
}) offsetCenter: [0, '20%'], //
nameList.push(row.command) color: 'yellow', // , #333
}) fontSize: 20 // , 15
},
const commandStatsInstance = echarts.init(commandStatsRef.value!, 'macarons') toolbox: {
show: false,
commandStatsInstance.setOption({ feature: {
title: { restore: { show: true },
text: '命令统计', saveAsImage: { show: true }
left: 'center' }
}, },
tooltip: { series: [
trigger: 'item', {
formatter: '{a} <br/>{b} : {c} ({d}%)' name: '峰值',
}, type: 'gauge',
legend: { min: 0,
type: 'scroll', max: 50,
orient: 'vertical', splitNumber: 10,
right: 30, //
top: 10, color: '#F5C74E',
bottom: 20, radius: '85%',
data: nameList, center: ['50%', '50%'],
textStyle: { startAngle: 225,
color: '#a1a1a1' endAngle: -45,
axisLine: {
// 线
lineStyle: {
// lineStyle线
color: [
[0.2, '#7FFF00'],
[0.8, '#00FFFF'],
[1, '#FF0000']
],
//width: 6
width: 10
}
},
axisTick: {
//
//线5线
length: 5, // length线
lineStyle: {
// lineStyle线
color: '#76D9D7'
}
},
splitLine: {
// 线
length: 20, // length线
lineStyle: {
// lineStylelineStyle线
color: '#76D9D7'
}
},
axisLabel: {
color: '#76D9D7',
distance: 15,
fontSize: 15
},
pointer: {
//
width: 7,
show: true
},
detail: {
textStyle: {
fontWeight: 'normal',
// 50
fontSize: 15,
color: '#FFFFFF'
},
valueAnimation: true
},
progress: {
show: true
} }
}, }
series: [ ]
{ })
name: '命令',
type: 'pie', // 使
radius: [20, 120], const commandStatsRefChika = reactive({
center: ['40%', '60%'], title: {
data: commandStats, text: '命令统计',
roseType: 'radius', left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 30,
top: 10,
bottom: 20,
data: [] as any[],
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: [] as any[],
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: { label: {
show: true show: true
}, },
emphasis: { itemStyle: {
label: { shadowBlur: 10,
show: true shadowOffsetX: 0,
}, shadowColor: 'rgba(0, 0, 0, 0.5)'
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
} }
} }
] }
}) ]
})
const usedMemoryInstance = echarts.init(usedmemory.value!, 'macarons') /** 加载数据 */
usedMemoryInstance.setOption({ const getSummary = () => {
title: { //
text: '内存使用情况', initCommandStatsChart()
left: 'center' usedMemoryInstance()
},
tooltip: {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 100,
progress: {
show: true
},
detail: {
formatter: cache.value!.info.used_memory_human
},
data: [
{
value: parseFloat(cache.value!.info.used_memory_human),
name: '内存消耗'
}
]
}
]
})
} }
onBeforeMount(() => { /** 命令使用情况 */
// TODO @hiiwbs 使 Echart const initCommandStatsChart = async () => {
usedmemoryEchartChika.series[0].data = []
//
try {
const data = await RedisApi.getCache()
cache.value = data
//
const commandStats = [] as any[]
const nameList = [] as string[]
data.commandStats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
commandStatsRefChika.legend.data = nameList
commandStatsRefChika.series[0].data = commandStats
} catch {}
}
const usedMemoryInstance = async () => {
try {
const data = await RedisApi.getCache()
cache.value = data
//
usedmemoryEchartChika.series[0].detail = {
show: true, // , true
offsetCenter: [0, '50%'], //
color: 'auto', // , auto
fontSize: 30, // , 15
formatter: cache.value!.info.used_memory_human //
}
usedmemoryEchartChika.series[0].data[0] = {
value: cache.value!.info.used_memory_human,
name: '内存消耗'
}
console.log(cache.value!.info)
usedmemoryEchartChika.tooltip = {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
}
} catch {}
}
/** 初始化 **/
onMounted(() => {
echarts.use([ToolboxComponent])
echarts.use([GaugeChart])
// redis
readRedisInfo() readRedisInfo()
//
getSummary()
}) })
</script> </script>

View File

@ -4,27 +4,30 @@
ref="formRef" ref="formRef"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="80px" label-width="120px"
v-loading="formLoading" v-loading="formLoading"
> >
<el-form-item label="上级分类" prop="parentId"> <el-form-item label="上级分类" prop="parentId">
<el-tree-select <el-select v-model="formData.parentId" placeholder="请选择上级分类">
v-model="formData.parentId" <el-option :key="0" label="顶级分类" :value="0" />
:data="categoryTree" <el-option
:props="{ label: 'name', value: 'id' }" v-for="item in categoryList"
:render-after-expand="false" :key="item.id"
placeholder="请选择上级分类" :label="item.name"
check-strictly :value="item.id"
default-expand-all />
/> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="分类名称" prop="name"> <el-form-item label="分类名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入分类名称" /> <el-input v-model="formData.name" placeholder="请输入分类名称" />
</el-form-item> </el-form-item>
<el-form-item label="分类图" prop="picUrl"> <el-form-item label="移动端分类图" prop="picUrl">
<UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" /> <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" />
<div v-if="formData.parentId === 0" style="font-size: 10px">推荐 200x100 图片分辨率</div> <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
<div v-else style="font-size: 10px">推荐 100x100 图片分辨率</div> </el-form-item>
<el-form-item label="PC 端分类图" prop="bigPicUrl">
<UploadImg v-model="formData.bigPicUrl" :limit="1" :is-show-tip="false" />
<div style="font-size: 10px" class="pl-10px">推荐 468x340 图片分辨率</div>
</el-form-item> </el-form-item>
<el-form-item label="分类排序" prop="sort"> <el-form-item label="分类排序" prop="sort">
<el-input-number v-model="formData.sort" controls-position="right" :min="0" /> <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
@ -40,9 +43,6 @@
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="分类描述">
<el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -53,7 +53,6 @@
<script setup lang="ts" name="ProductCategory"> <script setup lang="ts" name="ProductCategory">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { handleTree } from '@/utils/tree'
import * as ProductCategoryApi from '@/api/mall/product/category' import * as ProductCategoryApi from '@/api/mall/product/category'
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
@ -66,8 +65,8 @@ const formData = ref({
id: undefined, id: undefined,
name: '', name: '',
picUrl: '', picUrl: '',
status: CommonStatusEnum.ENABLE, bigPicUrl: '',
description: '' status: CommonStatusEnum.ENABLE
}) })
const formRules = reactive({ const formRules = reactive({
parentId: [{ required: true, message: '请选择上级分类', trigger: 'blur' }], parentId: [{ required: true, message: '请选择上级分类', trigger: 'blur' }],
@ -77,7 +76,7 @@ const formRules = reactive({
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }] status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
const categoryTree = ref<any[]>([]) // const categoryList = ref<any[]>([]) //
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
@ -95,7 +94,7 @@ const open = async (type: string, id?: number) => {
} }
} }
// //
await getTree() categoryList.value = await ProductCategoryApi.getCategoryList({ parentId: 0 })
} }
defineExpose({ open }) // open defineExpose({ open }) // open
@ -131,17 +130,9 @@ const resetForm = () => {
id: undefined, id: undefined,
name: '', name: '',
picUrl: '', picUrl: '',
status: CommonStatusEnum.ENABLE, bigPicUrl: '',
description: '' status: CommonStatusEnum.ENABLE
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
/** 获得分类树 */
const getTree = async () => {
const data = await ProductCategoryApi.getCategoryList({})
const tree = handleTree(data, 'id', 'parentId')
const menu = { id: 0, name: '顶级分类', children: tree }
categoryTree.value = [menu]
}
</script> </script>

View File

@ -36,9 +36,9 @@
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" row-key="id" default-expand-all> <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
<el-table-column label="分类名称" prop="name" sortable /> <el-table-column label="分类名称" prop="name" sortable />
<el-table-column label="分类图" align="center" prop="picUrl"> <el-table-column label="移动端分类图" align="center" prop="picUrl">
<template #default="scope"> <template #default="scope">
<img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="分类图" class="h-100px" /> <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-100px" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="分类排序" align="center" prop="sort" /> <el-table-column label="分类排序" align="center" prop="sort" />

View File

@ -2,42 +2,49 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<el-form <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px" label-width="68px"
> >
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
clearable
placeholder="请输入名称"
@keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="创建时间" prop="createTime"> <el-form-item label="创建时间" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button <el-button
v-hasPermi="['product:property:create']"
plain plain
type="primary" type="primary"
@click="openForm('create')" @click="openForm('create')"
v-hasPermi="['product:property:create']"
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon class="mr-5px" icon="ep:plus" />
新增
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -46,23 +53,23 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" /> <el-table-column align="center" label="编号" prop="id" />
<el-table-column label="名称" align="center" /> <el-table-column align="center" label="名称" prop="name" />
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" /> <el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" />
<el-table-column <el-table-column
label="创建时间" :formatter="dateFormatter"
align="center" align="center"
label="创建时间"
prop="createTime" prop="createTime"
width="180" width="180"
:formatter="dateFormatter"
/> />
<el-table-column label="操作" align="center"> <el-table-column align="center" label="操作">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-hasPermi="['product:property:update']"
link link
type="primary" type="primary"
@click="openForm('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['product:property:update']"
> >
编辑 编辑
</el-button> </el-button>
@ -70,10 +77,10 @@
<router-link :to="'/property/value/' + scope.row.id">属性值</router-link> <router-link :to="'/property/value/' + scope.row.id">属性值</router-link>
</el-button> </el-button>
<el-button <el-button
v-hasPermi="['product:property:delete']"
link link
type="danger" type="danger"
@click="handleDelete(scope.row.id)" @click="handleDelete(scope.row.id)"
v-hasPermi="['product:property:delete']"
> >
删除 删除
</el-button> </el-button>
@ -82,9 +89,9 @@
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize" v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList" @pagination="getList"
/> />
</ContentWrap> </ContentWrap>
@ -92,10 +99,11 @@
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<PropertyForm ref="formRef" @success="getList" /> <PropertyForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="ProductProperty"> <script lang="ts" name="ProductProperty" setup>
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as PropertyApi from '@/api/mall/product/property' import * as PropertyApi from '@/api/mall/product/property'
import PropertyForm from './PropertyForm.vue' import PropertyForm from './PropertyForm.vue'
const message = useMessage() // const message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //

View File

@ -0,0 +1,240 @@
<template>
<ContentWrap v-loading="formLoading">
<el-tabs v-model="activeName">
<el-tab-pane label="商品信息" name="basicInfo">
<BasicInfoForm
ref="BasicInfoRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="商品详情" name="description">
<DescriptionForm
ref="DescriptionRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="otherSettings">
<OtherSettingsForm
ref="OtherSettingsRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
</el-tabs>
<el-form>
<el-form-item style="float: right">
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="close">返回</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" name="ProductManagementForm" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
import type { SpuType } from '@/api/mall/product/management/type/spuType' // api
import * as managementApi from '@/api/mall/product/management/spu'
import * as PropertyApi from '@/api/mall/product/property'
const { t } = useI18n() //
const message = useMessage() //
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
const formLoading = ref(false) // 12
const activeName = ref('basicInfo') // Tag
const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
const formData = ref<SpuType>({
name: '213', //
categoryId: null, //
keyword: '213', //
unit: null, //
picUrl:
'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', //
sliderPicUrls: [
{
name: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png',
url: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png'
}
], //
introduction: '213', //
deliveryTemplateId: 0, //
specType: false, //
subCommissionType: false, //
skus: [
{
/**
* 商品价格单位 TODO @puhui999注释放在尾巴哈简洁一点~
*/
price: 0,
/**
* 市场价单位
*/
marketPrice: 0,
/**
* 成本价单位
*/
costPrice: 0,
/**
* 商品条码
*/
barCode: '',
/**
* 图片地址
*/
picUrl: '',
/**
* 库存
*/
stock: 0,
/**
* 商品重量单位kg 千克
*/
weight: 0,
/**
* 商品体积单位m^3 平米
*/
volume: 0,
/**
* 一级分销的佣金单位
*/
subCommissionFirstPrice: 0,
/**
* 二级分销的佣金单位
*/
subCommissionSecondPrice: 0
}
],
description: '5425', //
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //
recommendHot: false, //
recommendBenefit: false, //
recommendBest: false, //
recommendNew: false, //
recommendGood: false //
})
/** 获得详情 */
const getDetail = async () => {
const id = query.id as unknown as number
if (id) {
formLoading.value = true
try {
const res = (await managementApi.getSpu(id)) as SpuType
formData.value = res
// id
// TODO @puhui999 propertyName id + uniapp
const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId)
const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds })
await nextTick()
//
BasicInfoRef.value.addAttribute(PropertyS)
} finally {
formLoading.value = false
}
}
}
/** 提交按钮 */
const submitForm = async () => {
//
formLoading.value = true
const newSkus = JSON.parse(JSON.stringify(formData.value.skus)) //skus使
// TODO
//
try {
await unref(BasicInfoRef)?.validate()
await unref(DescriptionRef)?.validate()
await unref(OtherSettingsRef)?.validate()
// TODO @puhui server
//
formData.value.skus.forEach((item) => {
// sku name
item.name = formData.value.name
// skusvalue
if (formData.value.specType) {
item.properties.forEach((item2) => {
delete item2.valueName
})
}
})
//
const newSliderPicUrls = []
formData.value.sliderPicUrls.forEach((item) => {
//
// TODO @puhui999 object
if (typeof item === 'object') {
newSliderPicUrls.push(item.url)
} else {
newSliderPicUrls.push(item)
}
})
formData.value.sliderPicUrls = newSliderPicUrls
//
const data = formData.value as SpuType
// skus.
const id = query.id as unknown as number
if (!id) {
await managementApi.createSpu(data)
message.success(t('common.createSuccess'))
} else {
await managementApi.updateSpu(data)
message.success(t('common.updateSuccess'))
}
close()
} catch (e) {
// ,skus
if (typeof e === 'string') {
formData.value.skus = newSkus
}
} finally {
formLoading.value = false
}
}
/**
* 重置表单
*/
const resetForm = async () => {
formData.value = {
name: '', //
categoryId: 0, //
keyword: '', //
unit: '', //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 0, //
selectRule: '',
specType: false, //
subCommissionType: false, //
description: '', //
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //
recommendHot: false, //
recommendBenefit: false, //
recommendBest: false, //
recommendNew: false, //
recommendGood: false //
}
}
/** 关闭按钮 */
const close = () => {
// TODO @puhui999 reset close
resetForm()
delView(unref(currentRoute))
push('/product/product-management')
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>

View File

@ -0,0 +1,238 @@
<template>
<el-form ref="ProductManagementBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="商品名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入商品名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<!-- TODO @puhui999只能选根节点 -->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="formData.categoryId"
:data="categoryList"
:props="defaultProps"
check-strictly
node-key="id"
placeholder="请选择商品分类"
class="w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品关键字" prop="keyword">
<el-input v-model="formData.keyword" placeholder="请输入商品关键字" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位" prop="unit">
<el-select v-model="formData.unit" placeholder="请选择单位" class="w-1/1">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品简介" prop="introduction">
<el-input
v-model="formData.introduction"
:rows="3"
placeholder="请输入商品简介"
type="textarea"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品封面图" prop="picUrl">
<UploadImg v-model="formData.picUrl" height="80px" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="商品轮播图" prop="sliderPicUrls">
<UploadImgs v-model="formData.sliderPicUrls" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运费模板" prop="deliveryTemplateId">
<el-select v-model="formData.deliveryTemplateId" placeholder="请选择" class="w-1/1">
<el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-button class="ml-20px">运费模板</el-button>
</el-col>
<el-col :span="12">
<el-form-item label="商品规格" props="specType">
<el-radio-group v-model="formData.specType" @change="onChangeSpec">
<el-radio :label="false" class="radio">单规格</el-radio>
<el-radio :label="true">多规格</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分销类型" props="subCommissionType">
<el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
<el-radio :label="false">默认设置</el-radio>
<el-radio :label="true" class="radio">自行设置</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- 多规格添加-->
<el-col :span="24">
<el-form-item v-if="formData.specType" label="商品属性">
<!-- TODO @puhui999参考 https://admin.java.crmeb.net/store/list/creatProduct -->
<el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open">添加规格</el-button>
<ProductAttributes :attribute-data="attributeList" />
</el-form-item>
<template v-if="formData.specType && attributeList.length > 0">
<el-form-item label="批量设置">
<SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" />
</el-form-item>
<el-form-item label="属性列表">
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
</el-form-item>
</template>
<el-form-item v-if="!formData.specType">
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<ProductAttributesAddForm ref="AttributesAddFormRef" @success="addAttribute" />
</template>
<script lang="ts" name="ProductManagementBasicInfoForm" setup>
import { PropType } from 'vue'
import { defaultProps, handleTree } from '@/utils/tree'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import { UploadImg, UploadImgs } from '@/components/UploadFile'
import { copyValueToTarget } from '@/utils/object'
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
import * as ProductCategoryApi from '@/api/mall/product/category'
import { propTypes } from '@/utils/propTypes'
const message = useMessage() //
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
default: () => {}
},
activeName: propTypes.string.def('')
})
const AttributesAddFormRef = ref() // TODO @puhui999
const ProductManagementBasicInfoRef = ref() // Ref TODO @puhui999
// TODO @puhui999attributeList propertyList
const attributeList = ref([]) //
/** 添加商品属性 */ // TODO @puhui999propFormData
const addAttribute = (property: any) => {
if (Array.isArray(property)) {
attributeList.value = property
return
}
attributeList.value.push(property)
}
const formData = reactive<SpuType>({
name: '', //
categoryId: undefined, //
keyword: '', //
unit: '', //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
specType: false, //
subCommissionType: false, //
skus: []
})
const rules = reactive({
name: [required],
categoryId: [required],
keyword: [required],
unit: [required],
introduction: [required],
picUrl: [required],
sliderPicUrls: [required],
// deliveryTemplateId: [required],
specType: [required],
subCommissionType: [required]
})
/**
* 将传进来的值赋值给 formData
*/
watch(
() => props.propFormData,
(data) => {
if (!data) return
copyValueToTarget(formData, data)
},
{
deep: true,
immediate: true
}
)
/**
* 表单校验
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!ProductManagementBasicInfoRef) return
return await unref(ProductManagementBasicInfoRef).validate((valid) => {
if (!valid) {
message.warning('商品信息未完善!!')
emit('update:activeName', 'basicInfo')
//
throw new Error('商品信息未完善!!')
} else {
//
Object.assign(props.propFormData, formData)
}
})
}
defineExpose({ validate, addAttribute })
/** 分销类型 */
const changeSubCommissionType = () => {
//
for (const item of formData.skus) {
item.subCommissionFirstPrice = 0
item.subCommissionSecondPrice = 0
}
}
/** 选择规格 */
const onChangeSpec = () => {
//
attributeList.value = []
// sku
formData.skus = [
{
price: 0,
marketPrice: 0,
costPrice: 0,
barCode: '',
picUrl: '',
stock: 0,
weight: 0,
volume: 0,
subCommissionFirstPrice: 0,
subCommissionSecondPrice: 0
}
]
}
const categoryList = ref() //
onMounted(async () => {
//
const data = await ProductCategoryApi.getCategoryList({})
categoryList.value = handleTree(data, 'id', 'parentId')
})
</script>

View File

@ -0,0 +1,84 @@
<template>
<el-form ref="DescriptionFormRef" :model="formData" :rules="rules" label-width="120px">
<!--富文本编辑器组件-->
<el-form-item label="商品详情" prop="description">
<Editor v-model:modelValue="formData.description" />
</el-form-item>
</el-form>
</template>
<script lang="ts" name="DescriptionForm" setup>
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import { Editor } from '@/components/Editor'
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils/object'
import { propTypes } from '@/utils/propTypes'
const message = useMessage() //
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
default: () => {}
},
activeName: propTypes.string.def('')
})
const DescriptionFormRef = ref() // Ref
const formData = ref<SpuType>({
description: '' //
})
//
const rules = reactive({
description: [required]
})
/**
* 富文本编辑器如果输入过再清空会有残留需再重置一次
*/
watch(
() => formData.value.description,
(newValue) => {
if ('<p><br></p>' === newValue) {
formData.value.description = ''
}
},
{
deep: true,
immediate: true
}
)
/**
* 将传进来的值赋值给formData
*/
watch(
() => props.propFormData,
(data) => {
if (!data) return
copyValueToTarget(formData.value, data)
},
{
deep: true,
immediate: true
}
)
/**
* 表单校验
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!DescriptionFormRef) return
return unref(DescriptionFormRef).validate((valid) => {
if (!valid) {
message.warning('商品详情为完善!!')
emit('update:activeName', 'description')
//
throw new Error('商品详情为完善!!')
} else {
//
Object.assign(props.propFormData, formData.value)
}
})
}
defineExpose({ validate })
</script>

View File

@ -0,0 +1,156 @@
<template>
<el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<!-- TODO @puhui999横着三个哈 -->
<el-col :span="24">
<el-col :span="8">
<el-form-item label="商品排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="赠送积分" prop="giveIntegral">
<el-input-number v-model="formData.giveIntegral" :min="0" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="虚拟销量" prop="virtualSalesCount">
<el-input-number
v-model="formData.virtualSalesCount"
:min="0"
placeholder="请输入虚拟销量"
/>
</el-form-item>
</el-col>
</el-col>
<el-col :span="24">
<el-form-item label="商品推荐">
<el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup">
<el-checkbox v-for="(item, index) in recommend" :key="index" :label="item.value">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-col>
<el-col :span="24">
<!-- TODO tag展示暂时不考虑排序 -->
<el-form-item label="活动优先级">
<el-tag>默认</el-tag>
<el-tag class="ml-2" type="success">秒杀</el-tag>
<el-tag class="ml-2" type="info">砍价</el-tag>
<el-tag class="ml-2" type="warning">拼团</el-tag>
</el-form-item>
</el-col>
<!-- TODO @puhui999等优惠劵 ok 在搞 -->
<el-col :span="24">
<el-form-item label="赠送优惠劵">
<el-button>选择优惠券</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" name="OtherSettingsForm" setup>
import type { SpuType } from '@/api/mall/product/management/type/spuType'
import { PropType } from 'vue'
import { copyValueToTarget } from '@/utils/object'
import { propTypes } from '@/utils/propTypes'
const message = useMessage() //
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
default: () => {}
},
activeName: propTypes.string.def('')
})
// TODO @puhui999 recommendOptions
const recommend = [
{ name: '是否热卖', value: 'recommendHot' },
{ name: '是否优惠', value: 'recommendBenefit' },
{ name: '是否精品', value: 'recommendBest' },
{ name: '是否新品', value: 'recommendNew' },
{ name: '是否优品', value: 'recommendGood' }
]
const checkboxGroup = ref<string[]>(['recommendHot']) //
/** 选择商品后赋值 */
const onChangeGroup = () => {
// TODO @puhui999 recommend
checkboxGroup.value.includes('recommendHot')
? (formData.value.recommendHot = true)
: (formData.value.recommendHot = false)
checkboxGroup.value.includes('recommendBenefit')
? (formData.value.recommendBenefit = true)
: (formData.value.recommendBenefit = false)
checkboxGroup.value.includes('recommendBest')
? (formData.value.recommendBest = true)
: (formData.value.recommendBest = false)
checkboxGroup.value.includes('recommendNew')
? (formData.value.recommendNew = true)
: (formData.value.recommendNew = false)
checkboxGroup.value.includes('recommendGood')
? (formData.value.recommendGood = true)
: (formData.value.recommendGood = false)
}
const OtherSettingsFormRef = ref() // Ref
//
const formData = ref<SpuType>({
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //
recommendHot: false, //
recommendBenefit: false, //
recommendBest: false, //
recommendNew: false, //
recommendGood: false //
})
//
const rules = reactive({
sort: [required],
giveIntegral: [required],
virtualSalesCount: [required]
})
/**
* 将传进来的值赋值给formData
*/
watch(
() => props.propFormData,
(data) => {
if (!data) return
copyValueToTarget(formData.value, data)
// TODO
checkboxGroup.value = []
formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : ''
formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : ''
formData.value.recommendBest ? checkboxGroup.value.push('recommendBest') : ''
formData.value.recommendNew ? checkboxGroup.value.push('recommendNew') : ''
formData.value.recommendGood ? checkboxGroup.value.push('recommendGood') : ''
},
{
deep: true,
immediate: true
}
)
/**
* 表单校验
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
//
if (!OtherSettingsFormRef) return
return await unref(OtherSettingsFormRef).validate((valid) => {
if (!valid) {
message.warning('商品其他设置未完善!!')
emit('update:activeName', 'otherSettings')
//
throw new Error('商品其他设置未完善!!')
} else {
//
Object.assign(props.propFormData, formData.value)
}
})
}
defineExpose({ validate })
</script>

View File

@ -0,0 +1,102 @@
<template>
<el-col v-for="(item, index) in attributeList" :key="index">
<div>
<el-text class="mx-1">属性名</el-text>
<el-text class="mx-1">{{ item.name }}</el-text>
</div>
<div>
<el-text class="mx-1">属性值</el-text>
<el-tag
v-for="(value, valueIndex) in item.values"
:key="value.id"
:disable-transitions="false"
class="mx-1"
closable
@close="handleClose(index, valueIndex)"
>
{{ value.name }}
</el-tag>
<el-input
v-show="inputVisible(index)"
ref="InputRef"
v-model="inputValue"
class="!w-20"
size="small"
@blur="handleInputConfirm(index, item.id)"
@keyup.enter="handleInputConfirm(index, item.id)"
/>
<el-button
v-show="!inputVisible(index)"
class="button-new-tag ml-1"
size="small"
@click="showInput(index)"
>
+ 添加
</el-button>
</div>
<el-divider class="my-10px" />
</el-col>
</template>
<script lang="ts" name="ProductAttributes" setup>
import { ElInput } from 'element-plus'
import * as PropertyApi from '@/api/mall/product/property'
const { t } = useI18n() //
const message = useMessage() //
const inputValue = ref('') //
const attributeIndex = ref<number | null>(null) // index
//
const inputVisible = computed(() => (index) => {
if (attributeIndex.value === null) return false
if (attributeIndex.value === index) return true
})
const InputRef = ref() //Ref
const attributeList = ref([]) //
const props = defineProps({
attributeData: {
type: Array,
default: () => {}
}
})
watch(
() => props.attributeData,
(data) => {
if (!data) return
attributeList.value = data
},
{
deep: true,
immediate: true
}
)
/** 删除标签 tagValue 标签值*/
const handleClose = (index, valueIndex) => {
attributeList.value[index].values?.splice(valueIndex, 1)
}
/** 显示输入框并获取焦点 */
const showInput = async (index) => {
attributeIndex.value = index
// refRef
InputRef.value[index]!.input!.focus()
}
/** 输入框失去焦点或点击回车时触发 */
const handleInputConfirm = async (index, propertyId) => {
if (inputValue.value) {
//
try {
const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
attributeList.value[index].values.push({ id, name: inputValue.value })
message.success(t('common.createSuccess'))
} catch {
message.error('添加失败,请重试') // TODO
}
}
attributeIndex.value = null
inputValue.value = ''
}
</script>

View File

@ -0,0 +1,85 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="ProductPropertyForm" setup>
import * as PropertyApi from '@/api/mall/product/property'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('添加商品属性') //
const formLoading = ref(false) // 12
const formData = ref({
name: '',
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as PropertyApi.PropertyVO
//
const res = await PropertyApi.getPropertyListAndValue({ name: data.name })
if (res.length === 0) {
const propertyId = await PropertyApi.createProperty(data)
emit('success', { id: propertyId, ...formData.value, values: [] })
} else {
if (res[0].values === null) {
res[0].values = []
}
emit('success', res[0]) //
}
message.success(t('common.createSuccess'))
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
name: '',
remark: ''
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,309 @@
<template>
<el-table
:data="isBatch ? SkuData : formData.skus"
border
class="tabNumWidth"
max-height="500"
size="small"
>
<el-table-column align="center" fixed="left" label="图片" min-width="100">
<template #default="{ row }">
<UploadImg v-model="row.picUrl" height="80px" width="100%" />
</template>
</el-table-column>
<template v-if="formData.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaderList"
:key="index"
:label="item.label"
align="center"
min-width="120"
>
<template #default="{ row }">
{{ row.properties[index]?.valueName }}
</template>
</el-table-column>
</template>
<!-- TODO @puhui999 controls-position="right" 可以去掉哈不然太长了手动输入更方便 -->
<el-table-column align="center" label="商品条码" min-width="168">
<template #default="{ row }">
<el-input v-model="row.barCode" class="w-100%" />
</template>
</el-table-column>
<!-- TODO @puhui999用户输入的时候是按照元分主要是我们自己用 -->
<el-table-column align="center" label="销售价(分)" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" />
</template>
</el-table-column>
<el-table-column align="center" label="市场价(分)" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.marketPrice"
:min="0"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="成本价(分)" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.costPrice"
:min="0"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="库存" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
</template>
</el-table-column>
<el-table-column align="center" label="重量(kg)" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.weight" :min="0" class="w-100%" controls-position="right" />
</template>
</el-table-column>
<el-table-column align="center" label="体积(m^3)" min-width="168">
<template #default="{ row }">
<el-input-number v-model="row.volume" :min="0" class="w-100%" controls-position="right" />
</template>
</el-table-column>
<template v-if="formData.subCommissionType">
<el-table-column align="center" label="一级返佣(分)" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.subCommissionFirstPrice"
:min="0"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column align="center" label="二级返佣(分)" min-width="168">
<template #default="{ row }">
<el-input-number
v-model="row.subCommissionSecondPrice"
:min="0"
class="w-100%"
controls-position="right"
/>
</template>
</el-table-column>
</template>
<el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
<template #default>
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
批量添加
</el-button>
<el-button v-else link size="small" type="primary">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" name="SkuList" setup>
import { UploadImg } from '@/components/UploadFile'
import { PropType } from 'vue'
import { SpuType } from '@/api/mall/product/management/type/spuType'
import { propTypes } from '@/utils/propTypes'
import { SkuType } from '@/api/mall/product/management/type/skuType'
import { copyValueToTarget } from '@/utils/object'
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
default: () => {}
},
attributeList: {
type: Array,
default: () => []
},
isBatch: propTypes.bool.def(false) //
})
const formData = ref<SpuType>() //
// TODO @puhui999
const SkuData = ref<SkuType[]>([
{
/**
* 商品价格单位
*/
price: 0,
/**
* 市场价单位
*/
marketPrice: 0,
/**
* 成本价单位
*/
costPrice: 0,
/**
* 商品条码
*/
barCode: '',
/**
* 图片地址
*/
picUrl: '',
/**
* 库存
*/
stock: 0,
/**
* 商品重量单位kg 千克
*/
weight: 0,
/**
* 商品体积单位m^3 平米
*/
volume: 0,
/**
* 一级分销的佣金单位
*/
subCommissionFirstPrice: 0,
/**
* 二级分销的佣金单位
*/
subCommissionSecondPrice: 0
}
])
/** 批量添加 */
const batchAdd = () => {
formData.value.skus.forEach((item) => {
copyValueToTarget(item, SkuData.value[0])
})
}
const tableHeaderList = ref<{ prop: string; label: string }[]>([])
/**
* 将传进来的值赋值给SkuData
*/
watch(
() => props.propFormData,
(data) => {
if (!data) return
formData.value = data
},
{
deep: true,
immediate: true
}
)
// TODO @ chatgpt
/** 生成表数据 */
const generateTableData = (data: any[]) => {
//
const propertiesItemList = []
for (const item of data) {
const objList = []
for (const v of item.values) {
const obj = { propertyId: 0, valueId: 0, valueName: '' }
obj.propertyId = item.id
obj.valueId = v.id
obj.valueName = v.name
objList.push(obj)
}
propertiesItemList.push(objList)
}
const buildList = build(propertiesItemList)
// sku,
if (
buildList.length === formData.value.skus.length ||
data.some((item) => item.values.length === 0)
) {
return
}
//
formData.value!.skus = []
buildList.forEach((item) => {
const row = {
properties: [],
price: 0,
marketPrice: 0,
costPrice: 0,
barCode: '',
picUrl: '',
stock: 0,
weight: 0,
volume: 0,
subCommissionFirstPrice: 0,
subCommissionSecondPrice: 0
}
//
if (Array.isArray(item)) {
row.properties = item
} else {
row.properties.push(item)
}
formData.value.skus.push(row)
})
}
/** 构建所有排列组合 */
const build = (list: any[]) => {
if (list.length === 0) {
return []
} else if (list.length === 1) {
return list[0]
} else {
const result = []
const rest = build(list.slice(1))
for (let i = 0; i < list[0].length; i++) {
for (let j = 0; j < rest.length; j++) {
//
if (Array.isArray(rest[j])) {
result.push([list[0][i], ...rest[j]])
} else {
result.push([list[0][i], rest[j]])
}
}
}
return result
}
}
/** 监听属性列表生成相关参数和表头 */
watch(
() => props.attributeList,
(data) => {
//
if (!formData.value.specType) return
// 使
if (props.isBatch) {
SkuData.value = [
{
price: 0,
marketPrice: 0,
costPrice: 0,
barCode: '',
picUrl: '',
stock: 0,
weight: 0,
volume: 0,
subCommissionFirstPrice: 0,
subCommissionSecondPrice: 0
}
]
}
//
if (JSON.stringify(data) === '[]') return
//
tableHeaderList.value = []
//
data.forEach((item, index) => {
// nameindex
tableHeaderList.value.push({ prop: `name${index}`, label: item.name })
})
generateTableData(data)
},
{
deep: true,
immediate: true
}
)
</script>

View File

@ -0,0 +1,15 @@
import BasicInfoForm from './BasicInfoForm.vue'
import DescriptionForm from './DescriptionForm.vue'
import OtherSettingsForm from './OtherSettingsForm.vue'
import ProductAttributes from './ProductAttributes.vue'
import ProductAttributesAddForm from './ProductAttributesAddForm.vue'
import SkuList from './SkuList.vue'
export {
BasicInfoForm,
DescriptionForm,
OtherSettingsForm,
ProductAttributes,
ProductAttributesAddForm,
SkuList
}

View File

@ -0,0 +1,388 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<!-- TODO @puhui999https://admin.java.crmeb.net/store/index使 + -->
<el-form-item label="品牌名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入品牌名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['product:brand:create']" plain type="primary" @click="openForm">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<!-- TODO @puhui999增加一个导出操作 -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-tabs v-model="queryParams.tabType" @tab-click="handleClick">
<el-tab-pane
v-for="item in tabsData"
:key="item.type"
:label="item.name + '(' + item.count + ')'"
:name="item.type"
/>
</el-tabs>
<el-table v-loading="loading" :data="list">
<!-- TODO puhui999: ID 编号的展示 -->
<!-- TODO 暂时不做折叠数据 -->
<!-- <el-table-column type="expand">-->
<!-- <template #default="{ row }">-->
<!-- <el-form inline label-position="left">-->
<!-- <el-form-item label="市场价:">-->
<!-- <span>{{ row.marketPrice }}</span>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="成本价:">-->
<!-- <span>{{ row.costPrice }}</span>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="虚拟销量:">-->
<!-- <span>{{ row.virtualSalesCount }}</span>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image
:src="row.picUrl"
style="width: 36px; height: 36px"
@click="imagePreview(row.picUrl)"
/>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
<!-- TODO 价格 / 100.0 -->
<el-table-column align="center" label="商品售价" min-width="90" prop="price" />
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180"
/>
<el-table-column fixed="right" label="状态" min-width="80">
<template #default="{ row }">
<!-- TODO @puhui是不是不用 Number(row.status) 去比较哈直接 row.status < 0 -->
<el-switch
v-model="row.status"
:active-value="1"
:disabled="Number(row.status) < 0"
:inactive-value="0"
active-text="上架"
inactive-text="下架"
inline-prompt
@change="changeStatus(row)"
/>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" min-width="150">
<template #default="{ row }">
<!-- TODO @puhui999详情可以后面点做哈 -->
<template v-if="queryParams.tabType === 4">
<el-button
v-hasPermi="['product:spu:delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
<el-button
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="addToTrash(row, ProductSpuStatusEnum.DISABLE.status)"
>
恢复到仓库
</el-button>
</template>
<template v-else>
<el-button
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="openForm(row.id)"
>
修改
</el-button>
<el-button
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="addToTrash(row, ProductSpuStatusEnum.RECYCLE.status)"
>
加入回收站
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html -->
<!-- 必须在表格外面展示不然单元格会遮挡图层 -->
<el-image-viewer
v-if="imgViewVisible"
:url-list="imageViewerList"
@close="imgViewVisible = false"
/>
</template>
<script lang="ts" name="ProductList" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
// TODO @puhui999managementApi=ProductSpuApi
import * as managementApi from '@/api/mall/product/management/spu'
import { ProductSpuStatusEnum } from '@/utils/constants'
import { TabsPaneContext } from 'element-plus'
const message = useMessage() //
const { t } = useI18n() //
const { currentRoute, push } = useRouter() //
const loading = ref(false) //
const total = ref(0) //
const list = ref<any[]>([]) //
// tabs
const tabsData = ref([
{
count: 0,
name: '出售中商品',
type: 0
},
{
count: 0,
name: '仓库中商品',
type: 1
},
{
count: 0,
name: '已经售空商品',
type: 2
},
{
count: 0,
name: '警戒库存',
type: 3
},
{
count: 0,
name: '商品回收站',
type: 4
}
])
/** 获得每个 Tab 的数量 */
const getTabsCount = async () => {
// TODO @puhui999 try catch
try {
const res = await managementApi.getTabsCount()
for (let objName in res) {
tabsData.value[Number(objName)].count = res[objName]
}
} catch {}
}
const imgViewVisible = ref(false) //
const imageViewerList = ref<string[]>([]) //
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0
})
const queryFormRef = ref() //
// TODO @puhui999 handleTabClick
const handleClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName
getList()
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await managementApi.getSpuList(queryParams.value)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// TODO @puhui999 changeStatus addToTrash
/**
* 更改 SPU 状态
*
* @param row
* @param status 更改前的值
*/
const changeStatus = async (row, status?: number) => {
// TODO
try {
let text = ''
switch (row.status) {
case ProductSpuStatusEnum.DISABLE.status:
text = ProductSpuStatusEnum.DISABLE.name
break
case ProductSpuStatusEnum.ENABLE.status:
text = ProductSpuStatusEnum.ENABLE.name
break
case ProductSpuStatusEnum.RECYCLE.status:
text = `加入${ProductSpuStatusEnum.RECYCLE.name}`
break
}
await message.confirm(
row.status === -1 ? `确认要将[${row.name}]${text}吗?` : `确认要${text}[${row.name}]吗?`
)
await managementApi.updateStatus({ id: row.id, status: row.status })
message.success('更新状态成功')
// tabs
await getTabsCount()
//
await getList()
} catch {
//
if (typeof status !== 'undefined') {
row.status = status
return
}
//
row.status =
row.status === ProductSpuStatusEnum.DISABLE.status
? ProductSpuStatusEnum.ENABLE.status
: ProductSpuStatusEnum.DISABLE.status
}
}
/**
* 加入回收站
*
* @param row
* @param status
*/
const addToTrash = (row, status) => {
//
const num = Number(`${row.status}`)
row.status = status
changeStatus(row, num)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await managementApi.deleteSpu(id)
message.success(t('common.delSuccess'))
// tabs
await getTabsCount()
//
await getList()
} catch {}
}
/**
* 商品图预览
* @param imgUrl
*/
const imagePreview = (imgUrl: string) => {
imageViewerList.value = [imgUrl]
imgViewVisible.value = true
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/**
* 新增或修改
*
* @param id 商品 SPU 编号
*/
const openForm = (id?: number) => {
//
if (typeof id === 'number') {
push('/product/productManagementAdd?id=' + id)
return
}
//
push('/product/productManagementAdd')
}
// TODO @puhui999
watch(
() => currentRoute.value,
() => {
getList()
},
{
immediate: true
}
)
/** 初始化 **/
onMounted(() => {
getTabsCount()
getList()
})
</script>

View File

@ -0,0 +1,78 @@
<template>
<div>
<el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
<el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
<el-select v-model="replyForm.requestMessageType" placeholder="请选择">
<template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
<el-option
v-if="RequestMessageTypes.includes(dict.value)"
:label="dict.label"
:value="dict.value"
/>
</template>
</el-select>
</el-form-item>
<el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
<el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
<el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
</el-form-item>
<el-form-item label="回复消息">
<WxReplySelect v-model="reply" />
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts" name="ReplyForm">
import WxReplySelect, { type Reply } from '@/views/mp/components/wx-reply'
import type { FormInstance } from 'element-plus'
import { MsgType } from './types'
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
const props = defineProps<{
modelValue: any
reply: Reply
msgType: MsgType
}>()
const emit = defineEmits<{
(e: 'update:reply', v: Reply)
(e: 'update:modelValue', v: any)
}>()
const reply = computed<Reply>({
get: () => props.reply,
set: (val) => emit('update:reply', val)
})
const replyForm = computed<any>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const formRef = ref<FormInstance | null>(null) // ref
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] //
//
const rules = {
requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }],
requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
}
defineExpose({
resetFields: () => formRef.value?.resetFields(),
validate: async () => formRef.value?.validate()
})
</script>
<style scoped></style>

View File

@ -53,38 +53,13 @@
@on-delete="onDelete" @on-delete="onDelete"
/> />
<!-- 添加或修改自动回复的对话框 --> <el-dialog
<!-- TODO @Dhb52 --> :title="isCreating ? '新增自动回复' : '修改自动回复'"
<el-dialog :title="dialogTitle" v-model="showFormDialog" width="800px" destroy-on-close> v-model="showDialog"
<el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px"> width="800px"
<el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message"> destroy-on-close
<el-select v-model="replyForm.requestMessageType" placeholder="请选择"> >
<template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> <ReplyForm v-model="replyForm" v-model:reply="reply" :msg-type="msgType" ref="formRef" />
<el-option
v-if="RequestMessageTypes.includes(dict.value)"
:label="dict.label"
:value="dict.value"
/>
</template>
</el-select>
</el-form-item>
<el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
<el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
<el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
</el-form-item>
<el-form-item label="回复消息">
<WxReplySelect v-model="reply" />
</el-form-item>
</el-form>
<template #footer> <template #footer>
<el-button @click="cancel"> </el-button> <el-button @click="cancel"> </el-button>
<el-button type="primary" @click="onSubmit"> </el-button> <el-button type="primary" @click="onSubmit"> </el-button>
@ -93,52 +68,43 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts" name="MpAutoReply"> <script setup lang="ts" name="MpAutoReply">
import WxReplySelect, { type Reply, ReplyType } from '@/views/mp/components/wx-reply' import ReplyForm from '@/views/mp/autoReply/components/ReplyForm.vue'
import { type Reply, ReplyType } from '@/views/mp/components/wx-reply'
import WxAccountSelect from '@/views/mp/components/wx-account-select' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import * as MpAutoReplyApi from '@/api/mp/autoReply' import * as MpAutoReplyApi from '@/api/mp/autoReply'
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import type { FormInstance, TabPaneName } from 'element-plus' import type { TabPaneName } from 'element-plus'
import ReplyTable from './components/ReplyTable.vue' import ReplyTable from './components/ReplyTable.vue'
import { MsgType } from './components/types' import { MsgType } from './components/types'
const message = useMessage() // const message = useMessage() //
const accountId = ref(-1) // ID
const msgType = ref<MsgType>(MsgType.Keyword) // const msgType = ref<MsgType>(MsgType.Keyword) //
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] //
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const formRef = ref<FormInstance | null>(null) // ref const formRef = ref<InstanceType<typeof ReplyForm> | null>(null) // ref
// //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
accountId: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0 accountId: accountId
}) })
const dialogTitle = ref('') // const isCreating = ref(false) //
const showFormDialog = ref(false) // const showDialog = ref(false) //
const replyForm = ref<any>({}) // const replyForm = ref<any>({}) //
// //
const reply = ref<Reply>({ const reply = ref<Reply>({
type: ReplyType.Text, type: ReplyType.Text,
accountId: 0 accountId: -1
}) })
//
const rules = {
requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }],
requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
}
/** 侦听账号变化 */ /** 侦听账号变化 */
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id accountId.value = id
reply.value.accountId = id reply.value.accountId = id
queryParams.pageNo = 1
getList() getList()
} }
@ -177,8 +143,8 @@ const onCreate = () => {
accountId: queryParams.accountId accountId: queryParams.accountId
} }
dialogTitle.value = '新增自动回复' isCreating.value = true
showFormDialog.value = true showDialog.value = true
} }
/** 修改按钮操作 */ /** 修改按钮操作 */
@ -210,8 +176,8 @@ const onUpdate = async (id: number) => {
} }
// //
dialogTitle.value = '修改自动回复' isCreating.value = false
showFormDialog.value = true showDialog.value = true
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
@ -223,8 +189,7 @@ const onDelete = async (id: number) => {
} }
const onSubmit = async () => { const onSubmit = async () => {
const valid = await formRef.value?.validate() await formRef.value?.validate()
if (!valid) return
// //
const submitForm: any = { ...replyForm.value } const submitForm: any = { ...replyForm.value }
@ -248,7 +213,7 @@ const onSubmit = async () => {
message.success('新增成功') message.success('新增成功')
} }
showFormDialog.value = false showDialog.value = false
await getList() await getList()
} }
@ -267,7 +232,7 @@ const reset = () => {
// //
const cancel = () => { const cancel = () => {
showFormDialog.value = false showDialog.value = false
reset() reset()
} }
</script> </script>

View File

@ -8,13 +8,14 @@
import * as MpAccountApi from '@/api/mp/account' import * as MpAccountApi from '@/api/mp/account'
const account: MpAccountApi.AccountVO = reactive({ const account: MpAccountApi.AccountVO = reactive({
id: undefined, id: -1,
name: '' name: ''
}) })
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
const accountList = ref<MpAccountApi.AccountVO[]>([])
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', id: number, name: string): void (e: 'change', id: number, name: string)
}>() }>()
const handleQuery = async () => { const handleQuery = async () => {

View File

@ -0,0 +1,67 @@
<template>
<div>
<MsgEvent v-if="item.type === MsgType.Event" :item="item" />
<div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
<div v-else-if="item.type === MsgType.Voice">
<WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
</div>
<div v-else-if="item.type === MsgType.Image">
<a target="_blank" :href="item.mediaUrl">
<img :src="item.mediaUrl" style="width: 100px" />
</a>
</div>
<div
v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
style="text-align: center"
>
<WxVideoPlayer :url="item.mediaUrl" />
</div>
<div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
<el-link type="success" :underline="false" target="_blank" :href="item.url">
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
</el-link>
<div class="avue-card__info" style="height: unset">{{ item.description }}</div>
</div>
<div v-else-if="item.type === MsgType.Location">
<WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" />
</div>
<div v-else-if="item.type === MsgType.News" style="width: 300px">
<WxNews :articles="item.articles" />
</div>
<div v-else-if="item.type === MsgType.Music">
<WxMusic
:title="item.title"
:description="item.description"
:thumb-media-url="item.thumbMediaUrl"
:music-url="item.musicUrl"
:hq-music-url="item.hqMusicUrl"
/>
</div>
</div>
</template>
<script setup lang="ts" name="Msg">
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxNews from '@/views/mp/components/wx-news'
import WxLocation from '@/views/mp/components/wx-location'
import WxMusic from '@/views/mp/components/wx-music'
import MsgEvent from './MsgEvent.vue'
import { MsgType } from '../types'
const props = defineProps<{
item: any
}>()
const item = ref<any>(props.item)
</script>
<style scoped></style>

View File

@ -0,0 +1,49 @@
<template>
<div>
<div v-if="item.event === 'subscribe'">
<el-tag type="success">关注</el-tag>
</div>
<div v-else-if="item.event === 'unsubscribe'">
<el-tag type="danger">取消关注</el-tag>
</div>
<div v-else-if="item.event === 'CLICK'">
<el-tag>点击菜单</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.event === 'VIEW'">
<el-tag>点击菜单链接</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.event === 'scancode_waitmsg'">
<el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.event === 'scancode_push'">
<el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.event === 'pic_sysphoto'">
<el-tag>系统拍照发图</el-tag>
</div>
<div v-else-if="item.event === 'pic_photo_or_album'">
<el-tag>拍照或者相册</el-tag>
</div>
<div v-else-if="item.event === 'pic_weixin'">
<el-tag>微信相册</el-tag>
</div>
<div v-else-if="item.event === 'location_select'">
<el-tag>选择地理位置</el-tag>
</div>
<div v-else>
<el-tag type="danger">未知事件类型</el-tag>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
item: any
}>()
const item = ref(props.item)
</script>

View File

@ -0,0 +1,60 @@
<template>
<div class="execution" v-for="item in props.list" :key="item.id">
<div
class="avue-comment"
:class="{ 'avue-comment--reverse': item.sendFrom === SendFrom.MpBot }"
>
<div class="avatar-div">
<img :src="getAvatar(item.sendFrom)" class="avue-comment__avatar" />
<div class="avue-comment__author">
{{ getNickname(item.sendFrom) }}
</div>
</div>
<div class="avue-comment__main">
<div class="avue-comment__header">
<div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
</div>
<div
class="avue-comment__body"
:style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''"
>
<Msg :item="item" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="MsgList">
import Msg from './Msg.vue'
import { formatDate } from '@/utils/formatTime'
import { User } from '../types'
import avatarWechat from '@/assets/imgs/wechat.png'
const props = defineProps<{
list: any[]
accountId: number
user: User
}>()
enum SendFrom {
User = 1,
MpBot = 2
}
const getAvatar = (sendFrom: SendFrom) =>
sendFrom === SendFrom.User ? props.user.avatar : avatarWechat
const getNickname = (sendFrom: SendFrom) =>
sendFrom === SendFrom.User ? props.user.nickname : '公众号'
</script>
<style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
@import '../comment.scss';
@import '../card.scss';
.avatar-div {
text-align: center;
width: 80px;
}
</style>

View File

@ -7,123 +7,22 @@
--> -->
<template> <template>
<ContentWrap> <ContentWrap>
<div class="msg-div" :id="'msg-div' + nowStr"> <div class="msg-div" ref="msgDivRef">
<!-- 加载更多 --> <!-- 加载更多 -->
<div v-loading="loading"></div> <div v-loading="loading"></div>
<div v-if="!loading"> <div v-if="!loading">
<div class="el-table__empty-block" v-if="loadMore" @click="loadingMore" <div class="el-table__empty-block" v-if="hasMore" @click="loadMore"
><span class="el-table__empty-text">点击加载更多</span></div ><span class="el-table__empty-text">点击加载更多</span></div
> >
<div class="el-table__empty-block" v-if="!loadMore" <div class="el-table__empty-block" v-if="!hasMore"
><span class="el-table__empty-text">没有更多了</span></div ><span class="el-table__empty-text">没有更多了</span></div
> >
</div> </div>
<!-- 消息列表 --> <!-- 消息列表 -->
<div class="execution" v-for="item in list" :key="item.id"> <MsgList :list="list" :account-id="accountId" :user="user" />
<div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''">
<div class="avatar-div">
<img
:src="item.sendFrom === 1 ? user.avatar : mp.avatar"
class="avue-comment__avatar"
/>
<div class="avue-comment__author"
>{{ item.sendFrom === 1 ? user.nickname : mp.nickname }}
</div>
</div>
<div class="avue-comment__main">
<div class="avue-comment__header">
<div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
</div>
<div
class="avue-comment__body"
:style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
>
<!-- 事件区域 -->
<div v-if="item.type === MsgType.Event && item.event === 'subscribe'">
<el-tag type="success">关注</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'">
<el-tag type="danger">取消关注</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'">
<el-tag>点击菜单</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'">
<el-tag>点击菜单链接</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'">
<el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'">
<el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'">
<el-tag>系统拍照发图</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'">
<el-tag>拍照或者相册</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'">
<el-tag>微信相册</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event && item.event === 'location_select'">
<el-tag>选择地理位置</el-tag>
</div>
<div v-else-if="item.type === MsgType.Event">
<el-tag type="danger">未知事件类型</el-tag>
</div>
<!-- 消息区域 -->
<div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
<div v-else-if="item.type === MsgType.Voice">
<WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
</div>
<div v-else-if="item.type === MsgType.Image">
<a target="_blank" :href="item.mediaUrl">
<img :src="item.mediaUrl" style="width: 100px" />
</a>
</div>
<div
v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
style="text-align: center"
>
<WxVideoPlayer :url="item.mediaUrl" />
</div>
<div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
<el-link type="success" :underline="false" target="_blank" :href="item.url">
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
</el-link>
<div class="avue-card__info" style="height: unset">{{ item.description }}</div>
</div>
<!-- TODO 芋艿待完善 -->
<div v-else-if="item.type === MsgType.Location">
<WxLocation
:label="item.label"
:location-y="item.locationY"
:location-x="item.locationX"
/>
</div>
<div v-else-if="item.type === MsgType.News" style="width: 300px">
<!-- TODO 芋艿待测试详情页也存在类似的情况 -->
<WxNews :articles="item.articles" />
</div>
<div v-else-if="item.type === MsgType.Music">
<WxMusic
:title="item.title"
:description="item.description"
:thumb-media-url="item.thumbMediaUrl"
:music-url="item.musicUrl"
:hq-music-url="item.hqMusicUrl"
/>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<div class="msg-send" v-loading="sendLoading"> <div class="msg-send" v-loading="sendLoading">
<WxReplySelect ref="replySelectRef" v-model="reply" /> <WxReplySelect ref="replySelectRef" v-model="reply" />
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button> <el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
@ -132,18 +31,12 @@
</template> </template>
<script setup lang="ts" name="WxMsg"> <script setup lang="ts" name="WxMsg">
import WxReplySelect from '@/views/mp/components/wx-reply' import WxReplySelect, { Reply, ReplyType } from '@/views/mp/components/wx-reply'
import WxVideoPlayer from '@/views/mp/components/wx-video-play' import MsgList from './components/MsgList.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxNews from '@/views/mp/components/wx-news'
import WxLocation from '@/views/mp/components/wx-location'
import WxMusic from '@/views/mp/components/wx-music'
import { getMessagePage, sendMessage } from '@/api/mp/message' import { getMessagePage, sendMessage } from '@/api/mp/message'
import { getUser } from '@/api/mp/user' import { getUser } from '@/api/mp/user'
import { formatDate } from '@/utils/formatTime'
import profile from '@/assets/imgs/profile.jpg' import profile from '@/assets/imgs/profile.jpg'
import wechat from '@/assets/imgs/wechat.png' import { User } from './types'
import { MsgType } from './types'
const message = useMessage() // const message = useMessage() //
@ -154,61 +47,41 @@ const props = defineProps({
} }
}) })
const nowStr = ref(new Date().getTime()) // :id="'msg-div' + nowStr" const accountId = ref(-1) // IDuserId
const loading = ref(false) // const loading = ref(false) //
const loadMore = ref(true) // const hasMore = ref(true) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, // pageNo: 1, //
pageSize: 14, // pageSize: 14, //
accountId: undefined accountId: accountId
}) })
interface User {
nickname: string
avatar: string
accountId: number
}
// 使 // 使
const user: User = reactive({ const user: User = reactive({
nickname: '用户', nickname: '用户',
avatar: profile, avatar: profile,
accountId: 0 // accountId: accountId //
})
interface Mp {
nickname: string
avatar: string
}
const mp: Mp = reactive({
nickname: '公众号',
avatar: wechat
}) })
// ========= ========= // ========= =========
const sendLoading = ref(false) // const sendLoading = ref(false) //
interface Reply {
type: MsgType
accountId: number | null
articles: any[]
}
// //
const reply = ref<Reply>({ const reply = ref<Reply>({
type: MsgType.Text, type: ReplyType.Text,
accountId: null, accountId: -1,
articles: [] articles: []
}) })
const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelectref
const msgDivRef = ref<HTMLDivElement | null>(null) // ref
/** 完成加载 */ /** 完成加载 */
onMounted(async () => { onMounted(async () => {
const data = await getUser(props.userId) const data = await getUser(props.userId)
user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
user.accountId = data.accountId accountId.value = data.accountId
queryParams.accountId = data.accountId
reply.value.accountId = data.accountId reply.value.accountId = data.accountId
refreshChange() refreshChange()
@ -216,11 +89,15 @@ onMounted(async () => {
// //
const sendMsg = async () => { const sendMsg = async () => {
if (!reply) { if (!unref(reply)) {
return return
} }
// //
if (reply.value.type === MsgType.News && reply.value.articles.length > 1) { if (
reply.value.type === ReplyType.News &&
reply.value.articles &&
reply.value.articles.length > 1
) {
reply.value.articles = [reply.value.articles[0]] reply.value.articles = [reply.value.articles[0]]
message.success('图文消息条数限制在 1 条以内,已默认发送第一条') message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
} }
@ -229,18 +106,18 @@ const sendMsg = async () => {
sendLoading.value = false sendLoading.value = false
list.value = [...list.value, ...[data]] list.value = [...list.value, ...[data]]
scrollToBottom() await scrollToBottom()
// //
replySelectRef.value?.clear() replySelectRef.value?.clear()
} }
const loadingMore = () => { const loadMore = () => {
queryParams.pageNo++ queryParams.pageNo++
getPage(queryParams, null) getPage(queryParams, null)
} }
const getPage = async (page, params) => { const getPage = async (page: any, params: any = null) => {
loading.value = true loading.value = true
let dataTemp = await getMessagePage( let dataTemp = await getMessagePage(
Object.assign( Object.assign(
@ -254,62 +131,45 @@ const getPage = async (page, params) => {
) )
) )
const msgDiv = document.getElementById('msg-div' + nowStr.value) const scrollHeight = msgDivRef.value?.scrollHeight ?? 0
let scrollHeight = 0
if (msgDiv) {
scrollHeight = msgDiv.scrollHeight
}
// //
const data = dataTemp.list.reverse() const data = dataTemp.list.reverse()
list.value = [...data, ...list.value] list.value = [...data, ...list.value]
loading.value = false loading.value = false
if (data.length < queryParams.pageSize || data.length === 0) { if (data.length < queryParams.pageSize || data.length === 0) {
loadMore.value = false hasMore.value = false
} }
queryParams.pageNo = page.pageNo queryParams.pageNo = page.pageNo
queryParams.pageSize = page.pageSize queryParams.pageSize = page.pageSize
// //
if (queryParams.pageNo === 1) { if (queryParams.pageNo === 1) {
// //
scrollToBottom() await scrollToBottom()
} else if (data.length !== 0) { } else if (data.length !== 0) {
// //
await nextTick(() => { await nextTick()
if (scrollHeight !== 0) { if (scrollHeight !== 0) {
let div = document.getElementById('msg-div' + nowStr.value) if (msgDivRef.value) {
if (div && msgDiv) { msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight - scrollHeight - 100
msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
}
} }
}) }
} }
} }
const refreshChange = () => { const refreshChange = () => {
getPage(queryParams, null) getPage(queryParams)
} }
/** 定位到消息底部 */ /** 定位到消息底部 */
const scrollToBottom = () => { const scrollToBottom = async () => {
nextTick(() => { await nextTick()
let div = document.getElementById('msg-div' + nowStr.value) if (msgDivRef.value) {
if (div) { msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight
div.scrollTop = div.scrollHeight }
}
})
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
@import './comment.scss';
@import './card.scss';
.msg-main {
margin-top: -30px;
padding: 10px;
}
.msg-div { .msg-div {
height: 50vh; height: 50vh;
overflow: auto; overflow: auto;
@ -322,11 +182,6 @@ const scrollToBottom = () => {
padding: 10px; padding: 10px;
} }
.avatar-div {
text-align: center;
width: 80px;
}
.send-but { .send-but {
float: right; float: right;
margin-top: 8px; margin-top: 8px;

View File

@ -9,3 +9,9 @@ export enum MsgType {
Music = 'music', Music = 'music',
News = 'news' News = 'news'
} }
export interface User {
nickname: string
avatar: string
accountId: number
}

View File

@ -55,6 +55,6 @@ defineExpose({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */ /* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scss */
@import url('../wx-msg/card.scss'); @import '../wx-msg/card.scss';
</style> </style>

View File

@ -51,7 +51,7 @@
> >
<WxMaterialSelect <WxMaterialSelect
type="image" type="image"
:account-id="accountId" :account-id="accountId!"
@select-material="onMaterialSelected" @select-material="onMaterialSelected"
/> />
</el-dialog> </el-dialog>
@ -93,11 +93,11 @@ const showImageDialog = ref(false)
const fileList = ref<UploadFiles>([]) const fileList = ref<UploadFiles>([])
interface UploadData { interface UploadData {
type: UploadType type: UploadType
accountId: number | undefined accountId: number
} }
const uploadData: UploadData = reactive({ const uploadData: UploadData = reactive({
type: UploadType.Image, type: UploadType.Image,
accountId: accountId accountId: accountId!
}) })
/** 素材选择完成事件*/ /** 素材选择完成事件*/

View File

@ -125,7 +125,7 @@
</el-container> </el-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" name="NewsForm">
import { Editor } from '@/components/Editor' import { Editor } from '@/components/Editor'
import { createEditorConfig } from '../editor-config' import { createEditorConfig } from '../editor-config'
import CoverSelect from './CoverSelect.vue' import CoverSelect from './CoverSelect.vue'

View File

@ -76,30 +76,17 @@ import {
const message = useMessage() // const message = useMessage() //
const accountId = ref<number>(0) const accountId = ref(-1)
provide('accountId', accountId) provide('accountId', accountId)
const loading = ref(true) // const loading = ref(true) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const total = ref(0) // const total = ref(0) //
interface QueryParams {
pageNo: number const queryParams = reactive({
pageSize: number
accountId: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0 accountId: accountId
})
interface UploadData {
type: 'image' | 'video' | 'audio'
accountId: number
}
const uploadData: UploadData = reactive({
type: 'image',
accountId: 0
}) })
// ========== 稿 or ========== // ========== 稿 or ==========
@ -111,7 +98,8 @@ const isSubmitting = ref(false)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
setAccountId(id) accountId.value = id
queryParams.pageNo = 1
getList() getList()
} }
@ -124,12 +112,6 @@ const onBeforeDialogClose = async (onDone: () => {}) => {
} }
// ======================== ======================== // ======================== ========================
/** 设置账号编号 */
const setAccountId = (id: number) => {
queryParams.accountId = id
uploadData.accountId = id
}
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
@ -170,10 +152,10 @@ const onSubmitNewsItem = async () => {
isSubmitting.value = true isSubmitting.value = true
try { try {
if (isCreating.value) { if (isCreating.value) {
await MpDraftApi.createDraft(queryParams.accountId, newsList.value) await MpDraftApi.createDraft(accountId.value, newsList.value)
message.notifySuccess('新增成功') message.notifySuccess('新增成功')
} else { } else {
await MpDraftApi.updateDraft(queryParams.accountId, mediaId.value, newsList.value) await MpDraftApi.updateDraft(accountId.value, mediaId.value, newsList.value)
message.notifySuccess('更新成功') message.notifySuccess('更新成功')
} }
} finally { } finally {
@ -185,7 +167,6 @@ const onSubmitNewsItem = async () => {
// ======================== 稿 ======================== // ======================== 稿 ========================
const onPublish = async (item: Article) => { const onPublish = async (item: Article) => {
const accountId = queryParams.accountId
const mediaId = item.mediaId const mediaId = item.mediaId
const content = const content =
'你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。' + '你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。' +
@ -193,7 +174,7 @@ const onPublish = async (item: Article) => {
'发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。' '发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。'
try { try {
await message.confirm(content) await message.confirm(content)
await MpFreePublishApi.submitFreePublish(accountId, mediaId) await MpFreePublishApi.submitFreePublish(accountId.value, mediaId)
message.notifySuccess('发布成功') message.notifySuccess('发布成功')
await getList() await getList()
} catch {} } catch {}
@ -201,11 +182,10 @@ const onPublish = async (item: Article) => {
/** 删除按钮操作 */ /** 删除按钮操作 */
const onDelete = async (item: Article) => { const onDelete = async (item: Article) => {
const accountId = queryParams.accountId
const mediaId = item.mediaId const mediaId = item.mediaId
try { try {
await message.confirm('此操作将永久删除该草稿, 是否继续?') await message.confirm('此操作将永久删除该草稿, 是否继续?')
await MpDraftApi.deleteDraft(accountId, mediaId) await MpDraftApi.deleteDraft(accountId.value, mediaId)
message.notifySuccess('删除成功') message.notifySuccess('删除成功')
await getList() await getList()
} catch {} } catch {}

View File

@ -59,20 +59,16 @@ const loading = ref(true) // 列表的加载中
const total = ref(0) // const total = ref(0) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
accountId: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0 accountId: -1
}) })
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
queryParams.pageNo = 1
getList() getList()
} }

View File

@ -100,16 +100,10 @@ const loading = ref(false) // 遮罩层
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const total = ref(0) // const total = ref(0) //
// //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
accountId: number
permanent: boolean
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0, accountId: -1,
permanent: true permanent: true
}) })
const showCreateVideo = ref(false) // const showCreateVideo = ref(false) //
@ -117,6 +111,7 @@ const showCreateVideo = ref(false) // 是否新建视频的弹窗
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
queryParams.pageNo = 1
getList() getList()
} }

View File

@ -4,7 +4,7 @@
item-key="id" item-key="id"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
:animation="400" :animation="400"
@end="onDragEnd" @end="onParentDragEnd"
> >
<template #item="{ element: parent, index: x }"> <template #item="{ element: parent, index: x }">
<div class="menu_bottom"> <div class="menu_bottom">
@ -23,6 +23,7 @@
item-key="id" item-key="id"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
:animation="400" :animation="400"
@end="onChildDragEnd"
> >
<template #item="{ element: child, index: y }"> <template #item="{ element: child, index: y }">
<div class="subtitle menu_bottom"> <div class="subtitle menu_bottom">
@ -118,42 +119,49 @@ const subMenuClicked = (child: Menu, x: number, y: number) => {
} }
/** /**
* 处理一级菜单展开后被拖动 * 处理一级菜单展开后被拖动激活(展开)原来活动的一级菜单
* *
* @param oldIndex: 一级菜单拖动前的位置 * @param oldIndex: 一级菜单拖动前的位置
* @param newIndex: 一级菜单拖动后的位置 * @param newIndex: 一级菜单拖动后的位置
*/ */
const onDragEnd = ({ oldIndex, newIndex }) => { const onParentDragEnd = ({ oldIndex, newIndex }) => {
// //
if (props.activeIndex === '__MENU_NOT_SELECTED__') { if (props.activeIndex === '__MENU_NOT_SELECTED__') {
return return
} }
let newParent = props.parentIndex // 使`newParent`
if (props.parentIndex === oldIndex) { let positions = new Array<boolean>(menuList.value.length).fill(false)
newParent = newIndex positions[props.parentIndex] = true
} else if (props.parentIndex === newIndex) { const [out] = positions.splice(oldIndex, 1) // out
newParent = oldIndex positions.splice(newIndex, 0, out) // out
} else { const newParentIndex = positions.indexOf(true)
// `props.parentIndex`
// 使`newParent`
let positions = new Array<boolean>(menuList.value.length).fill(false)
positions[props.parentIndex] = true
positions.splice(oldIndex, 1)
positions.splice(newIndex, 0, true)
newParent = positions.indexOf(true)
}
// //
const parent = menuList.value[newParent] const parent = menuList.value[newParentIndex]
emit('menu-clicked', parent, newParent) emit('menu-clicked', parent, newParentIndex)
}
/**
* 处理二级菜单展开后被拖动激活被拖动的菜单
*
* @param newIndex 二级菜单拖动后的位置
*/
const onChildDragEnd = ({ newIndex }) => {
const x = props.parentIndex
const y = newIndex
const children = menuList.value[x]?.children
if (children && children?.length > 0) {
const child = children[y]
emit('submenu-clicked', child, x, y)
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.menu_bottom { .menu_bottom {
position: relative; position: relative;
display: inline-block; display: block;
float: left; float: left;
width: 85.5px; width: 85.5px;
text-align: center; text-align: center;

View File

@ -65,7 +65,7 @@ const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
// ======================== ======================== // ======================== ========================
const loading = ref(false) // const loading = ref(false) //
const accountId = ref<number>(0) const accountId = ref(-1)
const accountName = ref<string>('') const accountName = ref<string>('')
const menuList = ref<Menu[]>([]) const menuList = ref<Menu[]>([])
@ -339,7 +339,7 @@ div {
.left { .left {
position: relative; position: relative;
display: inline-block; display: block;
float: left; float: left;
width: 350px; width: 350px;
height: 715px; height: 715px;

View File

@ -93,20 +93,12 @@ const total = ref(0) // 数据的总页数
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
// //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
openid: string | undefined
accountId: number
type: MsgType | undefined
createTime: string[] | []
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
openid: undefined, openid: '',
accountId: 0, accountId: -1,
type: undefined, type: MsgType.Text,
createTime: [] createTime: []
}) })
const queryFormRef = ref<FormInstance | null>(null) // const queryFormRef = ref<FormInstance | null>(null) //
@ -120,6 +112,7 @@ const messageBox = reactive({
/** 侦听accountId */ /** 侦听accountId */
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
queryParams.pageNo = 1
handleQuery() handleQuery()
} }

View File

@ -84,7 +84,7 @@ const dateRange = ref([
beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)), beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)),
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)) endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
]) ])
const accountId = ref() // const accountId = ref(-1) //
const accountList = ref<MpAccountApi.AccountVO[]>([]) // const accountList = ref<MpAccountApi.AccountVO[]>([]) //
const xAxisDate = ref([] as any[]) // X const xAxisDate = ref([] as any[]) // X
@ -232,7 +232,7 @@ const getAccountList = async () => {
accountList.value = await MpAccountApi.getSimpleAccountList() accountList.value = await MpAccountApi.getSimpleAccountList()
// //
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
accountId.value = accountList.value[0].id accountId.value = accountList.value[0].id!
} }
} }

View File

@ -95,23 +95,18 @@ const loading = ref(true) // 列表的加载中
const total = ref(0) // const total = ref(0) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
accountId: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0 accountId: -1
}) })
const formRef = ref<InstanceType<typeof TagForm> | null>(null) const formRef = ref<InstanceType<typeof TagForm> | null>(null)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.pageNo = 1
queryParams.accountId = id queryParams.accountId = id
queryParams.pageNo = 1
getList() getList()
} }

View File

@ -113,27 +113,20 @@ const loading = ref(true) // 列表的加载中
const total = ref(0) // const total = ref(0) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
interface QueryParams { const queryParams = reactive({
pageNo: number
pageSize: number
accountId: number
openid: string | null
nickname: string | null
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: 0, accountId: -1,
openid: null, openid: '',
nickname: null nickname: ''
}) })
const queryFormRef = ref<FormInstance | null>(null) // const queryFormRef = ref<FormInstance | null>(null) //
const tagList = ref<any[]>([]) // const tagList = ref<any[]>([]) //
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number) => { const onAccountChanged = (id: number) => {
queryParams.pageNo = 1
queryParams.accountId = id queryParams.accountId = id
queryParams.pageNo = 1
getList() getList()
} }

View File

@ -2,36 +2,36 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<el-form <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px" label-width="68px"
> >
<el-form-item label="字典名称" prop="name"> <el-form-item label="字典名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入字典名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
clearable
placeholder="请输入字典名称"
@keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="字典类型" prop="type"> <el-form-item label="字典类型" prop="type">
<el-input <el-input
v-model="queryParams.type" v-model="queryParams.type"
placeholder="请输入字典类型"
clearable
@keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
clearable
placeholder="请输入字典类型"
@keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select <el-select
v-model="queryParams.status" v-model="queryParams.status"
placeholder="请选择字典状态"
clearable
class="!w-240px" class="!w-240px"
clearable
placeholder="请选择字典状态"
> >
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@ -44,33 +44,41 @@
<el-form-item label="创建时间" prop="createTime"> <el-form-item label="创建时间" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="yyyy-MM-dd HH:mm:ss"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon class="mr-5px" icon="ep:search" />
<el-button 搜索
type="primary" </el-button>
plain <el-button @click="resetQuery">
@click="openForm('create')" <Icon class="mr-5px" icon="ep:refresh" />
v-hasPermi="['system:dict:create']" 重置
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button <el-button
type="success" v-hasPermi="['system:dict:create']"
plain plain
@click="handleExport" type="primary"
:loading="exportLoading" @click="openForm('create')"
v-hasPermi="['system:dict:export']"
> >
<Icon icon="ep:download" class="mr-5px" /> 导出 <Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
v-hasPermi="['system:dict:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
>
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -79,29 +87,29 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="字典编号" align="center" prop="id" /> <el-table-column align="center" label="字典编号" prop="id" />
<el-table-column label="字典名称" align="center" prop="name" show-overflow-tooltip /> <el-table-column align="center" label="字典名称" prop="name" show-overflow-tooltip />
<el-table-column label="字典类型" align="center" prop="type" width="300" /> <el-table-column align="center" label="字典类型" prop="type" width="300" />
<el-table-column label="状态" align="center" prop="status"> <el-table-column align="center" label="状态" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column align="center" label="备注" prop="remark" />
<el-table-column <el-table-column
label="创建时间"
:formatter="dateFormatter" :formatter="dateFormatter"
align="center" align="center"
label="创建时间"
prop="createTime" prop="createTime"
width="180" width="180"
/> />
<el-table-column label="操作" align="center"> <el-table-column align="center" label="操作">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-hasPermi="['system:dict:update']"
link link
type="primary" type="primary"
@click="openForm('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:dict:update']"
> >
修改 修改
</el-button> </el-button>
@ -109,10 +117,10 @@
<el-button link type="primary">数据</el-button> <el-button link type="primary">数据</el-button>
</router-link> </router-link>
<el-button <el-button
v-hasPermi="['system:dict:delete']"
link link
type="danger" type="danger"
@click="handleDelete(scope.row.id)" @click="handleDelete(scope.row.id)"
v-hasPermi="['system:dict:delete']"
> >
删除 删除
</el-button> </el-button>
@ -121,9 +129,9 @@
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize" v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList" @pagination="getList"
/> />
</ContentWrap> </ContentWrap>
@ -132,12 +140,13 @@
<DictTypeForm ref="formRef" @success="getList" /> <DictTypeForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="SystemDictType"> <script lang="ts" name="SystemDictType" setup>
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as DictTypeApi from '@/api/system/dict/dict.type' import * as DictTypeApi from '@/api/system/dict/dict.type'
import DictTypeForm from './DictTypeForm.vue' import DictTypeForm from './DictTypeForm.vue'
import download from '@/utils/download' import download from '@/utils/download'
const message = useMessage() // const message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //