commit
47f20e6d4c
@ -17,17 +17,17 @@ export interface CategoryVO {
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* 分类图片
|
||||
* 移动端分类图
|
||||
*/
|
||||
picUrl: string
|
||||
/**
|
||||
* PC 端分类图
|
||||
*/
|
||||
bigPicUrl?: string
|
||||
/**
|
||||
* 分类排序
|
||||
*/
|
||||
sort?: number
|
||||
/**
|
||||
* 分类描述
|
||||
*/
|
||||
description?: string
|
||||
sort: number
|
||||
/**
|
||||
* 开启状态
|
||||
*/
|
||||
|
39
src/api/mall/product/management/spu.ts
Normal file
39
src/api/mall/product/management/spu.ts
Normal 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}` })
|
||||
}
|
79
src/api/mall/product/management/type/skuType.ts
Normal file
79
src/api/mall/product/management/type/skuType.ts
Normal 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
|
||||
}
|
25
src/api/mall/product/management/type/spuType.ts
Normal file
25
src/api/mall/product/management/type/spuType.ts
Normal 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 // 是否优品
|
||||
}
|
@ -71,8 +71,8 @@ export const getPropertyList = (params: any) => {
|
||||
}
|
||||
|
||||
// 获得属性项列表
|
||||
export const getPropertyListAndValue = (params: any) => {
|
||||
return request.get({ url: '/product/property/get-value-list', params })
|
||||
export const getPropertyListAndValue = (data: any) => {
|
||||
return request.post({ url: '/product/property/get-value-list', data })
|
||||
}
|
||||
|
||||
// ------------------------ 属性值 -------------------
|
||||
|
@ -1,7 +1,7 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface AccountVO {
|
||||
id?: number
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Slots } from 'vue'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import { PlaceholderMoel } from './types'
|
||||
import { PlaceholderModel } from './types'
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { ColProps } from '@/types/components'
|
||||
|
||||
@ -10,7 +10,7 @@ import { ColProps } from '@/types/components'
|
||||
* @returns 返回提示信息对象
|
||||
* @description 用于自动设置placeholder
|
||||
*/
|
||||
export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
|
||||
export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
|
||||
const { t } = useI18n()
|
||||
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
|
||||
const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
|
||||
@ -108,8 +108,8 @@ export const setItemComponentSlots = (
|
||||
/**
|
||||
*
|
||||
* @param schema Form表单结构化数组
|
||||
* @param formModel FormMoel
|
||||
* @returns FormMoel
|
||||
* @param formModel FormModel
|
||||
* @returns FormModel
|
||||
* @description 生成对应的formModel
|
||||
*/
|
||||
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
|
||||
export interface PlaceholderMoel {
|
||||
export interface PlaceholderModel {
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
|
@ -2,9 +2,9 @@ import { Layout } from '@/utils/routerHelper'
|
||||
|
||||
const { t } = useI18n()
|
||||
/**
|
||||
* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击
|
||||
* name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
|
||||
* meta : {
|
||||
* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击
|
||||
* name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
|
||||
* meta : {
|
||||
hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false)
|
||||
|
||||
alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式,
|
||||
@ -31,7 +31,7 @@ const { t } = useI18n()
|
||||
|
||||
canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
|
||||
}
|
||||
**/
|
||||
**/
|
||||
const remainingRouter: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/redirect',
|
||||
@ -345,6 +345,29 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -10,6 +10,12 @@
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// 解决表格内容超过表格总宽度后,横向滚动条前端顶不到表格边缘的问题
|
||||
.el-scrollbar__bar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* nprogress 适配 element-plus 的主题色 */
|
||||
#nprogress {
|
||||
& .bar {
|
||||
|
@ -220,3 +220,21 @@ export const PayRefundStatusEnum = {
|
||||
name: '退款关闭'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品SPU枚举类
|
||||
*/
|
||||
export const ProductSpuStatusEnum = {
|
||||
RECYCLE: {
|
||||
status: -1,
|
||||
name: '回收站'
|
||||
},
|
||||
DISABLE: {
|
||||
status: 0,
|
||||
name: '下架'
|
||||
},
|
||||
ENABLE: {
|
||||
status: 1,
|
||||
name: '上架'
|
||||
}
|
||||
}
|
||||
|
@ -144,5 +144,9 @@ export enum DICT_TYPE {
|
||||
|
||||
// ========== MP 模块 ==========
|
||||
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
18
src/utils/object.ts
Normal 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)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<doc-alert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" />
|
||||
<doc-alert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" />
|
||||
|
||||
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
|
||||
<el-row>
|
||||
<!-- 基本信息 -->
|
||||
@ -51,127 +50,224 @@
|
||||
<!-- 命令统计 -->
|
||||
<el-col :span="12" class="mt-3">
|
||||
<el-card :gutter="12" shadow="hover">
|
||||
<div ref="commandStatsRef" class="h-88"></div>
|
||||
<Echart :options="commandStatsRefChika" :height="420" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- 内存使用量统计 -->
|
||||
<el-col :span="12" class="mt-3">
|
||||
<el-card class="ml-3" :gutter="12" shadow="hover">
|
||||
<div ref="usedmemory" class="h-88"></div>
|
||||
<Echart :options="usedmemoryEchartChika" :height="420" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<script setup lang="ts" name="InfraRedis">
|
||||
import * as echarts from 'echarts'
|
||||
<script setup lang="ts">
|
||||
import echarts from '@/plugins/echarts'
|
||||
import { GaugeChart } from 'echarts/charts'
|
||||
import { ToolboxComponent } from 'echarts/components'
|
||||
import * as RedisApi from '@/api/infra/redis'
|
||||
import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
|
||||
|
||||
const cache = ref<RedisMonitorInfoVO>()
|
||||
|
||||
// 基本信息
|
||||
const readRedisInfo = async () => {
|
||||
const data = await RedisApi.getCache()
|
||||
cache.value = data
|
||||
loadEchartOptions(data.commandStats)
|
||||
}
|
||||
// 图表
|
||||
const commandStatsRef = ref<HTMLElement>()
|
||||
const usedmemory = ref<HTMLDivElement>()
|
||||
|
||||
const loadEchartOptions = (stats) => {
|
||||
const commandStats = [] as any[]
|
||||
const nameList = [] as string[]
|
||||
stats.forEach((row) => {
|
||||
commandStats.push({
|
||||
name: row.command,
|
||||
value: row.calls
|
||||
})
|
||||
nameList.push(row.command)
|
||||
})
|
||||
|
||||
const commandStatsInstance = echarts.init(commandStatsRef.value!, 'macarons')
|
||||
|
||||
commandStatsInstance.setOption({
|
||||
title: {
|
||||
text: '命令统计',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 10,
|
||||
bottom: 20,
|
||||
data: nameList,
|
||||
textStyle: {
|
||||
color: '#a1a1a1'
|
||||
// 内存使用情况
|
||||
const usedmemoryEchartChika = reactive({
|
||||
title: {
|
||||
// 仪表盘标题。
|
||||
text: '内存使用情况',
|
||||
left: 'center',
|
||||
show: true, // 是否显示标题,默认 true。
|
||||
offsetCenter: [0, '20%'], //相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
|
||||
color: 'yellow', // 文字的颜色,默认 #333。
|
||||
fontSize: 20 // 文字的字体大小,默认 15。
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
feature: {
|
||||
restore: { show: true },
|
||||
saveAsImage: { show: true }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '峰值',
|
||||
type: 'gauge',
|
||||
min: 0,
|
||||
max: 50,
|
||||
splitNumber: 10,
|
||||
//这是指针的颜色
|
||||
color: '#F5C74E',
|
||||
radius: '85%',
|
||||
center: ['50%', '50%'],
|
||||
startAngle: 225,
|
||||
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: {
|
||||
// 属性lineStyle(详见lineStyle)控制线条样式
|
||||
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],
|
||||
center: ['40%', '60%'],
|
||||
data: commandStats,
|
||||
roseType: 'radius',
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 指令使用情况
|
||||
const commandStatsRefChika = reactive({
|
||||
title: {
|
||||
text: '命令统计',
|
||||
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: {
|
||||
show: true
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
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({
|
||||
title: {
|
||||
text: '内存使用情况',
|
||||
left: 'center'
|
||||
},
|
||||
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: '内存消耗'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
/** 加载数据 */
|
||||
const getSummary = () => {
|
||||
// 初始化命令图表
|
||||
initCommandStatsChart()
|
||||
usedMemoryInstance()
|
||||
}
|
||||
|
||||
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()
|
||||
// 加载数据
|
||||
getSummary()
|
||||
})
|
||||
</script>
|
||||
|
@ -4,27 +4,30 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="上级分类" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="formData.parentId"
|
||||
:data="categoryTree"
|
||||
:props="{ label: 'name', value: 'id' }"
|
||||
:render-after-expand="false"
|
||||
placeholder="请选择上级分类"
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
<el-select v-model="formData.parentId" placeholder="请选择上级分类">
|
||||
<el-option :key="0" label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入分类名称" />
|
||||
</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" />
|
||||
<div v-if="formData.parentId === 0" style="font-size: 10px">推荐 200x100 图片分辨率</div>
|
||||
<div v-else style="font-size: 10px">推荐 100x100 图片分辨率</div>
|
||||
<div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</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 label="分类排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" controls-position="right" :min="0" />
|
||||
@ -40,9 +43,6 @@
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类描述">
|
||||
<el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
@ -53,7 +53,6 @@
|
||||
<script setup lang="ts" name="ProductCategory">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { handleTree } from '@/utils/tree'
|
||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
@ -66,8 +65,8 @@ const formData = ref({
|
||||
id: undefined,
|
||||
name: '',
|
||||
picUrl: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: ''
|
||||
bigPicUrl: '',
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
const formRules = reactive({
|
||||
parentId: [{ required: true, message: '请选择上级分类', trigger: 'blur' }],
|
||||
@ -77,7 +76,7 @@ const formRules = reactive({
|
||||
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const categoryTree = ref<any[]>([]) // 分类树
|
||||
const categoryList = ref<any[]>([]) // 分类树
|
||||
|
||||
/** 打开弹窗 */
|
||||
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 方法,用于打开弹窗
|
||||
|
||||
@ -131,17 +130,9 @@ const resetForm = () => {
|
||||
id: undefined,
|
||||
name: '',
|
||||
picUrl: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: ''
|
||||
bigPicUrl: '',
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
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>
|
||||
|
@ -36,9 +36,9 @@
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
|
||||
<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">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类排序" align="center" prop="sort" />
|
||||
|
@ -2,42 +2,49 @@
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
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')]"
|
||||
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 icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<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:property:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@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-form-item>
|
||||
</el-form>
|
||||
@ -46,23 +53,23 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="名称" align="center" />
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column align="center" label="编号" prop="id" />
|
||||
<el-table-column align="center" label="名称" prop="name" />
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['product:property:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['product:property:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
@ -70,10 +77,10 @@
|
||||
<router-link :to="'/property/value/' + scope.row.id">属性值</router-link>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:property:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['product:property:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
@ -82,9 +89,9 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
@ -92,10 +99,11 @@
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<PropertyForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
<script setup lang="ts" name="ProductProperty">
|
||||
<script lang="ts" name="ProductProperty" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
import PropertyForm from './PropertyForm.vue'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
|
240
src/views/mall/product/spu/addForm.vue
Normal file
240
src/views/mall/product/spu/addForm.vue
Normal 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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
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
|
||||
// 多规格情况移除skus相关属性值value
|
||||
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>
|
238
src/views/mall/product/spu/components/BasicInfoForm.vue
Normal file
238
src/views/mall/product/spu/components/BasicInfoForm.vue
Normal 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 @puhui999:attributeList 改成 propertyList,会更统一一点
|
||||
const attributeList = ref([]) // 商品属性列表
|
||||
/** 添加商品属性 */ // TODO @puhui999:propFormData 算出来
|
||||
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>
|
84
src/views/mall/product/spu/components/DescriptionForm.vue
Normal file
84
src/views/mall/product/spu/components/DescriptionForm.vue
Normal 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>
|
156
src/views/mall/product/spu/components/OtherSettingsForm.vue
Normal file
156
src/views/mall/product/spu/components/OtherSettingsForm.vue
Normal 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>
|
102
src/views/mall/product/spu/components/ProductAttributes.vue
Normal file
102
src/views/mall/product/spu/components/ProductAttributes.vue
Normal 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
|
||||
// 因为组件在ref中所以需要用索引获取对应的Ref
|
||||
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>
|
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
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>
|
309
src/views/mall/product/spu/components/SkuList.vue
Normal file
309
src/views/mall/product/spu/components/SkuList.vue
Normal 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) => {
|
||||
// name加属性项index区分属性值
|
||||
tableHeaderList.value.push({ prop: `name${index}`, label: item.name })
|
||||
})
|
||||
generateTableData(data)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
15
src/views/mall/product/spu/components/index.ts
Normal file
15
src/views/mall/product/spu/components/index.ts
Normal 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
|
||||
}
|
388
src/views/mall/product/spu/index.vue
Normal file
388
src/views/mall/product/spu/index.vue
Normal file
@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<!-- TODO @puhui999:https://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 @puhui999:managementApi=》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>
|
78
src/views/mp/autoReply/components/ReplyForm.vue
Normal file
78
src/views/mp/autoReply/components/ReplyForm.vue
Normal 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>
|
@ -53,38 +53,13 @@
|
||||
@on-delete="onDelete"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改自动回复的对话框 -->
|
||||
<!-- TODO @Dhb52 -->
|
||||
<el-dialog :title="dialogTitle" v-model="showFormDialog" width="800px" destroy-on-close>
|
||||
<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>
|
||||
<el-dialog
|
||||
:title="isCreating ? '新增自动回复' : '修改自动回复'"
|
||||
v-model="showDialog"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ReplyForm v-model="replyForm" v-model:reply="reply" :msg-type="msgType" ref="formRef" />
|
||||
<template #footer>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确 定</el-button>
|
||||
@ -93,52 +68,43 @@
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<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 * as MpAutoReplyApi from '@/api/mp/autoReply'
|
||||
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
|
||||
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 { MsgType } from './components/types'
|
||||
const message = useMessage() // 消息
|
||||
|
||||
const accountId = ref(-1) // 公众号ID
|
||||
const msgType = ref<MsgType>(MsgType.Keyword) // 消息类型
|
||||
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型
|
||||
const loading = ref(true) // 遮罩层
|
||||
const total = ref(0) // 总条数
|
||||
const list = ref<any[]>([]) // 自动回复列表
|
||||
const formRef = ref<FormInstance | null>(null) // 表单 ref
|
||||
const formRef = ref<InstanceType<typeof ReplyForm> | null>(null) // 表单 ref
|
||||
// 查询参数
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0
|
||||
accountId: accountId
|
||||
})
|
||||
|
||||
const dialogTitle = ref('') // 弹出层标题
|
||||
const showFormDialog = ref(false) // 是否显示弹出层
|
||||
const isCreating = ref(false) // 是否新建(否则编辑)
|
||||
const showDialog = ref(false) // 是否显示弹出层
|
||||
const replyForm = ref<any>({}) // 表单参数
|
||||
// 回复消息
|
||||
const reply = ref<Reply>({
|
||||
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) => {
|
||||
queryParams.accountId = id
|
||||
accountId.value = id
|
||||
reply.value.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
@ -177,8 +143,8 @@ const onCreate = () => {
|
||||
accountId: queryParams.accountId
|
||||
}
|
||||
|
||||
dialogTitle.value = '新增自动回复'
|
||||
showFormDialog.value = true
|
||||
isCreating.value = true
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
@ -210,8 +176,8 @@ const onUpdate = async (id: number) => {
|
||||
}
|
||||
|
||||
// 打开表单
|
||||
dialogTitle.value = '修改自动回复'
|
||||
showFormDialog.value = true
|
||||
isCreating.value = false
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
@ -223,8 +189,7 @@ const onDelete = async (id: number) => {
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
await formRef.value?.validate()
|
||||
|
||||
// 处理回复消息
|
||||
const submitForm: any = { ...replyForm.value }
|
||||
@ -248,7 +213,7 @@ const onSubmit = async () => {
|
||||
message.success('新增成功')
|
||||
}
|
||||
|
||||
showFormDialog.value = false
|
||||
showDialog.value = false
|
||||
await getList()
|
||||
}
|
||||
|
||||
@ -267,7 +232,7 @@ const reset = () => {
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
showFormDialog.value = false
|
||||
showDialog.value = false
|
||||
reset()
|
||||
}
|
||||
</script>
|
||||
|
@ -8,13 +8,14 @@
|
||||
import * as MpAccountApi from '@/api/mp/account'
|
||||
|
||||
const account: MpAccountApi.AccountVO = reactive({
|
||||
id: undefined,
|
||||
id: -1,
|
||||
name: ''
|
||||
})
|
||||
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
|
||||
|
||||
const accountList = ref<MpAccountApi.AccountVO[]>([])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', id: number, name: string): void
|
||||
(e: 'change', id: number, name: string)
|
||||
}>()
|
||||
|
||||
const handleQuery = async () => {
|
||||
|
67
src/views/mp/components/wx-msg/components/Msg.vue
Normal file
67
src/views/mp/components/wx-msg/components/Msg.vue
Normal 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>
|
49
src/views/mp/components/wx-msg/components/MsgEvent.vue
Normal file
49
src/views/mp/components/wx-msg/components/MsgEvent.vue
Normal 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>
|
60
src/views/mp/components/wx-msg/components/MsgList.vue
Normal file
60
src/views/mp/components/wx-msg/components/MsgList.vue
Normal 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>
|
@ -7,123 +7,22 @@
|
||||
-->
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<div class="msg-div" :id="'msg-div' + nowStr">
|
||||
<div class="msg-div" ref="msgDivRef">
|
||||
<!-- 加载更多 -->
|
||||
<div v-loading="loading"></div>
|
||||
<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
|
||||
>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="execution" v-for="item in list" :key="item.id">
|
||||
<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>
|
||||
<MsgList :list="list" :account-id="accountId" :user="user" />
|
||||
</div>
|
||||
|
||||
<div class="msg-send" v-loading="sendLoading">
|
||||
<WxReplySelect ref="replySelectRef" v-model="reply" />
|
||||
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
|
||||
@ -132,18 +31,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="WxMsg">
|
||||
import WxReplySelect from '@/views/mp/components/wx-reply'
|
||||
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 WxReplySelect, { Reply, ReplyType } from '@/views/mp/components/wx-reply'
|
||||
import MsgList from './components/MsgList.vue'
|
||||
import { getMessagePage, sendMessage } from '@/api/mp/message'
|
||||
import { getUser } from '@/api/mp/user'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import profile from '@/assets/imgs/profile.jpg'
|
||||
import wechat from '@/assets/imgs/wechat.png'
|
||||
import { MsgType } from './types'
|
||||
import { User } from './types'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
@ -154,61 +47,41 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
|
||||
const accountId = ref(-1) // 公众号ID,需要通过userId初始化
|
||||
const loading = ref(false) // 消息列表是否正在加载中
|
||||
const loadMore = ref(true) // 是否可以加载更多
|
||||
const hasMore = ref(true) // 是否可以加载更多
|
||||
const list = ref<any[]>([]) // 消息列表
|
||||
const queryParams = reactive({
|
||||
pageNo: 1, // 当前页数
|
||||
pageSize: 14, // 每页显示多少条
|
||||
accountId: undefined
|
||||
accountId: accountId
|
||||
})
|
||||
|
||||
interface User {
|
||||
nickname: string
|
||||
avatar: string
|
||||
accountId: number
|
||||
}
|
||||
// 由于微信不再提供昵称,直接使用“用户”展示
|
||||
const user: User = reactive({
|
||||
nickname: '用户',
|
||||
avatar: profile,
|
||||
accountId: 0 // 公众号账号编号
|
||||
})
|
||||
|
||||
interface Mp {
|
||||
nickname: string
|
||||
avatar: string
|
||||
}
|
||||
const mp: Mp = reactive({
|
||||
nickname: '公众号',
|
||||
avatar: wechat
|
||||
accountId: accountId // 公众号账号编号
|
||||
})
|
||||
|
||||
// ========= 消息发送 =========
|
||||
const sendLoading = ref(false) // 发送消息是否加载中
|
||||
interface Reply {
|
||||
type: MsgType
|
||||
accountId: number | null
|
||||
articles: any[]
|
||||
}
|
||||
|
||||
// 微信发送消息
|
||||
const reply = ref<Reply>({
|
||||
type: MsgType.Text,
|
||||
accountId: null,
|
||||
type: ReplyType.Text,
|
||||
accountId: -1,
|
||||
articles: []
|
||||
})
|
||||
|
||||
const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null)
|
||||
const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelect组件ref,用于消息发送成功后清除内容
|
||||
const msgDivRef = ref<HTMLDivElement | null>(null) // 消息显示窗口ref,用于滚动到底部
|
||||
|
||||
/** 完成加载 */
|
||||
onMounted(async () => {
|
||||
const data = await getUser(props.userId)
|
||||
user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
|
||||
user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
|
||||
user.accountId = data.accountId
|
||||
queryParams.accountId = data.accountId
|
||||
accountId.value = data.accountId
|
||||
reply.value.accountId = data.accountId
|
||||
|
||||
refreshChange()
|
||||
@ -216,11 +89,15 @@ onMounted(async () => {
|
||||
|
||||
// 执行发送
|
||||
const sendMsg = async () => {
|
||||
if (!reply) {
|
||||
if (!unref(reply)) {
|
||||
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]]
|
||||
message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
|
||||
}
|
||||
@ -229,18 +106,18 @@ const sendMsg = async () => {
|
||||
sendLoading.value = false
|
||||
|
||||
list.value = [...list.value, ...[data]]
|
||||
scrollToBottom()
|
||||
await scrollToBottom()
|
||||
|
||||
// 发送后清空数据
|
||||
replySelectRef.value?.clear()
|
||||
}
|
||||
|
||||
const loadingMore = () => {
|
||||
const loadMore = () => {
|
||||
queryParams.pageNo++
|
||||
getPage(queryParams, null)
|
||||
}
|
||||
|
||||
const getPage = async (page, params) => {
|
||||
const getPage = async (page: any, params: any = null) => {
|
||||
loading.value = true
|
||||
let dataTemp = await getMessagePage(
|
||||
Object.assign(
|
||||
@ -254,62 +131,45 @@ const getPage = async (page, params) => {
|
||||
)
|
||||
)
|
||||
|
||||
const msgDiv = document.getElementById('msg-div' + nowStr.value)
|
||||
let scrollHeight = 0
|
||||
if (msgDiv) {
|
||||
scrollHeight = msgDiv.scrollHeight
|
||||
}
|
||||
const scrollHeight = msgDivRef.value?.scrollHeight ?? 0
|
||||
// 处理数据
|
||||
const data = dataTemp.list.reverse()
|
||||
list.value = [...data, ...list.value]
|
||||
loading.value = false
|
||||
if (data.length < queryParams.pageSize || data.length === 0) {
|
||||
loadMore.value = false
|
||||
hasMore.value = false
|
||||
}
|
||||
queryParams.pageNo = page.pageNo
|
||||
queryParams.pageSize = page.pageSize
|
||||
// 滚动到原来的位置
|
||||
if (queryParams.pageNo === 1) {
|
||||
// 定位到消息底部
|
||||
scrollToBottom()
|
||||
await scrollToBottom()
|
||||
} else if (data.length !== 0) {
|
||||
// 定位滚动条
|
||||
await nextTick(() => {
|
||||
if (scrollHeight !== 0) {
|
||||
let div = document.getElementById('msg-div' + nowStr.value)
|
||||
if (div && msgDiv) {
|
||||
msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
|
||||
}
|
||||
await nextTick()
|
||||
if (scrollHeight !== 0) {
|
||||
if (msgDivRef.value) {
|
||||
msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight - scrollHeight - 100
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshChange = () => {
|
||||
getPage(queryParams, null)
|
||||
getPage(queryParams)
|
||||
}
|
||||
|
||||
/** 定位到消息底部 */
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
let div = document.getElementById('msg-div' + nowStr.value)
|
||||
if (div) {
|
||||
div.scrollTop = div.scrollHeight
|
||||
}
|
||||
})
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
if (msgDivRef.value) {
|
||||
msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
height: 50vh;
|
||||
overflow: auto;
|
||||
@ -322,11 +182,6 @@ const scrollToBottom = () => {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.avatar-div {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.send-but {
|
||||
float: right;
|
||||
margin-top: 8px;
|
||||
|
@ -9,3 +9,9 @@ export enum MsgType {
|
||||
Music = 'music',
|
||||
News = 'news'
|
||||
}
|
||||
|
||||
export interface User {
|
||||
nickname: string
|
||||
avatar: string
|
||||
accountId: number
|
||||
}
|
||||
|
@ -55,6 +55,6 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */
|
||||
@import url('../wx-msg/card.scss');
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scss */
|
||||
@import '../wx-msg/card.scss';
|
||||
</style>
|
||||
|
@ -51,7 +51,7 @@
|
||||
>
|
||||
<WxMaterialSelect
|
||||
type="image"
|
||||
:account-id="accountId"
|
||||
:account-id="accountId!"
|
||||
@select-material="onMaterialSelected"
|
||||
/>
|
||||
</el-dialog>
|
||||
@ -93,11 +93,11 @@ const showImageDialog = ref(false)
|
||||
const fileList = ref<UploadFiles>([])
|
||||
interface UploadData {
|
||||
type: UploadType
|
||||
accountId: number | undefined
|
||||
accountId: number
|
||||
}
|
||||
const uploadData: UploadData = reactive({
|
||||
type: UploadType.Image,
|
||||
accountId: accountId
|
||||
accountId: accountId!
|
||||
})
|
||||
|
||||
/** 素材选择完成事件*/
|
||||
|
@ -125,7 +125,7 @@
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" name="NewsForm">
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { createEditorConfig } from '../editor-config'
|
||||
import CoverSelect from './CoverSelect.vue'
|
||||
|
@ -76,30 +76,17 @@ import {
|
||||
|
||||
const message = useMessage() // 消息
|
||||
|
||||
const accountId = ref<number>(0)
|
||||
const accountId = ref(-1)
|
||||
provide('accountId', accountId)
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0
|
||||
})
|
||||
|
||||
interface UploadData {
|
||||
type: 'image' | 'video' | 'audio'
|
||||
accountId: number
|
||||
}
|
||||
const uploadData: UploadData = reactive({
|
||||
type: 'image',
|
||||
accountId: 0
|
||||
accountId: accountId
|
||||
})
|
||||
|
||||
// ========== 草稿新建 or 修改 ==========
|
||||
@ -111,7 +98,8 @@ const isSubmitting = ref(false)
|
||||
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
setAccountId(id)
|
||||
accountId.value = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
@ -124,12 +112,6 @@ const onBeforeDialogClose = async (onDone: () => {}) => {
|
||||
}
|
||||
|
||||
// ======================== 列表查询 ========================
|
||||
/** 设置账号编号 */
|
||||
const setAccountId = (id: number) => {
|
||||
queryParams.accountId = id
|
||||
uploadData.accountId = id
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
@ -170,10 +152,10 @@ const onSubmitNewsItem = async () => {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
if (isCreating.value) {
|
||||
await MpDraftApi.createDraft(queryParams.accountId, newsList.value)
|
||||
await MpDraftApi.createDraft(accountId.value, newsList.value)
|
||||
message.notifySuccess('新增成功')
|
||||
} else {
|
||||
await MpDraftApi.updateDraft(queryParams.accountId, mediaId.value, newsList.value)
|
||||
await MpDraftApi.updateDraft(accountId.value, mediaId.value, newsList.value)
|
||||
message.notifySuccess('更新成功')
|
||||
}
|
||||
} finally {
|
||||
@ -185,7 +167,6 @@ const onSubmitNewsItem = async () => {
|
||||
|
||||
// ======================== 草稿箱发布 ========================
|
||||
const onPublish = async (item: Article) => {
|
||||
const accountId = queryParams.accountId
|
||||
const mediaId = item.mediaId
|
||||
const content =
|
||||
'你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。' +
|
||||
@ -193,7 +174,7 @@ const onPublish = async (item: Article) => {
|
||||
'发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。'
|
||||
try {
|
||||
await message.confirm(content)
|
||||
await MpFreePublishApi.submitFreePublish(accountId, mediaId)
|
||||
await MpFreePublishApi.submitFreePublish(accountId.value, mediaId)
|
||||
message.notifySuccess('发布成功')
|
||||
await getList()
|
||||
} catch {}
|
||||
@ -201,11 +182,10 @@ const onPublish = async (item: Article) => {
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const onDelete = async (item: Article) => {
|
||||
const accountId = queryParams.accountId
|
||||
const mediaId = item.mediaId
|
||||
try {
|
||||
await message.confirm('此操作将永久删除该草稿, 是否继续?')
|
||||
await MpDraftApi.deleteDraft(accountId, mediaId)
|
||||
await MpDraftApi.deleteDraft(accountId.value, mediaId)
|
||||
message.notifySuccess('删除成功')
|
||||
await getList()
|
||||
} catch {}
|
||||
|
@ -59,20 +59,16 @@ const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0
|
||||
accountId: -1
|
||||
})
|
||||
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
|
@ -100,16 +100,10 @@ const loading = ref(false) // 遮罩层
|
||||
const list = ref<any[]>([]) // 总条数
|
||||
const total = ref(0) // 数据列表
|
||||
// 查询参数
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
permanent: boolean
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0,
|
||||
accountId: -1,
|
||||
permanent: true
|
||||
})
|
||||
const showCreateVideo = ref(false) // 是否新建视频的弹窗
|
||||
@ -117,6 +111,7 @@ const showCreateVideo = ref(false) // 是否新建视频的弹窗
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
item-key="id"
|
||||
ghost-class="draggable-ghost"
|
||||
:animation="400"
|
||||
@end="onDragEnd"
|
||||
@end="onParentDragEnd"
|
||||
>
|
||||
<template #item="{ element: parent, index: x }">
|
||||
<div class="menu_bottom">
|
||||
@ -23,6 +23,7 @@
|
||||
item-key="id"
|
||||
ghost-class="draggable-ghost"
|
||||
:animation="400"
|
||||
@end="onChildDragEnd"
|
||||
>
|
||||
<template #item="{ element: child, index: y }">
|
||||
<div class="subtitle menu_bottom">
|
||||
@ -118,42 +119,49 @@ const subMenuClicked = (child: Menu, x: number, y: number) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一级菜单展开后被拖动
|
||||
* 处理一级菜单展开后被拖动,激活(展开)原来活动的一级菜单
|
||||
*
|
||||
* @param oldIndex: 一级菜单拖动前的位置
|
||||
* @param newIndex: 一级菜单拖动后的位置
|
||||
*/
|
||||
const onDragEnd = ({ oldIndex, newIndex }) => {
|
||||
const onParentDragEnd = ({ oldIndex, newIndex }) => {
|
||||
// 二级菜单没有展开,直接返回
|
||||
if (props.activeIndex === '__MENU_NOT_SELECTED__') {
|
||||
return
|
||||
}
|
||||
|
||||
let newParent = props.parentIndex
|
||||
if (props.parentIndex === oldIndex) {
|
||||
newParent = newIndex
|
||||
} else if (props.parentIndex === newIndex) {
|
||||
newParent = oldIndex
|
||||
} else {
|
||||
// 如果展开的二级菜单下标`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)
|
||||
}
|
||||
// 使用一个辅助数组来模拟菜单移动,然后找到展开的二级菜单的新下标`newParent`
|
||||
let positions = new Array<boolean>(menuList.value.length).fill(false)
|
||||
positions[props.parentIndex] = true
|
||||
const [out] = positions.splice(oldIndex, 1) // 移出菜单,保存到变量out
|
||||
positions.splice(newIndex, 0, out) // 把out变量插入被移出的菜单
|
||||
const newParentIndex = positions.indexOf(true)
|
||||
|
||||
// 找到菜单元素,触发一级菜单点击
|
||||
const parent = menuList.value[newParent]
|
||||
emit('menu-clicked', parent, newParent)
|
||||
const parent = menuList.value[newParentIndex]
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu_bottom {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
float: left;
|
||||
width: 85.5px;
|
||||
text-align: center;
|
||||
|
@ -65,7 +65,7 @@ const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
|
||||
|
||||
// ======================== 列表查询 ========================
|
||||
const loading = ref(false) // 遮罩层
|
||||
const accountId = ref<number>(0)
|
||||
const accountId = ref(-1)
|
||||
const accountName = ref<string>('')
|
||||
const menuList = ref<Menu[]>([])
|
||||
|
||||
@ -339,7 +339,7 @@ div {
|
||||
|
||||
.left {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
float: left;
|
||||
width: 350px;
|
||||
height: 715px;
|
||||
|
@ -93,20 +93,12 @@ const total = ref(0) // 数据的总页数
|
||||
const list = ref<any[]>([]) // 当前页的列表数据
|
||||
|
||||
// 搜索参数
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
openid: string | undefined
|
||||
accountId: number
|
||||
type: MsgType | undefined
|
||||
createTime: string[] | []
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
openid: undefined,
|
||||
accountId: 0,
|
||||
type: undefined,
|
||||
openid: '',
|
||||
accountId: -1,
|
||||
type: MsgType.Text,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
|
||||
@ -120,6 +112,7 @@ const messageBox = reactive({
|
||||
/** 侦听accountId */
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ const dateRange = ref([
|
||||
beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)),
|
||||
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
|
||||
])
|
||||
const accountId = ref() // 选中的公众号编号
|
||||
const accountId = ref(-1) // 选中的公众号编号
|
||||
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
|
||||
|
||||
const xAxisDate = ref([] as any[]) // X 轴的日期范围
|
||||
@ -232,7 +232,7 @@ const getAccountList = async () => {
|
||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
accountId.value = accountList.value[0].id
|
||||
accountId.value = accountList.value[0].id!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,23 +95,18 @@ const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0
|
||||
accountId: -1
|
||||
})
|
||||
|
||||
const formRef = ref<InstanceType<typeof TagForm> | null>(null)
|
||||
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.pageNo = 1
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
|
@ -113,27 +113,20 @@ const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
|
||||
interface QueryParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
accountId: number
|
||||
openid: string | null
|
||||
nickname: string | null
|
||||
}
|
||||
const queryParams: QueryParams = reactive({
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: 0,
|
||||
openid: null,
|
||||
nickname: null
|
||||
accountId: -1,
|
||||
openid: '',
|
||||
nickname: ''
|
||||
})
|
||||
const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
|
||||
const tagList = ref<any[]>([]) // 公众号标签列表
|
||||
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.pageNo = 1
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
|
@ -2,36 +2,36 @@
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="字典名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入字典名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入字典名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型" prop="type">
|
||||
<el-input
|
||||
v-model="queryParams.type"
|
||||
placeholder="请输入字典类型"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入字典类型"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择字典状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请选择字典状态"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
@ -44,33 +44,41 @@
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
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')]"
|
||||
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 icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['system:dict:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
<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
|
||||
type="success"
|
||||
v-hasPermi="['system:dict:create']"
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['system:dict:export']"
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
@ -79,29 +87,29 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="字典编号" align="center" prop="id" />
|
||||
<el-table-column label="字典名称" align="center" prop="name" show-overflow-tooltip />
|
||||
<el-table-column label="字典类型" align="center" prop="type" width="300" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<el-table-column align="center" label="字典编号" prop="id" />
|
||||
<el-table-column align="center" label="字典名称" prop="name" show-overflow-tooltip />
|
||||
<el-table-column align="center" label="字典类型" prop="type" width="300" />
|
||||
<el-table-column align="center" label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column align="center" label="备注" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['system:dict:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['system:dict:update']"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
@ -109,10 +117,10 @@
|
||||
<el-button link type="primary">数据</el-button>
|
||||
</router-link>
|
||||
<el-button
|
||||
v-hasPermi="['system:dict:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['system:dict:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
@ -121,9 +129,9 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
@ -132,12 +140,13 @@
|
||||
<DictTypeForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="SystemDictType">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
<script lang="ts" name="SystemDictType" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as DictTypeApi from '@/api/system/dict/dict.type'
|
||||
import DictTypeForm from './DictTypeForm.vue'
|
||||
import download from '@/utils/download'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user