Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/im

# Conflicts:
#	package.json
#	pnpm-lock.yaml
#	src/router/modules/remaining.ts
#	vite.config.ts
feature/im
YunaiV 2024-10-14 12:46:57 +08:00
commit 251de48342
526 changed files with 29402 additions and 6129 deletions

5
.env
View File

@ -18,3 +18,8 @@ VITE_APP_DOCALERT_ENABLE=true
# 百度统计 # 百度统计
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

1
.gitignore vendored
View File

@ -2,7 +2,6 @@ node_modules
.DS_Store .DS_Store
dist dist
dist-ssr dist-ssr
*.local
/dist* /dist*
pnpm-debug pnpm-debug
auto-*.d.ts auto-*.d.ts

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -83,10 +83,11 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "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",

View File

@ -55,9 +55,8 @@
推荐 VS Code 开发,配合插件如下: 推荐 VS Code 开发,配合插件如下:
| 插件名 | 功能 | | 插件名 | 功能 |
|-------------------------------|--------------------------| |-------------------------------|---------------------|
| TypeScript Vue Plugin (Volar) | 用于 TypeScript 的 Vue 插件 | | Vue - Official | Vue 与 TypeScript 支持 |
| Vue Language Features (Volar) | Vue3.0 语法支持 |
| unocss | unocss for vscode | | unocss | unocss for vscode |
| Iconify IntelliSense | Iconify 预览和搜索 | | Iconify IntelliSense | Iconify 预览和搜索 |
| i18n Ally | 国际化智能提示 | | i18n Ally | 国际化智能提示 |
@ -192,26 +191,24 @@ ps核心功能已经实现正在对接微信小程序中...
### 商城系统 ### 商城系统
演示地址:<https://doc.iocoder.cn/mall-preview/>
![功能图](/.image/common/mall-feature.png) ![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png) ![功能图](/.image/common/mall-preview.png)
_前端基于 crmeb uniapp 经过授权重构优化代码实现接入芋道快速开发平台_
演示地址:<https://doc.iocoder.cn/mall-preview/>
### ERP 系统 ### ERP 系统
![功能图](/.image/common/erp-feature.png)
演示地址:<https://doc.iocoder.cn/erp-preview/> 演示地址:<https://doc.iocoder.cn/erp-preview/>
![功能图](/.image/common/erp-feature.png)
### CRM 系统 ### CRM 系统
![功能图](/.image/common/crm-feature.png)
演示地址:<https://doc.iocoder.cn/crm-preview/> 演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
## 🐷 演示图 ## 🐷 演示图
### 系统功能 ### 系统功能

View File

@ -27,6 +27,12 @@ const include = [
'echarts-wordcloud', 'echarts-wordcloud',
'@wangeditor/editor', '@wangeditor/editor',
'@wangeditor/editor-for-vue', '@wangeditor/editor-for-vue',
'@microsoft/fetch-event-source',
'markdown-it',
'markmap-view',
'markmap-lib',
'markmap-toolbar',
'highlight.js',
'element-plus', 'element-plus',
'element-plus/es', 'element-plus/es',
'element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/zh-cn',
@ -104,7 +110,11 @@ const include = [
'element-plus/es/components/collapse/style/css', 'element-plus/es/components/collapse/style/css',
'element-plus/es/components/collapse-item/style/css', 'element-plus/es/components/collapse-item/style/css',
'element-plus/es/components/button-group/style/css', 'element-plus/es/components/button-group/style/css',
'element-plus/es/components/text/style/css' 'element-plus/es/components/text/style/css',
'element-plus/es/components/segmented/style/css',
'@element-plus/icons-vue',
'element-plus/es/components/footer/style/css',
'element-plus/es/components/empty/style/css'
] ]
const exclude = ['@iconify/json'] const exclude = ['@iconify/json']

View File

@ -1,22 +1,22 @@
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.1.0-snapshot", "version": "2.3.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,
"scripts": { "scripts": {
"i": "pnpm install", "i": "pnpm install",
"dev": "vite", "dev": "vite --mode env.local",
"dev-server": "vite --mode dev", "dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit", "ts:check": "vue-tsc --noEmit",
"build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", "build:local": "node ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", "build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", "build:test": "node ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", "build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", "build:prod": "node ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev", "serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod", "serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local-dev && vite preview", "preview": "pnpm build:local && vite preview",
"clean": "npx rimraf node_modules", "clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache", "clean:cache": "npx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
@ -26,9 +26,10 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@form-create/designer": "^3.1.3", "@form-create/designer": "^3.2.6",
"@form-create/element-ui": "^3.1.24", "@form-create/element-ui": "^3.2.11",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@videojs-player/vue": "^1.0.0", "@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
@ -46,12 +47,17 @@
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.6.1", "element-plus": "2.8.4",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",
"markmap-lib": "^0.16.1",
"markmap-toolbar": "^0.17.0",
"markmap-view": "^0.16.0",
"min-dash": "^4.1.1", "min-dash": "^4.1.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
@ -62,7 +68,7 @@
"steady-xml": "^0.1.0", "steady-xml": "^0.1.0",
"url": "^0.11.3", "url": "^0.11.3",
"video.js": "^7.21.5", "video.js": "^7.21.5",
"vue": "3.4.21", "vue": "3.5.12",
"vue-at": "3.0.0-alpha.2", "vue-at": "3.0.0-alpha.2",
"vue-dompurify-html": "^4.1.4", "vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.10.2", "vue-i18n": "9.10.2",
@ -85,8 +91,8 @@
"@types/qs": "^6.9.12", "@types/qs": "^6.9.12",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.1.0",
"@unocss/transformer-variant-group": "^0.58.5",
"@unocss/eslint-config": "^0.57.4", "@unocss/eslint-config": "^0.57.4",
"@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-legacy": "^5.3.1", "@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
@ -126,7 +132,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"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
import request from '@/config/axios'
// AI 聊天对话 VO
export interface ChatConversationVO {
id: number // ID 编号
userId: number // 用户编号
title: string // 对话标题
pinned: boolean // 是否置顶
roleId: number // 角色编号
modelId: number // 模型编号
model: string // 模型标志
temperature: number // 温度参数
maxTokens: number // 单条回复的最大 Token 数量
maxContexts: number // 上下文的最大 Message 数量
createTime?: Date // 创建时间
// 额外字段
systemMessage?: string // 角色设定
modelName?: string // 模型名字
roleAvatar?: string // 角色头像
modelMaxTokens?: string // 模型的单条回复的最大 Token 数量
modelMaxContexts?: string // 模型的上下文的最大 Message 数量
}
// AI 聊天对话 API
export const ChatConversationApi = {
// 获得【我的】聊天对话
getChatConversationMy: async (id: number) => {
return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` })
},
// 新增【我的】聊天对话
createChatConversationMy: async (data?: ChatConversationVO) => {
return await request.post({ url: `/ai/chat/conversation/create-my`, data })
},
// 更新【我的】聊天对话
updateChatConversationMy: async (data: ChatConversationVO) => {
return await request.put({ url: `/ai/chat/conversation/update-my`, data })
},
// 删除【我的】聊天对话
deleteChatConversationMy: async (id: string) => {
return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` })
},
// 删除【我的】所有对话,置顶除外
deleteChatConversationMyByUnpinned: async () => {
return await request.delete({ url: `/ai/chat/conversation/delete-by-unpinned` })
},
// 获得【我的】聊天对话列表
getChatConversationMyList: async () => {
return await request.get({ url: `/ai/chat/conversation/my-list` })
},
// 获得对话分页
getChatConversationPage: async (params: any) => {
return await request.get({ url: `/ai/chat/conversation/page`, params })
},
// 管理员删除消息
deleteChatConversationByAdmin: async (id: number) => {
return await request.delete({ url: `/ai/chat/conversation/delete-by-admin?id=${id}` })
}
}

View File

@ -0,0 +1,83 @@
import request from '@/config/axios'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
// 聊天VO
export interface ChatMessageVO {
id: number // 编号
conversationId: number // 对话编号
type: string // 消息类型
userId: string // 用户编号
roleId: string // 角色编号
model: number // 模型标志
modelId: number // 模型编号
content: string // 聊天内容
tokens: number // 消耗 Token 数量
createTime: Date // 创建时间
roleAvatar: string // 角色头像
userAvatar: string // 创建时间
}
// AI chat 聊天
export const ChatMessageApi = {
// 消息列表
getChatMessageListByConversationId: async (conversationId: number | null) => {
return await request.get({
url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
})
},
// 发送 Stream 消息
// 为什么不用 axios 呢?因为它不支持 SSE 调用
sendChatMessageStream: async (
conversationId: number,
content: string,
ctrl,
enableContext: boolean,
onMessage,
onError,
onClose
) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/chat/message/send-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify({
conversationId,
content,
useContext: enableContext
}),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 删除消息
deleteChatMessage: async (id: string) => {
return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
},
// 删除指定对话的消息
deleteByConversationId: async (conversationId: number) => {
return await request.delete({
url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`
})
},
// 获得消息分页
getChatMessagePage: async (params: any) => {
return await request.get({ url: '/ai/chat/message/page', params })
},
// 管理员删除消息
deleteChatMessageByAdmin: async (id: number) => {
return await request.delete({ url: `/ai/chat/message/delete-by-admin?id=${id}` })
}
}

103
src/api/ai/image/index.ts Normal file
View File

@ -0,0 +1,103 @@
import request from '@/config/axios'
// AI 绘图 VO
export interface ImageVO {
id: number // 编号
platform: string // 平台
model: string // 模型
prompt: string // 提示词
width: number // 图片宽度
height: number // 图片高度
status: number // 状态
publicStatus: boolean // 公开状态
picUrl: string // 任务地址
errorMessage: string // 错误信息
options: any // 配置 Map<string, string>
taskId: number // 任务编号
buttons: ImageMidjourneyButtonsVO[] // mj 操作按钮
createTime: Date // 创建时间
finishTime: Date // 完成时间
}
export interface ImageDrawReqVO {
platform: string // 平台
prompt: string // 提示词
model: string // 模型
style: string // 图像生成的风格
width: string // 图片宽度
height: string // 图片高度
options: object // 绘制参数Map<String, String>
}
export interface ImageMidjourneyImagineReqVO {
prompt: string // 提示词
model: string // 模型 mj nijj
base64Array: string[] // size不能为空
width: string // 图片宽度
height: string // 图片高度
version: string // 版本
}
export interface ImageMidjourneyActionVO {
id: number // 图片编号
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
}
export interface ImageMidjourneyButtonsVO {
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
emoji: string // 图标 emoji
label: string // Make Variations 文本
style: number // 样式: 2Primary、3Green
}
// AI 图片 API
export const ImageApi = {
// 获取【我的】绘图分页
getImagePageMy: async (params: any) => {
return await request.get({ url: `/ai/image/my-page`, params })
},
// 获取【我的】绘图记录
getImageMy: async (id: number) => {
return await request.get({ url: `/ai/image/get-my?id=${id}` })
},
// 获取【我的】绘图记录列表
getImageListMyByIds: async (ids: number[]) => {
return await request.get({ url: `/ai/image/my-list-by-ids`, params: { ids: ids.join(',') } })
},
// 生成图片
drawImage: async (data: ImageDrawReqVO) => {
return await request.post({ url: `/ai/image/draw`, data })
},
// 删除【我的】绘画记录
deleteImageMy: async (id: number) => {
return await request.delete({ url: `/ai/image/delete-my?id=${id}` })
},
// ================ midjourney 专属 ================
// 【Midjourney】生成图片
midjourneyImagine: async (data: ImageMidjourneyImagineReqVO) => {
return await request.post({ url: `/ai/image/midjourney/imagine`, data })
},
// 【Midjourney】Action 操作(二次生成图片)
midjourneyAction: async (data: ImageMidjourneyActionVO) => {
return await request.post({ url: `/ai/image/midjourney/action`, data })
},
// ================ 绘图管理 ================
// 查询绘画分页
getImagePage: async (params: any) => {
return await request.get({ url: `/ai/image/page`, params })
},
// 更新绘画发布状态
updateImage: async (data: any) => {
return await request.put({ url: '/ai/image/update', data })
},
// 删除绘画
deleteImage: async (id: number) => {
return await request.delete({ url: `/ai/image/delete?id=` + id })
}
}

View File

@ -0,0 +1,60 @@
import { getAccessToken } from '@/utils/auth'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { config } from '@/config/axios/config'
import request from '@/config/axios' // AI 思维导图 VO
// AI 思维导图 VO
export interface MindMapVO {
id: number // 编号
userId: number // 用户编号
prompt: string // 生成内容提示
generatedContent: string // 生成的思维导图内容
platform: string // 平台
model: string // 模型
errorMessage: string // 错误信息
}
// AI 思维导图生成 VO
export interface AiMindMapGenerateReqVO {
prompt: string
}
export const AiMindMapApi = {
generateMindMap: ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: AiMindMapGenerateReqVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/mind-map/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 查询思维导图分页
getMindMapPage: async (params: any) => {
return await request.get({ url: `/ai/mind-map/page`, params })
},
// 删除思维导图
deleteMindMap: async (id: number) => {
return await request.delete({ url: `/ai/mind-map/delete?id=` + id })
}
}

View File

@ -0,0 +1,44 @@
import request from '@/config/axios'
// AI API 密钥 VO
export interface ApiKeyVO {
id: number // 编号
name: string // 名称
apiKey: string // 密钥
platform: string // 平台
url: string // 自定义 API 地址
status: number // 状态
}
// AI API 密钥 API
export const ApiKeyApi = {
// 查询 API 密钥分页
getApiKeyPage: async (params: any) => {
return await request.get({ url: `/ai/api-key/page`, params })
},
// 获得 API 密钥列表
getApiKeySimpleList: async () => {
return await request.get({ url: `/ai/api-key/simple-list` })
},
// 查询 API 密钥详情
getApiKey: async (id: number) => {
return await request.get({ url: `/ai/api-key/get?id=` + id })
},
// 新增 API 密钥
createApiKey: async (data: ApiKeyVO) => {
return await request.post({ url: `/ai/api-key/create`, data })
},
// 修改 API 密钥
updateApiKey: async (data: ApiKeyVO) => {
return await request.put({ url: `/ai/api-key/update`, data })
},
// 删除 API 密钥
deleteApiKey: async (id: number) => {
return await request.delete({ url: `/ai/api-key/delete?id=` + id })
}
}

View File

@ -0,0 +1,53 @@
import request from '@/config/axios'
// AI 聊天模型 VO
export interface ChatModelVO {
id: number // 编号
keyId: number // API 秘钥编号
name: string // 模型名字
model: string // 模型标识
platform: string // 模型平台
sort: number // 排序
status: number // 状态
temperature: number // 温度参数
maxTokens: number // 单条回复的最大 Token 数量
maxContexts: number // 上下文的最大 Message 数量
}
// AI 聊天模型 API
export const ChatModelApi = {
// 查询聊天模型分页
getChatModelPage: async (params: any) => {
return await request.get({ url: `/ai/chat-model/page`, params })
},
// 获得聊天模型列表
getChatModelSimpleList: async (status?: number) => {
return await request.get({
url: `/ai/chat-model/simple-list`,
params: {
status
}
})
},
// 查询聊天模型详情
getChatModel: async (id: number) => {
return await request.get({ url: `/ai/chat-model/get?id=` + id })
},
// 新增聊天模型
createChatModel: async (data: ChatModelVO) => {
return await request.post({ url: `/ai/chat-model/create`, data })
},
// 修改聊天模型
updateChatModel: async (data: ChatModelVO) => {
return await request.put({ url: `/ai/chat-model/update`, data })
},
// 删除聊天模型
deleteChatModel: async (id: number) => {
return await request.delete({ url: `/ai/chat-model/delete?id=` + id })
}
}

View File

@ -0,0 +1,80 @@
import request from '@/config/axios'
// AI 聊天角色 VO
export interface ChatRoleVO {
id: number // 角色编号
modelId: number // 模型编号
name: string // 角色名称
avatar: string // 角色头像
category: string // 角色类别
sort: number // 角色排序
description: string // 角色描述
systemMessage: string // 角色设定
welcomeMessage: string // 角色设定
publicStatus: boolean // 是否公开
status: number // 状态
}
// AI 聊天角色 分页请求 vo
export interface ChatRolePageReqVO {
name?: string // 角色名称
category?: string // 角色类别
publicStatus: boolean // 是否公开
pageNo: number // 是否公开
pageSize: number // 是否公开
}
// AI 聊天角色 API
export const ChatRoleApi = {
// 查询聊天角色分页
getChatRolePage: async (params: any) => {
return await request.get({ url: `/ai/chat-role/page`, params })
},
// 查询聊天角色详情
getChatRole: async (id: number) => {
return await request.get({ url: `/ai/chat-role/get?id=` + id })
},
// 新增聊天角色
createChatRole: async (data: ChatRoleVO) => {
return await request.post({ url: `/ai/chat-role/create`, data })
},
// 修改聊天角色
updateChatRole: async (data: ChatRoleVO) => {
return await request.put({ url: `/ai/chat-role/update`, data })
},
// 删除聊天角色
deleteChatRole: async (id: number) => {
return await request.delete({ url: `/ai/chat-role/delete?id=` + id })
},
// ======= chat 聊天
// 获取 my role
getMyPage: async (params: ChatRolePageReqVO) => {
return await request.get({ url: `/ai/chat-role/my-page`, params})
},
// 获取角色分类
getCategoryList: async () => {
return await request.get({ url: `/ai/chat-role/category-list`})
},
// 创建角色
createMy: async (data: ChatRoleVO) => {
return await request.post({ url: `/ai/chat-role/create-my`, data})
},
// 更新角色
updateMy: async (data: ChatRoleVO) => {
return await request.put({ url: `/ai/chat-role/update-my`, data})
},
// 删除角色 my
deleteMy: async (id: number) => {
return await request.delete({ url: `/ai/chat-role/delete-my?id=` + id })
},
}

41
src/api/ai/music/index.ts Normal file
View File

@ -0,0 +1,41 @@
import request from '@/config/axios'
// AI 音乐 VO
export interface MusicVO {
id: number // 编号
userId: number // 用户编号
title: string // 音乐名称
lyric: string // 歌词
imageUrl: string // 图片地址
audioUrl: string // 音频地址
videoUrl: string // 视频地址
status: number // 音乐状态
gptDescriptionPrompt: string // 描述词
prompt: string // 提示词
platform: string // 模型平台
model: string // 模型
generateMode: number // 生成模式
tags: string // 音乐风格标签
duration: number // 音乐时长
publicStatus: boolean // 是否发布
taskId: string // 任务id
errorMessage: string // 错误信息
}
// AI 音乐 API
export const MusicApi = {
// 查询音乐分页
getMusicPage: async (params: any) => {
return await request.get({ url: `/ai/music/page`, params })
},
// 更新音乐
updateMusic: async (data: any) => {
return await request.put({ url: '/ai/music/update', data })
},
// 删除音乐
deleteMusic: async (id: number) => {
return await request.delete({ url: `/ai/music/delete?id=` + id })
}
}

85
src/api/ai/write/index.ts Normal file
View File

@ -0,0 +1,85 @@
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
import request from '@/config/axios'
export interface WriteVO {
type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:撰写 2:回复
prompt: string // 写作内容提示 1。撰写 2回复
originalContent: string // 原文
length: number // 长度
format: number // 格式
tone: number // 语气
language: number // 语言
userId?: number // 用户编号
platform?: string // 平台
model?: string // 模型
generatedContent?: string // 生成的内容
errorMessage?: string // 错误信息
createTime?: Date // 创建时间
}
export interface AiWritePageReqVO extends PageParam {
userId?: number // 用户编号
type?: AiWriteTypeEnum // 写作类型
platform?: string // 平台
createTime?: [string, string] // 创建时间
}
export interface AiWriteRespVo {
id: number
userId: number
type: number
platform: string
model: string
prompt: string
generatedContent: string
originalContent: string
length: number
format: number
tone: number
language: number
errorMessage: string
createTime: string
}
export const WriteApi = {
writeStream: ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: WriteVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 获取写作列表
getWritePage: (params: AiWritePageReqVO) => {
return request.get<PageResult<AiWriteRespVo[]>>({ url: `/ai/write/page`, params })
},
// 删除写作
deleteWrite(id: number) {
return request.delete({ url: `/ai/write/delete`, params: { id } })
}
}

View File

@ -1,6 +1,6 @@
import request from '@/config/axios' import request from '@/config/axios'
export const getProcessDefinition = async (id: number, key: string) => { export const getProcessDefinition = async (id?: string, key?: string) => {
return await request.get({ return await request.get({
url: '/bpm/process-definition/get', url: '/bpm/process-definition/get',
params: { id, key } params: { id, key }

View File

@ -5,6 +5,7 @@ export type ProcessDefinitionVO = {
version: number version: number
deploymentTIme: string deploymentTIme: string
suspensionState: number suspensionState: number
formType?: number
} }
export type ModelVO = { export type ModelVO = {
@ -29,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params }) return await request.get({ url: '/bpm/model/page', params })
} }
export const getModel = async (id: number) => { export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id }) return await request.get({ url: '/bpm/model/get?id=' + id })
} }
@ -37,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data }) return await request.put({ url: '/bpm/model/update', data: data })
} }
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
}
// 任务状态修改 // 任务状态修改
export const updateModelState = async (id: number, state: number) => { export const updateModelState = async (id: number, state: number) => {
const data = { const data = {

View File

@ -1,5 +1,6 @@
import request from '@/config/axios' import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = { export type Task = {
id: string id: string
name: string name: string
@ -18,17 +19,36 @@ export type ProcessInstanceVO = {
businessKey: string businessKey: string
createTime: string createTime: string
endTime: string endTime: string
processDefinition?: ProcessDefinitionVO
} }
export type ProcessInstanceCopyVO = { // 用户信息
type: number export type User = {
taskName: string id: number,
taskKey: string nickname: string,
processInstanceName: string avatar: string
processInstanceKey: string }
startUserId: string
options: string[] // 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
} }
export const getProcessInstanceMyPage = async (params: any) => { export const getProcessInstanceMyPage = async (params: any) => {
@ -66,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => { export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params }) return await request.get({ url: '/bpm/process-instance/copy/page', params })
} }
// 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}

View File

@ -0,0 +1,15 @@
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',
data: data
})
}
export const getBpmSimpleModel = async (id) => {
return await request.get({
url: '/bpm/model/simple/get?id=' + id
})
}

View File

@ -1,5 +1,51 @@
import request from '@/config/axios' import request from '@/config/axios'
/**
*
*/
export enum TaskStatusEnum {
/**
*
*/
NOT_START = -1,
/**
*
*/
WAIT = 0,
/**
*
*/
RUNNING = 1,
/**
*
*/
APPROVE = 2,
/**
*
*/
REJECT = 3,
/**
*
*/
CANCEL = 4,
/**
* 退
*/
RETURN = 5,
/**
*
*/
DELEGATE = 6,
/**
*
*/
APPROVING = 7,
}
export type TaskVO = { export type TaskVO = {
id: number id: number
} }

View File

@ -13,8 +13,8 @@ export interface ProductCategoryVO {
// ERP 产品分类 API // ERP 产品分类 API
export const ProductCategoryApi = { export const ProductCategoryApi = {
// 查询产品分类列表 // 查询产品分类列表
getProductCategoryList: async (params) => { getProductCategoryList: async () => {
return await request.get({ url: `/erp/product-category/list`, params }) return await request.get({ url: `/erp/product-category/list` })
}, },
// 查询产品分类精简列表 // 查询产品分类精简列表

View File

@ -7,8 +7,8 @@ export interface Demo02CategoryVO {
} }
// 查询示例分类列表 // 查询示例分类列表
export const getDemo02CategoryList = async (params) => { export const getDemo02CategoryList = async () => {
return await request.get({ url: `/infra/demo02-category/list`, params }) return await request.get({ url: `/infra/demo02-category/list` })
} }
// 查询示例分类详情 // 查询示例分类详情

View File

@ -12,6 +12,7 @@ export interface JobLogVO {
duration: string duration: string
status: number status: number
createTime: string createTime: string
result: string
} }
// 任务日志列表 // 任务日志列表

View File

@ -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 })
}
}

View File

@ -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' })
}
}

View File

@ -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 })
}
}

View File

@ -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() })

View File

@ -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
}

View File

@ -0,0 +1,10 @@
import request from '@/config/axios'
/**
*
*
* @param params
*/
export const getBrowseHistoryPage = (params: any) => {
return request.get({ url: '/product/browse-history/page', params })
}

View File

@ -24,20 +24,6 @@ export interface PropertyValueVO {
remark?: string remark?: string
} }
/**
*
*/
export interface PropertyValueDetailVO {
/** 属性项的编号 */
propertyId: number // 属性的编号
/** 属性的名称 */
propertyName: string
/** 属性值的编号 */
valueId: number
/** 属性值的名称 */
valueName: string
}
// ------------------------ 属性项 ------------------- // ------------------------ 属性项 -------------------
// 创建属性项 // 创建属性项
@ -65,6 +51,11 @@ export const getPropertyPage = (params: PageParam) => {
return request.get({ url: '/product/property/page', params }) return request.get({ url: '/product/property/page', params })
} }
// 获得属性项精简列表
export const getPropertySimpleList = (): Promise<PropertyVO[]> => {
return request.get({ url: '/product/property/simple-list' })
}
// ------------------------ 属性值 ------------------- // ------------------------ 属性值 -------------------
// 获得属性值分页 // 获得属性值分页
@ -91,3 +82,8 @@ export const updatePropertyValue = (data: PropertyValueVO) => {
export const deletePropertyValue = (id: number) => { export const deletePropertyValue = (id: number) => {
return request.delete({ url: `/product/property/value/delete?id=${id}` }) return request.delete({ url: `/product/property/value/delete?id=${id}` })
} }
// 获得属性值精简列表
export const getPropertyValueSimpleList = (propertyId: number): Promise<PropertyValueVO[]> => {
return request.get({ url: '/product/property/value/simple-list', params: { propertyId } })
}

View File

@ -50,6 +50,8 @@ export interface Spu {
giveIntegral?: number // 赠送积分 giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量 virtualSalesCount?: number // 虚拟销量
price?: number // 商品价格 price?: number // 商品价格
combinationPrice?: number // 商品拼团价格
seckillPrice?: number // 商品秒杀价格
salesCount?: number // 商品销量 salesCount?: number // 商品销量
marketPrice?: number // 市场价 marketPrice?: number // 市场价
costPrice?: number // 成本价 costPrice?: number // 成本价

View File

@ -16,6 +16,7 @@ export interface CombinationActivityVO {
virtualGroup?: number virtualGroup?: number
status?: number status?: number
limitDuration?: number limitDuration?: number
combinationPrice?: number
products: CombinationProductVO[] products: CombinationProductVO[]
} }
@ -36,7 +37,7 @@ export interface SpuExtension extends Spu {
} }
// 查询拼团活动列表 // 查询拼团活动列表
export const getCombinationActivityPage = async (params) => { export const getCombinationActivityPage = async (params: any) => {
return await request.get({ url: '/promotion/combination-activity/page', params }) return await request.get({ url: '/promotion/combination-activity/page', params })
} }
@ -45,6 +46,11 @@ export const getCombinationActivity = async (id: number) => {
return await request.get({ url: '/promotion/combination-activity/get?id=' + id }) return await request.get({ url: '/promotion/combination-activity/get?id=' + id })
} }
// 获得拼团活动列表,基于活动编号数组
export const getCombinationActivityListByIds = (ids: number[]) => {
return request.get({ url: `/promotion/combination-activity/list-by-ids?ids=${ids}` })
}
// 新增拼团活动 // 新增拼团活动
export const createCombinationActivity = async (data: CombinationActivityVO) => { export const createCombinationActivity = async (data: CombinationActivityVO) => {
return await request.post({ url: '/promotion/combination-activity/create', data }) return await request.post({ url: '/promotion/combination-activity/create', data })

View File

@ -74,7 +74,7 @@ export function getCouponTemplatePage(params: PageParam) {
} }
// 获得优惠劵模板分页 // 获得优惠劵模板分页
export function getCouponTemplateList(ids: number[]) { export function getCouponTemplateList(ids: number[]): Promise<CouponTemplateVO[]> {
return request.get({ return request.get({
url: `/promotion/coupon-template/list?ids=${ids}` url: `/promotion/coupon-template/list?ids=${ids}`
}) })

View File

@ -0,0 +1,35 @@
import request from '@/config/axios'
export interface KeFuConversationRespVO {
id: number // 编号
userId: number // 会话所属用户
userAvatar: string // 会话所属用户头像
userNickname: string // 会话所属用户昵称
lastMessageTime: Date // 最后聊天时间
lastMessageContent: string // 最后聊天内容
lastMessageContentType: number // 最后发送的消息类型
adminPinned: boolean // 管理端置顶
userDeleted: boolean // 用户是否可见
adminDeleted: boolean // 管理员是否可见
adminUnreadMessageCount: number // 管理员未读消息数
createTime?: string // 创建时间
}
// 客服会话 API
export const KeFuConversationApi = {
// 获得客服会话列表
getConversationList: async () => {
return await request.get({ url: '/promotion/kefu-conversation/list' })
},
// 客服会话置顶
updateConversationPinned: async (data: any) => {
return await request.put({
url: '/promotion/kefu-conversation/update-conversation-pinned',
data
})
},
// 删除客服会话
deleteConversation: async (id: number) => {
return await request.delete({ url: `/promotion/kefu-conversation/delete?id=${id}`})
}
}

View File

@ -0,0 +1,36 @@
import request from '@/config/axios'
export interface KeFuMessageRespVO {
id: number // 编号
conversationId: number // 会话编号
senderId: number // 发送人编号
senderAvatar: string // 发送人头像
senderType: number // 发送人类型
receiverId: number // 接收人编号
receiverType: number // 接收人类型
contentType: number // 消息类型
content: string // 消息
readStatus: boolean // 是否已读
createTime: Date // 创建时间
}
// 客服会话 API
export const KeFuMessageApi = {
// 发送客服消息
sendKeFuMessage: async (data: any) => {
return await request.post({
url: '/promotion/kefu-message/send',
data
})
},
// 更新客服消息已读状态
updateKeFuMessageReadStatus: async (conversationId: number) => {
return await request.put({
url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
})
},
// 获得消息分页数据
getKeFuMessagePage: async (params: any) => {
return await request.get({ url: '/promotion/kefu-message/page', params })
}
}

View File

@ -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 })
}
}

View File

@ -1,34 +1,39 @@
import request from '@/config/axios' import request from '@/config/axios'
export interface DiscountActivityVO { export interface RewardActivityVO {
id?: number id?: number
name?: string name?: string
startTime?: Date startTime?: Date
endTime?: Date endTime?: Date
startAndEndTime?: Date[] // 只前端使用
remark?: string remark?: string
conditionType?: number conditionType?: number
productScope?: number productScope?: number
rules: RewardRule[]
// 如下仅用于表单,不提交
productScopeValues?: number[] // 商品范围:值为品类编号列表、商品编号列表
productCategoryIds?: number[]
productSpuIds?: number[] productSpuIds?: number[]
rules?: DiscountProductVO[]
} }
// 优惠规则 // 优惠规则
export interface DiscountProductVO { export interface RewardRule {
limit: number limit?: number
discountPrice: number discountPrice?: number
freeDelivery: boolean freeDelivery?: boolean
point: number point: number
couponIds: number[] giveCouponTemplateCounts?: {
couponCounts: number[] [key: number]: number
}
} }
// 新增满减送活动 // 新增满减送活动
export const createRewardActivity = async (data: DiscountActivityVO) => { export const createRewardActivity = async (data: RewardActivityVO) => {
return await request.post({ url: '/promotion/reward-activity/create', data }) return await request.post({ url: '/promotion/reward-activity/create', data })
} }
// 更新满减送活动 // 更新满减送活动
export const updateRewardActivity = async (data: DiscountActivityVO) => { export const updateRewardActivity = async (data: RewardActivityVO) => {
return await request.put({ url: '/promotion/reward-activity/update', data }) return await request.put({ url: '/promotion/reward-activity/update', data })
} }
@ -42,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 })
}

View File

@ -18,12 +18,14 @@ export interface SeckillActivityVO {
singleLimitCount?: number singleLimitCount?: number
stock?: number stock?: number
totalStock?: number totalStock?: number
seckillPrice?: number
products?: SeckillProductVO[] products?: SeckillProductVO[]
} }
// 秒杀活动所需属性 // 秒杀活动所需属性
export interface SeckillProductVO { export interface SeckillProductVO {
skuId: number skuId: number
spuId: number
seckillPrice: number seckillPrice: number
stock: number stock: number
} }
@ -42,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 })

View File

@ -19,7 +19,7 @@ export const SeckillConfigApi = {
// 查询秒杀时段列表 // 查询秒杀时段列表
getSimpleSeckillConfigList: async () => { getSimpleSeckillConfigList: async () => {
return await request.get({ url: `/promotion/seckill-config/simple-list` }) return await request.get({ url: `/promotion/seckill-config/list` })
}, },
// 查询秒杀时段详情 // 查询秒杀时段详情

View File

@ -141,7 +141,7 @@ export const getExpressTrackList = async (id: number | null) => {
} }
export interface DeliveryVO { export interface DeliveryVO {
id: number // 订单编号 id?: number // 订单编号
logisticsId: number | null // 物流公司编号 logisticsId: number | null // 物流公司编号
logisticsNo: string // 物流编号 logisticsNo: string // 物流编号
} }

View File

@ -46,8 +46,3 @@ export const updateUserLevel = async (data: any) => {
export const updateUserPoint = async (data: any) => { export const updateUserPoint = async (data: any) => {
return await request.put({ url: `/member/user/update-point`, data }) return await request.put({ url: `/member/user/update-point`, data })
} }
// 修改会员用户余额
export const updateUserBalance = async (data: any) => {
return await request.put({ url: `/member/user/update-balance`, data })
}

View File

@ -2,6 +2,7 @@ import request from '@/config/axios'
export interface AppVO { export interface AppVO {
id: number id: number
appKey: string
name: string name: string
status: number status: number
remark: string remark: string

View File

@ -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
}
})
} }
// 获得支付订单的明细 // 获得支付订单的明细

View File

@ -4,6 +4,7 @@ import request from '@/config/axios'
export interface PayWalletUserReqVO { export interface PayWalletUserReqVO {
userId: number userId: number
} }
/** 钱包 VO */ /** 钱包 VO */
export interface WalletVO { export interface WalletVO {
id: number id: number
@ -20,7 +21,12 @@ export const getWallet = async (params: PayWalletUserReqVO) => {
return await request.get<WalletVO>({ url: `/pay/wallet/get`, params }) return await request.get<WalletVO>({ url: `/pay/wallet/get`, params })
} }
// 查询会员钱包列表 /** 查询会员钱包列表 */
export const getWalletPage = async (params) => { export const getWalletPage = async (params: any) => {
return await request.get({ url: `/pay/wallet/page`, params }) return await request.get({ url: `/pay/wallet/page`, params })
} }
/** 修改会员钱包余额 */
export const updateWalletBalance = async (data: any) => {
return await request.put({ url: `/pay/wallet/update-balance`, data })
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715606039621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M878.250667 981.333333H375.338667a104.661333 104.661333 0 0 1-104.661334-104.661333V375.338667a104.661333 104.661333 0 0 1 104.661334-104.661334h502.912a104.661333 104.661333 0 0 1 104.661333 104.661334v502.912C981.333333 934.485333 934.485333 981.333333 878.250667 981.333333zM375.338667 364.373333a10.666667 10.666667 0 0 0-10.922667 10.965334v502.912c0 6.229333 4.693333 10.922667 10.922667 10.922666h502.912a10.666667 10.666667 0 0 0 10.922666-10.922666V375.338667a10.666667 10.666667 0 0 0-10.922666-10.922667H375.338667z" fill="#ffffff" p-id="4257"></path><path d="M192.597333 753.322667H147.328A104.661333 104.661333 0 0 1 42.666667 648.661333V147.328A104.661333 104.661333 0 0 1 147.328 42.666667H650.24a104.661333 104.661333 0 0 1 104.618667 104.661333v49.962667c0 26.538667-20.309333 46.848-46.848 46.848a46.037333 46.037333 0 0 1-46.848-46.848V147.328a10.666667 10.666667 0 0 0-10.922667-10.965333H147.328a10.666667 10.666667 0 0 0-10.965333 10.965333V650.24c0 6.229333 4.693333 10.922667 10.965333 10.922667h45.269333c26.538667 0 46.848 20.309333 46.848 46.848 0 26.538667-21.845333 45.312-46.848 45.312z" fill="#ffffff" p-id="4258"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/assets/ai/copy.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715352878351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1499" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M624.5 786.3c92.9 0 168.2-75.3 168.2-168.2V309c0-92.4-75.3-168.2-168.2-168.2H303.6c-92.4 0-168.2 75.3-168.2 168.2v309.1c0 92.4 75.3 168.2 168.2 168.2h320.9zM178.2 618.1V309c0-69.4 56.1-125.5 125.5-125.5h320.9c69.4 0 125.5 56.1 125.5 125.5v309.1c0 69.4-56.1 125.5-125.5 125.5h-321c-69.4 0-125.4-56.1-125.4-125.5z" p-id="1500" fill="#8a8a8a"></path><path d="M849.8 295.1v361.5c0 102.7-83.6 186.3-186.3 186.3H279.1v42.7h384.4c126.3 0 229.1-102.8 229.1-229.1V295.1h-42.8zM307.9 361.8h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4zM307.9 484.6h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4z" p-id="1501" fill="#8a8a8a"></path><path d="M620.2 607.4c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.8 9.6 21.4 21.4 21.4h312.3z" p-id="1502" fill="#8a8a8a"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/ai/dall2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
src/assets/ai/dall3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

1
src/assets/ai/delete.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715354120346" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M907.1 263.7H118.9c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4H907c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3257"></path><path d="M772.5 928.3H257.4c-27.7 0-50.2-22.5-50.2-50.2V247.2c0-9.1 7.3-16.4 16.4-16.4H801c12.1 0 21.9 9.8 21.9 21.9v625.2c0 27.8-22.6 50.4-50.4 50.4zM240 263.7v614.4c0 9.6 7.8 17.4 17.4 17.4h515.2c9.7 0 17.5-7.9 17.5-17.5V263.7H240zM657.4 131.1H368.6c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4h288.7c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3258"></path><path d="M416 754.5c-9.1 0-16.4-7.3-16.4-16.4V517.8c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0.1 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" p-id="3259"></path><path d="M416 465.2c-9.1 0-16.4-7.3-16.4-16.4v-59.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v59.4c0.1 9.1-7.3 16.4-16.4 16.4zM604.9 754.5c-9.1 0-16.4-7.3-16.4-16.4v-67.2c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" opacity=".4" p-id="3260"></path><path d="M604.9 619.1c-9.1 0-16.4-7.3-16.4-16.4V389.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v213.3c0 9.1-7.3 16.4-16.4 16.4z" fill="#8a8a8a" p-id="3261"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/assets/ai/gpt.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1716345268026" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5622" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M956.408445 419.226665a250.670939 250.670939 0 0 0-22.425219-209.609236A263.163526 263.163526 0 0 0 652.490412 85.715535 259.784384 259.784384 0 0 0 457.728923 0.008192a261.422756 261.422756 0 0 0-249.44216 178.582564 258.453206 258.453206 0 0 0-172.848261 123.901894c-57.03583 96.868753-44.031251 219.132275 32.153053 302.279661a250.670939 250.670939 0 0 0 22.32282 209.609237 263.163526 263.163526 0 0 0 281.595213 123.901893A259.067596 259.067596 0 0 0 566.271077 1023.990784a260.60357 260.60357 0 0 0 249.339762-178.889759 258.453206 258.453206 0 0 0 172.848261-123.901893c57.445423-96.868753 44.13365-218.82508-32.050655-302.074865zM566.578272 957.124721c-45.362429 0-89.496079-15.666934-124.516283-44.543243 1.638372-0.921584 4.198329-2.150363 6.143895-3.481541l206.537289-117.757998a32.35785 32.35785 0 0 0 16.895713-29.081105V474.82892l87.243317 49.97035c1.023983 0.307195 1.638372 1.228779 1.638372 2.252762v238.075953c0 105.8798-86.936122 191.689541-193.942303 191.996736zM148.588578 781.102113a189.846373 189.846373 0 0 1-23.346803-128.612213c1.535974 1.023983 4.09593 2.559956 6.143895 3.48154L337.922959 773.729439c10.444622 6.143896 23.346803 6.143896 34.098621 0l252.30931-143.664758v99.531108c0 1.023983-0.307195 1.945567-1.331177 2.559956l-208.892449 118.986778a196.297463 196.297463 0 0 1-265.518686-70.04041zM94.112704 335.97688c22.630015-39.013737 58.367008-68.81163 101.16948-84.171369V494.591784c0 11.7758 6.45109 22.93721 16.793315 28.978707l252.30931 143.767156L377.141493 716.796006a3.174346 3.174346 0 0 1-2.867152 0.307195l-208.892448-118.986777A190.870355 190.870355 0 0 1 94.215102 335.874482z m717.607001 164.861198L559.410394 357.070922 646.653711 307.20297a3.174346 3.174346 0 0 1 2.969549-0.307195l208.892449 118.986777a190.358364 190.358364 0 0 1 70.961994 262.139544 194.556693 194.556693 0 0 1-101.16948 84.171369V529.407192a31.538664 31.538664 0 0 0-16.588518-28.671513z m87.03852-129.329002c-1.74077-1.023983-4.300727-2.559956-6.246294-3.48154l-206.639687-117.757999a34.09862 34.09862 0 0 0-33.996222 0L399.566711 393.934295v-99.531108c0-1.023983 0.307195-1.945567 1.331178-2.559956l208.892449-119.089176a195.990268 195.990268 0 0 1 265.518686 70.450003c22.732414 38.706542 31.129071 84.171369 23.346803 128.305018zM352.258716 548.862861l-87.243317-49.560757a2.457558 2.457558 0 0 1-1.638372-2.252762V258.870991c0-105.8798 87.243317-191.996736 194.556692-191.689541a194.556693 194.556693 0 0 1 124.209089 44.543243c-1.638372 0.921584-4.198329 2.252762-6.143896 3.48154l-206.639687 117.757999a31.948257 31.948257 0 0 0-16.793315 29.081105l-0.307194 286.715126z m47.307995-100.759887L512 384.001664l112.535687 63.998912v127.997824l-112.228492 63.998912-112.535687-63.998912-0.307195-127.997824z" p-id="5623" fill="#707070"></path></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/assets/ai/qingxi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
src/assets/ai/ziran.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

1
src/assets/svgs/send.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724297262365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1396" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M707.91 103c16.28 0 29.522 13.007 29.897 29.195l0.009 0.706v111.878a29.96 29.96 0 0 1-0.898 7.3l171.177-0.001c16.28 0 29.522 13.007 29.897 29.195l0.008 0.706v637.12c0 16.278-13.01 29.518-29.2 29.893l-0.705 0.008H270.884c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.706V787.274c0-16.514 13.389-29.9 29.905-29.9 16.28 0 29.522 13.007 29.897 29.194l0.008 0.706v101.924h577.4V311.88h-577.4v88.787c0 16.278-13.009 29.518-29.2 29.893l-0.705 0.008c-16.28 0-29.522-13.008-29.897-29.195l-0.008-0.706V281.979c0-16.278 13.009-29.518 29.2-29.893l0.705-0.008h408.019a29.916 29.916 0 0 1-0.89-6.593l-0.008-0.706v-81.978H132.808v407.113h385.787L408.223 456.982c-11.36-11.624-11.329-30.143-0.066-41.729l0.554-0.555c11.625-11.358 30.147-11.327 41.734-0.066l0.555 0.554 161.028 164.762c11.244 11.504 11.344 29.793 0.362 41.42l-0.55 0.565-161.027 161.849c-11.648 11.707-30.583 11.757-42.292 0.11-11.524-11.461-11.754-29.979-0.657-41.723l0.546-0.563 111.319-111.89H102.905c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.705V132.9c0-16.278 13.01-29.518 29.2-29.893l0.705-0.008H707.91z" p-id="1397"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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'

View File

@ -54,7 +54,7 @@ const currentLocale = computed(() => localeStore.currentLocale)
<ElConfigProvider <ElConfigProvider
:namespace="variables.elNamespace" :namespace="variables.elNamespace"
:locale="currentLocale.elLocale" :locale="currentLocale.elLocale"
:message="{ max: 1 }" :message="{ max: 5 }"
:size="size" :size="size"
> >
<slot></slot> <slot></slot>

View File

@ -10,12 +10,13 @@ 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: '10px' })
}) })
</script> </script>
<template> <template>
<ElCard :class="[prefixCls, 'mb-15px']" shadow="never"> <ElCard :body-style="bodyStyle" :class="[prefixCls, 'mb-15px']" shadow="never">
<template v-if="title" #header> <template v-if="title" #header>
<div class="flex items-center"> <div class="flex items-center">
<span class="text-16px font-700">{{ title }}</span> <span class="text-16px font-700">{{ title }}</span>
@ -30,8 +31,6 @@ defineProps({
</div> </div>
</div> </div>
</template> </template>
<div>
<slot></slot> <slot></slot>
</div>
</ElCard> </ElCard>
</template> </template>

View File

@ -548,10 +548,10 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.second.type"> <el-radio-group v-model="cronValue.second.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.second.type == '1'" label="范围"> <el-form-item v-if="cronValue.second.type == '1'" label="范围">
@ -607,10 +607,10 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.minute.type"> <el-radio-group v-model="cronValue.minute.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.minute.type == '1'" label="范围"> <el-form-item v-if="cronValue.minute.type == '1'" label="范围">
@ -666,10 +666,10 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.hour.type"> <el-radio-group v-model="cronValue.hour.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.hour.type == '1'" label="范围"> <el-form-item v-if="cronValue.hour.type == '1'" label="范围">
@ -725,12 +725,12 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.day.type"> <el-radio-group v-model="cronValue.day.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一天</el-radio-button> <el-radio-button value="4">本月最后一天</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button> <el-radio-button value="5">不指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.day.type == '1'" label="范围"> <el-form-item v-if="cronValue.day.type == '1'" label="范围">
@ -786,10 +786,10 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.month.type"> <el-radio-group v-model="cronValue.month.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.month.type == '1'" label="范围"> <el-form-item v-if="cronValue.month.type == '1'" label="范围">
@ -846,12 +846,12 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.week.type"> <el-radio-group v-model="cronValue.week.type">
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一周</el-radio-button> <el-radio-button value="4">本月最后一周</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button> <el-radio-button value="5">不指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.week.type == '1'" label="范围"> <el-form-item v-if="cronValue.week.type == '1'" label="范围">
@ -925,11 +925,11 @@ const inputChange = () => {
<el-form> <el-form>
<el-form-item label="类型"> <el-form-item label="类型">
<el-radio-group v-model="cronValue.year.type"> <el-radio-group v-model="cronValue.year.type">
<el-radio-button label="-1">忽略</el-radio-button> <el-radio-button value="-1">忽略</el-radio-button>
<el-radio-button label="0">任意值</el-radio-button> <el-radio-button value="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button> <el-radio-button value="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button> <el-radio-button value="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button> <el-radio-button value="3">指定</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="cronValue.year.type == '1'" label="范围"> <el-form-item v-if="cronValue.year.type == '1'" label="范围">

View File

@ -1,3 +0,0 @@
import DictSelect from './src/DictSelect.vue'
export { DictSelect }

View File

@ -1,46 +0,0 @@
<!-- 数据字典 Select 选择器 -->
<template>
<el-select class="w-1/1" v-bind="attrs">
<template v-if="valueType === 'int'">
<el-option
v-for="(dict, index) in getIntDictOptions(dictType)"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</template>
<template v-if="valueType === 'str'">
<el-option
v-for="(dict, index) in getStrDictOptions(dictType)"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</template>
<template v-if="valueType === 'bool'">
<el-option
v-for="(dict, index) in getBoolDictOptions(dictType)"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</template>
</el-select>
</template>
<script lang="ts" setup>
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
//
interface Props {
dictType: string //
valueType: string //
}
withDefaults(defineProps<Props>(), {
dictType: '',
valueType: 'str'
})
const attrs = useAttrs()
defineOptions({ name: 'DictSelect' })
</script>

View File

@ -1,8 +1,9 @@
<script lang="tsx"> <script lang="tsx">
import { defineComponent, PropType, ref } from 'vue' import { computed, defineComponent, PropType } from 'vue'
import { isHexColor } from '@/utils/color' import { isHexColor } from '@/utils/color'
import { ElTag } from 'element-plus' import { ElTag } from 'element-plus'
import { DictDataType, getDictOptions } from '@/utils/dict' import { DictDataType, getDictOptions } from '@/utils/dict'
import { isArray, isBoolean, isNumber, isString } from '@/utils/is'
export default defineComponent({ export default defineComponent({
name: 'DictTag', name: 'DictTag',
@ -12,49 +13,78 @@ export default defineComponent({
required: true required: true
}, },
value: { value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>, type: [String, Number, Boolean, Array],
required: true required: true
},
// props.value
separator: {
type: String as PropType<string>,
default: ','
},
// tag 5px el-row gutter
gutter: {
type: String as PropType<string>,
default: '5px'
} }
}, },
setup(props) { setup(props) {
const dictData = ref<DictDataType>() const valueArr: any = computed(() => {
const getDictObj = (dictType: string, value: string) => { // 1. Number Boolean
const dictOptions = getDictOptions(dictType) if (isNumber(props.value) || isBoolean(props.value)) {
dictOptions.forEach((dict: DictDataType) => { return [String(props.value)]
if (dict.value === value) {
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
dict.colorType = ''
} }
dictData.value = dict // 2. -> props.sepSymbol
else if (isString(props.value)) {
return props.value.split(props.separator)
} }
// 3.
else if (isArray(props.value)) {
return props.value.map(String)
}
return []
}) })
} const renderDictTag = () => {
const rederDictTag = () => {
if (!props.type) { if (!props.type) {
return null return null
} }
// //
if (props.value === undefined || props.value === null) { if (props.value === undefined || props.value === null || props.value === '') {
return null return null
} }
getDictObj(props.type, props.value.toString()) const dictOptions = getDictOptions(props.type)
//
return ( return (
<ElTag <div
style={dictData.value?.cssClass ? 'color: #fff' : ''} class="dict-tag"
type={dictData.value?.colorType} style={{
color={ display: 'inline-flex',
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) gap: props.gutter,
? dictData.value?.cssClass justifyContent: 'center',
: '' alignItems: 'center'
}}
>
{dictOptions.map((dict: DictDataType) => {
if (valueArr.value.includes(dict.value)) {
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
dict.colorType = ''
} }
return (
//
<ElTag
style={dict?.cssClass ? 'color: #fff' : ''}
type={dict?.colorType || null}
color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
disableTransitions={true} disableTransitions={true}
> >
{dictData.value?.label} {dict?.label}
</ElTag> </ElTag>
) )
} }
return () => rederDictTag() })}
</div>
)
}
return () => renderDictTag()
} }
}) })
</script> </script>

View File

@ -165,6 +165,7 @@ $toolbar-position: -55px;
width: 80px; width: 80px;
height: 25px; height: 25px;
font-size: 12px; font-size: 12px;
color: #6a6a6a;
line-height: 25px; line-height: 25px;
text-align: center; text-align: center;
background: #fff; background: #fff;

View File

@ -11,8 +11,8 @@
<el-form :model="formData" label-width="80px"> <el-form :model="formData" label-width="80px">
<el-form-item label="组件背景" prop="bgType"> <el-form-item label="组件背景" prop="bgType">
<el-radio-group v-model="formData.bgType"> <el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio> <el-radio value="color">纯色</el-radio>
<el-radio label="img">图片</el-radio> <el-radio value="img">图片</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'"> <el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">

View File

@ -95,6 +95,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
.editor-left { .editor-left {
z-index: 1; z-index: 1;
flex-shrink: 0; flex-shrink: 0;
user-select: none;
box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%); box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
:deep(.el-collapse) { :deep(.el-collapse) {

View File

@ -5,12 +5,12 @@
<el-form-item label="样式" prop="type"> <el-form-item label="样式" prop="type">
<el-radio-group v-model="formData.type"> <el-radio-group v-model="formData.type">
<el-tooltip class="item" content="默认" placement="bottom"> <el-tooltip class="item" content="默认" placement="bottom">
<el-radio-button label="default"> <el-radio-button value="default">
<Icon icon="system-uicons:carousel" /> <Icon icon="system-uicons:carousel" />
</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 label="card"> <el-radio-button value="card">
<Icon icon="ic:round-view-carousel" /> <Icon icon="ic:round-view-carousel" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
@ -18,8 +18,8 @@
</el-form-item> </el-form-item>
<el-form-item label="指示器" prop="indicator"> <el-form-item label="指示器" prop="indicator">
<el-radio-group v-model="formData.indicator"> <el-radio-group v-model="formData.indicator">
<el-radio label="dot">小圆点</el-radio> <el-radio value="dot">小圆点</el-radio>
<el-radio label="number">数字</el-radio> <el-radio value="number">数字</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="是否轮播" prop="autoplay"> <el-form-item label="是否轮播" prop="autoplay">
@ -43,8 +43,8 @@
<template #default="{ element }"> <template #default="{ element }">
<el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px"> <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px">
<el-radio-group v-model="element.type"> <el-radio-group v-model="element.type">
<el-radio label="img">图片</el-radio> <el-radio value="img">图片</el-radio>
<el-radio label="video">视频</el-radio> <el-radio value="video">视频</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item <el-form-item

View File

@ -26,17 +26,17 @@
<el-form-item label="列数" prop="type"> <el-form-item label="列数" prop="type">
<el-radio-group v-model="formData.columns"> <el-radio-group v-model="formData.columns">
<el-tooltip class="item" content="一列" placement="bottom"> <el-tooltip class="item" content="一列" placement="bottom">
<el-radio-button :label="1"> <el-radio-button :value="1">
<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 :label="2"> <el-radio-button :value="2">
<Icon icon="fluent:text-column-two-24-filled" /> <Icon icon="fluent:text-column-two-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 :label="3"> <el-radio-button :value="3">
<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>

View File

@ -11,7 +11,7 @@
:key="index" :key="index"
:content="item.text" :content="item.text"
> >
<el-radio-button :label="item.type"> <el-radio-button :value="item.type">
<Icon :icon="item.icon" /> <Icon :icon="item.icon" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
@ -24,12 +24,12 @@
<el-form-item label="左右边距" prop="paddingType"> <el-form-item label="左右边距" prop="paddingType">
<el-radio-group v-model="formData!.paddingType"> <el-radio-group v-model="formData!.paddingType">
<el-tooltip content="无边距" placement="top"> <el-tooltip content="无边距" placement="top">
<el-radio-button label="none"> <el-radio-button value="none">
<Icon icon="tabler:box-padding" /> <Icon icon="tabler:box-padding" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="左右留边" placement="top"> <el-tooltip content="左右留边" placement="top">
<el-radio-button label="horizontal"> <el-radio-button value="horizontal">
<Icon icon="vaadin:padding" /> <Icon icon="vaadin:padding" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>

View File

@ -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

View File

@ -3,8 +3,8 @@
<el-card header="按钮配置" class="property-group" shadow="never"> <el-card header="按钮配置" class="property-group" shadow="never">
<el-form-item label="展开方向" prop="direction"> <el-form-item label="展开方向" prop="direction">
<el-radio-group v-model="formData.direction"> <el-radio-group v-model="formData.direction">
<el-radio label="vertical">垂直</el-radio> <el-radio value="vertical">垂直</el-radio>
<el-radio label="horizontal">水平</el-radio> <el-radio value="horizontal">水平</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="显示文字" prop="showText"> <el-form-item label="显示文字" prop="showText">

View File

@ -4,8 +4,8 @@
<el-form label-width="80px" :model="formData" class="m-t-8px"> <el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="每行数量" prop="column"> <el-form-item label="每行数量" prop="column">
<el-radio-group v-model="formData.column"> <el-radio-group v-model="formData.column">
<el-radio :label="3">3</el-radio> <el-radio :value="3">3</el-radio>
<el-radio :label="4">4</el-radio> <el-radio :value="4">4</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -4,21 +4,21 @@
<el-form label-width="80px" :model="formData" class="m-t-8px"> <el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="布局" prop="layout"> <el-form-item label="布局" prop="layout">
<el-radio-group v-model="formData.layout"> <el-radio-group v-model="formData.layout">
<el-radio label="iconText">图标+文字</el-radio> <el-radio value="iconText">图标+文字</el-radio>
<el-radio label="icon">仅图标</el-radio> <el-radio value="icon">仅图标</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="行数" prop="row"> <el-form-item label="行数" prop="row">
<el-radio-group v-model="formData.row"> <el-radio-group v-model="formData.row">
<el-radio :label="1">1</el-radio> <el-radio :value="1">1</el-radio>
<el-radio :label="2">2</el-radio> <el-radio :value="2">2</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="列数" prop="column"> <el-form-item label="列数" prop="column">
<el-radio-group v-model="formData.column"> <el-radio-group v-model="formData.column">
<el-radio :label="3">3</el-radio> <el-radio :value="3">3</el-radio>
<el-radio :label="4">4</el-radio> <el-radio :value="4">4</el-radio>
<el-radio :label="5">5</el-radio> <el-radio :value="5">5</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -14,9 +14,9 @@
<template v-if="selectedHotAreaIndex === cellIndex"> <template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`"> <el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type"> <el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio> <el-radio value="text">文字</el-radio>
<el-radio label="image">图片</el-radio> <el-radio value="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio> <el-radio value="search">搜索框</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<!-- 1. 文字 --> <!-- 1. 文字 -->

View File

@ -2,27 +2,27 @@
<el-form label-width="80px" :model="formData" :rules="rules"> <el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="样式" prop="styleType"> <el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType"> <el-radio-group v-model="formData!.styleType">
<el-radio label="normal">标准</el-radio> <el-radio value="normal">标准</el-radio>
<el-tooltip <el-tooltip
content="沉侵式头部仅支持微信小程序、APP建议页面第一个组件为图片展示类组件" content="沉侵式头部仅支持微信小程序、APP建议页面第一个组件为图片展示类组件"
placement="top" placement="top"
> >
<el-radio label="inner">沉浸式</el-radio> <el-radio value="inner">沉浸式</el-radio>
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'"> <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow"> <el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio> <el-radio :value="false">关闭</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top"> <el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio> <el-radio :value="true">开启</el-radio>
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="背景类型" prop="bgType"> <el-form-item label="背景类型" prop="bgType">
<el-radio-group v-model="formData.bgType"> <el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio> <el-radio value="color">纯色</el-radio>
<el-radio label="img">图片</el-radio> <el-radio value="img">图片</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'"> <el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">

View File

@ -11,10 +11,10 @@
<el-form-item label="显示次数" :prop="`list[${index}].showType`"> <el-form-item label="显示次数" :prop="`list[${index}].showType`">
<el-radio-group v-model="element.showType"> <el-radio-group v-model="element.showType">
<el-tooltip content="只显示一次,下次打开时不显示" placement="bottom"> <el-tooltip content="只显示一次,下次打开时不显示" placement="bottom">
<el-radio label="once">一次</el-radio> <el-radio value="once">一次</el-radio>
</el-tooltip> </el-tooltip>
<el-tooltip content="每次打开时都会显示" placement="bottom"> <el-tooltip content="每次打开时都会显示" placement="bottom">
<el-radio label="always">不限</el-radio> <el-radio value="always">不限</el-radio>
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>

View File

@ -67,15 +67,15 @@
class="text-16px" class="text-16px"
:style="{ color: property.fields.price.color }" :style="{ color: property.fields.price.color }"
> >
{{ spu.price }} {{ fenToYuan(spu.price as any) }}
</span> </span>
<!-- 市场价 --> <!-- 市场价 -->
<span <span
v-if="property.fields.marketPrice.show && spu.marketPrice" v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through" class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }" :style="{ color: property.fields.marketPrice.color }"
>{{ spu.marketPrice }}</span >{{ fenToYuan(spu.marketPrice) }}
> </span>
</div> </div>
<div class="text-12px"> <div class="text-12px">
<!-- 销量 --> <!-- 销量 -->
@ -117,6 +117,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProductCardProperty } from './config' import { ProductCardProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu' import * as ProductSpuApi from '@/api/mall/product/spu'
import { fenToYuan } from '../../../../../utils'
/** 商品卡片 */ /** 商品卡片 */
defineOptions({ name: 'ProductCard' }) defineOptions({ name: 'ProductCard' })

View File

@ -8,17 +8,17 @@
<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 label="oneColBigImg"> <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 label="oneColSmallImg"> <el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" /> <Icon icon="fluent:text-column-two-left-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 label="twoCol"> <el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" /> <Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
@ -74,8 +74,8 @@
<el-card header="按钮" class="property-group" shadow="never"> <el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type"> <el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type"> <el-radio-group v-model="formData.btnBuy.type">
<el-radio-button label="text">文字</el-radio-button> <el-radio-button value="text">文字</el-radio-button>
<el-radio-button label="img">图片</el-radio-button> <el-radio-button value="img">图片</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<template v-if="formData.btnBuy.type === 'text'"> <template v-if="formData.btnBuy.type === 'text'">

View File

@ -54,7 +54,7 @@
class="text-12px" class="text-12px"
:style="{ color: property.fields.price.color }" :style="{ color: property.fields.price.color }"
> >
{{ spu.price }} {{ fenToYuan(spu.price) }}
</span> </span>
</div> </div>
</div> </div>
@ -65,6 +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 '@/utils'
/** 商品栏 */ /** 商品栏 */
defineOptions({ name: 'ProductList' }) defineOptions({ name: 'ProductList' })

View File

@ -8,17 +8,17 @@
<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 label="twoCol"> <el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" /> <Icon icon="fluent:text-column-two-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 label="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-tooltip class="item" content="水平滑动" placement="bottom"> <el-tooltip class="item" content="水平滑动" placement="bottom">
<el-radio-button label="horizSwiper"> <el-radio-button value="horizSwiper">
<Icon icon="system-uicons:carousel" /> <Icon icon="system-uicons:carousel" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>

View File

@ -3,13 +3,21 @@ import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 拼团属性 */ /** 拼团属性 */
export interface PromotionCombinationProperty { export interface PromotionCombinationProperty {
// 布局类型:单列 | 三列 // 布局类型:单列 | 三列
layoutType: 'oneCol' | 'threeCol' layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段 // 商品字段
fields: { fields: {
// 商品名称 // 商品名称
name: PromotionCombinationFieldProperty name: PromotionCombinationFieldProperty
// 商品简介
introduction: PromotionCombinationFieldProperty
// 商品价格 // 商品价格
price: PromotionCombinationFieldProperty price: PromotionCombinationFieldProperty
// 市场价
marketPrice: PromotionCombinationFieldProperty
// 商品销量
salesCount: PromotionCombinationFieldProperty
// 商品库存
stock: PromotionCombinationFieldProperty
} }
// 角标 // 角标
badge: { badge: {
@ -18,6 +26,19 @@ export interface PromotionCombinationProperty {
// 角标图片 // 角标图片
imgUrl: string imgUrl: string
} }
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角 // 上圆角
borderRadiusTop: number borderRadiusTop: number
// 下圆角 // 下圆角
@ -25,7 +46,7 @@ export interface PromotionCombinationProperty {
// 间距 // 间距
space: number space: number
// 拼团活动编号 // 拼团活动编号
activityId: number activityIds: number[]
// 组件样式 // 组件样式
style: ComponentStyle style: ComponentStyle
} }
@ -44,12 +65,23 @@ export const component = {
name: '拼团', name: '拼团',
icon: 'mdi:account-group', icon: 'mdi:account-group',
property: { property: {
layoutType: 'oneCol', layoutType: 'oneColBigImg',
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,

View File

@ -1,18 +1,10 @@
<template> <template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<!-- 商品网格 -->
<div
class="grid overflow-x-auto"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
}"
>
<!-- 商品 -->
<div <div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{ :style="{
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`, borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`, borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`, borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
@ -22,104 +14,188 @@
:key="index" :key="index"
> >
<!-- 角标 --> <!-- 角标 -->
<div <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
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" /> <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div> </div>
<!-- 商品封面图 --> <!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<div <div
:class="[ :class="[
'flex flex-col gap-8px p-8px box-border', 'h-140px',
{ {
'w-[calc(100%-64px)]': columns === 2, 'w-full': property.layoutType !== 'oneColSmallImg',
'w-full': columns === 3 'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</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 <div
v-if="property.fields.name.show" v-if="property.fields.name.show"
class="truncate text-12px" :class="[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }" :style="{ color: property.fields.name.color }"
> >
{{ spu.name }} {{ spu.name }}
</div> </div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div> <div>
<!-- 商品价格 --> <!-- 价格 -->
<span <span
v-if="property.fields.price.show" v-if="property.fields.price.show"
class="text-12px" class="text-16px"
:style="{ color: property.fields.price.color }" :style="{ color: property.fields.price.color }"
> >
{{ spu.price }} {{ fenToYuan(spu.price || Infinity) }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>{{ fenToYuan(spu.marketPrice) }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<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> </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>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PromotionCombinationProperty } from './config' import { PromotionCombinationProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu' import * as ProductSpuApi from '@/api/mall/product/spu'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
import { fenToYuan } from '@/utils'
/** 拼团 */ /** 拼团卡片 */
defineOptions({ name: 'PromotionCombination' }) defineOptions({ name: 'PromotionCombination' })
// //
const props = defineProps<{ property: PromotionCombinationProperty }>() const props = defineProps<{ property: PromotionCombinationProperty }>()
// //
const spuList = ref<ProductSpuApi.Spu[]>([]) const spuList = ref<ProductSpuApi.Spu[]>([])
const spuIdList = ref<number[]>([])
const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
watch( watch(
() => props.property.activityId, () => props.property.activityIds,
async () => { async () => {
if (!props.property.activityId) return try {
const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId) // ID
if (!activity?.spuId) return const activityIds = props.property.activityIds
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] // ID
if (Array.isArray(activityIds) && activityIds.length > 0) {
//
combinationActivityList.value =
await CombinationActivityApi.getCombinationActivityListByIds(activityIds)
// SPU
spuList.value = []
spuIdList.value = combinationActivityList.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
combinationActivityList.value.forEach((activity) => {
// spuId
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
if (spu) {
// 便
spu.price = Math.min(activity.combinationPrice || 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 scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -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"> <CombinationShowcase 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 label="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 label="threeCol"> <el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-three-24-filled" /> <Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button> </el-radio-button>
</el-tooltip> </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-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 { PromotionCombinationProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util' import { usePropertyForm } from '@/components/DiyEditor/util'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue'
// //
defineOptions({ name: 'PromotionCombinationProperty' }) defineOptions({ name: 'PromotionCombinationProperty' })
@ -100,7 +152,7 @@ const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
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<CombinationActivityApi.CombinationActivityVO>([]) const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
onMounted(async () => { onMounted(async () => {
const { list } = await CombinationActivityApi.getCombinationActivityPage({ const { list } = await CombinationActivityApi.getCombinationActivityPage({
status: CommonStatusEnum.ENABLE status: CommonStatusEnum.ENABLE

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -1,18 +1,10 @@
<template> <template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<!-- 商品网格 -->
<div
class="grid overflow-x-auto"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
}"
>
<!-- 商品 -->
<div <div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{ :style="{
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`, borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`, borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`, borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
@ -22,104 +14,188 @@
:key="index" :key="index"
> >
<!-- 角标 --> <!-- 角标 -->
<div <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
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" /> <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div> </div>
<!-- 商品封面图 --> <!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<div <div
:class="[ :class="[
'flex flex-col gap-8px p-8px box-border', 'h-140px',
{ {
'w-[calc(100%-64px)]': columns === 2, 'w-full': property.layoutType !== 'oneColSmallImg',
'w-full': columns === 3 'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</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 <div
v-if="property.fields.name.show" v-if="property.fields.name.show"
class="truncate text-12px" :class="[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }" :style="{ color: property.fields.name.color }"
> >
{{ spu.name }} {{ spu.name }}
</div> </div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div> <div>
<!-- 商品价格 --> <!-- 价格 -->
<span <span
v-if="property.fields.price.show" v-if="property.fields.price.show"
class="text-12px" class="text-16px"
:style="{ color: property.fields.price.color }" :style="{ color: property.fields.price.color }"
> >
{{ spu.price }} {{ fenToYuan(spu.price || Infinity) }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>{{ fenToYuan(spu.marketPrice) }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<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> </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>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <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 * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
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
if (Array.isArray(activityIds) && activityIds.length > 0) {
//
seckillActivityList.value =
await SeckillActivityApi.getSeckillActivityListByIds(activityIds)
// 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 scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -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 label="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 label="threeCol"> <el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-three-24-filled" /> <Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button> </el-radio-button>
</el-tooltip> </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-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

View File

@ -13,12 +13,12 @@
<el-form-item label="框体样式"> <el-form-item label="框体样式">
<el-radio-group v-model="formData!.borderRadius"> <el-radio-group v-model="formData!.borderRadius">
<el-tooltip content="方形" placement="top"> <el-tooltip content="方形" placement="top">
<el-radio-button :label="0"> <el-radio-button :value="0">
<Icon icon="tabler:input-search" /> <Icon icon="tabler:input-search" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="圆形" placement="top"> <el-tooltip content="圆形" placement="top">
<el-radio-button :label="10"> <el-radio-button :value="10">
<Icon icon="iconoir:input-search" /> <Icon icon="iconoir:input-search" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
@ -30,12 +30,12 @@
<el-form-item label="文本位置" prop="placeholderPosition"> <el-form-item label="文本位置" prop="placeholderPosition">
<el-radio-group v-model="formData!.placeholderPosition"> <el-radio-group v-model="formData!.placeholderPosition">
<el-tooltip content="居左" placement="top"> <el-tooltip content="居左" placement="top">
<el-radio-button label="left"> <el-radio-button value="left">
<Icon icon="ant-design:align-left-outlined" /> <Icon icon="ant-design:align-left-outlined" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="居中" placement="top"> <el-tooltip content="居中" placement="top">
<el-radio-button label="center"> <el-radio-button value="center">
<Icon icon="ant-design:align-center-outlined" /> <Icon icon="ant-design:align-center-outlined" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>

View File

@ -27,8 +27,8 @@
</el-form-item> </el-form-item>
<el-form-item label="导航背景"> <el-form-item label="导航背景">
<el-radio-group v-model="formData!.style.bgType"> <el-radio-group v-model="formData!.style.bgType">
<el-radio-button label="color">纯色</el-radio-button> <el-radio-button value="color">纯色</el-radio-button>
<el-radio-button label="img">图片</el-radio-button> <el-radio-button value="img">图片</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'"> <el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'">
@ -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)

Some files were not shown because too many files have changed in this diff Show More