Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
commit
28f768f3b2
2
.env.dev
2
.env.dev
|
@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
# 上传路径
|
|
||||||
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
|
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_URL=/admin-api
|
VITE_API_URL=/admin-api
|
||||||
|
|
|
@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
# 上传路径
|
|
||||||
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
|
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_URL=/admin-api
|
VITE_API_URL=/admin-api
|
||||||
|
|
|
@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
# 上传路径
|
|
||||||
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
|
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_URL=/admin-api
|
VITE_API_URL=/admin-api
|
||||||
|
|
|
@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
# 上传路径
|
|
||||||
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
|
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_URL=/admin-api
|
VITE_API_URL=/admin-api
|
||||||
|
|
|
@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
# 上传路径
|
|
||||||
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
|
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_URL=/admin-api
|
VITE_API_URL=/admin-api
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
"source.fixAll.stylelint": "explicit"
|
"source.fixAll.stylelint": "explicit"
|
||||||
},
|
},
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"i18n-ally.localesPaths": ["src/locales"],
|
"i18n-ally.localesPaths": ["src/locales"],
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
|
|
|
@ -130,7 +130,7 @@
|
||||||
"vite-plugin-progress": "^0.0.7",
|
"vite-plugin-progress": "^0.0.7",
|
||||||
"vite-plugin-purge-icons": "^0.10.0",
|
"vite-plugin-purge-icons": "^0.10.0",
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.4.4",
|
||||||
"vue-eslint-parser": "^9.3.2",
|
"vue-eslint-parser": "^9.3.2",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^1.8.27"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// IoT 设备 VO
|
||||||
|
export interface DeviceVO {
|
||||||
|
id: number // 设备 ID,主键,自增
|
||||||
|
deviceKey: string // 设备唯一标识符
|
||||||
|
deviceName: string // 设备名称
|
||||||
|
productId: number // 产品编号
|
||||||
|
productKey: string // 产品标识
|
||||||
|
deviceType: number // 设备类型
|
||||||
|
nickname: string // 设备备注名称
|
||||||
|
gatewayId: number // 网关设备 ID
|
||||||
|
status: number // 设备状态
|
||||||
|
statusLastUpdateTime: Date // 设备状态最后更新时间
|
||||||
|
lastOnlineTime: Date // 最后上线时间
|
||||||
|
lastOfflineTime: Date // 最后离线时间
|
||||||
|
activeTime: Date // 设备激活时间
|
||||||
|
createTime: Date // 创建时间
|
||||||
|
ip: string // 设备的 IP 地址
|
||||||
|
firmwareVersion: string // 设备的固件版本
|
||||||
|
deviceSecret: string // 设备密钥,用于设备认证,需安全存储
|
||||||
|
mqttClientId: string // MQTT 客户端 ID
|
||||||
|
mqttUsername: string // MQTT 用户名
|
||||||
|
mqttPassword: string // MQTT 密码
|
||||||
|
authType: string // 认证类型
|
||||||
|
latitude: number // 设备位置的纬度
|
||||||
|
longitude: number // 设备位置的经度
|
||||||
|
areaId: number // 地区编码
|
||||||
|
address: string // 设备详细地址
|
||||||
|
serialNumber: string // 设备序列号
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceUpdateStatusVO {
|
||||||
|
id: number // 设备 ID,主键,自增
|
||||||
|
status: number // 设备状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备 API
|
||||||
|
export const DeviceApi = {
|
||||||
|
// 查询设备分页
|
||||||
|
getDevicePage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/iot/device/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询设备详情
|
||||||
|
getDevice: async (id: number) => {
|
||||||
|
return await request.get({ url: `/iot/device/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增设备
|
||||||
|
createDevice: async (data: DeviceVO) => {
|
||||||
|
return await request.post({ url: `/iot/device/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改设备
|
||||||
|
updateDevice: async (data: DeviceVO) => {
|
||||||
|
return await request.put({ url: `/iot/device/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改设备状态
|
||||||
|
updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
|
||||||
|
return await request.put({ url: `/iot/device/update-status`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除设备
|
||||||
|
deleteDevice: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/iot/device/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取设备数量
|
||||||
|
getDeviceCount: async (productId: number) => {
|
||||||
|
return await request.get({ url: `/iot/device/count?productId=` + productId })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// IoT 产品 VO
|
||||||
|
export interface ProductVO {
|
||||||
|
id: number // 产品编号
|
||||||
|
name: string // 产品名称
|
||||||
|
productKey: string // 产品标识
|
||||||
|
protocolId: number // 协议编号
|
||||||
|
categoryId: number // 产品所属品类标识符
|
||||||
|
description: string // 产品描述
|
||||||
|
validateType: number // 数据校验级别
|
||||||
|
status: number // 产品状态
|
||||||
|
deviceType: number // 设备类型
|
||||||
|
netType: number // 联网方式
|
||||||
|
protocolType: number // 接入网关协议
|
||||||
|
dataFormat: number // 数据格式
|
||||||
|
deviceCount: number // 设备数量
|
||||||
|
createTime: Date // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// IoT 产品 API
|
||||||
|
export const ProductApi = {
|
||||||
|
// 查询产品分页
|
||||||
|
getProductPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/iot/product/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询产品详情
|
||||||
|
getProduct: async (id: number) => {
|
||||||
|
return await request.get({ url: `/iot/product/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增产品
|
||||||
|
createProduct: async (data: ProductVO) => {
|
||||||
|
return await request.post({ url: `/iot/product/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改产品
|
||||||
|
updateProduct: async (data: ProductVO) => {
|
||||||
|
return await request.put({ url: `/iot/product/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除产品
|
||||||
|
deleteProduct: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/iot/product/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导出产品 Excel
|
||||||
|
exportProduct: async (params) => {
|
||||||
|
return await request.download({ url: `/iot/product/export-excel`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新产品状态
|
||||||
|
updateProductStatus: async (id: number, status: number) => {
|
||||||
|
return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询产品(精简)列表
|
||||||
|
getSimpleProductList() {
|
||||||
|
return request.get({ url: '/iot/product/list-all-simple' })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// IoT 产品物模型 VO
|
||||||
|
export interface ThinkModelFunctionVO {
|
||||||
|
id: number // 物模型功能编号
|
||||||
|
identifier: string // 功能标识
|
||||||
|
name: string // 功能名称
|
||||||
|
description: string // 功能描述
|
||||||
|
productId: number // 产品编号
|
||||||
|
productKey: string // 产品标识
|
||||||
|
type: number // 功能类型
|
||||||
|
property: string // 属性
|
||||||
|
event: string // 事件
|
||||||
|
service: string // 服务
|
||||||
|
}
|
||||||
|
|
||||||
|
// IoT 产品物模型 API
|
||||||
|
export const ThinkModelFunctionApi = {
|
||||||
|
// 查询产品物模型分页
|
||||||
|
getThinkModelFunctionPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/iot/think-model-function/page`, params })
|
||||||
|
},
|
||||||
|
// 获得产品物模型
|
||||||
|
getThinkModelFunctionListByProductId: async (params: any) => {
|
||||||
|
return await request.get({
|
||||||
|
url: `/iot/think-model-function/list-by-product-id`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询产品物模型详情
|
||||||
|
getThinkModelFunction: async (id: number) => {
|
||||||
|
return await request.get({ url: `/iot/think-model-function/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增产品物模型
|
||||||
|
createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
|
||||||
|
return await request.post({ url: `/iot/think-model-function/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改产品物模型
|
||||||
|
updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
|
||||||
|
return await request.put({ url: `/iot/think-model-function/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除产品物模型
|
||||||
|
deleteThinkModelFunction: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导出产品物模型 Excel
|
||||||
|
exportThinkModelFunction: async (params) => {
|
||||||
|
return await request.download({ url: `/iot/think-model-function/export-excel`, params })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
import { getRefreshToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import type { UserLoginVO } from './types'
|
import type { RegisterVO, UserLoginVO } from './types'
|
||||||
|
|
||||||
export interface SmsCodeVO {
|
export interface SmsCodeVO {
|
||||||
mobile: string
|
mobile: string
|
||||||
|
@ -17,6 +17,11 @@ export const login = (data: UserLoginVO) => {
|
||||||
return request.post({ url: '/system/auth/login', data })
|
return request.post({ url: '/system/auth/login', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
export const register = (data: RegisterVO) => {
|
||||||
|
return request.post({ url: '/system/auth/register', data })
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新访问令牌
|
// 刷新访问令牌
|
||||||
export const refreshToken = () => {
|
export const refreshToken = () => {
|
||||||
return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
|
return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
|
||||||
|
|
|
@ -29,3 +29,10 @@ export type UserVO = {
|
||||||
loginIp: string
|
loginIp: string
|
||||||
loginDate: string
|
loginDate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RegisterVO = {
|
||||||
|
tenantName: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
captchaVerification: string
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
import { Sku, Spu } from '@/api/mall/product/spu' // 积分商城活动 VO
|
||||||
|
|
||||||
|
// 积分商城活动 VO
|
||||||
|
export interface PointActivityVO {
|
||||||
|
id: number // 积分商城活动编号
|
||||||
|
spuId: number // 积分商城活动商品
|
||||||
|
status: number // 活动状态
|
||||||
|
stock: number // 积分商城活动库存
|
||||||
|
totalStock: number // 积分商城活动总库存
|
||||||
|
remark?: string // 备注
|
||||||
|
sort: number // 排序
|
||||||
|
createTime: string // 创建时间
|
||||||
|
products: PointProductVO[] // 积分商城商品
|
||||||
|
|
||||||
|
// ========== 商品字段 ==========
|
||||||
|
spuName: string // 商品名称
|
||||||
|
picUrl: string // 商品主图
|
||||||
|
marketPrice: number // 商品市场价,单位:分
|
||||||
|
|
||||||
|
//======================= 显示所需兑换积分最少的 sku 信息 =======================
|
||||||
|
point: number // 兑换积分
|
||||||
|
price: number // 兑换金额,单位:分
|
||||||
|
}
|
||||||
|
|
||||||
|
// 秒杀活动所需属性
|
||||||
|
export interface PointProductVO {
|
||||||
|
id?: number // 积分商城商品编号
|
||||||
|
activityId?: number // 积分商城活动 id
|
||||||
|
spuId?: number // 商品 SPU 编号
|
||||||
|
skuId: number // 商品 SKU 编号
|
||||||
|
count: number // 可兑换数量
|
||||||
|
point: number // 兑换积分
|
||||||
|
price: number // 兑换金额,单位:分
|
||||||
|
stock: number // 积分商城商品库存
|
||||||
|
activityStatus?: number // 积分商城商品状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展 Sku 配置
|
||||||
|
export type SkuExtension = Sku & {
|
||||||
|
productConfig: PointProductVO
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpuExtension extends Spu {
|
||||||
|
skus: SkuExtension[] // 重写类型
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpuExtension0 extends Spu {
|
||||||
|
pointStock: number // 积分商城活动库存
|
||||||
|
pointTotalStock: number // 积分商城活动总库存
|
||||||
|
point: number // 兑换积分
|
||||||
|
pointPrice: number // 兑换金额,单位:分
|
||||||
|
}
|
||||||
|
|
||||||
|
// 积分商城活动 API
|
||||||
|
export const PointActivityApi = {
|
||||||
|
// 查询积分商城活动分页
|
||||||
|
getPointActivityPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/promotion/point-activity/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询积分商城活动详情
|
||||||
|
getPointActivity: async (id: number) => {
|
||||||
|
return await request.get({ url: `/promotion/point-activity/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询积分商城活动列表,基于活动编号数组
|
||||||
|
getPointActivityListByIds: async (ids: number[]) => {
|
||||||
|
return request.get({ url: `/promotion/point-activity/list-by-ids?ids=${ids}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增积分商城活动
|
||||||
|
createPointActivity: async (data: PointActivityVO) => {
|
||||||
|
return await request.post({ url: `/promotion/point-activity/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改积分商城活动
|
||||||
|
updatePointActivity: async (data: PointActivityVO) => {
|
||||||
|
return await request.put({ url: `/promotion/point-activity/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除积分商城活动
|
||||||
|
deletePointActivity: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/promotion/point-activity/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭秒杀活动
|
||||||
|
closePointActivity: async (id: number) => {
|
||||||
|
return await request.put({ url: '/promotion/point-activity/close?id=' + id })
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,12 @@ export const getReward = async (id: number) => {
|
||||||
return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
|
return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除限时折扣活动
|
// 删除满减送活动
|
||||||
export const deleteRewardActivity = async (id: number) => {
|
export const deleteRewardActivity = async (id: number) => {
|
||||||
return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })
|
return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭满减送活动
|
||||||
|
export const closeRewardActivity = async (id: number) => {
|
||||||
|
return await request.put({ url: '/promotion/reward-activity/close?id=' + id })
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface SeckillActivityVO {
|
||||||
singleLimitCount?: number
|
singleLimitCount?: number
|
||||||
stock?: number
|
stock?: number
|
||||||
totalStock?: number
|
totalStock?: number
|
||||||
|
seckillPrice?: number
|
||||||
products?: SeckillProductVO[]
|
products?: SeckillProductVO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +44,11 @@ export const getSeckillActivityPage = async (params) => {
|
||||||
return await request.get({ url: '/promotion/seckill-activity/page', params })
|
return await request.get({ url: '/promotion/seckill-activity/page', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询秒杀活动列表,基于活动编号数组
|
||||||
|
export const getSeckillActivityListByIds = (ids: number[]) => {
|
||||||
|
return request.get({ url: `/promotion/seckill-activity/list-by-ids?ids=${ids}` })
|
||||||
|
}
|
||||||
|
|
||||||
// 查询秒杀活动详情
|
// 查询秒杀活动详情
|
||||||
export const getSeckillActivity = async (id: number) => {
|
export const getSeckillActivity = async (id: number) => {
|
||||||
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
|
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
|
||||||
|
|
|
@ -84,8 +84,14 @@ export const getOrderPage = async (params: OrderPageReqVO) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询详情支付订单
|
// 查询详情支付订单
|
||||||
export const getOrder = async (id: number) => {
|
export const getOrder = async (id: number, sync?: boolean) => {
|
||||||
return await request.get({ url: '/pay/order/get?id=' + id })
|
return await request.get({
|
||||||
|
url: '/pay/order/get',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
sync
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得支付订单的明细
|
// 获得支付订单的明细
|
||||||
|
|
|
@ -5,6 +5,7 @@ export interface AppLinkGroup {
|
||||||
// 链接列表
|
// 链接列表
|
||||||
links: AppLink[]
|
links: AppLink[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// APP 链接
|
// APP 链接
|
||||||
export interface AppLink {
|
export interface AppLink {
|
||||||
// 链接名称
|
// 链接名称
|
||||||
|
@ -21,6 +22,8 @@ export const enum APP_LINK_TYPE_ENUM {
|
||||||
ACTIVITY_COMBINATION,
|
ACTIVITY_COMBINATION,
|
||||||
// 秒杀活动
|
// 秒杀活动
|
||||||
ACTIVITY_SECKILL,
|
ACTIVITY_SECKILL,
|
||||||
|
// 积分商城活动
|
||||||
|
ACTIVITY_POINT,
|
||||||
// 文章详情
|
// 文章详情
|
||||||
ARTICLE_DETAIL,
|
ARTICLE_DETAIL,
|
||||||
// 优惠券详情
|
// 优惠券详情
|
||||||
|
@ -130,6 +133,11 @@ export const APP_LINK_GROUP_LIST = [
|
||||||
path: '/pages/activity/seckill/list',
|
path: '/pages/activity/seckill/list',
|
||||||
type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
|
type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '积分商城活动',
|
||||||
|
path: '/pages/activity/point/list',
|
||||||
|
type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '签到中心',
|
name: '签到中心',
|
||||||
path: '/pages/app/sign'
|
path: '/pages/app/sign'
|
||||||
|
|
|
@ -11,7 +11,7 @@ const prefixCls = getPrefixCls('content-wrap')
|
||||||
defineProps({
|
defineProps({
|
||||||
title: propTypes.string.def(''),
|
title: propTypes.string.def(''),
|
||||||
message: propTypes.string.def(''),
|
message: propTypes.string.def(''),
|
||||||
bodyStyle: propTypes.object.def({ padding: '20px' })
|
bodyStyle: propTypes.object.def({ padding: '10px' })
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ defineOptions({ name: 'FloatingActionButton' })
|
||||||
defineProps<{ property: FloatingActionButtonProperty }>()
|
defineProps<{ property: FloatingActionButtonProperty }>()
|
||||||
|
|
||||||
// 是否展开
|
// 是否展开
|
||||||
const expanded = ref(true)
|
const expanded = ref(false)
|
||||||
// 处理展开/折叠
|
// 处理展开/折叠
|
||||||
const handleToggleFab = () => {
|
const handleToggleFab = () => {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
class="text-16px"
|
class="text-16px"
|
||||||
:style="{ color: property.fields.price.color }"
|
:style="{ color: property.fields.price.color }"
|
||||||
>
|
>
|
||||||
¥{{ fenToYuan(spu.price) }}
|
¥{{ fenToYuan(spu.price as any) }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 市场价 -->
|
<!-- 市场价 -->
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ProductListProperty } from './config'
|
import { ProductListProperty } from './config'
|
||||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||||
import { fenToYuan } from './index'
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
/** 商品栏 */
|
/** 商品栏 */
|
||||||
defineOptions({ name: 'ProductList' })
|
defineOptions({ name: 'ProductList' })
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
|
||||||
|
|
||||||
|
/** 积分商城属性 */
|
||||||
|
export interface PromotionPointProperty {
|
||||||
|
// 布局类型:单列 | 三列
|
||||||
|
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
|
||||||
|
// 商品字段
|
||||||
|
fields: {
|
||||||
|
// 商品名称
|
||||||
|
name: PromotionPointFieldProperty
|
||||||
|
// 商品简介
|
||||||
|
introduction: PromotionPointFieldProperty
|
||||||
|
// 商品价格
|
||||||
|
price: PromotionPointFieldProperty
|
||||||
|
// 市场价
|
||||||
|
marketPrice: PromotionPointFieldProperty
|
||||||
|
// 商品销量
|
||||||
|
salesCount: PromotionPointFieldProperty
|
||||||
|
// 商品库存
|
||||||
|
stock: PromotionPointFieldProperty
|
||||||
|
}
|
||||||
|
// 角标
|
||||||
|
badge: {
|
||||||
|
// 是否显示
|
||||||
|
show: boolean
|
||||||
|
// 角标图片
|
||||||
|
imgUrl: string
|
||||||
|
}
|
||||||
|
// 按钮
|
||||||
|
btnBuy: {
|
||||||
|
// 类型:文字 | 图片
|
||||||
|
type: 'text' | 'img'
|
||||||
|
// 文字
|
||||||
|
text: string
|
||||||
|
// 文字按钮:背景渐变起始颜色
|
||||||
|
bgBeginColor: string
|
||||||
|
// 文字按钮:背景渐变结束颜色
|
||||||
|
bgEndColor: string
|
||||||
|
// 图片按钮:图片地址
|
||||||
|
imgUrl: string
|
||||||
|
}
|
||||||
|
// 上圆角
|
||||||
|
borderRadiusTop: number
|
||||||
|
// 下圆角
|
||||||
|
borderRadiusBottom: number
|
||||||
|
// 间距
|
||||||
|
space: number
|
||||||
|
// 秒杀活动编号
|
||||||
|
activityIds: number[]
|
||||||
|
// 组件样式
|
||||||
|
style: ComponentStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品字段
|
||||||
|
export interface PromotionPointFieldProperty {
|
||||||
|
// 是否显示
|
||||||
|
show: boolean
|
||||||
|
// 颜色
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义组件
|
||||||
|
export const component = {
|
||||||
|
id: 'PromotionPoint',
|
||||||
|
name: '积分商城',
|
||||||
|
icon: 'ep:present',
|
||||||
|
property: {
|
||||||
|
layoutType: 'oneColBigImg',
|
||||||
|
fields: {
|
||||||
|
name: { show: true, color: '#000' },
|
||||||
|
introduction: { show: true, color: '#999' },
|
||||||
|
price: { show: true, color: '#ff3000' },
|
||||||
|
marketPrice: { show: true, color: '#c4c4c4' },
|
||||||
|
salesCount: { show: true, color: '#c4c4c4' },
|
||||||
|
stock: { show: false, color: '#c4c4c4' }
|
||||||
|
},
|
||||||
|
badge: { show: false, imgUrl: '' },
|
||||||
|
btnBuy: {
|
||||||
|
type: 'text',
|
||||||
|
text: '立即兑换',
|
||||||
|
bgBeginColor: '#FF6000',
|
||||||
|
bgEndColor: '#FE832A',
|
||||||
|
imgUrl: ''
|
||||||
|
},
|
||||||
|
borderRadiusTop: 8,
|
||||||
|
borderRadiusBottom: 8,
|
||||||
|
space: 8,
|
||||||
|
style: {
|
||||||
|
bgType: 'color',
|
||||||
|
bgColor: '',
|
||||||
|
marginLeft: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 8
|
||||||
|
} as ComponentStyle
|
||||||
|
}
|
||||||
|
} as DiyComponent<PromotionPointProperty>
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`">
|
||||||
|
<div
|
||||||
|
v-for="(spu, index) in spuList"
|
||||||
|
:key="index"
|
||||||
|
:style="{
|
||||||
|
...calculateSpace(index),
|
||||||
|
...calculateWidth(),
|
||||||
|
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
||||||
|
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
||||||
|
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||||
|
borderBottomRightRadius: `${property.borderRadiusBottom}px`
|
||||||
|
}"
|
||||||
|
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||||
|
>
|
||||||
|
<!-- 角标 -->
|
||||||
|
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
|
||||||
|
<el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
|
||||||
|
</div>
|
||||||
|
<!-- 商品封面图 -->
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'h-140px',
|
||||||
|
{
|
||||||
|
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||||
|
'w-140px': property.layoutType === 'oneColSmallImg'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-image :src="spu.picUrl" class="h-full w-full" fit="cover" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
' flex flex-col gap-8px p-8px box-border',
|
||||||
|
{
|
||||||
|
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||||
|
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 商品名称 -->
|
||||||
|
<div
|
||||||
|
v-if="property.fields.name.show"
|
||||||
|
:class="[
|
||||||
|
'text-14px ',
|
||||||
|
{
|
||||||
|
truncate: property.layoutType !== 'oneColSmallImg',
|
||||||
|
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="{ color: property.fields.name.color }"
|
||||||
|
>
|
||||||
|
{{ spu.name }}
|
||||||
|
</div>
|
||||||
|
<!-- 商品简介 -->
|
||||||
|
<div
|
||||||
|
v-if="property.fields.introduction.show"
|
||||||
|
:style="{ color: property.fields.introduction.color }"
|
||||||
|
class="truncate text-12px"
|
||||||
|
>
|
||||||
|
{{ spu.introduction }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- 积分 -->
|
||||||
|
<span
|
||||||
|
v-if="property.fields.price.show"
|
||||||
|
:style="{ color: property.fields.price.color }"
|
||||||
|
class="text-16px"
|
||||||
|
>
|
||||||
|
{{ spu.point }}积分
|
||||||
|
{{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}元` }}
|
||||||
|
</span>
|
||||||
|
<!-- 市场价 -->
|
||||||
|
<span
|
||||||
|
v-if="property.fields.marketPrice.show && spu.marketPrice"
|
||||||
|
:style="{ color: property.fields.marketPrice.color }"
|
||||||
|
class="ml-4px text-10px line-through"
|
||||||
|
>
|
||||||
|
¥{{ fenToYuan(spu.marketPrice) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-12px">
|
||||||
|
<!-- 销量 -->
|
||||||
|
<span
|
||||||
|
v-if="property.fields.salesCount.show"
|
||||||
|
:style="{ color: property.fields.salesCount.color }"
|
||||||
|
>
|
||||||
|
已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件
|
||||||
|
</span>
|
||||||
|
<!-- 库存 -->
|
||||||
|
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
|
||||||
|
库存{{ spu.pointTotalStock || 0 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 购买按钮 -->
|
||||||
|
<div class="absolute bottom-8px right-8px">
|
||||||
|
<!-- 文字按钮 -->
|
||||||
|
<span
|
||||||
|
v-if="property.btnBuy.type === 'text'"
|
||||||
|
:style="{
|
||||||
|
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
|
||||||
|
}"
|
||||||
|
class="rounded-full p-x-12px p-y-4px text-12px text-white"
|
||||||
|
>
|
||||||
|
{{ property.btnBuy.text }}
|
||||||
|
</span>
|
||||||
|
<!-- 图片按钮 -->
|
||||||
|
<el-image
|
||||||
|
v-else
|
||||||
|
:src="property.btnBuy.imgUrl"
|
||||||
|
class="h-28px w-28px rounded-full"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PromotionPointProperty } from './config'
|
||||||
|
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||||
|
import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point'
|
||||||
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
|
/** 积分商城卡片 */
|
||||||
|
defineOptions({ name: 'PromotionPoint' })
|
||||||
|
// 定义属性
|
||||||
|
const props = defineProps<{ property: PromotionPointProperty }>()
|
||||||
|
// 商品列表
|
||||||
|
const spuList = ref<SpuExtension0[]>([])
|
||||||
|
const spuIdList = ref<number[]>([])
|
||||||
|
const pointActivityList = ref<PointActivityVO[]>([])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.property.activityIds,
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// 新添加的积分商城组件,是没有活动ID的
|
||||||
|
const activityIds = props.property.activityIds
|
||||||
|
// 检查活动ID的有效性
|
||||||
|
if (Array.isArray(activityIds) && activityIds.length > 0) {
|
||||||
|
// 获取积分商城活动详情列表
|
||||||
|
pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds)
|
||||||
|
|
||||||
|
// 获取积分商城活动的 SPU 详情列表
|
||||||
|
spuList.value = []
|
||||||
|
spuIdList.value = pointActivityList.value.map((activity) => activity.spuId)
|
||||||
|
if (spuIdList.value.length > 0) {
|
||||||
|
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 SPU 的最低兑换积分和所需兑换金额
|
||||||
|
pointActivityList.value.forEach((activity) => {
|
||||||
|
// 匹配spuId
|
||||||
|
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
|
||||||
|
if (spu) {
|
||||||
|
spu.pointStock = activity.stock
|
||||||
|
spu.pointTotalStock = activity.totalStock
|
||||||
|
spu.point = activity.point
|
||||||
|
spu.pointPrice = activity.price
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取积分商城活动细节或 SPU 细节时出错:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算商品的间距
|
||||||
|
* @param index 商品索引
|
||||||
|
*/
|
||||||
|
const calculateSpace = (index: number) => {
|
||||||
|
// 商品的列数
|
||||||
|
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
|
||||||
|
// 第一列没有左边距
|
||||||
|
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
|
||||||
|
// 第一行没有上边距
|
||||||
|
const marginTop = index < columns ? '0' : props.property.space + 'px'
|
||||||
|
|
||||||
|
return { marginLeft, marginTop }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 容器
|
||||||
|
const containerRef = ref()
|
||||||
|
// 计算商品的宽度
|
||||||
|
const calculateWidth = () => {
|
||||||
|
let width = '100%'
|
||||||
|
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||||
|
if (props.property.layoutType === 'twoCol') {
|
||||||
|
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
|
||||||
|
}
|
||||||
|
return { width }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<ComponentContainerProperty v-model="formData.style">
|
||||||
|
<el-form :model="formData" label-width="80px">
|
||||||
|
<el-card class="property-group" header="积分商城活动" shadow="never">
|
||||||
|
<PointShowcase v-model="formData.activityIds" />
|
||||||
|
</el-card>
|
||||||
|
<el-card class="property-group" header="商品样式" shadow="never">
|
||||||
|
<el-form-item label="布局" prop="type">
|
||||||
|
<el-radio-group v-model="formData.layoutType">
|
||||||
|
<el-tooltip class="item" content="单列大图" placement="bottom">
|
||||||
|
<el-radio-button value="oneColBigImg">
|
||||||
|
<Icon icon="fluent:text-column-one-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip class="item" content="单列小图" placement="bottom">
|
||||||
|
<el-radio-button value="oneColSmallImg">
|
||||||
|
<Icon icon="fluent:text-column-two-left-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip class="item" content="双列" placement="bottom">
|
||||||
|
<el-radio-button value="twoCol">
|
||||||
|
<Icon icon="fluent:text-column-two-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<!--<el-tooltip class="item" content="三列" placement="bottom">
|
||||||
|
<el-radio-button value="threeCol">
|
||||||
|
<Icon icon="fluent:text-column-three-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>-->
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称" prop="fields.name.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.name.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.name.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品简介" prop="fields.introduction.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.introduction.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.introduction.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品价格" prop="fields.price.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.price.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.price.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="市场价" prop="fields.marketPrice.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.marketPrice.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.marketPrice.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品销量" prop="fields.salesCount.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.salesCount.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.salesCount.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品库存" prop="fields.stock.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.stock.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.stock.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="property-group" header="角标" shadow="never">
|
||||||
|
<el-form-item label="角标" prop="badge.show">
|
||||||
|
<el-switch v-model="formData.badge.show" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
|
||||||
|
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
|
||||||
|
<template #tip> 建议尺寸:36 * 22</template>
|
||||||
|
</UploadImg>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="property-group" header="按钮" shadow="never">
|
||||||
|
<el-form-item label="按钮类型" prop="btnBuy.type">
|
||||||
|
<el-radio-group v-model="formData.btnBuy.type">
|
||||||
|
<el-radio-button value="text">文字</el-radio-button>
|
||||||
|
<el-radio-button value="img">图片</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="formData.btnBuy.type === 'text'">
|
||||||
|
<el-form-item label="按钮文字" prop="btnBuy.text">
|
||||||
|
<el-input v-model="formData.btnBuy.text" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
|
||||||
|
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
|
||||||
|
<ColorInput v-model="formData.btnBuy.bgEndColor" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="图片" prop="btnBuy.imgUrl">
|
||||||
|
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
|
||||||
|
<template #tip> 建议尺寸:56 * 56</template>
|
||||||
|
</UploadImg>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="property-group" header="商品样式" shadow="never">
|
||||||
|
<el-form-item label="上圆角" prop="borderRadiusTop">
|
||||||
|
<el-slider
|
||||||
|
v-model="formData.borderRadiusTop"
|
||||||
|
:max="100"
|
||||||
|
:min="0"
|
||||||
|
:show-input-controls="false"
|
||||||
|
input-size="small"
|
||||||
|
show-input
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="下圆角" prop="borderRadiusBottom">
|
||||||
|
<el-slider
|
||||||
|
v-model="formData.borderRadiusBottom"
|
||||||
|
:max="100"
|
||||||
|
:min="0"
|
||||||
|
:show-input-controls="false"
|
||||||
|
input-size="small"
|
||||||
|
show-input
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="间隔" prop="space">
|
||||||
|
<el-slider
|
||||||
|
v-model="formData.space"
|
||||||
|
:max="100"
|
||||||
|
:min="0"
|
||||||
|
:show-input-controls="false"
|
||||||
|
input-size="small"
|
||||||
|
show-input
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
</el-form>
|
||||||
|
</ComponentContainerProperty>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PromotionPointProperty } from './config'
|
||||||
|
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||||
|
import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue'
|
||||||
|
|
||||||
|
// 秒杀属性面板
|
||||||
|
defineOptions({ name: 'PromotionPointProperty' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: PromotionPointProperty }>()
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -3,13 +3,21 @@ import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||||
/** 秒杀属性 */
|
/** 秒杀属性 */
|
||||||
export interface PromotionSeckillProperty {
|
export interface PromotionSeckillProperty {
|
||||||
// 布局类型:单列 | 三列
|
// 布局类型:单列 | 三列
|
||||||
layoutType: 'oneCol' | 'threeCol'
|
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
|
||||||
// 商品字段
|
// 商品字段
|
||||||
fields: {
|
fields: {
|
||||||
// 商品名称
|
// 商品名称
|
||||||
name: PromotionSeckillFieldProperty
|
name: PromotionSeckillFieldProperty
|
||||||
|
// 商品简介
|
||||||
|
introduction: PromotionSeckillFieldProperty
|
||||||
// 商品价格
|
// 商品价格
|
||||||
price: PromotionSeckillFieldProperty
|
price: PromotionSeckillFieldProperty
|
||||||
|
// 市场价
|
||||||
|
marketPrice: PromotionSeckillFieldProperty
|
||||||
|
// 商品销量
|
||||||
|
salesCount: PromotionSeckillFieldProperty
|
||||||
|
// 商品库存
|
||||||
|
stock: PromotionSeckillFieldProperty
|
||||||
}
|
}
|
||||||
// 角标
|
// 角标
|
||||||
badge: {
|
badge: {
|
||||||
|
@ -18,6 +26,19 @@ export interface PromotionSeckillProperty {
|
||||||
// 角标图片
|
// 角标图片
|
||||||
imgUrl: string
|
imgUrl: string
|
||||||
}
|
}
|
||||||
|
// 按钮
|
||||||
|
btnBuy: {
|
||||||
|
// 类型:文字 | 图片
|
||||||
|
type: 'text' | 'img'
|
||||||
|
// 文字
|
||||||
|
text: string
|
||||||
|
// 文字按钮:背景渐变起始颜色
|
||||||
|
bgBeginColor: string
|
||||||
|
// 文字按钮:背景渐变结束颜色
|
||||||
|
bgEndColor: string
|
||||||
|
// 图片按钮:图片地址
|
||||||
|
imgUrl: string
|
||||||
|
}
|
||||||
// 上圆角
|
// 上圆角
|
||||||
borderRadiusTop: number
|
borderRadiusTop: number
|
||||||
// 下圆角
|
// 下圆角
|
||||||
|
@ -25,10 +46,11 @@ export interface PromotionSeckillProperty {
|
||||||
// 间距
|
// 间距
|
||||||
space: number
|
space: number
|
||||||
// 秒杀活动编号
|
// 秒杀活动编号
|
||||||
activityId: number
|
activityIds: number[]
|
||||||
// 组件样式
|
// 组件样式
|
||||||
style: ComponentStyle
|
style: ComponentStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
// 商品字段
|
// 商品字段
|
||||||
export interface PromotionSeckillFieldProperty {
|
export interface PromotionSeckillFieldProperty {
|
||||||
// 是否显示
|
// 是否显示
|
||||||
|
@ -43,13 +65,23 @@ export const component = {
|
||||||
name: '秒杀',
|
name: '秒杀',
|
||||||
icon: 'mdi:calendar-time',
|
icon: 'mdi:calendar-time',
|
||||||
property: {
|
property: {
|
||||||
activityId: undefined,
|
layoutType: 'oneColBigImg',
|
||||||
layoutType: 'oneCol',
|
|
||||||
fields: {
|
fields: {
|
||||||
name: { show: true, color: '#000' },
|
name: { show: true, color: '#000' },
|
||||||
price: { show: true, color: '#ff3000' }
|
introduction: { show: true, color: '#999' },
|
||||||
|
price: { show: true, color: '#ff3000' },
|
||||||
|
marketPrice: { show: true, color: '#c4c4c4' },
|
||||||
|
salesCount: { show: true, color: '#c4c4c4' },
|
||||||
|
stock: { show: false, color: '#c4c4c4' }
|
||||||
},
|
},
|
||||||
badge: { show: false, imgUrl: '' },
|
badge: { show: false, imgUrl: '' },
|
||||||
|
btnBuy: {
|
||||||
|
type: 'text',
|
||||||
|
text: '立即秒杀',
|
||||||
|
bgBeginColor: '#FF6000',
|
||||||
|
bgEndColor: '#FE832A',
|
||||||
|
imgUrl: ''
|
||||||
|
},
|
||||||
borderRadiusTop: 8,
|
borderRadiusTop: 8,
|
||||||
borderRadiusBottom: 8,
|
borderRadiusBottom: 8,
|
||||||
space: 8,
|
space: 8,
|
||||||
|
|
|
@ -1,135 +1,201 @@
|
||||||
<template>
|
<template>
|
||||||
<el-scrollbar ref="containerRef" class="z-1 min-h-30px" wrap-class="w-full">
|
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
|
||||||
<!-- 商品网格 -->
|
|
||||||
<div
|
<div
|
||||||
|
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||||
:style="{
|
:style="{
|
||||||
gridGap: `${property.space}px`,
|
...calculateSpace(index),
|
||||||
gridTemplateColumns,
|
...calculateWidth(),
|
||||||
width: scrollbarWidth
|
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
||||||
|
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
||||||
|
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||||
|
borderBottomRightRadius: `${property.borderRadiusBottom}px`
|
||||||
}"
|
}"
|
||||||
class="grid overflow-x-auto"
|
v-for="(spu, index) in spuList"
|
||||||
|
:key="index"
|
||||||
>
|
>
|
||||||
<!-- 商品 -->
|
<!-- 角标 -->
|
||||||
|
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
|
||||||
|
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
|
||||||
|
</div>
|
||||||
|
<!-- 商品封面图 -->
|
||||||
<div
|
<div
|
||||||
v-for="(spu, index) in spuList"
|
:class="[
|
||||||
:key="index"
|
'h-140px',
|
||||||
:style="{
|
{
|
||||||
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||||
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
'w-140px': property.layoutType === 'oneColSmallImg'
|
||||||
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
}
|
||||||
borderBottomRightRadius: `${property.borderRadiusBottom}px`
|
]"
|
||||||
}"
|
|
||||||
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
|
||||||
>
|
>
|
||||||
<!-- 角标 -->
|
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
|
||||||
<div
|
</div>
|
||||||
v-if="property.badge.show"
|
<div
|
||||||
class="absolute left-0 top-0 z-1 items-center justify-center"
|
:class="[
|
||||||
>
|
' flex flex-col gap-8px p-8px box-border',
|
||||||
<el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
|
{
|
||||||
</div>
|
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||||
<!-- 商品封面图 -->
|
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
|
||||||
<el-image :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" fit="cover" />
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 商品名称 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="property.fields.name.show"
|
||||||
:class="[
|
:class="[
|
||||||
'flex flex-col gap-8px p-8px box-border',
|
'text-14px ',
|
||||||
{
|
{
|
||||||
'w-[calc(100%-64px)]': columns === 2,
|
truncate: property.layoutType !== 'oneColSmallImg',
|
||||||
'w-full': columns === 3
|
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
|
:style="{ color: property.fields.name.color }"
|
||||||
>
|
>
|
||||||
<!-- 商品名称 -->
|
{{ spu.name }}
|
||||||
<div
|
</div>
|
||||||
v-if="property.fields.name.show"
|
<!-- 商品简介 -->
|
||||||
:style="{ color: property.fields.name.color }"
|
<div
|
||||||
class="truncate text-12px"
|
v-if="property.fields.introduction.show"
|
||||||
|
class="truncate text-12px"
|
||||||
|
:style="{ color: property.fields.introduction.color }"
|
||||||
|
>
|
||||||
|
{{ spu.introduction }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- 价格 -->
|
||||||
|
<span
|
||||||
|
v-if="property.fields.price.show"
|
||||||
|
class="text-16px"
|
||||||
|
:style="{ color: property.fields.price.color }"
|
||||||
>
|
>
|
||||||
{{ spu.name }}
|
¥{{ fenToYuan(spu.price || Infinity) }}
|
||||||
</div>
|
</span>
|
||||||
<div>
|
<!-- 市场价 -->
|
||||||
<!-- 商品价格 -->
|
<span
|
||||||
<span
|
v-if="property.fields.marketPrice.show && spu.marketPrice"
|
||||||
v-if="property.fields.price.show"
|
class="ml-4px text-10px line-through"
|
||||||
:style="{ color: property.fields.price.color }"
|
:style="{ color: property.fields.marketPrice.color }"
|
||||||
class="text-12px"
|
>¥{{ fenToYuan(spu.marketPrice) }}</span
|
||||||
>
|
>
|
||||||
¥{{ fenToYuan(spu.seckillPrice || spu.price || 0) }}
|
</div>
|
||||||
</span>
|
<div class="text-12px">
|
||||||
</div>
|
<!-- 销量 -->
|
||||||
|
<span
|
||||||
|
v-if="property.fields.salesCount.show"
|
||||||
|
:style="{ color: property.fields.salesCount.color }"
|
||||||
|
>
|
||||||
|
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
|
||||||
|
</span>
|
||||||
|
<!-- 库存 -->
|
||||||
|
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
|
||||||
|
库存{{ spu.stock || 0 }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 购买按钮 -->
|
||||||
|
<div class="absolute bottom-8px right-8px">
|
||||||
|
<!-- 文字按钮 -->
|
||||||
|
<span
|
||||||
|
v-if="property.btnBuy.type === 'text'"
|
||||||
|
class="rounded-full p-x-12px p-y-4px text-12px text-white"
|
||||||
|
:style="{
|
||||||
|
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ property.btnBuy.text }}
|
||||||
|
</span>
|
||||||
|
<!-- 图片按钮 -->
|
||||||
|
<el-image
|
||||||
|
v-else
|
||||||
|
class="h-28px w-28px rounded-full"
|
||||||
|
fit="cover"
|
||||||
|
:src="property.btnBuy.imgUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { PromotionSeckillProperty } from './config'
|
import { PromotionSeckillProperty } from './config'
|
||||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||||
import { Spu } from '@/api/mall/product/spu'
|
|
||||||
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
||||||
import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
|
|
||||||
import { fenToYuan } from '@/utils'
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
/** 秒杀 */
|
/** 秒杀卡片 */
|
||||||
defineOptions({ name: 'PromotionSeckill' })
|
defineOptions({ name: 'PromotionSeckill' })
|
||||||
// 定义属性
|
// 定义属性
|
||||||
const props = defineProps<{ property: PromotionSeckillProperty }>()
|
const props = defineProps<{ property: PromotionSeckillProperty }>()
|
||||||
// 商品列表
|
// 商品列表
|
||||||
const spuList = ref<ProductSpuApi.Spu[]>([])
|
const spuList = ref<ProductSpuApi.Spu[]>([])
|
||||||
|
const spuIdList = ref<number[]>([])
|
||||||
|
const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.property.activityId,
|
() => props.property.activityIds,
|
||||||
async () => {
|
async () => {
|
||||||
if (!props.property.activityId) return
|
try {
|
||||||
const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
|
// 新添加的秒杀组件,是没有活动ID的
|
||||||
if (!activity?.spuId) return
|
const activityIds = props.property.activityIds
|
||||||
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
|
// 检查活动ID的有效性
|
||||||
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
|
if (Array.isArray(activityIds) && activityIds.length > 0) {
|
||||||
// 循环活动信息,赋值秒杀最低价格
|
// 获取秒杀活动详情列表
|
||||||
activity.products.forEach((product: SeckillProductVO) => {
|
seckillActivityList.value =
|
||||||
spuList.value.forEach((spu: Spu) => {
|
await SeckillActivityApi.getSeckillActivityListByIds(activityIds)
|
||||||
spu.seckillPrice = Math.min(spu.seckillPrice || Infinity, product.seckillPrice) // 设置 SPU 的最低价格
|
|
||||||
})
|
// 获取秒杀活动的 SPU 详情列表
|
||||||
})
|
spuList.value = []
|
||||||
|
spuIdList.value = seckillActivityList.value
|
||||||
|
.map((activity) => activity.spuId)
|
||||||
|
.filter((spuId): spuId is number => typeof spuId === 'number')
|
||||||
|
if (spuIdList.value.length > 0) {
|
||||||
|
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 SPU 的最低价格
|
||||||
|
seckillActivityList.value.forEach((activity) => {
|
||||||
|
// 匹配spuId
|
||||||
|
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
|
||||||
|
if (spu) {
|
||||||
|
// 赋值活动价格,哪个最便宜就赋值哪个
|
||||||
|
spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取秒杀活动细节或 SPU 细节时出错:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// 手机宽度
|
|
||||||
const phoneWidth = ref(375)
|
/**
|
||||||
|
* 计算商品的间距
|
||||||
|
* @param index 商品索引
|
||||||
|
*/
|
||||||
|
const calculateSpace = (index: number) => {
|
||||||
|
// 商品的列数
|
||||||
|
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
|
||||||
|
// 第一列没有左边距
|
||||||
|
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
|
||||||
|
// 第一行没有上边距
|
||||||
|
const marginTop = index < columns ? '0' : props.property.space + 'px'
|
||||||
|
|
||||||
|
return { marginLeft, marginTop }
|
||||||
|
}
|
||||||
|
|
||||||
// 容器
|
// 容器
|
||||||
const containerRef = ref()
|
const containerRef = ref()
|
||||||
// 商品的列数
|
// 计算商品的宽度
|
||||||
const columns = ref(2)
|
const calculateWidth = () => {
|
||||||
// 滚动条宽度
|
let width = '100%'
|
||||||
const scrollbarWidth = ref('100%')
|
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||||
// 商品图大小
|
if (props.property.layoutType === 'twoCol') {
|
||||||
const imageSize = ref('0')
|
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
|
||||||
// 商品网络列数
|
}
|
||||||
const gridTemplateColumns = ref('')
|
return { width }
|
||||||
// 计算布局参数
|
}
|
||||||
watch(
|
|
||||||
() => [props.property, phoneWidth, spuList.value.length],
|
|
||||||
() => {
|
|
||||||
// 计算列数
|
|
||||||
columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
|
|
||||||
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
|
|
||||||
const productWidth =
|
|
||||||
(phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
|
|
||||||
// 商品图布局:2列时,左右布局 3列时,上下布局
|
|
||||||
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
|
|
||||||
// 指定列数
|
|
||||||
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
|
|
||||||
// 不滚动
|
|
||||||
scrollbarWidth.value = '100%'
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
onMounted(() => {
|
|
||||||
// 提取手机宽度
|
|
||||||
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|
|
@ -2,30 +2,31 @@
|
||||||
<ComponentContainerProperty v-model="formData.style">
|
<ComponentContainerProperty v-model="formData.style">
|
||||||
<el-form label-width="80px" :model="formData">
|
<el-form label-width="80px" :model="formData">
|
||||||
<el-card header="秒杀活动" class="property-group" shadow="never">
|
<el-card header="秒杀活动" class="property-group" shadow="never">
|
||||||
<el-form-item label="秒杀活动" prop="activityId">
|
<SeckillShowcase v-model="formData.activityIds" />
|
||||||
<el-select v-model="formData.activityId">
|
|
||||||
<el-option
|
|
||||||
v-for="activity in activityList"
|
|
||||||
:key="activity.id"
|
|
||||||
:label="activity.name"
|
|
||||||
:value="activity.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card header="商品样式" class="property-group" shadow="never">
|
<el-card header="商品样式" class="property-group" shadow="never">
|
||||||
<el-form-item label="布局" prop="type">
|
<el-form-item label="布局" prop="type">
|
||||||
<el-radio-group v-model="formData.layoutType">
|
<el-radio-group v-model="formData.layoutType">
|
||||||
<el-tooltip class="item" content="单列" placement="bottom">
|
<el-tooltip class="item" content="单列大图" placement="bottom">
|
||||||
<el-radio-button value="oneCol">
|
<el-radio-button value="oneColBigImg">
|
||||||
<Icon icon="fluent:text-column-one-24-filled" />
|
<Icon icon="fluent:text-column-one-24-filled" />
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip class="item" content="三列" placement="bottom">
|
<el-tooltip class="item" content="单列小图" placement="bottom">
|
||||||
|
<el-radio-button value="oneColSmallImg">
|
||||||
|
<Icon icon="fluent:text-column-two-left-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip class="item" content="双列" placement="bottom">
|
||||||
|
<el-radio-button value="twoCol">
|
||||||
|
<Icon icon="fluent:text-column-two-24-filled" />
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<!--<el-tooltip class="item" content="三列" placement="bottom">
|
||||||
<el-radio-button value="threeCol">
|
<el-radio-button value="threeCol">
|
||||||
<Icon icon="fluent:text-column-three-24-filled" />
|
<Icon icon="fluent:text-column-three-24-filled" />
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-tooltip>
|
</el-tooltip>-->
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="商品名称" prop="fields.name.show">
|
<el-form-item label="商品名称" prop="fields.name.show">
|
||||||
|
@ -34,12 +35,36 @@
|
||||||
<el-checkbox v-model="formData.fields.name.show" />
|
<el-checkbox v-model="formData.fields.name.show" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="商品简介" prop="fields.introduction.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.introduction.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.introduction.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="商品价格" prop="fields.price.show">
|
<el-form-item label="商品价格" prop="fields.price.show">
|
||||||
<div class="flex gap-8px">
|
<div class="flex gap-8px">
|
||||||
<ColorInput v-model="formData.fields.price.color" />
|
<ColorInput v-model="formData.fields.price.color" />
|
||||||
<el-checkbox v-model="formData.fields.price.show" />
|
<el-checkbox v-model="formData.fields.price.show" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="市场价" prop="fields.marketPrice.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.marketPrice.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.marketPrice.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品销量" prop="fields.salesCount.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.salesCount.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.salesCount.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品库存" prop="fields.stock.show">
|
||||||
|
<div class="flex gap-8px">
|
||||||
|
<ColorInput v-model="formData.fields.stock.color" />
|
||||||
|
<el-checkbox v-model="formData.fields.stock.show" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card header="角标" class="property-group" shadow="never">
|
<el-card header="角标" class="property-group" shadow="never">
|
||||||
<el-form-item label="角标" prop="badge.show">
|
<el-form-item label="角标" prop="badge.show">
|
||||||
|
@ -47,10 +72,36 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
|
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
|
||||||
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
|
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
|
||||||
<template #tip> 建议尺寸:36 * 22 </template>
|
<template #tip> 建议尺寸:36 * 22</template>
|
||||||
</UploadImg>
|
</UploadImg>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
<el-card header="按钮" class="property-group" shadow="never">
|
||||||
|
<el-form-item label="按钮类型" prop="btnBuy.type">
|
||||||
|
<el-radio-group v-model="formData.btnBuy.type">
|
||||||
|
<el-radio-button value="text">文字</el-radio-button>
|
||||||
|
<el-radio-button value="img">图片</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="formData.btnBuy.type === 'text'">
|
||||||
|
<el-form-item label="按钮文字" prop="btnBuy.text">
|
||||||
|
<el-input v-model="formData.btnBuy.text" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
|
||||||
|
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
|
||||||
|
<ColorInput v-model="formData.btnBuy.bgEndColor" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="图片" prop="btnBuy.imgUrl">
|
||||||
|
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
|
||||||
|
<template #tip> 建议尺寸:56 * 56</template>
|
||||||
|
</UploadImg>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
<el-card header="商品样式" class="property-group" shadow="never">
|
<el-card header="商品样式" class="property-group" shadow="never">
|
||||||
<el-form-item label="上圆角" prop="borderRadiusTop">
|
<el-form-item label="上圆角" prop="borderRadiusTop">
|
||||||
<el-slider
|
<el-slider
|
||||||
|
@ -92,6 +143,7 @@ import { PromotionSeckillProperty } from './config'
|
||||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||||
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue'
|
||||||
|
|
||||||
// 秒杀属性面板
|
// 秒杀属性面板
|
||||||
defineOptions({ name: 'PromotionSeckillProperty' })
|
defineOptions({ name: 'PromotionSeckillProperty' })
|
||||||
|
@ -100,7 +152,7 @@ const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||||
// 活动列表
|
// 活动列表
|
||||||
const activityList = ref<SeckillActivityApi.SeckillActivityVO>([])
|
const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const { list } = await SeckillActivityApi.getSeckillActivityPage({
|
const { list } = await SeckillActivityApi.getSeckillActivityPage({
|
||||||
status: CommonStatusEnum.ENABLE
|
status: CommonStatusEnum.ENABLE
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TabBarProperty, THEME_LIST } from './config'
|
import { TabBarProperty, component, THEME_LIST } from './config'
|
||||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||||
// 底部导航栏
|
// 底部导航栏
|
||||||
defineOptions({ name: 'TabBarProperty' })
|
defineOptions({ name: 'TabBarProperty' })
|
||||||
|
@ -88,6 +88,9 @@ const props = defineProps<{ modelValue: TabBarProperty }>()
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||||
|
|
||||||
|
// 将数据库的值更新到右侧属性栏
|
||||||
|
component.property.items = formData.value.items
|
||||||
|
|
||||||
// 要的主题
|
// 要的主题
|
||||||
const handleThemeChange = () => {
|
const handleThemeChange = () => {
|
||||||
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
|
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { isNumber } from '@/utils/is'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useLocaleStore } from '@/store/modules/locale'
|
import { useLocaleStore } from '@/store/modules/locale'
|
||||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||||
|
import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
|
||||||
|
|
||||||
defineOptions({ name: 'Editor' })
|
defineOptions({ name: 'Editor' })
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ const editorConfig = computed((): IEditorConfig => {
|
||||||
scroll: true,
|
scroll: true,
|
||||||
MENU_CONF: {
|
MENU_CONF: {
|
||||||
['uploadImage']: {
|
['uploadImage']: {
|
||||||
server: import.meta.env.VITE_UPLOAD_URL,
|
server: getUploadUrl(),
|
||||||
// 单个文件的最大体积限制,默认为 2M
|
// 单个文件的最大体积限制,默认为 2M
|
||||||
maxFileSize: 5 * 1024 * 1024,
|
maxFileSize: 5 * 1024 * 1024,
|
||||||
// 最多可上传几个文件,默认为 100
|
// 最多可上传几个文件,默认为 100
|
||||||
|
@ -136,7 +137,7 @@ const editorConfig = computed((): IEditorConfig => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
['uploadVideo']: {
|
['uploadVideo']: {
|
||||||
server: import.meta.env.VITE_UPLOAD_URL,
|
server: getUploadUrl(),
|
||||||
// 单个文件的最大体积限制,默认为 10M
|
// 单个文件的最大体积限制,默认为 10M
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
// 最多可上传几个文件,默认为 100
|
// 最多可上传几个文件,默认为 100
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const useDictSelectRule = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
field: 'dictValueType',
|
field: 'valueType',
|
||||||
title: '字典值类型',
|
title: '字典值类型',
|
||||||
value: 'str',
|
value: 'str',
|
||||||
options: [
|
options: [
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<template #file="{ file }">
|
<template #file="{ file }">
|
||||||
<img :src="file.url" class="upload-image" />
|
<img :src="file.url" class="upload-image" />
|
||||||
<div class="upload-handle" @click.stop>
|
<div class="upload-handle" @click.stop>
|
||||||
<div class="handle-icon" @click="handlePictureCardPreview(file)">
|
<div class="handle-icon" @click="imagePreview(file.url!)">
|
||||||
<Icon icon="ep:zoom-in" />
|
<Icon icon="ep:zoom-in" />
|
||||||
<span>查看</span>
|
<span>查看</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,16 +39,12 @@
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
<slot name="tip"></slot>
|
<slot name="tip"></slot>
|
||||||
</div>
|
</div>
|
||||||
<el-image-viewer
|
|
||||||
v-if="imgViewVisible"
|
|
||||||
:url-list="[viewImageUrl]"
|
|
||||||
@close="imgViewVisible = false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
|
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
|
||||||
import { ElNotification } from 'element-plus'
|
import { ElNotification } from 'element-plus'
|
||||||
|
import { createImageViewer } from '@/components/ImageViewer'
|
||||||
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||||
|
@ -56,6 +52,13 @@ import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||||
defineOptions({ name: 'UploadImgs' })
|
defineOptions({ name: 'UploadImgs' })
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
// 查看图片
|
||||||
|
const imagePreview = (imgUrl: string) => {
|
||||||
|
createImageViewer({
|
||||||
|
zIndex: 9999999,
|
||||||
|
urlList: [imgUrl]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type FileTypes =
|
type FileTypes =
|
||||||
| 'image/apng'
|
| 'image/apng'
|
||||||
|
@ -178,14 +181,6 @@ const handleExceed = () => {
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片预览
|
|
||||||
const viewImageUrl = ref('')
|
|
||||||
const imgViewVisible = ref(false)
|
|
||||||
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
|
|
||||||
viewImageUrl.value = uploadFile.url!
|
|
||||||
imgViewVisible.value = true
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -3,9 +3,16 @@ import CryptoJS from 'crypto-js'
|
||||||
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
|
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得上传 URL
|
||||||
|
*/
|
||||||
|
export const getUploadUrl = (): string => {
|
||||||
|
return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
|
||||||
|
}
|
||||||
|
|
||||||
export const useUpload = () => {
|
export const useUpload = () => {
|
||||||
// 后端上传地址
|
// 后端上传地址
|
||||||
const uploadUrl = import.meta.env.VITE_UPLOAD_URL
|
const uploadUrl = getUploadUrl()
|
||||||
// 是否使用前端直连上传
|
// 是否使用前端直连上传
|
||||||
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
|
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
|
||||||
// 重写ElUpload上传方法
|
// 重写ElUpload上传方法
|
||||||
|
@ -17,16 +24,18 @@ export const useUpload = () => {
|
||||||
// 1.2 获取文件预签名地址
|
// 1.2 获取文件预签名地址
|
||||||
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
|
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
|
||||||
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
|
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
|
||||||
return axios.put(presignedInfo.uploadUrl, options.file, {
|
return axios
|
||||||
headers: {
|
.put(presignedInfo.uploadUrl, options.file, {
|
||||||
'Content-Type': options.file.type,
|
headers: {
|
||||||
}
|
'Content-Type': options.file.type
|
||||||
}).then(() => {
|
}
|
||||||
// 1.4. 记录文件信息到后端(异步)
|
})
|
||||||
createFile(presignedInfo, fileName, options.file)
|
.then(() => {
|
||||||
// 通知成功,数据格式保持与后端上传的返回结果一致
|
// 1.4. 记录文件信息到后端(异步)
|
||||||
return { data: presignedInfo.url }
|
createFile(presignedInfo, fileName, options.file)
|
||||||
})
|
// 通知成功,数据格式保持与后端上传的返回结果一致
|
||||||
|
return { data: presignedInfo.url }
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// 模式二:后端上传
|
// 模式二:后端上传
|
||||||
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
||||||
|
|
|
@ -5,7 +5,7 @@ import remainingRouter from './modules/remaining'
|
||||||
|
|
||||||
// 创建路由实例
|
// 创建路由实例
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#
|
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||||
strict: true,
|
strict: true,
|
||||||
routes: remainingRouter as RouteRecordRaw[],
|
routes: remainingRouter as RouteRecordRaw[],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||||
|
|
|
@ -610,6 +610,38 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||||
hidden: true,
|
hidden: true,
|
||||||
breadcrumb: false
|
breadcrumb: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/iot',
|
||||||
|
component: Layout,
|
||||||
|
name: 'IOT',
|
||||||
|
meta: {
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'product/detail/:id',
|
||||||
|
name: 'IoTProductDetail',
|
||||||
|
meta: {
|
||||||
|
title: '产品详情',
|
||||||
|
noCache: true,
|
||||||
|
hidden: true,
|
||||||
|
activeMenu: '/iot/product'
|
||||||
|
},
|
||||||
|
component: () => import('@/views/iot/product/detail/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'device/detail/:id',
|
||||||
|
name: 'IoTDeviceDetail',
|
||||||
|
meta: {
|
||||||
|
title: '设备详情',
|
||||||
|
noCache: true,
|
||||||
|
hidden: true,
|
||||||
|
activeMenu: '/iot/device'
|
||||||
|
},
|
||||||
|
component: () => import('@/views/iot/device/detail/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,6 @@ export enum DICT_TYPE {
|
||||||
PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
|
PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
|
||||||
PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
|
PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
|
||||||
PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
|
PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
|
||||||
PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
|
|
||||||
PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
|
PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
|
||||||
PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
|
PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
|
||||||
PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态
|
PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态
|
||||||
|
@ -227,5 +226,18 @@ export enum DICT_TYPE {
|
||||||
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
||||||
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
||||||
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
|
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
|
||||||
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
|
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
|
||||||
|
|
||||||
|
// ========== IOT - 物联网模块 ==========
|
||||||
|
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
|
||||||
|
IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别
|
||||||
|
IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
|
||||||
|
IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
|
||||||
|
IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
|
||||||
|
IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
|
||||||
|
IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
|
||||||
|
IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
|
||||||
|
IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
|
||||||
|
IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
|
||||||
|
IOT_RW_TYPE = 'iot_rw_type' // IOT 读写类型
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,8 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
|
||||||
// 2. 生成 data(AppRouteRecordRaw)
|
// 2. 生成 data(AppRouteRecordRaw)
|
||||||
// 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive
|
// 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive
|
||||||
let data: AppRouteRecordRaw = {
|
let data: AppRouteRecordRaw = {
|
||||||
path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
|
path:
|
||||||
|
route.path.indexOf('?') > -1 && !isUrl(route.path) ? route.path.split('?')[0] : route.path, // 注意,需要排除 http 这种 url,避免它带 ? 参数被截取掉
|
||||||
name:
|
name:
|
||||||
route.componentName && route.componentName.length > 0
|
route.componentName && route.componentName.length > 0
|
||||||
? route.componentName
|
? route.componentName
|
||||||
|
|
|
@ -1,142 +1,279 @@
|
||||||
<template>
|
<template>
|
||||||
<Form
|
<el-form
|
||||||
v-show="getShow"
|
v-show="getShow"
|
||||||
:rules="rules"
|
ref="formLogin"
|
||||||
:schema="schema"
|
:model="registerData.registerForm"
|
||||||
class="w-[100%] dark:(border-1 border-[var(--el-border-color)] border-solid)"
|
:rules="registerRules"
|
||||||
hide-required-asterisk
|
class="login-form"
|
||||||
label-position="top"
|
label-position="top"
|
||||||
|
label-width="120px"
|
||||||
size="large"
|
size="large"
|
||||||
@register="register"
|
|
||||||
>
|
>
|
||||||
<template #title>
|
<el-row style="margin-right: -10px; margin-left: -10px">
|
||||||
<LoginFormTitle style="width: 100%" />
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
</template>
|
<el-form-item>
|
||||||
|
<LoginFormTitle style="width: 100%" />
|
||||||
<template #code="form">
|
</el-form-item>
|
||||||
<div class="w-[100%] flex">
|
</el-col>
|
||||||
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
</div>
|
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
|
||||||
</template>
|
<el-input
|
||||||
|
v-model="registerData.registerForm.tenantName"
|
||||||
<template #register>
|
:placeholder="t('login.tenantname')"
|
||||||
<div class="w-[100%]">
|
:prefix-icon="iconHouse"
|
||||||
<XButton
|
link
|
||||||
:loading="loading"
|
type="primary"
|
||||||
:title="t('login.register')"
|
size="large"
|
||||||
class="w-[100%]"
|
/>
|
||||||
type="primary"
|
</el-form-item>
|
||||||
@click="loginRegister()"
|
</el-col>
|
||||||
/>
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
</div>
|
<el-form-item prop="username">
|
||||||
<div class="mt-15px w-[100%]">
|
<el-input
|
||||||
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
v-model="registerData.registerForm.username"
|
||||||
</div>
|
:placeholder="t('login.username')"
|
||||||
</template>
|
size="large"
|
||||||
</Form>
|
:prefix-icon="iconAvatar"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="registerData.registerForm.nickname"
|
||||||
|
placeholder="昵称"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="iconAvatar"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="registerData.registerForm.password"
|
||||||
|
type="password"
|
||||||
|
auto-complete="off"
|
||||||
|
:placeholder="t('login.password')"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="iconLock"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
|
<el-form-item prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="registerData.registerForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
size="large"
|
||||||
|
auto-complete="off"
|
||||||
|
:placeholder="t('login.checkPassword')"
|
||||||
|
:prefix-icon="iconLock"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||||
|
<el-form-item>
|
||||||
|
<XButton
|
||||||
|
:loading="loginLoading"
|
||||||
|
:title="t('login.register')"
|
||||||
|
class="w-[100%]"
|
||||||
|
type="primary"
|
||||||
|
@click="getCode()"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<Verify
|
||||||
|
ref="verify"
|
||||||
|
:captchaType="captchaType"
|
||||||
|
:imgSize="{ width: '400px', height: '200px' }"
|
||||||
|
mode="pop"
|
||||||
|
@success="handleRegister"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
||||||
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { FormRules } from 'element-plus'
|
import { ElLoading } from 'element-plus'
|
||||||
|
|
||||||
import { useForm } from '@/hooks/web/useForm'
|
|
||||||
import { useValidator } from '@/hooks/web/useValidator'
|
|
||||||
import LoginFormTitle from './LoginFormTitle.vue'
|
import LoginFormTitle from './LoginFormTitle.vue'
|
||||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
import { useIcon } from '@/hooks/web/useIcon'
|
||||||
|
import * as authUtil from '@/utils/auth'
|
||||||
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
|
import * as LoginApi from '@/api/login'
|
||||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||||
import { FormSchema } from '@/types/form'
|
|
||||||
|
|
||||||
defineOptions({ name: 'RegisterForm' })
|
defineOptions({ name: 'RegisterForm' })
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { required } = useValidator()
|
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||||
const { register, elFormRef } = useForm()
|
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
||||||
|
const iconLock = useIcon({ icon: 'ep:lock' })
|
||||||
|
const formLogin = ref()
|
||||||
const { handleBackLogin, getLoginState } = useLoginState()
|
const { handleBackLogin, getLoginState } = useLoginState()
|
||||||
|
const { currentRoute, push } = useRouter()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
const redirect = ref<string>('')
|
||||||
|
const loginLoading = ref(false)
|
||||||
|
const verify = ref()
|
||||||
|
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
|
||||||
|
|
||||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||||
|
|
||||||
const schema = reactive<FormSchema[]>([
|
const equalToPassword = (rule, value, callback) => {
|
||||||
{
|
if (registerData.registerForm.password !== value) {
|
||||||
field: 'title',
|
callback(new Error('两次输入的密码不一致'))
|
||||||
colProps: {
|
} else {
|
||||||
span: 24
|
callback()
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'username',
|
|
||||||
label: t('login.username'),
|
|
||||||
value: '',
|
|
||||||
component: 'Input',
|
|
||||||
colProps: {
|
|
||||||
span: 24
|
|
||||||
},
|
|
||||||
componentProps: {
|
|
||||||
placeholder: t('login.usernamePlaceholder')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'password',
|
|
||||||
label: t('login.password'),
|
|
||||||
value: '',
|
|
||||||
component: 'InputPassword',
|
|
||||||
colProps: {
|
|
||||||
span: 24
|
|
||||||
},
|
|
||||||
componentProps: {
|
|
||||||
style: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
strength: true,
|
|
||||||
placeholder: t('login.passwordPlaceholder')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'check_password',
|
|
||||||
label: t('login.checkPassword'),
|
|
||||||
value: '',
|
|
||||||
component: 'InputPassword',
|
|
||||||
colProps: {
|
|
||||||
span: 24
|
|
||||||
},
|
|
||||||
componentProps: {
|
|
||||||
style: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
strength: true,
|
|
||||||
placeholder: t('login.passwordPlaceholder')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'code',
|
|
||||||
label: t('login.code'),
|
|
||||||
colProps: {
|
|
||||||
span: 24
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'register',
|
|
||||||
colProps: {
|
|
||||||
span: 24
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
])
|
|
||||||
|
|
||||||
const rules: FormRules = {
|
|
||||||
username: [required()],
|
|
||||||
password: [required()],
|
|
||||||
check_password: [required()],
|
|
||||||
code: [required()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const registerRules = {
|
||||||
|
tenantName: [
|
||||||
|
{ required: true, trigger: 'blur', message: '请输入您所属的租户' },
|
||||||
|
{ min: 2, max: 20, message: '租户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{ required: true, trigger: 'blur', message: '请输入您的账号' },
|
||||||
|
{ min: 4, max: 30, message: '用户账号长度必须介于 4 和 30 之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
nickname: [
|
||||||
|
{ required: true, trigger: 'blur', message: '请输入您的昵称' },
|
||||||
|
{ min: 0, max: 30, message: '昵称长度必须介于 0 和 30 之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, trigger: 'blur', message: '请输入您的密码' },
|
||||||
|
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
|
||||||
|
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
|
||||||
|
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const loginRegister = async () => {
|
const registerData = reactive({
|
||||||
const formRef = unref(elFormRef)
|
isShowPassword: false,
|
||||||
formRef?.validate(async (valid) => {
|
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||||
if (valid) {
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||||
try {
|
registerForm: {
|
||||||
loading.value = true
|
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
|
||||||
} finally {
|
nickname: '',
|
||||||
loading.value = false
|
tenantId: 0,
|
||||||
}
|
username: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
captchaVerification: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提交注册
|
||||||
|
const handleRegister = async (params: any) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
if (registerData.tenantEnable) {
|
||||||
|
await getTenantId()
|
||||||
|
registerData.registerForm.tenantId = authUtil.getTenantId()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if (registerData.captchaEnable) {
|
||||||
|
registerData.registerForm.captchaVerification = params.captchaVerification
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await LoginApi.register(registerData.registerForm)
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '正在加载系统中...',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
})
|
||||||
|
|
||||||
|
authUtil.removeLoginForm()
|
||||||
|
|
||||||
|
authUtil.setToken(res)
|
||||||
|
if (!redirect.value) {
|
||||||
|
redirect.value = '/'
|
||||||
|
}
|
||||||
|
// 判断是否为SSO登录
|
||||||
|
if (redirect.value.indexOf('sso') !== -1) {
|
||||||
|
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||||
|
} else {
|
||||||
|
push({ path: redirect.value || permissionStore.addRouters[0].path })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loginLoading.value = false
|
||||||
|
loading.value.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
const getCode = async () => {
|
||||||
|
// 情况一,未开启:则直接注册
|
||||||
|
if (registerData.captchaEnable === 'false') {
|
||||||
|
await handleRegister({})
|
||||||
|
} else {
|
||||||
|
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行注册
|
||||||
|
// 弹出验证码
|
||||||
|
verify.value.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取租户 ID
|
||||||
|
const getTenantId = async () => {
|
||||||
|
if (registerData.tenantEnable === 'true') {
|
||||||
|
const res = await LoginApi.getTenantIdByName(registerData.registerForm.tenantName)
|
||||||
|
authUtil.setTenantId(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据域名,获得租户信息
|
||||||
|
const getTenantByWebsite = async () => {
|
||||||
|
const website = location.host
|
||||||
|
const res = await LoginApi.getTenantByWebsite(website)
|
||||||
|
if (res) {
|
||||||
|
registerData.registerForm.tenantName = res.name
|
||||||
|
authUtil.setTenantId(res.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loading = ref() // ElLoading.service 返回的实例
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentRoute.value,
|
||||||
|
(route: RouteLocationNormalizedLoaded) => {
|
||||||
|
redirect.value = route?.query?.redirect as string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
// getCookie()
|
||||||
|
getTenantByWebsite()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.anticon) {
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-code {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100px;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="所属流程" prop="processDefinitionId">
|
<el-form-item label="所属流程" prop="processDefinitionKey">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.processDefinitionId"
|
v-model="queryParams.processDefinitionKey"
|
||||||
placeholder="请输入流程定义的编号"
|
placeholder="请输入流程定义的标识"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
|
@ -183,7 +183,7 @@ const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
name: '',
|
name: '',
|
||||||
processDefinitionId: undefined,
|
processDefinitionKey: undefined,
|
||||||
category: undefined,
|
category: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
createTime: []
|
createTime: []
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
ref="permissionListRef"
|
ref="permissionListRef"
|
||||||
:biz-id="contract.id!"
|
:biz-id="contract.id!"
|
||||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||||
:show-action="!permissionListRef?.isPool || false"
|
:show-action="true"
|
||||||
@quit-team="close"
|
@quit-team="close"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import { useWebSocket } from '@vueuse/core'
|
import { useWebSocket } from '@vueuse/core'
|
||||||
import { getAccessToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import * as UserApi from '@/api/system/user'
|
import * as UserApi from '@/api/system/user'
|
||||||
|
|
||||||
defineOptions({ name: 'InfraWebSocket' })
|
defineOptions({ name: 'InfraWebSocket' })
|
||||||
|
@ -79,7 +79,9 @@ defineOptions({ name: 'InfraWebSocket' })
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
const server = ref(
|
const server = ref(
|
||||||
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
|
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
|
||||||
|
'?token=' +
|
||||||
|
getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
|
||||||
) // WebSocket 服务地址
|
) // WebSocket 服务地址
|
||||||
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
|
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
|
||||||
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
|
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="产品" prop="productId">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.productId"
|
||||||
|
placeholder="请选择产品"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="product in products"
|
||||||
|
:key="product.id"
|
||||||
|
:label="product.name"
|
||||||
|
:value="product.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="DeviceName" prop="deviceName">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.deviceName"
|
||||||
|
placeholder="请输入 DeviceName"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注名称" prop="nickname">
|
||||||
|
<el-input v-model="formData.nickname" placeholder="请输入备注名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DeviceApi, DeviceVO } from '@/api/iot/device'
|
||||||
|
import { ProductApi } from '@/api/iot/product'
|
||||||
|
|
||||||
|
/** IoT 设备 表单 */
|
||||||
|
defineOptions({ name: 'IoTDeviceForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('') // 弹窗的标题
|
||||||
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
deviceName: undefined,
|
||||||
|
nickname: undefined
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
|
||||||
|
deviceName: [
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
|
||||||
|
message:
|
||||||
|
'支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nickname: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
|
||||||
|
if (length < 4 || length > 64) {
|
||||||
|
callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符'))
|
||||||
|
} else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
|
||||||
|
callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线(_)'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
// 修改时,设置数据
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await DeviceApi.getDevice(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 校验表单
|
||||||
|
await formRef.value.validate()
|
||||||
|
// 提交请求
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as DeviceVO
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await DeviceApi.createDevice(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await DeviceApi.updateDevice(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 发送操作成功的事件
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
deviceName: undefined,
|
||||||
|
nickname: undefined
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询字典下拉列表 */
|
||||||
|
const products = ref()
|
||||||
|
const getProducts = async () => {
|
||||||
|
products.value = await ProductApi.getSimpleProductList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getProducts()
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<el-col>
|
||||||
|
<el-row>
|
||||||
|
<span class="text-xl font-bold">{{ device.deviceName }}</span>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- 右上:按钮 -->
|
||||||
|
<el-button
|
||||||
|
@click="openForm('update', device.id)"
|
||||||
|
v-hasPermi="['iot:device:update']"
|
||||||
|
v-if="product.status === 0"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ContentWrap class="mt-10px">
|
||||||
|
<el-descriptions :column="5" direction="horizontal">
|
||||||
|
<el-descriptions-item label="产品">
|
||||||
|
<el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ProductKey">
|
||||||
|
{{ product.productKey }}
|
||||||
|
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</ContentWrap>
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<DeviceForm ref="formRef" @success="emit('refresh')" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import DeviceForm from '@/views/iot/device/DeviceForm.vue'
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
import { DeviceVO } from '@/api/iot/device'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 操作修改
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将文本复制到剪贴板
|
||||||
|
*
|
||||||
|
* @param text 需要复制的文本
|
||||||
|
*/
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
// TODO @haohao:可以考虑用 await 异步转同步哈
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
message.success('复制成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到产品详情页面
|
||||||
|
*
|
||||||
|
* @param productId 产品 ID
|
||||||
|
*/
|
||||||
|
const goToProductDetail = (productId: number) => {
|
||||||
|
router.push({ name: 'IoTProductDetail', params: { id: productId } })
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<el-collapse v-model="activeNames">
|
||||||
|
<el-descriptions :column="3" title="设备信息">
|
||||||
|
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ProductKey">
|
||||||
|
{{ product.productKey }}
|
||||||
|
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备类型">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="DeviceName">
|
||||||
|
{{ device.deviceName }}
|
||||||
|
<el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ formatDate(device.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="激活时间">
|
||||||
|
{{ formatDate(device.activeTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后上线时间">
|
||||||
|
{{ formatDate(device.lastOnlineTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前状态">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后离线时间" :span="3">
|
||||||
|
{{ formatDate(device.lastOfflineTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="MQTT 连接参数">
|
||||||
|
<el-button type="primary" @click="openMqttParams">查看</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<!-- MQTT 连接参数弹框 -->
|
||||||
|
<Dialog
|
||||||
|
title="MQTT 连接参数"
|
||||||
|
v-model="mqttDialogVisible"
|
||||||
|
width="50%"
|
||||||
|
:before-close="handleCloseMqttDialog"
|
||||||
|
>
|
||||||
|
<el-form :model="mqttParams" label-width="120px">
|
||||||
|
<el-form-item label="clientId">
|
||||||
|
<el-input v-model="mqttParams.mqttClientId" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
|
||||||
|
<Icon icon="ph:copy" />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="username">
|
||||||
|
<el-input v-model="mqttParams.mqttUsername" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
|
||||||
|
<Icon icon="ph:copy" />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="passwd">
|
||||||
|
<el-input v-model="mqttParams.mqttPassword" readonly type="password">
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
|
||||||
|
<Icon icon="ph:copy" />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="mqttDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
import { DeviceVO } from '@/api/iot/device'
|
||||||
|
|
||||||
|
const message = useMessage() // 消息提示
|
||||||
|
|
||||||
|
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh']) // 定义 Emits
|
||||||
|
|
||||||
|
const activeNames = ref(['basicInfo']) // 展示的折叠面板
|
||||||
|
const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
|
||||||
|
const mqttParams = ref({
|
||||||
|
mqttClientId: '',
|
||||||
|
mqttUsername: '',
|
||||||
|
mqttPassword: ''
|
||||||
|
}) // 定义 MQTT 参数对象
|
||||||
|
|
||||||
|
/** 复制到剪贴板方法 */
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
message.success('复制成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开 MQTT 参数弹框的方法 */
|
||||||
|
const openMqttParams = () => {
|
||||||
|
mqttParams.value = {
|
||||||
|
mqttClientId: device.mqttClientId || 'N/A',
|
||||||
|
mqttUsername: device.mqttUsername || 'N/A',
|
||||||
|
mqttPassword: device.mqttPassword || 'N/A'
|
||||||
|
}
|
||||||
|
mqttDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭 MQTT 弹框的方法 */
|
||||||
|
const handleCloseMqttDialog = () => {
|
||||||
|
mqttDialogVisible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<DeviceDetailsHeader
|
||||||
|
:loading="loading"
|
||||||
|
:product="product"
|
||||||
|
:device="device"
|
||||||
|
@refresh="getDeviceData(id)"
|
||||||
|
/>
|
||||||
|
<el-col>
|
||||||
|
<el-tabs>
|
||||||
|
<el-tab-pane label="设备信息">
|
||||||
|
<DeviceDetailsInfo :product="product" :device="device" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Topic 列表" />
|
||||||
|
<el-tab-pane label="物模型数据" />
|
||||||
|
<el-tab-pane label="子设备管理" />
|
||||||
|
</el-tabs>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
|
import { DeviceApi, DeviceVO } from '@/api/iot/device'
|
||||||
|
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||||
|
import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue'
|
||||||
|
import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'IoTDeviceDetail' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
const id = Number(route.params.id) // 编号
|
||||||
|
const loading = ref(true) // 加载中
|
||||||
|
const product = ref<ProductVO>({} as ProductVO) // 产品详情
|
||||||
|
const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
|
||||||
|
|
||||||
|
/** 获取设备详情 */
|
||||||
|
const getDeviceData = async (id: number) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
device.value = await DeviceApi.getDevice(id)
|
||||||
|
console.log(product.value)
|
||||||
|
await getProductData(device.value.productId)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取产品详情 */
|
||||||
|
const getProductData = async (id: number) => {
|
||||||
|
product.value = await ProductApi.getProduct(id)
|
||||||
|
console.log(product.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取物模型 */
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
const { delView } = useTagsViewStore() // 视图操作
|
||||||
|
const { currentRoute } = useRouter() // 路由
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!id) {
|
||||||
|
message.warning('参数错误,产品不能为空!')
|
||||||
|
delView(unref(currentRoute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await getDeviceData(id)
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,267 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="产品" prop="productId">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.productId"
|
||||||
|
placeholder="请选择产品"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="product in products"
|
||||||
|
:key="product.id"
|
||||||
|
:label="product.name"
|
||||||
|
:value="product.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="DeviceName" prop="deviceName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deviceName"
|
||||||
|
placeholder="请输入 DeviceName"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注名称" prop="nickname">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.nickname"
|
||||||
|
placeholder="请输入备注名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备类型" prop="deviceType">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.deviceType"
|
||||||
|
placeholder="请选择设备类型"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备状态" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.status"
|
||||||
|
placeholder="请选择设备状态"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</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="['iot:device:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" />
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="DeviceName" align="center" prop="deviceName">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注名称" align="center" prop="nickname" />
|
||||||
|
<el-table-column label="设备所属产品" align="center" prop="productId">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ productMap[scope.row.productId] }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备类型" align="center" prop="deviceType">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备状态" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="最后上线时间"
|
||||||
|
align="center"
|
||||||
|
prop="lastOnlineTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作" align="center" min-width="120px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openDetail(scope.row.id)"
|
||||||
|
v-hasPermi="['iot:product:query']"
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
v-hasPermi="['iot:device:update']"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['iot:device:delete']"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<DeviceForm ref="formRef" @success="getList" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import { DeviceApi, DeviceVO } from '@/api/iot/device'
|
||||||
|
import DeviceForm from './DeviceForm.vue'
|
||||||
|
import { ProductApi } from '@/api/iot/product'
|
||||||
|
|
||||||
|
/** IoT 设备 列表 */
|
||||||
|
defineOptions({ name: 'IoTDevice' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<DeviceVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deviceName: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
deviceType: undefined,
|
||||||
|
nickname: undefined,
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
|
/** 产品标号和名称的映射 */
|
||||||
|
const productMap = reactive({})
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await DeviceApi.getDevicePage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
// 获取产品ID列表
|
||||||
|
const productIds = [...new Set(data.list.map((device) => device.productId))]
|
||||||
|
// 获取产品名称
|
||||||
|
// TODO @haohao:最好后端拼接哈
|
||||||
|
const products = await Promise.all(productIds.map((id) => ProductApi.getProduct(id)))
|
||||||
|
products.forEach((product) => {
|
||||||
|
productMap[product.id] = product.name
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改操作 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开详情 */
|
||||||
|
const { push } = useRouter()
|
||||||
|
const openDetail = (id: number) => {
|
||||||
|
push({ name: 'IoTDeviceDetail', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await DeviceApi.deleteDevice(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询字典下拉列表 */
|
||||||
|
const products = ref()
|
||||||
|
const getProducts = async () => {
|
||||||
|
products.value = await ProductApi.getSimpleProductList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
getProducts()
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,204 @@
|
||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="产品名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入产品名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="设备类型" prop="deviceType">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.deviceType"
|
||||||
|
placeholder="请选择设备类型"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item
|
||||||
|
v-if="formData.deviceType === 0 || formData.deviceType === 2"
|
||||||
|
label="联网方式"
|
||||||
|
prop="netType"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-model="formData.netType"
|
||||||
|
placeholder="请选择联网方式"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="formData.deviceType === 1" label="接入网关协议" prop="protocolType">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.protocolType"
|
||||||
|
placeholder="请选择接入网关协议"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="数据格式" prop="dataFormat">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.dataFormat"
|
||||||
|
placeholder="请选择接数据格式"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="数据校验级别" prop="validateType">
|
||||||
|
<el-radio-group v-model="formData.validateType" :disabled="formType === 'update'">
|
||||||
|
<el-radio
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="产品描述" prop="description">
|
||||||
|
<el-input type="textarea" v-model="formData.description" placeholder="请输入产品描述" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
|
||||||
|
defineOptions({ name: 'IoTProductForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const formLoading = ref(false)
|
||||||
|
const formType = ref('')
|
||||||
|
const formData = ref({
|
||||||
|
name: undefined,
|
||||||
|
id: undefined,
|
||||||
|
productKey: undefined,
|
||||||
|
protocolId: undefined,
|
||||||
|
categoryId: undefined,
|
||||||
|
description: undefined,
|
||||||
|
validateType: undefined,
|
||||||
|
status: undefined,
|
||||||
|
deviceType: undefined,
|
||||||
|
netType: undefined,
|
||||||
|
protocolType: undefined,
|
||||||
|
dataFormat: undefined
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
|
||||||
|
deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
|
||||||
|
netType: [
|
||||||
|
{
|
||||||
|
// TODO @haohao:0、1、/2 最好前端也枚举下;另外,这里的 required 可以直接设置为 true。然后表单那些 v-if。只要不存在,它自动就不校验了哈
|
||||||
|
required: formData.deviceType === 0 || formData.deviceType === 2,
|
||||||
|
message: '联网方式不能为空',
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
protocolType: [
|
||||||
|
{ required: formData.deviceType === 1, message: '接入网关协议不能为空', trigger: 'change' }
|
||||||
|
],
|
||||||
|
dataFormat: [{ required: true, message: '数据格式不能为空', trigger: 'change' }],
|
||||||
|
validateType: [{ required: true, message: '数据校验级别不能为空', trigger: 'change' }]
|
||||||
|
})
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await ProductApi.getProduct(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open, close: () => (dialogVisible.value = false) })
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as ProductVO
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await ProductApi.createProduct(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await ProductApi.updateProduct(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false // 确保关闭弹框
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
name: undefined,
|
||||||
|
id: undefined,
|
||||||
|
productKey: undefined,
|
||||||
|
protocolId: undefined,
|
||||||
|
categoryId: undefined,
|
||||||
|
description: undefined,
|
||||||
|
validateType: undefined,
|
||||||
|
status: undefined,
|
||||||
|
deviceType: undefined,
|
||||||
|
netType: undefined,
|
||||||
|
protocolType: undefined,
|
||||||
|
dataFormat: undefined
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<el-col>
|
||||||
|
<el-row>
|
||||||
|
<span class="text-xl font-bold">{{ product.name }}</span>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- 右上:按钮 -->
|
||||||
|
<el-button
|
||||||
|
@click="openForm('update', product.id)"
|
||||||
|
v-hasPermi="['iot:product:update']"
|
||||||
|
v-if="product.status === 0"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="confirmPublish(product.id)"
|
||||||
|
v-hasPermi="['iot:product:update']"
|
||||||
|
v-if="product.status === 0"
|
||||||
|
>
|
||||||
|
发布
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
@click="confirmUnpublish(product.id)"
|
||||||
|
v-hasPermi="['iot:product:update']"
|
||||||
|
v-if="product.status === 1"
|
||||||
|
>
|
||||||
|
撤销发布
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ContentWrap class="mt-10px">
|
||||||
|
<el-descriptions :column="5" direction="horizontal">
|
||||||
|
<el-descriptions-item label="ProductKey">
|
||||||
|
{{ product.productKey }}
|
||||||
|
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-descriptions :column="5" direction="horizontal">
|
||||||
|
<el-descriptions-item label="设备数">
|
||||||
|
{{ product.deviceCount }}
|
||||||
|
<el-button @click="goToManagement(product.id)">前往管理</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</ContentWrap>
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<ProductForm ref="formRef" @success="emit('refresh')" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ProductForm from '@/views/iot/product/ProductForm.vue'
|
||||||
|
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { product } = defineProps<{ product: ProductVO }>() // 定义 Props
|
||||||
|
|
||||||
|
/** 处理复制 */
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
message.success('复制成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 路由跳转到设备管理 */
|
||||||
|
const { push } = useRouter()
|
||||||
|
const goToManagement = (productId: string) => {
|
||||||
|
push({ name: 'IoTDevice', query: { productId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 操作修改 */
|
||||||
|
const emit = defineEmits(['refresh']) // 定义 Emits
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
const confirmPublish = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await ProductApi.updateProductStatus(id, 1)
|
||||||
|
message.success('发布成功')
|
||||||
|
formRef.value.close() // 关闭弹框
|
||||||
|
emit('refresh')
|
||||||
|
} catch (error) {
|
||||||
|
message.error('发布失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const confirmUnpublish = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await ProductApi.updateProductStatus(id, 0)
|
||||||
|
message.success('撤销发布成功')
|
||||||
|
formRef.value.close() // 关闭弹框
|
||||||
|
emit('refresh')
|
||||||
|
} catch (error) {
|
||||||
|
message.error('撤销发布失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<el-collapse v-model="activeNames">
|
||||||
|
<el-descriptions :column="3" title="产品信息">
|
||||||
|
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备类型">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ formatDate(product.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数据格式">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_DATA_FORMAT" :value="product.dataFormat" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数据校验级别">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_VALIDATE_TYPE" :value="product.validateType" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品状态">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item
|
||||||
|
label="联网方式"
|
||||||
|
v-if="product.deviceType === 0 || product.deviceType === 2"
|
||||||
|
>
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="接入网关协议" v-if="product.deviceType === 1">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-collapse>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
|
||||||
|
const { product } = defineProps<{ product: ProductVO }>()
|
||||||
|
|
||||||
|
// 展示的折叠面板
|
||||||
|
const activeNames = ref(['basicInfo'])
|
||||||
|
</script>
|
|
@ -0,0 +1,243 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<el-tabs>
|
||||||
|
<el-tab-pane label="基础通信 Topic">
|
||||||
|
<Table
|
||||||
|
:columns="columns1"
|
||||||
|
:data="data1"
|
||||||
|
:span-method="createSpanMethod(data1)"
|
||||||
|
align="left"
|
||||||
|
headerAlign="left"
|
||||||
|
border="true"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="物模型通信 Topic">
|
||||||
|
<Table
|
||||||
|
:columns="columns2"
|
||||||
|
:data="data2"
|
||||||
|
:span-method="createSpanMethod(data2)"
|
||||||
|
align="left"
|
||||||
|
headerAlign="left"
|
||||||
|
border="true"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
|
||||||
|
const props = defineProps<{ product: ProductVO }>()
|
||||||
|
|
||||||
|
// 定义列
|
||||||
|
const columns1 = reactive([
|
||||||
|
{ label: '功能', field: 'function', width: 150 },
|
||||||
|
{ label: 'Topic 类', field: 'topicClass', width: 800 },
|
||||||
|
{ label: '操作权限', field: 'operationPermission', width: 100 },
|
||||||
|
{ label: '描述', field: 'description' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const columns2 = reactive([
|
||||||
|
{ label: '功能', field: 'function', width: 150 },
|
||||||
|
{ label: 'Topic 类', field: 'topicClass', width: 800 },
|
||||||
|
{ label: '操作权限', field: 'operationPermission', width: 100 },
|
||||||
|
{ label: '描述', field: 'description' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// TODO @haohao:这个,有没可能写到一个枚举里,方便后续维护? /Users/yunai/Java/yudao-ui-admin-vue3/src/views/ai/utils/constants.ts
|
||||||
|
const data1 = computed(() => {
|
||||||
|
if (!props.product || !props.product.productKey) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
function: 'OTA 升级',
|
||||||
|
topicClass: `/ota/device/inform/${props.product.productKey}/\${deviceName}`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备上报固件升级信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: 'OTA 升级',
|
||||||
|
topicClass: `/ota/device/upgrade/${props.product.productKey}/\${deviceName}`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '固件升级信息下行'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: 'OTA 升级',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备上报固件升级进度'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: 'OTA 升级',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备主动拉取固件升级信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备标签',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备上报标签数据'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备标签',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update_reply`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端响应标签上报'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备标签',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '设备删除标签信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备标签',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete_reply`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端响应标签删除'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '时钟同步',
|
||||||
|
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/request`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: 'NTP 时钟同步请求'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '时钟同步',
|
||||||
|
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/response`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: 'NTP 时钟同步响应'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备影子',
|
||||||
|
topicClass: `/shadow/update/${props.product.productKey}/\${deviceName}`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备影子发布'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '设备影子',
|
||||||
|
topicClass: `/shadow/get/${props.product.productKey}/\${deviceName}`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '设备接收影子变更'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '配置更新',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/push`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端主动下推配置信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '配置更新',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备端查询配置信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '配置更新',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get_reply`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端响应配置信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '广播',
|
||||||
|
topicClass: `/broadcast/${props.product.productKey}/\${identifier}`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '广播 Topic,identifier 为用户自定义字符串'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const data2 = computed(() => {
|
||||||
|
if (!props.product || !props.product.productKey) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
function: '属性上报',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备属性上报'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '属性上报',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post_reply`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端响应属性上报'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '属性设置',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/property/set`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '设备属性设置'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '事件上报',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备事件上报'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '事件上报',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post_reply`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '云端响应事件上报'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '服务调用',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}`,
|
||||||
|
operationPermission: '订阅',
|
||||||
|
description: '设备服务调用'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: '服务调用',
|
||||||
|
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}_reply`,
|
||||||
|
operationPermission: '发布',
|
||||||
|
description: '设备端响应服务调用'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 通用的单元格合并方法生成器
|
||||||
|
const createSpanMethod = (data: any[]) => {
|
||||||
|
// 预处理,计算每个功能的合并行数
|
||||||
|
const rowspanMap: Record<number, number> = {}
|
||||||
|
let currentFunction = ''
|
||||||
|
let startIndex = 0
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
if (item.function !== currentFunction) {
|
||||||
|
if (count > 0) {
|
||||||
|
rowspanMap[startIndex] = count
|
||||||
|
}
|
||||||
|
currentFunction = item.function
|
||||||
|
startIndex = index
|
||||||
|
count = 1
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理最后一组
|
||||||
|
if (count > 0) {
|
||||||
|
rowspanMap[startIndex] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 span 方法
|
||||||
|
return ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
// 仅对“功能”列进行合并
|
||||||
|
const rowspan = rowspanMap[rowIndex] || 0
|
||||||
|
if (rowspan > 0) {
|
||||||
|
return {
|
||||||
|
rowspan,
|
||||||
|
colspan: 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
rowspan: 0,
|
||||||
|
colspan: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="功能类型" prop="name">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.type"
|
||||||
|
placeholder="请选择功能类型"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</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="['iot:think-model-function:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" /> 添加功能
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
<ContentWrap>
|
||||||
|
<el-tabs>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="功能类型" align="center" prop="type">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE" :value="scope.row.type" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="功能名称" align="center" prop="name" />
|
||||||
|
<el-table-column label="标识符" align="center" prop="identifier" />
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
v-hasPermi="[`iot:think-model-function:update`]"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['iot:think-model-function:delete']"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-tabs>
|
||||||
|
</ContentWrap>
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<ThinkModelFunctionForm ref="formRef" :product="product" @success="getList" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import ThinkModelFunctionForm from '@/views/iot/product/detail/ThinkModelFunctionForm.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ product: ProductVO }>()
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<ThinkModelFunctionVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
type: undefined,
|
||||||
|
productId: -1
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
queryParams.productId = props.product.id
|
||||||
|
const data = await ThinkModelFunctionApi.getThinkModelFunctionPage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
queryParams.type = undefined
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改操作 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await ThinkModelFunctionApi.deleteThinkModelFunction(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,229 @@
|
||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="功能类型" prop="type">
|
||||||
|
<el-radio-group v-model="formData.type">
|
||||||
|
<el-radio-button value="1"> 属性 </el-radio-button>
|
||||||
|
<el-radio-button value="2"> 服务 </el-radio-button>
|
||||||
|
<el-radio-button value="3"> 事件 </el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="功能名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入功能名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标识符" prop="identifier">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.identifier"
|
||||||
|
placeholder="请输入标识符"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据类型" prop="type">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.property.dataType.type"
|
||||||
|
placeholder="请选择数据类型"
|
||||||
|
:disabled="formType === 'update'"
|
||||||
|
>
|
||||||
|
<el-option key="int" label="int32 (整数型)" value="int" />
|
||||||
|
<el-option key="float" label="float (单精度浮点型)" value="float" />
|
||||||
|
<el-option key="double" label="double (双精度浮点型)" value="double" />
|
||||||
|
<!-- <el-option key="text" label="text (文本型)" value="text" />-->
|
||||||
|
<!-- <el-option key="date" label="date (日期型)" value="date" />-->
|
||||||
|
<!-- <el-option key="bool" label="bool (布尔型)" value="bool" />-->
|
||||||
|
<!-- <el-option key="enum" label="enum (枚举型)" value="enum" />-->
|
||||||
|
<!-- <el-option key="struct" label="struct (结构体)" value="struct" />-->
|
||||||
|
<!-- <el-option key="array" label="array (数组)" value="array" />-->
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="取值范围" prop="max">
|
||||||
|
<el-input v-model="formData.property.dataType.specs.min" placeholder="请输入最小值" />
|
||||||
|
<span class="mx-2">~</span>
|
||||||
|
<el-input v-model="formData.property.dataType.specs.max" placeholder="请输入最大值" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="步长" prop="step">
|
||||||
|
<el-input v-model="formData.property.dataType.specs.step" placeholder="请输入步长" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="单位" prop="unit">
|
||||||
|
<el-input v-model="formData.property.dataType.specs.unit" placeholder="请输入单位" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="读写类型" prop="accessMode">
|
||||||
|
<el-radio-group v-model="formData.property.accessMode">
|
||||||
|
<el-radio label="rw">读写</el-radio>
|
||||||
|
<el-radio label="r">只读</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="属性描述" prop="property.description">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="formData.property.description"
|
||||||
|
placeholder="请输入属性描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ProductVO } from '@/api/iot/product'
|
||||||
|
import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
|
||||||
|
|
||||||
|
const props = defineProps<{ product: ProductVO }>()
|
||||||
|
|
||||||
|
defineOptions({ name: 'ThinkModelFunctionForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const formLoading = ref(false)
|
||||||
|
const formType = ref('')
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
productKey: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
type: '1',
|
||||||
|
property: {
|
||||||
|
identifier: undefined,
|
||||||
|
name: undefined,
|
||||||
|
accessMode: 'rw',
|
||||||
|
required: true,
|
||||||
|
dataType: {
|
||||||
|
type: undefined,
|
||||||
|
specs: {
|
||||||
|
min: undefined,
|
||||||
|
max: undefined,
|
||||||
|
step: undefined,
|
||||||
|
unit: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: undefined // 添加这一行
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
|
||||||
|
message:
|
||||||
|
'支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
|
||||||
|
identifier: [
|
||||||
|
{ required: true, message: '标识符不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z0-9_]{1,50}$/,
|
||||||
|
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
|
||||||
|
if (reservedKeywords.includes(value)) {
|
||||||
|
callback(
|
||||||
|
new Error(
|
||||||
|
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
property: {
|
||||||
|
dataType: {
|
||||||
|
type: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }]
|
||||||
|
},
|
||||||
|
accessMode: [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await ThinkModelFunctionApi.getThinkModelFunction(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open, close: () => (dialogVisible.value = false) })
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as ThinkModelFunctionVO
|
||||||
|
data.productId = props.product.id
|
||||||
|
data.productKey = props.product.productKey
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await ThinkModelFunctionApi.createThinkModelFunction(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await ThinkModelFunctionApi.updateThinkModelFunction(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false // 确保关闭弹框
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
productId: undefined,
|
||||||
|
productKey: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
type: '1', // todo @HAOHAO:看看枚举下
|
||||||
|
property: {
|
||||||
|
identifier: undefined,
|
||||||
|
name: undefined,
|
||||||
|
accessMode: 'rw',
|
||||||
|
required: true,
|
||||||
|
dataType: {
|
||||||
|
type: undefined,
|
||||||
|
specs: {
|
||||||
|
min: undefined,
|
||||||
|
max: undefined,
|
||||||
|
step: undefined,
|
||||||
|
unit: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: undefined // 确保重置 description 字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<ProductDetailsHeader :loading="loading" :product="product" @refresh="() => getProductData(id)" />
|
||||||
|
<el-col>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="产品信息" name="info">
|
||||||
|
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Topic 类列表" name="topic">
|
||||||
|
<ProductTopic v-if="activeTab === 'topic'" :product="product" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="功能定义" name="function">
|
||||||
|
<ThinkModelFunction v-if="activeTab === 'function'" :product="product" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="消息解析" name="message" />
|
||||||
|
<el-tab-pane label="服务端订阅" name="subscription" />
|
||||||
|
</el-tabs>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||||
|
import { DeviceApi } from '@/api/iot/device'
|
||||||
|
import ProductDetailsHeader from '@/views/iot/product/detail/ProductDetailsHeader.vue'
|
||||||
|
import ProductDetailsInfo from '@/views/iot/product/detail/ProductDetailsInfo.vue'
|
||||||
|
import ProductTopic from '@/views/iot/product/detail/ProductTopic.vue'
|
||||||
|
import ThinkModelFunction from '@/views/iot/product/detail/ThinkModelFunction.vue'
|
||||||
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
defineOptions({ name: 'IoTProductDetail' })
|
||||||
|
|
||||||
|
const { delView } = useTagsViewStore() // 视图操作
|
||||||
|
const { currentRoute } = useRouter()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
const id = Number(route.params.id) // 编号
|
||||||
|
const loading = ref(true) // 加载中
|
||||||
|
const product = ref<ProductVO>({} as ProductVO) // 详情
|
||||||
|
const activeTab = ref('info') // 默认激活的标签页
|
||||||
|
|
||||||
|
/** 获取详情 */
|
||||||
|
const getProductData = async (id: number) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
product.value = await ProductApi.getProduct(id)
|
||||||
|
console.log('Product data:', product.value)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备数量
|
||||||
|
const getDeviceCount = async (productId: number) => {
|
||||||
|
try {
|
||||||
|
const count = await DeviceApi.getDeviceCount(productId)
|
||||||
|
console.log('Device count response:', count)
|
||||||
|
return count
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching device count:', error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!id) {
|
||||||
|
message.warning('参数错误,产品不能为空!')
|
||||||
|
delView(unref(currentRoute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await getProductData(id)
|
||||||
|
// 查询设备数量
|
||||||
|
if (product.value.id) {
|
||||||
|
product.value.deviceCount = await getDeviceCount(product.value.id)
|
||||||
|
console.log('Device count:', product.value.deviceCount)
|
||||||
|
} else {
|
||||||
|
console.error('Product ID is undefined')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="产品名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.name"
|
||||||
|
placeholder="请输入产品名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ProductKey" prop="productKey">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.productKey"
|
||||||
|
placeholder="请输入产品标识"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</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="['iot:product:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="产品名称" align="center" prop="name">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="openDetail(scope.row.id)">{{ scope.row.name }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="ProductKey" align="center" prop="productKey" />
|
||||||
|
<el-table-column label="设备类型" align="center" prop="deviceType">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="创建时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column label="产品状态" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openDetail(scope.row.id)"
|
||||||
|
v-hasPermi="['iot:product:query']"
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['iot:product:delete']"
|
||||||
|
:disabled="scope.row.status === 1"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<ProductForm ref="formRef" @success="getList" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||||
|
import ProductForm from './ProductForm.vue'
|
||||||
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
|
|
||||||
|
/** iot 产品 列表 */
|
||||||
|
defineOptions({ name: 'IoTProduct' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<ProductVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
createTime: [],
|
||||||
|
productKey: undefined,
|
||||||
|
protocolId: undefined,
|
||||||
|
categoryId: undefined,
|
||||||
|
description: undefined,
|
||||||
|
validateType: undefined,
|
||||||
|
status: undefined,
|
||||||
|
deviceType: undefined,
|
||||||
|
netType: undefined,
|
||||||
|
protocolType: undefined,
|
||||||
|
dataFormat: undefined
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await ProductApi.getProductPage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改操作 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开详情 */
|
||||||
|
const { push } = useRouter()
|
||||||
|
const openDetail = (id: number) => {
|
||||||
|
push({ name: 'IoTProductDetail', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await ProductApi.deleteProduct(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div class="upload-container">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="title">
|
||||||
|
<div>选择数据源</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据源选择 -->
|
||||||
|
<div class="resource-btn" >导入已有文本</div>
|
||||||
|
|
||||||
|
<!-- 上传文件区域 -->
|
||||||
|
<el-form>
|
||||||
|
<div class="upload-section">
|
||||||
|
<div class="upload-label">上传文本文件</div>
|
||||||
|
<el-upload
|
||||||
|
class="upload-area"
|
||||||
|
action="#"
|
||||||
|
:file-list="fileList"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
list-type="text"
|
||||||
|
drag
|
||||||
|
>
|
||||||
|
<i class="el-icon-upload"></i>
|
||||||
|
<div class="el-upload__text">拖拽文件至此,或者 <em>选择文件</em></div>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
已支持 TXT、MARKDOWN、PDF、HTML、XLSX、XLS、DOCX、CSV、EML、MSG、PPTX、PPT、XML、EPUB,每个文件不超过 15MB。
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下一步按钮 -->
|
||||||
|
<div class="next-button">
|
||||||
|
<el-button type="primary" :disabled="!fileList.length">下一步</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 知识库创建 -->
|
||||||
|
<div class="create-knowledge">
|
||||||
|
<el-link type="primary" underline>创建一个空知识库</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const fileList = ref([])
|
||||||
|
|
||||||
|
const handleRemove = (file, fileList) => {
|
||||||
|
console.log(file, fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
fileList.value.push(file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.upload-container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 150px;
|
||||||
|
border: 1.5px solid #528bff;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text em {
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-button {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-knowledge {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-radio-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-radio-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-radio-button .el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,168 @@
|
||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<!-- Left Section -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<!-- 分段设置 -->
|
||||||
|
<el-form>
|
||||||
|
<el-form-item label="分段设置">
|
||||||
|
<el-radio-group v-model="segmentSetting">
|
||||||
|
<el-radio label="自动分段与清洗">自动分段与清洗</el-radio>
|
||||||
|
<el-radio label="自定义">自定义</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 索引方式 -->
|
||||||
|
<el-form-item label="索引方式">
|
||||||
|
<el-radio-group v-model="indexingMethod">
|
||||||
|
<el-radio label="高质量">高质量</el-radio>
|
||||||
|
<el-radio label="经济">经济</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- Embedding 模型 -->
|
||||||
|
<el-form-item label="Embedding 模型">
|
||||||
|
<el-select v-model="embeddingModel" placeholder="Select Embedding Model">
|
||||||
|
<el-option label="text-embedding-3-large" value="text-embedding-3-large"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 检索设置 -->
|
||||||
|
<el-form-item label="检索设置">
|
||||||
|
<el-card style="width: 400px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<span>向量检索</span>
|
||||||
|
</div>
|
||||||
|
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||||
|
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card style="width: 400px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<span>全文检索</span>
|
||||||
|
</div>
|
||||||
|
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||||
|
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card style="width: 400px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<span>混合检索</span>
|
||||||
|
</div>
|
||||||
|
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||||
|
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||||
|
</el-card>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- Right Section: 分段预览 -->
|
||||||
|
<el-col :span="9">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div class="previews-title">分段预览</div>
|
||||||
|
<template v-for="(segment, index) in segmentPreviews" :key="index">
|
||||||
|
<div class="segment-preview">
|
||||||
|
<div class="title">
|
||||||
|
<div class="left">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4.74999 1.5L3.24999 10.5M8.74998 1.5L7.24998 10.5M10.25 4H1.75M9.75 8H1.25"
|
||||||
|
stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="id">{{ segment.number }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4 3.5H8M6 3.5V8.5M3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V3.9C10.5 3.05992 10.5 2.63988 10.3365 2.31901C10.1927 2.03677 9.96323 1.8073 9.68099 1.66349C9.36012 1.5 8.94008 1.5 8.1 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5Z"
|
||||||
|
stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="char-size">7777 字符</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">{{ segment.text }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {ref} from 'vue';
|
||||||
|
|
||||||
|
// Reactive variables for form control
|
||||||
|
const segmentSetting = ref('自动分段与清洗');
|
||||||
|
const indexingMethod = ref('高质量');
|
||||||
|
const embeddingModel = ref('text-embedding-3-large');
|
||||||
|
const directionalSearch = ref(true);
|
||||||
|
const topK = ref(3);
|
||||||
|
const scoreThreshold = ref(0.5);
|
||||||
|
|
||||||
|
// Mock data for segment previews
|
||||||
|
const segmentPreviews = ref([
|
||||||
|
{number: '001', text: "同步obs模型...'UAE-large-V1'"},
|
||||||
|
{number: '002', text: "同步obs模型...'plip'"},
|
||||||
|
{number: '003', text: "同步obs模型...'phoBERT-base-v2'"},
|
||||||
|
{number: '004', text: "同步obs模型...'lama3-bb-bnb-4bit'"},
|
||||||
|
{number: '005', text: "同步obs模型...'t5-base-split-and-rephrase'"}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/* Add any custom styles here */
|
||||||
|
|
||||||
|
.previews-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-preview {
|
||||||
|
background-color: rgba(228, 228, 228, 0.38);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
border-right: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #676767;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.id {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.char-size {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(57, 57, 57, 0.66);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<div class="knowledge-base-container">
|
||||||
|
<div class="card-container">
|
||||||
|
<el-card class="create-card" shadow="hover">
|
||||||
|
<div class="create-content">
|
||||||
|
<el-icon class="create-icon"><Plus /></el-icon>
|
||||||
|
<span class="create-text">创建知识库</span>
|
||||||
|
</div>
|
||||||
|
<div class="create-footer">
|
||||||
|
导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="document-card" shadow="hover" v-for="index in 4" :key="index">
|
||||||
|
<div class="document-header">
|
||||||
|
<el-icon><Folder /></el-icon>
|
||||||
|
<span>接口鉴权示例代码.md</span>
|
||||||
|
</div>
|
||||||
|
<div class="document-info">
|
||||||
|
<el-tag size="small">1 文档</el-tag>
|
||||||
|
<el-tag size="small" type="info">5 千字符</el-tag>
|
||||||
|
<el-tag size="small" type="warning">0 关联应用</el-tag>
|
||||||
|
</div>
|
||||||
|
<p class="document-description">
|
||||||
|
useful for when you want to answer queries about the 接口鉴权示例代码.md
|
||||||
|
</p>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 30, 40]"
|
||||||
|
:small="false"
|
||||||
|
:disabled="false"
|
||||||
|
:background="true"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Folder, Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(100) // 假设总共有100条数据
|
||||||
|
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
console.log(`每页 ${val} 条`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
console.log(`当前页: ${val}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.knowledge-base-container {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
position: absolute;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: 0;
|
||||||
|
bottom: 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap; /* Enable wrapping */
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: auto; /* Pushes pagination to the bottom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-card, .document-card {
|
||||||
|
flex: 1 1 360px; /* Allow cards to grow and shrink */
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 400px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-card {
|
||||||
|
background-color: rgba(168, 168, 168, 0.22);
|
||||||
|
}
|
||||||
|
.create-card:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-footer {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-description {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -105,7 +105,7 @@ const list = ref([]) // 列表的数据
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
propertyId: Number(params.propertyId),
|
propertyId: params.propertyId,
|
||||||
name: undefined
|
name: undefined
|
||||||
})
|
})
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
|
@ -180,17 +180,17 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="销售价(元)" min-width="80">
|
<el-table-column align="center" label="销售价(元)" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatToFraction(row.price) }}
|
{{ row.price }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="市场价(元)" min-width="80">
|
<el-table-column align="center" label="市场价(元)" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatToFraction(row.marketPrice) }}
|
{{ row.marketPrice }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="成本价(元)" min-width="80">
|
<el-table-column align="center" label="成本价(元)" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatToFraction(row.costPrice) }}
|
{{ row.costPrice }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="库存" min-width="80">
|
<el-table-column align="center" label="库存" min-width="80">
|
||||||
|
@ -211,12 +211,12 @@
|
||||||
<template v-if="formData!.subCommissionType">
|
<template v-if="formData!.subCommissionType">
|
||||||
<el-table-column align="center" label="一级返佣(元)" min-width="80">
|
<el-table-column align="center" label="一级返佣(元)" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatToFraction(row.firstBrokeragePrice) }}
|
{{ row.firstBrokeragePrice }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="二级返佣(元)" min-width="80">
|
<el-table-column align="center" label="二级返佣(元)" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatToFraction(row.secondBrokeragePrice) }}
|
{{ row.secondBrokeragePrice }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
:show-word-limit="true"
|
:show-word-limit="true"
|
||||||
class="w-80!"
|
class="w-80!"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
placeholder="请输入商品名称"
|
placeholder="请输入商品简介"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
|
@ -4,27 +4,27 @@
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
class="-mb-15px"
|
|
||||||
:model="queryParams"
|
|
||||||
ref="queryFormRef"
|
ref="queryFormRef"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
|
:model="queryParams"
|
||||||
|
class="-mb-15px"
|
||||||
label-width="68px"
|
label-width="68px"
|
||||||
>
|
>
|
||||||
<el-form-item label="活动名称" prop="name">
|
<el-form-item label="活动名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
placeholder="请输入活动名称"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入活动名称"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="活动状态" prop="status">
|
<el-form-item label="活动状态" prop="status">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.status"
|
v-model="queryParams.status"
|
||||||
placeholder="请选择活动状态"
|
|
||||||
clearable
|
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择活动状态"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
|
@ -35,15 +35,22 @@
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
<el-button @click="handleQuery">
|
||||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
<Icon class="mr-5px" icon="ep:search" />
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<Icon class="mr-5px" icon="ep:refresh" />
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
@click="openForm('create')"
|
|
||||||
v-hasPermi="['promotion:combination-activity:create']"
|
v-hasPermi="['promotion:combination-activity:create']"
|
||||||
|
plain
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('create')"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
<Icon class="mr-5px" icon="ep:plus" />
|
||||||
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
@ -51,77 +58,77 @@
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||||
<el-table-column label="活动编号" prop="id" min-width="80" />
|
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||||
<el-table-column label="活动名称" prop="name" min-width="140" />
|
<el-table-column label="活动名称" min-width="140" prop="name" />
|
||||||
<el-table-column label="活动时间" min-width="210">
|
<el-table-column label="活动时间" min-width="210">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
|
{{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
|
||||||
~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
|
~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品图片" prop="spuName" min-width="80">
|
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-image
|
<el-image
|
||||||
|
:preview-src-list="[scope.row.picUrl]"
|
||||||
:src="scope.row.picUrl"
|
:src="scope.row.picUrl"
|
||||||
class="h-40px w-40px"
|
class="h-40px w-40px"
|
||||||
:preview-src-list="[scope.row.picUrl]"
|
|
||||||
preview-teleported
|
preview-teleported
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品标题" prop="spuName" min-width="300" />
|
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="原价"
|
|
||||||
prop="marketPrice"
|
|
||||||
min-width="100"
|
|
||||||
:formatter="fenToYuanFormat"
|
:formatter="fenToYuanFormat"
|
||||||
|
label="原价"
|
||||||
|
min-width="100"
|
||||||
|
prop="marketPrice"
|
||||||
/>
|
/>
|
||||||
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
|
<el-table-column label="拼团价" min-width="100" prop="seckillPrice">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatCombinationPrice(scope.row.products) }}
|
{{ formatCombinationPrice(scope.row.products) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
|
<el-table-column label="开团组数" min-width="100" prop="groupCount" />
|
||||||
<el-table-column label="成团组数" prop="groupSuccessCount" min-width="100" />
|
<el-table-column label="成团组数" min-width="100" prop="groupSuccessCount" />
|
||||||
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
|
<el-table-column label="购买次数" min-width="100" prop="recordCount" />
|
||||||
<el-table-column label="活动状态" align="center" prop="status" min-width="100">
|
<el-table-column align="center" label="活动状态" min-width="100" prop="status">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="创建时间"
|
|
||||||
align="center"
|
|
||||||
prop="createTime"
|
|
||||||
:formatter="dateFormatter"
|
:formatter="dateFormatter"
|
||||||
|
align="center"
|
||||||
|
label="创建时间"
|
||||||
|
prop="createTime"
|
||||||
width="180px"
|
width="180px"
|
||||||
/>
|
/>
|
||||||
<el-table-column label="操作" align="center" width="150px" fixed="right">
|
<el-table-column align="center" fixed="right" label="操作" width="150px">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
|
v-hasPermi="['promotion:combination-activity:update']"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openForm('update', scope.row.id)"
|
@click="openForm('update', scope.row.id)"
|
||||||
v-hasPermi="['promotion:combination-activity:update']"
|
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="scope.row.status === 0"
|
||||||
|
v-hasPermi="['promotion:combination-activity:close']"
|
||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleClose(scope.row.id)"
|
@click="handleClose(scope.row.id)"
|
||||||
v-if="scope.row.status === 0"
|
|
||||||
v-hasPermi="['promotion:combination-activity:close']"
|
|
||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
|
v-else
|
||||||
|
v-hasPermi="['promotion:combination-activity:delete']"
|
||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDelete(scope.row.id)"
|
@click="handleDelete(scope.row.id)"
|
||||||
v-else
|
|
||||||
v-hasPermi="['promotion:combination-activity:delete']"
|
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
@ -130,9 +137,9 @@
|
||||||
</el-table>
|
</el-table>
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
:total="total"
|
|
||||||
v-model:page="queryParams.pageNo"
|
|
||||||
v-model:limit="queryParams.pageSize"
|
v-model:limit="queryParams.pageSize"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
:total="total"
|
||||||
@pagination="getList"
|
@pagination="getList"
|
||||||
/>
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
|
@ -141,12 +148,11 @@
|
||||||
<CombinationActivityForm ref="formRef" @success="getList" />
|
<CombinationActivityForm ref="formRef" @success="getList" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { dateFormatter } from '@/utils/formatTime'
|
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||||
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
|
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
|
||||||
import CombinationActivityForm from './CombinationActivityForm.vue'
|
import CombinationActivityForm from './CombinationActivityForm.vue'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
|
||||||
import { fenToYuanFormat } from '@/utils/formatter'
|
import { fenToYuanFormat } from '@/utils/formatter'
|
||||||
import { fenToYuan } from '@/utils'
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
|
@ -165,7 +171,6 @@ const queryParams = reactive({
|
||||||
status: null
|
status: null
|
||||||
})
|
})
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
const exportLoading = ref(false) // 导出的加载中
|
|
||||||
|
|
||||||
/** 查询列表 */
|
/** 查询列表 */
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
|
@ -197,12 +202,11 @@ const openForm = (type: string, id?: number) => {
|
||||||
formRef.value.open(type, id)
|
formRef.value.open(type, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 芋艿:这里要改下
|
|
||||||
/** 关闭按钮操作 */
|
/** 关闭按钮操作 */
|
||||||
const handleClose = async (id: number) => {
|
const handleClose = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
// 关闭的二次确认
|
// 关闭的二次确认
|
||||||
await message.confirm('确认关闭该秒杀活动吗?')
|
await message.confirm('确认关闭该拼团活动吗?')
|
||||||
// 发起关闭
|
// 发起关闭
|
||||||
await CombinationActivityApi.closeCombinationActivity(id)
|
await CombinationActivityApi.closeCombinationActivity(id)
|
||||||
message.success('关闭成功')
|
message.success('关闭成功')
|
||||||
|
|
|
@ -30,13 +30,13 @@
|
||||||
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
|
<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="90" prop="stock" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="spuData.length > 1 && isDelete"
|
v-if="spuData.length > 1 && deletable"
|
||||||
align="center"
|
align="center"
|
||||||
label="操作"
|
label="操作"
|
||||||
min-width="90"
|
min-width="90"
|
||||||
>
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click="deleteSpu(scope.row.id)"> 删除 </el-button>
|
<el-button link type="primary" @click="deleteSpu(scope.row.id)"> 删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
@ -56,13 +56,13 @@ const props = defineProps<{
|
||||||
spuList: T[]
|
spuList: T[]
|
||||||
ruleConfig: RuleConfig[]
|
ruleConfig: RuleConfig[]
|
||||||
spuPropertyListP: SpuProperty<T>[]
|
spuPropertyListP: SpuProperty<T>[]
|
||||||
isDelete?: boolean // SPU 是否可删除;TODO deletable 换成这个名字好点。
|
deletable?: boolean // SPU 是否可删除;
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const spuData = ref<Spu[]>([]) // spu 详情数据列表
|
const spuData = ref<Spu[]>([]) // spu 详情数据列表
|
||||||
const skuListRef = ref() // 商品属性列表Ref
|
const skuListRef = ref() // 商品属性列表Ref
|
||||||
const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId 对应的 sku 的属性列表
|
const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId 对应的 sku 的属性列表
|
||||||
const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
|
const expandRowKeys = ref<string[]>([]) // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有 sku 活动配置
|
* 获取所有 sku 活动配置
|
||||||
|
@ -71,10 +71,10 @@ const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属
|
||||||
*/
|
*/
|
||||||
const getSkuConfigs = (extendedAttribute: string) => {
|
const getSkuConfigs = (extendedAttribute: string) => {
|
||||||
skuListRef.value.validateSku()
|
skuListRef.value.validateSku()
|
||||||
const seckillProducts = []
|
const seckillProducts: any[] = []
|
||||||
spuPropertyList.value.forEach((item) => {
|
spuPropertyList.value.forEach((item) => {
|
||||||
item.spuDetail.skus.forEach((sku) => {
|
item.spuDetail.skus?.forEach((sku: any) => {
|
||||||
seckillProducts.push(sku[extendedAttribute])
|
seckillProducts.push(sku[extendedAttribute] as any)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return seckillProducts
|
return seckillProducts
|
||||||
|
@ -124,10 +124,10 @@ watch(
|
||||||
() => props.spuPropertyListP,
|
() => props.spuPropertyListP,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
spuPropertyList.value = data as SpuProperty<T>[]
|
spuPropertyList.value = data as SpuProperty<T>[] as any
|
||||||
// 解决如果之前选择的是单规格 spu 的话后面选择多规格 sku 多规格属性信息不展示的问题。解决方法:让 SkuList 组件重新渲染(行折叠会干掉包含的组件展开时会重新加载)
|
// 解决如果之前选择的是单规格 spu 的话后面选择多规格 sku 多规格属性信息不展示的问题。解决方法:让 SkuList 组件重新渲染(行折叠会干掉包含的组件展开时会重新加载)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expandRowKeys.value = data.map((item) => item.spuId)
|
expandRowKeys.value = data.map((item) => item.spuId + '')
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -116,6 +116,7 @@ import {
|
||||||
validityTypeFormat
|
validityTypeFormat
|
||||||
} from '@/views/mall/promotion/coupon/formatter'
|
} from '@/views/mall/promotion/coupon/formatter'
|
||||||
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
|
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
|
||||||
|
import { CouponTemplateTakeTypeEnum } from '@/utils/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'CouponSelect' })
|
defineOptions({ name: 'CouponSelect' })
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ const emit = defineEmits<{
|
||||||
(e: 'change', v: CouponTemplateApi.CouponTemplateVO[]): void
|
(e: 'change', v: CouponTemplateApi.CouponTemplateVO[]): void
|
||||||
}>()
|
}>()
|
||||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
const dialogTitle = ref('选择优惠卷') // 弹窗的标题
|
const dialogTitle = ref('选择优惠劵') // 弹窗的标题
|
||||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
const loading = ref(true) // 列表的加载中
|
const loading = ref(true) // 列表的加载中
|
||||||
const total = ref(0) // 列表的总页数
|
const total = ref(0) // 列表的总页数
|
||||||
|
@ -138,7 +139,7 @@ const queryParams = reactive({
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
name: null,
|
name: null,
|
||||||
discountType: null,
|
discountType: null,
|
||||||
canTakeTypes: null
|
canTakeTypes: [CouponTemplateTakeTypeEnum.USER.type] // 只获得直接领取的券
|
||||||
})
|
})
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
const selectedCouponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) // 选择的数据
|
const selectedCouponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) // 选择的数据
|
||||||
|
|
|
@ -16,10 +16,14 @@ export const discountFormat = (row: CouponTemplateVO) => {
|
||||||
|
|
||||||
// 格式化【领取上限】
|
// 格式化【领取上限】
|
||||||
export const takeLimitCountFormat = (row: CouponTemplateVO) => {
|
export const takeLimitCountFormat = (row: CouponTemplateVO) => {
|
||||||
if (row.takeLimitCount === -1) {
|
if (row.takeLimitCount) {
|
||||||
return '无领取限制'
|
if (row.takeLimitCount === -1) {
|
||||||
|
return '无领取限制'
|
||||||
|
}
|
||||||
|
return `${row.takeLimitCount} 张/人`
|
||||||
|
} else {
|
||||||
|
return ' '
|
||||||
}
|
}
|
||||||
return `${row.takeLimitCount} 张/人`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化【有效期限】
|
// 格式化【有效期限】
|
||||||
|
@ -33,8 +37,19 @@ export const validityTypeFormat = (row: CouponTemplateVO) => {
|
||||||
return '未知【' + row.validityType + '】'
|
return '未知【' + row.validityType + '】'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化【totalCount】
|
||||||
|
export const totalCountFormat = (row: CouponTemplateVO) => {
|
||||||
|
if (row.totalCount === -1) {
|
||||||
|
return '不限制'
|
||||||
|
}
|
||||||
|
return row.totalCount
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化【剩余数量】
|
// 格式化【剩余数量】
|
||||||
export const remainedCountFormat = (row: CouponTemplateVO) => {
|
export const remainedCountFormat = (row: CouponTemplateVO) => {
|
||||||
|
if (row.totalCount === -1) {
|
||||||
|
return '不限制'
|
||||||
|
}
|
||||||
return row.totalCount - row.takeCount
|
return row.totalCount - row.takeCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@
|
||||||
<el-radio-group v-model="formData.takeType">
|
<el-radio-group v-model="formData.takeType">
|
||||||
<el-radio :key="1" :value="1">直接领取</el-radio>
|
<el-radio :key="1" :value="1">直接领取</el-radio>
|
||||||
<el-radio :key="2" :value="2">指定发放</el-radio>
|
<el-radio :key="2" :value="2">指定发放</el-radio>
|
||||||
|
<el-radio :key="2" :value="3">新人劵</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
|
<el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
|
||||||
|
@ -309,7 +310,9 @@ const submitForm = async () => {
|
||||||
validEndTime:
|
validEndTime:
|
||||||
formData.value.validTimes && formData.value.validTimes.length === 2
|
formData.value.validTimes && formData.value.validTimes.length === 2
|
||||||
? formData.value.validTimes[1]
|
? formData.value.validTimes[1]
|
||||||
: undefined
|
: undefined,
|
||||||
|
totalCount: formData.value.takeType === 1 ? formData.value.totalCount : -1,
|
||||||
|
takeLimitCount: formData.value.takeType === 1 ? formData.value.takeLimitCount : -1
|
||||||
} as unknown as CouponTemplateApi.CouponTemplateVO
|
} as unknown as CouponTemplateApi.CouponTemplateVO
|
||||||
|
|
||||||
// 设置商品范围
|
// 设置商品范围
|
||||||
|
|
|
@ -109,7 +109,12 @@
|
||||||
prop="validityType"
|
prop="validityType"
|
||||||
width="185"
|
width="185"
|
||||||
/>
|
/>
|
||||||
<el-table-column align="center" label="发放数量" prop="totalCount" />
|
<el-table-column
|
||||||
|
:formatter="totalCountFormat"
|
||||||
|
align="center"
|
||||||
|
label="发放数量"
|
||||||
|
prop="totalCount"
|
||||||
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:formatter="remainedCountFormat"
|
:formatter="remainedCountFormat"
|
||||||
align="center"
|
align="center"
|
||||||
|
@ -189,6 +194,7 @@ import {
|
||||||
discountFormat,
|
discountFormat,
|
||||||
remainedCountFormat,
|
remainedCountFormat,
|
||||||
takeLimitCountFormat,
|
takeLimitCountFormat,
|
||||||
|
totalCountFormat,
|
||||||
validityTypeFormat
|
validityTypeFormat
|
||||||
} from '@/views/mall/promotion/coupon/formatter'
|
} from '@/views/mall/promotion/coupon/formatter'
|
||||||
|
|
||||||
|
|
|
@ -8,28 +8,40 @@
|
||||||
:schema="allSchemas.formSchema"
|
:schema="allSchemas.formSchema"
|
||||||
>
|
>
|
||||||
<!-- 先选择 -->
|
<!-- 先选择 -->
|
||||||
<!-- TODO @zhangshuai:商品允许选择多个 -->
|
|
||||||
<!-- TODO @zhangshuai:选择后的 SKU,需要后面加个【删除】按钮 -->
|
|
||||||
<!-- TODO @zhangshuai:展示的金额,貌似不对,大了 100 倍,需要看下 -->
|
|
||||||
<!-- TODO @zhangshuai:“优惠类型”,是每个 SKU 可以自定义已设置哈。因为每个商品 SKU 的折扣和减少价格,可能不同。具体交互,可以注册一个 youzan.com 看看;它的交互方式是,如果设置了“优惠金额”,则算“减价”;如果再次设置了“折扣百分比”,就算“打折”;这样形成一个互斥的优惠类型 -->
|
|
||||||
<template #spuId>
|
<template #spuId>
|
||||||
<el-button @click="spuSelectRef.open()">选择商品</el-button>
|
<el-button @click="spuSelectRef.open()">选择商品</el-button>
|
||||||
<SpuAndSkuList
|
<SpuAndSkuList
|
||||||
ref="spuAndSkuListRef"
|
ref="spuAndSkuListRef"
|
||||||
|
:deletable="true"
|
||||||
:rule-config="ruleConfig"
|
:rule-config="ruleConfig"
|
||||||
:spu-list="spuList"
|
:spu-list="spuList"
|
||||||
:spu-property-list-p="spuPropertyList"
|
:spu-property-list-p="spuPropertyList"
|
||||||
:isDelete="true"
|
|
||||||
@delete="deleteSpu"
|
@delete="deleteSpu"
|
||||||
>
|
>
|
||||||
<el-table-column align="center" label="优惠金额" min-width="168">
|
<el-table-column align="center" label="优惠金额" min-width="168">
|
||||||
<template #default="{ row: sku }">
|
<template #default="{ row }">
|
||||||
<el-input-number v-model="sku.productConfig.discountPrice" :min="0" class="w-100%" />
|
<el-input-number
|
||||||
|
v-model="row.productConfig.discountPrice"
|
||||||
|
:max="parseFloat(fenToYuan(row.price))"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-100%"
|
||||||
|
@change="handleSkuDiscountPriceChange(row)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" label="折扣百分比(%)" min-width="168">
|
<el-table-column align="center" label="折扣百分比(%)" min-width="168">
|
||||||
<template #default="{ row: sku }">
|
<template #default="{ row }">
|
||||||
<el-input-number v-model="sku.productConfig.discountPercent" class="w-100%" />
|
<el-input-number
|
||||||
|
v-model="row.productConfig.discountPercent"
|
||||||
|
:max="100"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-100%"
|
||||||
|
@change="handleSkuDiscountPercentChange(row)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</SpuAndSkuList>
|
</SpuAndSkuList>
|
||||||
|
@ -45,11 +57,12 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../components'
|
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../components'
|
||||||
import { allSchemas, rules } from './discountActivity.data'
|
import { allSchemas, rules } from './discountActivity.data'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep, debounce } from 'lodash-es'
|
||||||
import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity'
|
import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity'
|
||||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||||
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
||||||
import { formatToFraction } from '@/utils'
|
import { convertToInteger, erpCalculatePercentage, fenToYuan, yuanToFen } from '@/utils'
|
||||||
|
import { PromotionDiscountTypeEnum } from '@/utils/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'PromotionDiscountActivityForm' })
|
defineOptions({ name: 'PromotionDiscountActivityForm' })
|
||||||
|
|
||||||
|
@ -65,7 +78,13 @@ const formRef = ref() // 表单 Ref
|
||||||
|
|
||||||
const spuSelectRef = ref() // 商品和属性选择 Ref
|
const spuSelectRef = ref() // 商品和属性选择 Ref
|
||||||
const spuAndSkuListRef = ref() // sku 限时折扣 配置组件Ref
|
const spuAndSkuListRef = ref() // sku 限时折扣 配置组件Ref
|
||||||
const ruleConfig: RuleConfig[] = []
|
const ruleConfig: RuleConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'productConfig.discountPrice',
|
||||||
|
rule: (arg) => arg > 0,
|
||||||
|
message: '商品优惠金额不能为 0 !!!'
|
||||||
|
}
|
||||||
|
]
|
||||||
const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 选择的 spu
|
const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 选择的 spu
|
||||||
const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([])
|
const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([])
|
||||||
const spuIds = ref<number[]>([])
|
const spuIds = ref<number[]>([])
|
||||||
|
@ -101,21 +120,20 @@ const getSpuDetails = async (
|
||||||
selectSkus?.forEach((sku) => {
|
selectSkus?.forEach((sku) => {
|
||||||
let config: DiscountActivityApi.DiscountProductVO = {
|
let config: DiscountActivityApi.DiscountProductVO = {
|
||||||
skuId: sku.id!,
|
skuId: sku.id!,
|
||||||
spuId: spu.id,
|
spuId: spu.id!,
|
||||||
discountType: 1,
|
discountType: 1,
|
||||||
discountPercent: 0,
|
discountPercent: 0,
|
||||||
discountPrice: 0
|
discountPrice: 0
|
||||||
}
|
}
|
||||||
if (typeof products !== 'undefined') {
|
if (typeof products !== 'undefined') {
|
||||||
const product = products.find((item) => item.skuId === sku.id)
|
const product = products.find((item) => item.skuId === sku.id)
|
||||||
|
if (product) {
|
||||||
|
product.discountPercent = fenToYuan(product.discountPercent) as any
|
||||||
|
product.discountPrice = fenToYuan(product.discountPrice) as any
|
||||||
|
}
|
||||||
config = product || config
|
config = product || config
|
||||||
}
|
}
|
||||||
sku.productConfig = config
|
sku.productConfig = config
|
||||||
sku.price = formatToFraction(sku.price)
|
|
||||||
sku.marketPrice = formatToFraction(sku.marketPrice)
|
|
||||||
sku.costPrice = formatToFraction(sku.costPrice)
|
|
||||||
sku.firstBrokeragePrice = formatToFraction(sku.firstBrokeragePrice)
|
|
||||||
sku.secondBrokeragePrice = formatToFraction(sku.secondBrokeragePrice)
|
|
||||||
})
|
})
|
||||||
spu.skus = selectSkus as DiscountActivityApi.SkuExtension[]
|
spu.skus = selectSkus as DiscountActivityApi.SkuExtension[]
|
||||||
spuPropertyList.value.push({
|
spuPropertyList.value.push({
|
||||||
|
@ -168,25 +186,13 @@ const submitForm = async () => {
|
||||||
// 提交请求
|
// 提交请求
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = formRef.value.formModel as DiscountActivityApi.DiscountActivityVO
|
|
||||||
// 获取折扣商品配置
|
// 获取折扣商品配置
|
||||||
const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
|
const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
|
||||||
// 校验优惠金额、折扣百分比,是否正确
|
|
||||||
// TODO @puhui999:这个交互,可以参考下 youzan 的
|
|
||||||
let discountInvalid = false
|
|
||||||
products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
|
products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
|
||||||
if (item.discountPrice != null && item.discountPrice > 0) {
|
item.discountPercent = convertToInteger(item.discountPercent)
|
||||||
item.discountType = 1
|
item.discountPrice = convertToInteger(item.discountPrice)
|
||||||
} else if (item.discountPercent != null && item.discountPercent > 0) {
|
|
||||||
item.discountType = 2
|
|
||||||
} else {
|
|
||||||
discountInvalid = true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if (discountInvalid) {
|
const data = cloneDeep(formRef.value.formModel) as DiscountActivityApi.DiscountActivityVO
|
||||||
message.error('优惠金额和折扣百分比需要填写一个')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.products = products
|
data.products = products
|
||||||
// 真正提交
|
// 真正提交
|
||||||
if (formType.value === 'create') {
|
if (formType.value === 'create') {
|
||||||
|
@ -204,6 +210,36 @@ const submitForm = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 处理 sku 优惠金额变动 */
|
||||||
|
const handleSkuDiscountPriceChange = debounce((row: any) => {
|
||||||
|
// 校验边界
|
||||||
|
if (row.productConfig.discountPrice <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置优惠类型:满减
|
||||||
|
row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type
|
||||||
|
// 设置折扣
|
||||||
|
row.productConfig.discountPercent = erpCalculatePercentage(
|
||||||
|
row.price - yuanToFen(row.productConfig.discountPrice),
|
||||||
|
row.price
|
||||||
|
)
|
||||||
|
}, 200)
|
||||||
|
/** 处理 sku 优惠折扣变动 */
|
||||||
|
const handleSkuDiscountPercentChange = debounce((row: any) => {
|
||||||
|
// 校验边界
|
||||||
|
if (row.productConfig.discountPercent <= 0 || row.productConfig.discountPercent >= 100) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置优惠类型:折扣
|
||||||
|
row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type
|
||||||
|
// 设置满减金额
|
||||||
|
row.productConfig.discountPrice = fenToYuan(
|
||||||
|
row.price - row.price * (row.productConfig.discountPercent / 100.0 || 0)
|
||||||
|
)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
/** 重置表单 */
|
/** 重置表单 */
|
||||||
const resetForm = async () => {
|
const resetForm = async () => {
|
||||||
spuList.value = []
|
spuList.value = []
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||||
import { dateFormatter2 } from '@/utils/formatTime'
|
import { dateFormatter2 } from '@/utils/formatTime'
|
||||||
|
|
||||||
// TODO @zhangshai:
|
|
||||||
// 表单校验
|
// 表单校验
|
||||||
export const rules = reactive({
|
export const rules = reactive({
|
||||||
spuId: [required],
|
|
||||||
name: [required],
|
name: [required],
|
||||||
startTime: [required],
|
startTime: [required],
|
||||||
endTime: [required],
|
endTime: [required],
|
||||||
|
|
|
@ -22,13 +22,15 @@
|
||||||
<div class="ml-10px w-100%">
|
<div class="ml-10px w-100%">
|
||||||
<div class="flex justify-between items-center w-100%">
|
<div class="flex justify-between items-center w-100%">
|
||||||
<span class="username">{{ item.userNickname }}</span>
|
<span class="username">{{ item.userNickname }}</span>
|
||||||
<span class="color-[var(--left-menu-text-color)]" style="font-size: 13px;">
|
<span class="color-[var(--left-menu-text-color)]" style="font-size: 13px">
|
||||||
{{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
|
{{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 最后聊天内容 -->
|
<!-- 最后聊天内容 -->
|
||||||
<div
|
<div
|
||||||
v-dompurify-html="getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)"
|
v-dompurify-html="
|
||||||
|
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
|
||||||
|
"
|
||||||
class="last-message flex items-center color-[var(--left-menu-text-color)]"
|
class="last-message flex items-center color-[var(--left-menu-text-color)]"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -205,7 +207,7 @@ watch(showRightMenu, (val) => {
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
border-left: 5px #3271ff solid;
|
border-left: 5px #3271ff solid;
|
||||||
background-color: var(--left-menu-bg-active-color);
|
background-color: var(--login-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned {
|
.pinned {
|
||||||
|
@ -215,7 +217,7 @@ watch(showRightMenu, (val) => {
|
||||||
.right-menu-ul {
|
.right-menu-ul {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: var(--app-content-bg-color);
|
background-color: var(--app-content-bg-color);
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style-type: none; /* 移除默认的项目符号 */
|
list-style-type: none; /* 移除默认的项目符号 */
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
<OrderItem
|
<OrderItem
|
||||||
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
|
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
|
||||||
:message="item"
|
:message="item"
|
||||||
class="max-w-70%"
|
class="max-w-100%"
|
||||||
/>
|
/>
|
||||||
</MessageItem>
|
</MessageItem>
|
||||||
</div>
|
</div>
|
||||||
|
@ -423,9 +423,9 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
||||||
|
|
||||||
// 消息气泡
|
// 消息气泡
|
||||||
.kefu-message {
|
.kefu-message {
|
||||||
color: #A9A9A9;
|
color: #a9a9a9;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 3px 3px 5px rgba(220,220,220, 0.1);
|
box-shadow: 3px 3px 5px rgba(220, 220, 220, 0.1);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isObject(getMessageContent)" @click="openDetail(getMessageContent.id)" style="cursor: pointer;">
|
<div v-if="isObject(getMessageContent)">
|
||||||
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
|
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
|
||||||
<div class="order-card-header flex items-center justify-between p-x-5px">
|
<div class="order-card-header flex items-center justify-between p-x-5px">
|
||||||
<div class="order-no">订单号:{{ getMessageContent.no }}</div>
|
<div class="order-no">
|
||||||
|
订单号:
|
||||||
|
<span style="cursor: pointer" @click="openDetail(getMessageContent.id)">
|
||||||
|
{{ getMessageContent.no }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
|
<div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
|
||||||
{{ formatOrderStatus(getMessageContent) }}
|
{{ formatOrderStatus(getMessageContent) }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,8 +118,15 @@ function formatOrderStatus(order: any) {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|
||||||
.order-no {
|
.order-no {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
|
span {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--left-menu-bg-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ export const useEmoji = () => {
|
||||||
)
|
)
|
||||||
for (const path in pathList) {
|
for (const path in pathList) {
|
||||||
const imageModule: any = await pathList[path]()
|
const imageModule: any = await pathList[path]()
|
||||||
emojiPathList.value.push(imageModule.default)
|
emojiPathList.value.push({ path: path, src: imageModule.default })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,10 @@ export const useEmoji = () => {
|
||||||
function getEmojiFileByName(name: string) {
|
function getEmojiFileByName(name: string) {
|
||||||
for (const emoji of emojiList) {
|
for (const emoji of emojiList) {
|
||||||
if (emoji.name === name) {
|
if (emoji.name === name) {
|
||||||
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
|
const emojiPath = emojiPathList.value.find(
|
||||||
|
(item: { path: string; src: string }) => item.path.indexOf(emoji.file) > -1
|
||||||
|
)
|
||||||
|
return emojiPath ? emojiPath.src : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
|
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
|
||||||
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
||||||
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
||||||
import { getAccessToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import { useWebSocket } from '@vueuse/core'
|
import { useWebSocket } from '@vueuse/core'
|
||||||
|
|
||||||
defineOptions({ name: 'KeFu' })
|
defineOptions({ name: 'KeFu' })
|
||||||
|
@ -34,7 +34,9 @@ const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
// ======================= WebSocket start =======================
|
// ======================= WebSocket start =======================
|
||||||
const server = ref(
|
const server = ref(
|
||||||
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
|
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
|
||||||
|
'?token=' +
|
||||||
|
getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
|
||||||
) // WebSocket 服务地址
|
) // WebSocket 服务地址
|
||||||
|
|
||||||
/** 发起 WebSocket 连接 */
|
/** 发起 WebSocket 连接 */
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
<template>
|
||||||
|
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
v-loading="formLoading"
|
||||||
|
:isCol="true"
|
||||||
|
:rules="rules"
|
||||||
|
:schema="allSchemas.formSchema"
|
||||||
|
>
|
||||||
|
<!-- 先选择 -->
|
||||||
|
<template #spuId>
|
||||||
|
<el-button v-if="!isFormUpdate" @click="spuSelectRef.open()">选择商品</el-button>
|
||||||
|
<SpuAndSkuList
|
||||||
|
ref="spuAndSkuListRef"
|
||||||
|
:rule-config="ruleConfig"
|
||||||
|
:spu-list="spuList"
|
||||||
|
:spu-property-list-p="spuPropertyList"
|
||||||
|
>
|
||||||
|
<el-table-column align="center" label="可兑换库存" min-width="168">
|
||||||
|
<template #default="{ row: sku }">
|
||||||
|
<el-input-number
|
||||||
|
v-model="sku.productConfig.stock"
|
||||||
|
:max="sku.stock"
|
||||||
|
:min="0"
|
||||||
|
class="w-100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="可兑换次数" min-width="168">
|
||||||
|
<template #default="{ row: sku }">
|
||||||
|
<el-input-number v-model="sku.productConfig.count" :min="0" class="w-100%" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="所需积分" min-width="168">
|
||||||
|
<template #default="{ row: sku }">
|
||||||
|
<el-input-number v-model="sku.productConfig.point" :min="0" class="w-100%" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="所需金额(元)" min-width="168">
|
||||||
|
<template #default="{ row: sku }">
|
||||||
|
<el-input-number
|
||||||
|
v-model="sku.productConfig.price"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</SpuAndSkuList>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
<SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
|
||||||
|
import { allSchemas, rules } from './pointActivity.data'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import {
|
||||||
|
PointActivityApi,
|
||||||
|
PointActivityVO,
|
||||||
|
PointProductVO,
|
||||||
|
SkuExtension,
|
||||||
|
SpuExtension
|
||||||
|
} from '@/api/mall/promotion/point'
|
||||||
|
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||||
|
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
||||||
|
import { convertToInteger, formatToFraction } from '@/utils'
|
||||||
|
|
||||||
|
defineOptions({ name: 'PromotionSeckillActivityForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('') // 弹窗的标题
|
||||||
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
const isFormUpdate = ref(false) // 是否更新表单
|
||||||
|
|
||||||
|
// ================= 商品选择相关 =================
|
||||||
|
|
||||||
|
const spuSelectRef = ref() // 商品和属性选择 Ref
|
||||||
|
const spuAndSkuListRef = ref() // sku 积分商城商品配置组件Ref
|
||||||
|
const ruleConfig: RuleConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'productConfig.stock',
|
||||||
|
rule: (arg) => arg >= 1,
|
||||||
|
message: '商品可兑换库存必须大于等于 1 !!!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'productConfig.point',
|
||||||
|
rule: (arg) => arg >= 1,
|
||||||
|
message: '商品所需兑换积分必须大于等于 1 !!!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'productConfig.count',
|
||||||
|
rule: (arg) => arg >= 1,
|
||||||
|
message: '商品可兑换次数必须大于等于 1 !!!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const spuList = ref<SpuExtension[]>([]) // 选择的 spu
|
||||||
|
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([])
|
||||||
|
const selectSpu = (spuId: number, skuIds: number[]) => {
|
||||||
|
formRef.value.setValues({ spuId })
|
||||||
|
getSpuDetails(spuId, skuIds)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取 SPU 详情
|
||||||
|
*/
|
||||||
|
const getSpuDetails = async (
|
||||||
|
spuId: number,
|
||||||
|
skuIds: number[] | undefined,
|
||||||
|
products?: PointProductVO[]
|
||||||
|
) => {
|
||||||
|
const spuProperties: SpuProperty<SpuExtension>[] = []
|
||||||
|
const res = (await ProductSpuApi.getSpuDetailList([spuId])) as SpuExtension[]
|
||||||
|
if (res.length == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spuList.value = []
|
||||||
|
// 因为只能选择一个
|
||||||
|
const spu = res[0]
|
||||||
|
const selectSkus =
|
||||||
|
typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
|
||||||
|
selectSkus?.forEach((sku) => {
|
||||||
|
let config: PointProductVO = {
|
||||||
|
skuId: sku.id!,
|
||||||
|
stock: 0,
|
||||||
|
price: 0,
|
||||||
|
point: 0,
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
if (typeof products !== 'undefined') {
|
||||||
|
const product = products.find((item) => item.skuId === sku.id)
|
||||||
|
if (product) {
|
||||||
|
product.price = formatToFraction(product.price) as any
|
||||||
|
}
|
||||||
|
config = product || config
|
||||||
|
}
|
||||||
|
sku.productConfig = config
|
||||||
|
})
|
||||||
|
spu.skus = selectSkus as SkuExtension[]
|
||||||
|
spuProperties.push({
|
||||||
|
spuId: spu.id!,
|
||||||
|
spuDetail: spu,
|
||||||
|
propertyList: getPropertyList(spu)
|
||||||
|
})
|
||||||
|
spuList.value.push(spu)
|
||||||
|
spuPropertyList.value = spuProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= end =================
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
await resetForm()
|
||||||
|
// 修改时,设置数据
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = (await PointActivityApi.getPointActivity(id)) as PointActivityVO
|
||||||
|
isFormUpdate.value = true
|
||||||
|
await getSpuDetails(
|
||||||
|
data.spuId!,
|
||||||
|
data.products?.map((sku) => sku.skuId),
|
||||||
|
data.products
|
||||||
|
)
|
||||||
|
formRef.value.setValues(data)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 校验表单
|
||||||
|
if (!formRef) return
|
||||||
|
const valid = await formRef.value.getElFormRef().validate()
|
||||||
|
if (!valid) return
|
||||||
|
// 提交请求
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
// 获取秒杀商品配置
|
||||||
|
const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
|
||||||
|
products.forEach((item: PointProductVO) => {
|
||||||
|
item.price = convertToInteger(item.price)
|
||||||
|
})
|
||||||
|
const data = formRef.value.formModel as PointActivityVO
|
||||||
|
data.products = products
|
||||||
|
// 真正提交
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await PointActivityApi.createPointActivity(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await PointActivityApi.updatePointActivity(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 发送操作成功的事件
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = async () => {
|
||||||
|
spuList.value = []
|
||||||
|
spuPropertyList.value = []
|
||||||
|
isFormUpdate.value = false
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.getElFormRef().resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,219 @@
|
||||||
|
<template>
|
||||||
|
<doc-alert title="【营销】积分商城活动" url="https://doc.iocoder.cn/mall/promotion-point/" />
|
||||||
|
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="queryParams"
|
||||||
|
class="-mb-15px"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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="['promotion:point-activity:create']"
|
||||||
|
plain
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('create')"
|
||||||
|
>
|
||||||
|
<Icon class="mr-5px" icon="ep:plus" />
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||||
|
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||||
|
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image
|
||||||
|
:preview-src-list="[scope.row.picUrl]"
|
||||||
|
:src="scope.row.picUrl"
|
||||||
|
class="h-40px w-40px"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||||
|
<el-table-column
|
||||||
|
:formatter="fenToYuanFormat"
|
||||||
|
label="原价"
|
||||||
|
min-width="100"
|
||||||
|
prop="marketPrice"
|
||||||
|
/>
|
||||||
|
<el-table-column label="原价" min-width="100" prop="marketPrice" />
|
||||||
|
<el-table-column align="center" label="活动状态" min-width="100" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="库存" min-width="80" prop="stock" />
|
||||||
|
<el-table-column align="center" label="总库存" min-width="80" prop="totalStock" />
|
||||||
|
<el-table-column align="center" label="已兑换数量" min-width="100" prop="redeemedQuantity">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getRedeemedQuantity(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
align="center"
|
||||||
|
label="创建时间"
|
||||||
|
prop="createTime"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column align="center" fixed="right" label="操作" width="150px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
v-hasPermi="['promotion:point-activity:update']"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.status === 0"
|
||||||
|
v-hasPermi="['promotion:point-activity:close']"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleClose(scope.row.id)"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
v-hasPermi="['promotion:point-activity:delete']"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
:total="total"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<PointActivityForm ref="formRef" @success="getList" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import PointActivityForm from './PointActivityForm.vue'
|
||||||
|
import { fenToYuanFormat } from '@/utils/formatter'
|
||||||
|
import { PointActivityApi } from '@/api/mall/promotion/point'
|
||||||
|
|
||||||
|
defineOptions({ name: 'PointActivity' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const list = ref([]) // 列表的数据
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: null,
|
||||||
|
status: null
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 获得商品已兑换数量
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await PointActivityApi.getPointActivityPage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改操作 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭按钮操作 */
|
||||||
|
const handleClose = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 关闭的二次确认
|
||||||
|
await message.confirm('确认关闭该积分商城活动吗?')
|
||||||
|
// 发起关闭
|
||||||
|
await PointActivityApi.closePointActivity(id)
|
||||||
|
message.success('关闭成功')
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await PointActivityApi.deletePointActivity(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList()
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||||
|
|
||||||
|
// 表单校验
|
||||||
|
export const rules = reactive({
|
||||||
|
spuId: [required],
|
||||||
|
sort: [required]
|
||||||
|
})
|
||||||
|
|
||||||
|
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
|
||||||
|
const crudSchemas = reactive<CrudSchema[]>([
|
||||||
|
{
|
||||||
|
label: '排序',
|
||||||
|
field: 'sort',
|
||||||
|
form: {
|
||||||
|
component: 'InputNumber',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
width: 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '积分商城活动商品',
|
||||||
|
field: 'spuId',
|
||||||
|
isTable: true,
|
||||||
|
isSearch: false,
|
||||||
|
form: {
|
||||||
|
colProps: {
|
||||||
|
span: 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
width: 300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '备注',
|
||||||
|
field: 'remark',
|
||||||
|
isSearch: false,
|
||||||
|
form: {
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 4
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
width: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
export const { allSchemas } = useCrudSchemas(crudSchemas)
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-wrap items-center gap-8px">
|
||||||
|
<div
|
||||||
|
v-for="(pointActivity, index) in pointActivityList"
|
||||||
|
:key="pointActivity.id"
|
||||||
|
class="select-box spu-pic"
|
||||||
|
>
|
||||||
|
<el-tooltip :content="pointActivity.name">
|
||||||
|
<div class="relative h-full w-full">
|
||||||
|
<el-image :src="pointActivity.picUrl" class="h-full w-full" />
|
||||||
|
<Icon
|
||||||
|
v-show="!disabled"
|
||||||
|
class="del-icon"
|
||||||
|
icon="ep:circle-close-filled"
|
||||||
|
@click="handleRemoveActivity(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<el-tooltip v-if="canAdd" content="选择活动">
|
||||||
|
<div class="select-box" @click="openSeckillActivityTableSelect">
|
||||||
|
<Icon icon="ep:plus" />
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 拼团活动选择对话框(表格形式) -->
|
||||||
|
<PointTableSelect
|
||||||
|
ref="pointActivityTableSelectRef"
|
||||||
|
:multiple="limit != 1"
|
||||||
|
@change="handleActivitySelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import PointTableSelect from './PointTableSelect.vue'
|
||||||
|
import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { oneOfType } from 'vue-types'
|
||||||
|
import { isArray } from '@/utils/is'
|
||||||
|
|
||||||
|
// 活动橱窗,一般用于装修时使用
|
||||||
|
// 提供功能:展示活动列表、添加活动、删除活动
|
||||||
|
defineOptions({ name: 'PointShowcase' })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
|
||||||
|
// 限制数量:默认不限制
|
||||||
|
limit: propTypes.number.def(Number.MAX_VALUE),
|
||||||
|
disabled: propTypes.bool.def(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算是否可以添加
|
||||||
|
const canAdd = computed(() => {
|
||||||
|
// 情况一:禁用时不可以添加
|
||||||
|
if (props.disabled) return false
|
||||||
|
// 情况二:未指定限制数量时,可以添加
|
||||||
|
if (!props.limit) return true
|
||||||
|
// 情况三:检查已添加数量是否小于限制数量
|
||||||
|
return pointActivityList.value.length < props.limit
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拼团活动列表
|
||||||
|
const pointActivityList = ref<PointActivityVO[]>([])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async () => {
|
||||||
|
const ids = isArray(props.modelValue)
|
||||||
|
? // 情况一:多选
|
||||||
|
props.modelValue
|
||||||
|
: // 情况二:单选
|
||||||
|
props.modelValue
|
||||||
|
? [props.modelValue]
|
||||||
|
: []
|
||||||
|
// 不需要返显
|
||||||
|
if (ids.length === 0) {
|
||||||
|
pointActivityList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 只有活动发生变化之后,才会查询活动
|
||||||
|
if (
|
||||||
|
pointActivityList.value.length === 0 ||
|
||||||
|
pointActivityList.value.some((pointActivity) => !ids.includes(pointActivity.id!))
|
||||||
|
) {
|
||||||
|
pointActivityList.value = await PointActivityApi.getPointActivityListByIds(ids)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 活动表格选择对话框 */
|
||||||
|
const pointActivityTableSelectRef = ref()
|
||||||
|
// 打开对话框
|
||||||
|
const openSeckillActivityTableSelect = () => {
|
||||||
|
pointActivityTableSelectRef.value.open(pointActivityList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择活动后触发
|
||||||
|
* @param activityList 选中的活动列表
|
||||||
|
*/
|
||||||
|
const handleActivitySelected = (activityList: PointActivityVO | PointActivityVO[]) => {
|
||||||
|
pointActivityList.value = isArray(activityList) ? activityList : [activityList]
|
||||||
|
emitActivityChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除活动
|
||||||
|
* @param index 活动索引
|
||||||
|
*/
|
||||||
|
const handleRemoveActivity = (index: number) => {
|
||||||
|
pointActivityList.value.splice(index, 1)
|
||||||
|
emitActivityChange()
|
||||||
|
}
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
const emitActivityChange = () => {
|
||||||
|
if (props.limit === 1) {
|
||||||
|
const pointActivity = pointActivityList.value.length > 0 ? pointActivityList.value[0] : null
|
||||||
|
emit('update:modelValue', pointActivity?.id || 0)
|
||||||
|
emit('change', pointActivity)
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
pointActivityList.value.map((pointActivity) => pointActivity.id)
|
||||||
|
)
|
||||||
|
emit('change', pointActivityList.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select-box {
|
||||||
|
display: flex;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 1px dashed var(--el-border-color-darker);
|
||||||
|
border-radius: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spu-pic {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
z-index: 1;
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,300 @@
|
||||||
|
<template>
|
||||||
|
<Dialog v-model="dialogVisible" :appendToBody="true" title="选择活动" width="70%">
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="queryParams"
|
||||||
|
class="-mb-15px"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
|
||||||
|
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||||
|
<el-table-column v-if="multiple" width="55">
|
||||||
|
<template #header>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="isCheckAll"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@change="handleCheckAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="checkedStatus[row.id]"
|
||||||
|
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- 2. 单选模式 -->
|
||||||
|
<el-table-column v-else label="#" width="55">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-radio
|
||||||
|
v-model="selectedActivityId"
|
||||||
|
:value="row.id"
|
||||||
|
@change="handleSingleSelected(row)"
|
||||||
|
>
|
||||||
|
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||||
|
|
||||||
|
</el-radio>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||||
|
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image
|
||||||
|
:preview-src-list="[scope.row.picUrl]"
|
||||||
|
:src="scope.row.picUrl"
|
||||||
|
class="h-40px w-40px"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||||
|
<el-table-column
|
||||||
|
:formatter="fenToYuanFormat"
|
||||||
|
label="原价"
|
||||||
|
min-width="100"
|
||||||
|
prop="marketPrice"
|
||||||
|
/>
|
||||||
|
<el-table-column label="原价" min-width="100" prop="marketPrice" />
|
||||||
|
<el-table-column align="center" label="活动状态" min-width="100" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="库存" min-width="80" prop="stock" />
|
||||||
|
<el-table-column align="center" label="总库存" min-width="80" prop="totalStock" />
|
||||||
|
<el-table-column align="center" label="已兑换数量" min-width="100" prop="redeemedQuantity">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getRedeemedQuantity(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
align="center"
|
||||||
|
label="创建时间"
|
||||||
|
prop="createTime"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
:total="total"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
<template v-if="multiple" #footer>
|
||||||
|
<el-button type="primary" @click="handleEmitChange">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { CHANGE_EVENT } from 'element-plus'
|
||||||
|
import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
|
||||||
|
import { fenToYuanFormat } from '@/utils/formatter'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动表格选择对话框
|
||||||
|
* 1. 单选模式:
|
||||||
|
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
|
||||||
|
* 1.2 再次打开时,保持选中状态
|
||||||
|
* 2. 多选模式:
|
||||||
|
* 2.1 点击表格左侧的多选框时,记录选中的活动
|
||||||
|
* 2.2 切换分页时,保持活动的选中状态
|
||||||
|
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
|
||||||
|
* 2.4 再次打开时,保持选中状态
|
||||||
|
*/
|
||||||
|
defineOptions({ name: 'PointTableSelect' })
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
// 多选模式
|
||||||
|
multiple: propTypes.bool.def(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 列表的总页数
|
||||||
|
const total = ref(0)
|
||||||
|
// 列表的数据
|
||||||
|
const list = ref<PointActivityVO[]>([])
|
||||||
|
// 列表的加载中
|
||||||
|
const loading = ref(false)
|
||||||
|
// 弹窗的是否展示
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: null,
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 获得商品已兑换数量
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = (pointList?: PointActivityVO[]) => {
|
||||||
|
// 重置
|
||||||
|
checkedActivities.value = []
|
||||||
|
checkedStatus.value = {}
|
||||||
|
isCheckAll.value = false
|
||||||
|
isIndeterminate.value = false
|
||||||
|
|
||||||
|
// 处理已选中
|
||||||
|
if (pointList && pointList.length > 0) {
|
||||||
|
checkedActivities.value = [...pointList]
|
||||||
|
checkedStatus.value = Object.fromEntries(pointList.map((activityVO) => [activityVO.id, true]))
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = true
|
||||||
|
resetQuery()
|
||||||
|
}
|
||||||
|
// 提供 open 方法,用于打开弹窗
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await PointActivityApi.getPointActivityPage(queryParams.value)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
// checkbox绑定undefined会有问题,需要给一个bool值
|
||||||
|
list.value.forEach(
|
||||||
|
(activityVO) =>
|
||||||
|
(checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
|
||||||
|
)
|
||||||
|
// 计算全选框状态
|
||||||
|
calculateIsCheckAll()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.value = {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: null,
|
||||||
|
status: undefined
|
||||||
|
}
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否全选
|
||||||
|
const isCheckAll = ref(false)
|
||||||
|
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
|
||||||
|
const isIndeterminate = ref(false)
|
||||||
|
// 选中的活动
|
||||||
|
const checkedActivities = ref<PointActivityVO[]>([])
|
||||||
|
// 选中状态:key为活动ID,value为是否选中
|
||||||
|
const checkedStatus = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// 选中的活动 activityId
|
||||||
|
const selectedActivityId = ref()
|
||||||
|
/** 单选中时触发 */
|
||||||
|
const handleSingleSelected = (pointActivityVO: PointActivityVO) => {
|
||||||
|
emits(CHANGE_EVENT, pointActivityVO)
|
||||||
|
// 关闭弹窗
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 记住上次选择的ID
|
||||||
|
selectedActivityId.value = pointActivityVO.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 多选完成 */
|
||||||
|
const handleEmitChange = () => {
|
||||||
|
// 关闭弹窗
|
||||||
|
dialogVisible.value = false
|
||||||
|
emits(CHANGE_EVENT, [...checkedActivities.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认选择时的触发事件 */
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: CHANGE_EVENT, v: PointActivityVO | PointActivityVO[] | any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 全选/全不选 */
|
||||||
|
const handleCheckAll = (checked: boolean) => {
|
||||||
|
isCheckAll.value = checked
|
||||||
|
isIndeterminate.value = false
|
||||||
|
|
||||||
|
list.value.forEach((pointActivity) => handleCheckOne(checked, pointActivity, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中一行
|
||||||
|
* @param checked 是否选中
|
||||||
|
* @param pointActivity 活动
|
||||||
|
* @param isCalcCheckAll 是否计算全选
|
||||||
|
*/
|
||||||
|
const handleCheckOne = (
|
||||||
|
checked: boolean,
|
||||||
|
pointActivity: PointActivityVO,
|
||||||
|
isCalcCheckAll: boolean
|
||||||
|
) => {
|
||||||
|
if (checked) {
|
||||||
|
checkedActivities.value.push(pointActivity)
|
||||||
|
checkedStatus.value[pointActivity.id] = true
|
||||||
|
} else {
|
||||||
|
const index = findCheckedIndex(pointActivity)
|
||||||
|
if (index > -1) {
|
||||||
|
checkedActivities.value.splice(index, 1)
|
||||||
|
checkedStatus.value[pointActivity.id] = false
|
||||||
|
isCheckAll.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算全选框状态
|
||||||
|
if (isCalcCheckAll) {
|
||||||
|
calculateIsCheckAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找活动在已选中活动列表中的索引
|
||||||
|
const findCheckedIndex = (activityVO: PointActivityVO) =>
|
||||||
|
checkedActivities.value.findIndex((item) => item.id === activityVO.id)
|
||||||
|
|
||||||
|
// 计算全选框状态
|
||||||
|
const calculateIsCheckAll = () => {
|
||||||
|
isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
|
||||||
|
// 计算中间状态:不是全部选中 && 任意一个选中
|
||||||
|
isIndeterminate.value =
|
||||||
|
!isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -56,7 +56,7 @@
|
||||||
label="分类"
|
label="分类"
|
||||||
prop="productCategoryIds"
|
prop="productCategoryIds"
|
||||||
>
|
>
|
||||||
<ProductCategorySelect v-model="formData.productCategoryIds" />
|
<ProductCategorySelect v-model="formData.productCategoryIds" :multiple="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||||
|
@ -119,6 +119,9 @@ const open = async (type: string, id?: number) => {
|
||||||
// 规则分转元
|
// 规则分转元
|
||||||
data.rules?.forEach((item: any) => {
|
data.rules?.forEach((item: any) => {
|
||||||
item.discountPrice = fenToYuan(item.discountPrice || 0)
|
item.discountPrice = fenToYuan(item.discountPrice || 0)
|
||||||
|
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||||
|
item.limit = fenToYuan(item.limit || 0)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
formData.value = data
|
formData.value = data
|
||||||
// 获得商品范围
|
// 获得商品范围
|
||||||
|
@ -151,6 +154,9 @@ const submitForm = async () => {
|
||||||
// 规则元转分
|
// 规则元转分
|
||||||
data.rules.forEach((item) => {
|
data.rules.forEach((item) => {
|
||||||
item.discountPrice = yuanToFen(item.discountPrice || 0)
|
item.discountPrice = yuanToFen(item.discountPrice || 0)
|
||||||
|
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||||
|
item.limit = yuanToFen(item.limit || 0)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// 设置商品范围
|
// 设置商品范围
|
||||||
setProductScopeValues(data)
|
setProductScopeValues(data)
|
||||||
|
@ -188,7 +194,7 @@ const getProductScope = async () => {
|
||||||
case PromotionProductScopeEnum.CATEGORY.scope:
|
case PromotionProductScopeEnum.CATEGORY.scope:
|
||||||
await nextTick()
|
await nextTick()
|
||||||
let productCategoryIds = formData.value.productScopeValues as any
|
let productCategoryIds = formData.value.productScopeValues as any
|
||||||
if (Array.isArray(productCategoryIds) && productCategoryIds.length > 0) {
|
if (Array.isArray(productCategoryIds) && productCategoryIds.length === 1) {
|
||||||
// 单选时使用数组不能反显
|
// 单选时使用数组不能反显
|
||||||
productCategoryIds = productCategoryIds[0]
|
productCategoryIds = productCategoryIds[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,25 @@
|
||||||
<el-form ref="formRef" :model="rule">
|
<el-form ref="formRef" :model="rule">
|
||||||
<el-form-item label="优惠门槛:" label-width="100px" prop="limit">
|
<el-form-item label="优惠门槛:" label-width="100px" prop="limit">
|
||||||
满
|
满
|
||||||
|
<el-input-number
|
||||||
|
v-if="PromotionConditionTypeEnum.PRICE.type === formData.conditionType"
|
||||||
|
v-model="rule.limit"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-150px! p-x-20px!"
|
||||||
|
placeholder=""
|
||||||
|
type="number"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
|
v-else
|
||||||
v-model="rule.limit"
|
v-model="rule.limit"
|
||||||
:min="0"
|
:min="0"
|
||||||
class="w-150px! p-x-20px!"
|
class="w-150px! p-x-20px!"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<!-- TODO @puhui999:走字典数据? -->
|
|
||||||
{{ PromotionConditionTypeEnum.PRICE.type === formData.conditionType ? '元' : '件' }}
|
{{ PromotionConditionTypeEnum.PRICE.type === formData.conditionType ? '元' : '件' }}
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="优惠内容:" label-width="100px">
|
<el-form-item label="优惠内容:" label-width="100px">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<el-button class="ml-10px" type="text" @click="selectCoupon">添加优惠卷</el-button>
|
<el-button class="ml-10px" type="text" @click="selectCoupon">添加优惠劵</el-button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in list"
|
v-for="(item, index) in list"
|
||||||
|
@ -57,7 +57,7 @@ const emits = defineEmits<{
|
||||||
const rewardRule = useVModel(props, 'modelValue', emits) // 赠送规则
|
const rewardRule = useVModel(props, 'modelValue', emits) // 赠送规则
|
||||||
const list = ref<GiveCouponVO[]>([]) // 选择的优惠券列表
|
const list = ref<GiveCouponVO[]>([]) // 选择的优惠券列表
|
||||||
|
|
||||||
/** 选择赠送的优惠卷类型拓展 */
|
/** 选择赠送的优惠类型拓展 */
|
||||||
interface GiveCouponVO extends CouponTemplateApi.CouponTemplateVO {
|
interface GiveCouponVO extends CouponTemplateApi.CouponTemplateVO {
|
||||||
giveCount?: number
|
giveCount?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
placeholder="请选择活动状态"
|
placeholder="请选择活动状态"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_ACTIVITY_STATUS)"
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
:label="dict.label"
|
:label="dict.label"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
重置
|
重置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['product:brand:create']"
|
v-hasPermi="['promotion:reward-activity:create']"
|
||||||
plain
|
plain
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openForm('create')"
|
@click="openForm('create')"
|
||||||
|
@ -71,6 +71,11 @@
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-table v-loading="loading" :data="list" default-expand-all row-key="id">
|
<el-table v-loading="loading" :data="list" default-expand-all row-key="id">
|
||||||
<el-table-column label="活动名称" prop="name" />
|
<el-table-column label="活动名称" prop="name" />
|
||||||
|
<el-table-column label="活动范围" prop="productScope" >
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:formatter="dateFormatter"
|
:formatter="dateFormatter"
|
||||||
align="center"
|
align="center"
|
||||||
|
@ -85,7 +90,7 @@
|
||||||
/>
|
/>
|
||||||
<el-table-column align="center" label="状态" prop="status">
|
<el-table-column align="center" label="状态" prop="status">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<dict-tag :type="DICT_TYPE.PROMOTION_ACTIVITY_STATUS" :value="scope.row.status" />
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
|
@ -98,7 +103,7 @@
|
||||||
<el-table-column align="center" label="操作">
|
<el-table-column align="center" label="操作">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['product:brand:update']"
|
v-hasPermi="['promotion:reward-activity:update']"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openForm('update', scope.row.id)"
|
@click="openForm('update', scope.row.id)"
|
||||||
|
@ -106,7 +111,16 @@
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['product:brand:delete']"
|
v-if="scope.row.status === 0"
|
||||||
|
v-hasPermi="['promotion:reward-activity:close']"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleClose(scope.row.id)"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-hasPermi="['promotion:reward-activity:delete']"
|
||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDelete(scope.row.id)"
|
@click="handleDelete(scope.row.id)"
|
||||||
|
@ -180,6 +194,19 @@ const openForm = (type: string, id?: number) => {
|
||||||
formRef.value?.open(type, id)
|
formRef.value?.open(type, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 关闭按钮操作 */
|
||||||
|
const handleClose = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 关闭的二次确认
|
||||||
|
await message.confirm('确认关闭该满减活动吗?')
|
||||||
|
// 发起关闭
|
||||||
|
await RewardActivityApi.closeRewardActivity(id)
|
||||||
|
message.success('关闭成功')
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-wrap items-center gap-8px">
|
||||||
|
<div
|
||||||
|
v-for="(seckillActivity, index) in Activitys"
|
||||||
|
:key="seckillActivity.id"
|
||||||
|
class="select-box spu-pic"
|
||||||
|
>
|
||||||
|
<el-tooltip :content="seckillActivity.name">
|
||||||
|
<div class="relative h-full w-full">
|
||||||
|
<el-image :src="seckillActivity.picUrl" class="h-full w-full" />
|
||||||
|
<Icon
|
||||||
|
v-show="!disabled"
|
||||||
|
class="del-icon"
|
||||||
|
icon="ep:circle-close-filled"
|
||||||
|
@click="handleRemoveActivity(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<el-tooltip content="选择活动" v-if="canAdd">
|
||||||
|
<div class="select-box" @click="openSeckillActivityTableSelect">
|
||||||
|
<Icon icon="ep:plus" />
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 拼团活动选择对话框(表格形式) -->
|
||||||
|
<SeckillTableSelect
|
||||||
|
ref="seckillActivityTableSelectRef"
|
||||||
|
:multiple="limit != 1"
|
||||||
|
@change="handleActivitySelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { oneOfType } from 'vue-types'
|
||||||
|
import { isArray } from '@/utils/is'
|
||||||
|
import SeckillTableSelect from '@/views/mall/promotion/seckill/components/SeckillTableSelect.vue'
|
||||||
|
|
||||||
|
// 活动橱窗,一般用于装修时使用
|
||||||
|
// 提供功能:展示活动列表、添加活动、删除活动
|
||||||
|
defineOptions({ name: 'SeckillShowcase' })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
|
||||||
|
// 限制数量:默认不限制
|
||||||
|
limit: propTypes.number.def(Number.MAX_VALUE),
|
||||||
|
disabled: propTypes.bool.def(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算是否可以添加
|
||||||
|
const canAdd = computed(() => {
|
||||||
|
// 情况一:禁用时不可以添加
|
||||||
|
if (props.disabled) return false
|
||||||
|
// 情况二:未指定限制数量时,可以添加
|
||||||
|
if (!props.limit) return true
|
||||||
|
// 情况三:检查已添加数量是否小于限制数量
|
||||||
|
return Activitys.value.length < props.limit
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拼团活动列表
|
||||||
|
const Activitys = ref<SeckillActivityApi.SeckillActivityVO[]>([])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async () => {
|
||||||
|
const ids = isArray(props.modelValue)
|
||||||
|
? // 情况一:多选
|
||||||
|
props.modelValue
|
||||||
|
: // 情况二:单选
|
||||||
|
props.modelValue
|
||||||
|
? [props.modelValue]
|
||||||
|
: []
|
||||||
|
// 不需要返显
|
||||||
|
if (ids.length === 0) {
|
||||||
|
Activitys.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 只有活动发生变化之后,才会查询活动
|
||||||
|
if (
|
||||||
|
Activitys.value.length === 0 ||
|
||||||
|
Activitys.value.some((seckillActivity) => !ids.includes(seckillActivity.id!))
|
||||||
|
) {
|
||||||
|
Activitys.value = await SeckillActivityApi.getSeckillActivityListByIds(ids)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 活动表格选择对话框 */
|
||||||
|
const seckillActivityTableSelectRef = ref()
|
||||||
|
// 打开对话框
|
||||||
|
const openSeckillActivityTableSelect = () => {
|
||||||
|
seckillActivityTableSelectRef.value.open(Activitys.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择活动后触发
|
||||||
|
* @param activityVOs 选中的活动列表
|
||||||
|
*/
|
||||||
|
const handleActivitySelected = (
|
||||||
|
activityVOs: SeckillActivityApi.SeckillActivityVO | SeckillActivityApi.SeckillActivityVO[]
|
||||||
|
) => {
|
||||||
|
Activitys.value = isArray(activityVOs) ? activityVOs : [activityVOs]
|
||||||
|
emitActivityChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除活动
|
||||||
|
* @param index 活动索引
|
||||||
|
*/
|
||||||
|
const handleRemoveActivity = (index: number) => {
|
||||||
|
Activitys.value.splice(index, 1)
|
||||||
|
emitActivityChange()
|
||||||
|
}
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
const emitActivityChange = () => {
|
||||||
|
if (props.limit === 1) {
|
||||||
|
const seckillActivity = Activitys.value.length > 0 ? Activitys.value[0] : null
|
||||||
|
emit('update:modelValue', seckillActivity?.id || 0)
|
||||||
|
emit('change', seckillActivity)
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
Activitys.value.map((seckillActivity) => seckillActivity.id)
|
||||||
|
)
|
||||||
|
emit('change', Activitys.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select-box {
|
||||||
|
display: flex;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 1px dashed var(--el-border-color-darker);
|
||||||
|
border-radius: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spu-pic {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
z-index: 1;
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,343 @@
|
||||||
|
<template>
|
||||||
|
<Dialog v-model="dialogVisible" :appendToBody="true" title="选择活动" width="70%">
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="活动状态" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.status"
|
||||||
|
placeholder="请选择活动状态"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
|
||||||
|
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||||
|
<el-table-column width="55" v-if="multiple">
|
||||||
|
<template #header>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="isCheckAll"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@change="handleCheckAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="checkedStatus[row.id]"
|
||||||
|
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- 2. 单选模式 -->
|
||||||
|
<el-table-column label="#" width="55" v-else>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-radio
|
||||||
|
:value="row.id"
|
||||||
|
v-model="selectedActivityId"
|
||||||
|
@change="handleSingleSelected(row)"
|
||||||
|
>
|
||||||
|
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||||
|
|
||||||
|
</el-radio>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="活动编号" prop="id" min-width="80" />
|
||||||
|
<el-table-column label="活动名称" prop="name" min-width="140" />
|
||||||
|
<el-table-column label="活动时间" min-width="210">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
|
||||||
|
~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品图片" prop="spuName" min-width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image
|
||||||
|
:src="scope.row.picUrl"
|
||||||
|
class="h-40px w-40px"
|
||||||
|
:preview-src-list="[scope.row.picUrl]"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品标题" prop="spuName" min-width="300" />
|
||||||
|
<el-table-column
|
||||||
|
label="原价"
|
||||||
|
prop="marketPrice"
|
||||||
|
min-width="100"
|
||||||
|
:formatter="fenToYuanFormat"
|
||||||
|
/>
|
||||||
|
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatSeckillPrice(scope.row.products) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
|
||||||
|
<el-table-column label="成团组数" prop="groupSuccessCount" min-width="100" />
|
||||||
|
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
|
||||||
|
<el-table-column label="活动状态" align="center" prop="status" min-width="100">
|
||||||
|
<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="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
:total="total"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
<template #footer v-if="multiple">
|
||||||
|
<el-button type="primary" @click="handleEmitChange">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { handleTree } from '@/utils/tree'
|
||||||
|
|
||||||
|
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { CHANGE_EVENT } from 'element-plus'
|
||||||
|
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
||||||
|
import { fenToYuanFormat } from '@/utils/formatter'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||||
|
import { fenToYuan } from '@/utils'
|
||||||
|
|
||||||
|
type SeckillActivityVO = Required<SeckillActivityApi.SeckillActivityVO>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动表格选择对话框
|
||||||
|
* 1. 单选模式:
|
||||||
|
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
|
||||||
|
* 1.2 再次打开时,保持选中状态
|
||||||
|
* 2. 多选模式:
|
||||||
|
* 2.1 点击表格左侧的多选框时,记录选中的活动
|
||||||
|
* 2.2 切换分页时,保持活动的选中状态
|
||||||
|
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
|
||||||
|
* 2.4 再次打开时,保持选中状态
|
||||||
|
*/
|
||||||
|
defineOptions({ name: 'SeckillTableSelect' })
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
// 多选模式
|
||||||
|
multiple: propTypes.bool.def(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 列表的总页数
|
||||||
|
const total = ref(0)
|
||||||
|
// 列表的数据
|
||||||
|
const list = ref<SeckillActivityVO[]>([])
|
||||||
|
// 列表的加载中
|
||||||
|
const loading = ref(false)
|
||||||
|
// 弹窗的是否展示
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = ref({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: null,
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = (SeckillList?: SeckillActivityVO[]) => {
|
||||||
|
// 重置
|
||||||
|
checkedActivitys.value = []
|
||||||
|
checkedStatus.value = {}
|
||||||
|
isCheckAll.value = false
|
||||||
|
isIndeterminate.value = false
|
||||||
|
|
||||||
|
// 处理已选中
|
||||||
|
if (SeckillList && SeckillList.length > 0) {
|
||||||
|
checkedActivitys.value = [...SeckillList]
|
||||||
|
checkedStatus.value = Object.fromEntries(SeckillList.map((activityVO) => [activityVO.id, true]))
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = true
|
||||||
|
resetQuery()
|
||||||
|
}
|
||||||
|
// 提供 open 方法,用于打开弹窗
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await SeckillActivityApi.getSeckillActivityPage(queryParams.value)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
// checkbox绑定undefined会有问题,需要给一个bool值
|
||||||
|
list.value.forEach(
|
||||||
|
(activityVO) =>
|
||||||
|
(checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
|
||||||
|
)
|
||||||
|
// 计算全选框状态
|
||||||
|
calculateIsCheckAll()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.value = {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
createTime: []
|
||||||
|
}
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化拼团价格
|
||||||
|
* @param products
|
||||||
|
*/
|
||||||
|
const formatSeckillPrice = (products) => {
|
||||||
|
const seckillPrice = Math.min(...products.map((item) => item.seckillPrice))
|
||||||
|
return `¥${fenToYuan(seckillPrice)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否全选
|
||||||
|
const isCheckAll = ref(false)
|
||||||
|
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
|
||||||
|
const isIndeterminate = ref(false)
|
||||||
|
// 选中的活动
|
||||||
|
const checkedActivitys = ref<SeckillActivityVO[]>([])
|
||||||
|
// 选中状态:key为活动ID,value为是否选中
|
||||||
|
const checkedStatus = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// 选中的活动 activityId
|
||||||
|
const selectedActivityId = ref()
|
||||||
|
/** 单选中时触发 */
|
||||||
|
const handleSingleSelected = (seckillActivityVO: SeckillActivityVO) => {
|
||||||
|
emits(CHANGE_EVENT, seckillActivityVO)
|
||||||
|
// 关闭弹窗
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 记住上次选择的ID
|
||||||
|
selectedActivityId.value = seckillActivityVO.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 多选完成 */
|
||||||
|
const handleEmitChange = () => {
|
||||||
|
// 关闭弹窗
|
||||||
|
dialogVisible.value = false
|
||||||
|
emits(CHANGE_EVENT, [...checkedActivitys.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认选择时的触发事件 */
|
||||||
|
const emits = defineEmits<{
|
||||||
|
change: [SeckillActivityApi: SeckillActivityVO | SeckillActivityVO[] | any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 全选/全不选 */
|
||||||
|
const handleCheckAll = (checked: boolean) => {
|
||||||
|
isCheckAll.value = checked
|
||||||
|
isIndeterminate.value = false
|
||||||
|
|
||||||
|
list.value.forEach((seckillActivity) => handleCheckOne(checked, seckillActivity, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中一行
|
||||||
|
* @param checked 是否选中
|
||||||
|
* @param seckillActivity 活动
|
||||||
|
* @param isCalcCheckAll 是否计算全选
|
||||||
|
*/
|
||||||
|
const handleCheckOne = (
|
||||||
|
checked: boolean,
|
||||||
|
seckillActivity: SeckillActivityVO,
|
||||||
|
isCalcCheckAll: boolean
|
||||||
|
) => {
|
||||||
|
if (checked) {
|
||||||
|
checkedActivitys.value.push(seckillActivity)
|
||||||
|
checkedStatus.value[seckillActivity.id] = true
|
||||||
|
} else {
|
||||||
|
const index = findCheckedIndex(seckillActivity)
|
||||||
|
if (index > -1) {
|
||||||
|
checkedActivitys.value.splice(index, 1)
|
||||||
|
checkedStatus.value[seckillActivity.id] = false
|
||||||
|
isCheckAll.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算全选框状态
|
||||||
|
if (isCalcCheckAll) {
|
||||||
|
calculateIsCheckAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找活动在已选中活动列表中的索引
|
||||||
|
const findCheckedIndex = (activityVO: SeckillActivityVO) =>
|
||||||
|
checkedActivitys.value.findIndex((item) => item.id === activityVO.id)
|
||||||
|
|
||||||
|
// 计算全选框状态
|
||||||
|
const calculateIsCheckAll = () => {
|
||||||
|
isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
|
||||||
|
// 计算中间状态:不是全部选中 && 任意一个选中
|
||||||
|
isIndeterminate.value =
|
||||||
|
!isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类列表
|
||||||
|
const categoryList = ref()
|
||||||
|
// 分类树
|
||||||
|
const categoryTreeList = ref()
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList()
|
||||||
|
// 获得分类树
|
||||||
|
categoryList.value = await ProductCategoryApi.getCategoryList({})
|
||||||
|
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -6,6 +6,11 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as MpAccountApi from '@/api/mp/account'
|
import * as MpAccountApi from '@/api/mp/account'
|
||||||
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { delView } = useTagsViewStore() // 视图操作
|
||||||
|
const { push, currentRoute } = useRouter() // 路由
|
||||||
|
|
||||||
defineOptions({ name: 'WxAccountSelect' })
|
defineOptions({ name: 'WxAccountSelect' })
|
||||||
|
|
||||||
|
@ -22,6 +27,12 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const handleQuery = async () => {
|
const handleQuery = async () => {
|
||||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||||
|
if (accountList.value.length == 0) {
|
||||||
|
message.error('未配置公众号,请在【公众号管理 -> 账号管理】菜单,进行配置')
|
||||||
|
delView(unref(currentRoute))
|
||||||
|
await push({ name: 'MpAccount' })
|
||||||
|
return
|
||||||
|
}
|
||||||
// 默认选中第一个
|
// 默认选中第一个
|
||||||
if (accountList.value.length > 0) {
|
if (accountList.value.length > 0) {
|
||||||
account.id = accountList.value[0].id
|
account.id = accountList.value[0].id
|
||||||
|
|
|
@ -3,14 +3,7 @@
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-form class="-mb-15px" ref="queryForm" :inline="true" label-width="68px">
|
<el-form class="-mb-15px" ref="queryForm" :inline="true" label-width="68px">
|
||||||
<el-form-item label="公众号" prop="accountId">
|
<el-form-item label="公众号" prop="accountId">
|
||||||
<el-select v-model="accountId" @change="getSummary" class="!w-240px">
|
<WxAccountSelect @change="onAccountChanged" />
|
||||||
<el-option
|
|
||||||
v-for="item in accountList"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="时间范围" prop="dateRange">
|
<el-form-item label="时间范围" prop="dateRange">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
|
@ -76,7 +69,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { formatDate, addTime, betweenDay, beginOfDay, endOfDay } from '@/utils/formatTime'
|
import { formatDate, addTime, betweenDay, beginOfDay, endOfDay } from '@/utils/formatTime'
|
||||||
import * as StatisticsApi from '@/api/mp/statistics'
|
import * as StatisticsApi from '@/api/mp/statistics'
|
||||||
import * as MpAccountApi from '@/api/mp/account'
|
import WxAccountSelect from '@/views/mp/components/wx-account-select'
|
||||||
|
|
||||||
defineOptions({ name: 'MpStatistics' })
|
defineOptions({ name: 'MpStatistics' })
|
||||||
|
|
||||||
|
@ -88,7 +81,6 @@ const dateRange = ref([
|
||||||
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
|
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
|
||||||
])
|
])
|
||||||
const accountId = ref(-1) // 选中的公众号编号
|
const accountId = ref(-1) // 选中的公众号编号
|
||||||
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
|
|
||||||
|
|
||||||
const xAxisDate = ref([] as any[]) // X 轴的日期范围
|
const xAxisDate = ref([] as any[]) // X 轴的日期范围
|
||||||
// 用户增减数据图表配置项
|
// 用户增减数据图表配置项
|
||||||
|
@ -230,13 +222,10 @@ const interfaceSummaryOption = reactive({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 加载公众号账号的列表 */
|
/** 侦听公众号变化 **/
|
||||||
const getAccountList = async () => {
|
const onAccountChanged = (id: number) => {
|
||||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
accountId.value = id
|
||||||
// 默认选中第一个
|
getSummary()
|
||||||
if (accountList.value.length > 0) {
|
|
||||||
accountId.value = accountList.value[0].id!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载数据 */
|
/** 加载数据 */
|
||||||
|
@ -357,12 +346,4 @@ const interfaceSummaryChart = async () => {
|
||||||
})
|
})
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(async () => {
|
|
||||||
// 获取公众号下拉列表
|
|
||||||
await getAccountList()
|
|
||||||
// 加载数据
|
|
||||||
getSummary()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -231,7 +231,7 @@ const getDetail = async () => {
|
||||||
goReturnUrl('cancel')
|
goReturnUrl('cancel')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = await PayOrderApi.getOrder(id.value)
|
const data = await PayOrderApi.getOrder(id.value, true)
|
||||||
payOrder.value = data
|
payOrder.value = data
|
||||||
// 1.2 无法查询到支付信息
|
// 1.2 无法查询到支付信息
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getAccessToken } from '@/utils/auth'
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
|
|
||||||
defineOptions({ name: 'JimuReport' })
|
defineOptions({ name: 'JimuReport' })
|
||||||
|
|
||||||
const src = ref(import.meta.env.VITE_BASE_URL + '/jmreport/list?token=' + getAccessToken())
|
// 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:积木报表无法方便的刷新访问令牌
|
||||||
|
const src = ref(import.meta.env.VITE_BASE_URL + '/jmreport/list?token=' + getRefreshToken())
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -19,7 +19,6 @@ interface ImportMetaEnv {
|
||||||
readonly VITE_APP_DEFAULT_LOGIN_PASSWORD: string
|
readonly VITE_APP_DEFAULT_LOGIN_PASSWORD: string
|
||||||
readonly VITE_APP_DOCALERT_ENABLE: string
|
readonly VITE_APP_DOCALERT_ENABLE: string
|
||||||
readonly VITE_BASE_URL: string
|
readonly VITE_BASE_URL: string
|
||||||
readonly VITE_UPLOAD_URL: string
|
|
||||||
readonly VITE_API_URL: string
|
readonly VITE_API_URL: string
|
||||||
readonly VITE_BASE_PATH: string
|
readonly VITE_BASE_PATH: string
|
||||||
readonly VITE_DROP_DEBUGGER: string
|
readonly VITE_DROP_DEBUGGER: string
|
||||||
|
|
Loading…
Reference in New Issue