!185 master-vxe分支合并master分支1.8.0版本的修改

Merge pull request !185 from clockdotnet/master-vxe
This commit is contained in:
芋道源码 2023-07-28 00:48:31 +00:00 committed by Gitee
commit fd46445147
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
95 changed files with 7845 additions and 895 deletions

View File

@ -1,5 +1,5 @@
# 开发环境
NODE_ENV=production
NODE_ENV=development
VITE_DEV=false

View File

@ -1,6 +1,4 @@
// @ts-check
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
module.exports = {
root: true,
env: {
browser: true,
@ -8,6 +6,7 @@ module.exports = defineConfig({
es6: true
},
parser: 'vue-eslint-parser',
plugins: ['vue'],
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
@ -17,16 +16,9 @@ module.exports = defineConfig({
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'./.eslintrc-auto-import.json'
],
extends: ['plugin:vue/vue3-recommended', 'prettier', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
rules: {
'vue/script-setup-uses-vars': 'error',
'vue/no-reserved-component-names': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
@ -39,8 +31,20 @@ module.exports = defineConfig({
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-unused-vars': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
@ -66,4 +70,4 @@ module.exports = defineConfig({
],
'vue/multi-word-component-names': 'off'
}
})
}

159
.vscode/settings.json vendored
View File

@ -1,36 +1,98 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.enable": true,
"editor.formatOnType": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"typescript.tsdk": "./node_modules/typescript/lib",
"volar.tsPlugin": true,
"volar.tsPluginStatus": false,
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"prettier.printWidth": 100, //
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
"**/*.log*": true,
"**/bower_components": true,
"**/dist": true,
"**/elehukouben": true,
"**/.git": true,
"**/.gitignore": true,
"**/.svn": true,
"**/.DS_Store": true,
"**/.idea": true,
"**/.vscode": false,
"**/yarn.lock": true,
"**/tmp": true,
"out": true,
"dist": true,
"node_modules": true,
"CHANGELOG.md": true,
"examples": true,
"res": true,
"screenshots": true,
"yarn-error.log": true,
"**/.yarn": true
},
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
"files.exclude": {
"**/.cache": true,
"**/.editorconfig": true,
"**/.eslintcache": true,
"**/bower_components": true,
"**/.idea": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true
},
"[javascript]": {
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.vscode/**": true,
"**/node_modules/**": true,
"**/tmp/**": true,
"**/bower_components/**": true,
"**/dist/**": true,
"**/yarn.lock": true
},
"stylelint.enable": true,
"stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"[typescriptreact]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
}
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
@ -39,16 +101,69 @@
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"god.tsconfig": "./tsconfig.json",
"vue-i18n.i18nPaths": "src/locales",
"cSpell.words": [
"vben",
"windicss",
"tailwind",
"browserslist",
"tailwindcss",
"esnext",
"antv",
"tinymce",
"qrcode",
"sider",
"pinia",
"sider",
"nprogress",
"INTLIFY",
"stylelint",
"esno",
"vitejs",
"sortablejs",
"codemirror",
"iconify",
"commitlint",
"vditor",
"echarts",
"cropperjs",
"logicflow",
"vueuse",
"zxcvbn",
"lintstagedrc",
"brotli",
"sider",
"pnpm",
"antd"
],
"vetur.format.scriptInitialIndent": true,
"vetur.format.styleInitialIndent": true,
"vetur.validation.script": false,
"MicroPython.executeButton": [
{
"text": "▶",
"tooltip": "运行",
"alignment": "left",
"command": "extension.executeFile",
"priority": 3.5
}
],
"MicroPython.syncButton": [
{
"text": "$(sync)",
"tooltip": "同步",
"alignment": "left",
"command": "extension.execute",
"priority": 4
}
],
//
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx",
"*.env": "$(capture).env.*",
"CHANGELOG.md": "CHANGELOG*",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc,vite.config.*,windi.*,tailwind.*,tsconfig.*,postcss*",
".eslintrc.js": ".eslintignore,.eslintrc-*,.prettierignore,.stylelintignore,.commitlintrc.js,.prettierrc.js,.stylelint*,stylelint*,prettier.*,.editorconfig"
}
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
},
"terminal.integrated.scrollback": 10000
}

View File

@ -9,7 +9,7 @@
## 🐶 新手必读
* nodejs > 16.0.0 && pnpm > 7.30.0
* nodejs > 16.0.0 && pnpm > 8.6.0 (强制使用pnpm)
* 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
* 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
@ -39,11 +39,11 @@
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|------------------|--------|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.4 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.3.8 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.3.4 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.3.9 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.3.7 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 5.0.4 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.3 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 10.1.2 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.4 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 10.2.0 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.2.1 |
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
@ -136,7 +136,7 @@ ps核心功能已经实现正在对接微信小程序中...
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持将文件存储到 S3MinIO、阿里云、腾讯云、七牛云、本地、FTP、数据库等 |
| 🚀 | 文件服务 | 支持将文件存储到 S3MinIO、阿里云、腾讯云、七牛云、本地、FTP、数据库等 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |

View File

@ -12,7 +12,6 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
import topLevelAwait from 'vite-plugin-top-level-await'
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
@ -28,7 +27,6 @@ export function createVitePlugins() {
WindiCSS(),
progress(),
PurgeIcons(),
vueSetupExtend(),
ElementPlus({}),
AutoImport({
include: [

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "1.7.3-snapshot",
"version": "1.8.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@ -9,12 +9,12 @@
"dev": "vite --mode base",
"front": "vite --mode front",
"ts:check": "vue-tsc --noEmit",
"build:pro": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode pro",
"build:dev": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode dev",
"build:stage": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode stage",
"build:test": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode test",
"build:static": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode static",
"build:front": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode front",
"build:pro": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode pro",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:static": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode static",
"build:front": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode front",
"serve:pro": "vite preview --mode pro",
"serve:dev": "vite preview --mode dev",
"serve:test": "vite preview --mode test",
@ -32,12 +32,12 @@
"@element-plus/icons-vue": "^2.1.0",
"@form-create/designer": "^3.1.3",
"@form-create/element-ui": "^3.1.18",
"@iconify/iconify": "^3.1.0",
"@iconify/iconify": "^3.1.1",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.1.2",
"@vueuse/core": "^10.2.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.1",
"@zxcvbn-ts/core": "^3.0.2",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"benz-amr-recorder": "^1.1.5",
@ -45,29 +45,30 @@
"camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "^1.5.13",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"dayjs": "^1.11.9",
"diagram-js": "^11.6.0",
"echarts": "^5.4.2",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.3.4",
"fast-xml-parser": "^4.2.2",
"element-plus": "2.3.7",
"fast-xml-parser": "^4.2.5",
"highlight.js": "^11.8.0",
"intro.js": "^7.0.1",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",
"mitt": "^3.0.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.1.3",
"pinia": "^2.1.4",
"qrcode": "^1.5.3",
"qs": "^6.11.2",
"steady-xml": "^0.1.0",
"url": "^0.11.0",
"url": "^0.11.1",
"video.js": "^8.3.0",
"vue": "3.3.4",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.2.2",
"vue-router": "^4.2.1",
"vue-types": "^5.0.3",
"vue-router": "^4.2.4",
"vue-types": "^5.1.0",
"vuedraggable": "^4.1.0",
"vxe-table": "^4.3.11",
"web-storage-cache": "^1.1.1",
@ -75,65 +76,61 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@iconify/json": "^2.2.67",
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6",
"@iconify/json": "^2.2.87",
"@intlify/unplugin-vue-i18n": "^0.12.1",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.0",
"@types/node": "^20.4.1",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qrcode": "^1.5.1",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"@vitejs/plugin-legacy": "^4.0.3",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-legacy": "^4.1.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"autoprefixer": "^10.4.14",
"bpmn-js": "^8.9.0",
"bpmn-js-properties-panel": "^0.46.0",
"consola": "^3.1.0",
"eslint": "^8.40.0",
"consola": "^3.2.3",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-define-config": "^1.20.0",
"eslint-define-config": "^1.21.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.13.0",
"lint-staged": "^13.2.2",
"postcss": "^8.4.23",
"eslint-plugin-vue": "^9.15.1",
"lint-staged": "^13.2.3",
"postcss": "^8.4.25",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.8",
"rimraf": "^5.0.1",
"rollup": "^3.22.0",
"sass": "^1.62.1",
"stylelint": "^15.6.2",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^33.0.0",
"rollup": "^3.26.2",
"sass": "^1.63.6",
"stylelint": "^15.10.1",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.17.4",
"typescript": "5.0.4",
"unplugin-auto-import": "^0.16.0",
"unplugin-element-plus": "^0.7.1",
"unplugin-vue-components": "^0.24.1",
"vite": "4.3.8",
"terser": "^5.18.2",
"typescript": "5.1.6",
"unplugin-auto-import": "^0.16.6",
"unplugin-element-plus": "^0.7.2",
"unplugin-vue-components": "^0.25.1",
"vite": "4.4.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.0",
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
"vite-plugin-top-level-await": "^1.3.1",
"vite-plugin-windicss": "^1.9.0",
"vue-tsc": "^1.6.5",
"vue-tsc": "^1.8.4",
"windicss": "^3.5.6"
},
"engines": {
"node": ">=16.0.0"
},
"license": "MIT",
"repository": {
"type": "git",
@ -142,5 +139,10 @@
"bugs": {
"url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
},
"homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3"
"homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
"packageManager": "pnpm@8.6.0",
"engines": {
"node": ">= 16.0.0",
"pnpm": ">=8.6.0"
}
}

View File

@ -38,10 +38,11 @@ $prefix-cls: #{$namespace}-app;
html,
body {
@extend .size;
padding: 0 !important;
margin: 0;
overflow: hidden;
@extend .size;
#app {
@extend .size;

View File

@ -7,8 +7,7 @@ export interface Property {
valueName?: string // 属性值名称
}
// TODO puhui999是不是直接叫 Sku 更简洁一点哈。type 待后面,总感觉有个类型?
export interface SkuType {
export interface Sku {
id?: number // 商品 SKU 编号
spuId?: number // SPU 编号
properties?: Property[] // 属性数组
@ -25,8 +24,7 @@ export interface SkuType {
salesCount?: number // 商品销量
}
// TODO puhui999是不是直接叫 Spu 更简洁一点哈。type 待后面,总感觉有个类型?
export interface SpuType {
export interface Spu {
id?: number
name?: string // 商品名称
categoryId?: number | null // 商品分类
@ -39,9 +37,9 @@ export interface SpuType {
brandId?: number | null // 商品品牌编号
specType?: boolean // 商品规格
subCommissionType?: boolean // 分销类型
skus: SkuType[] // sku数组
skus?: Sku[] // sku数组
description?: string // 商品详情
sort?: string // 商品排序
sort?: number // 商品排序
giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量
recommendHot?: boolean // 是否热卖
@ -49,6 +47,13 @@ export interface SpuType {
recommendBest?: boolean // 是否精品
recommendNew?: boolean // 是否新品
recommendGood?: boolean // 是否优品
price?: number // 商品价格
salesCount?: number // 商品销量
marketPrice?: number // 市场价
costPrice?: number // 成本价
stock?: number // 商品库存
createTime?: Date // 商品创建时间
status?: number // 商品状态
}
// 获得 Spu 列表
@ -62,12 +67,12 @@ export const getTabsCount = () => {
}
// 创建商品 Spu
export const createSpu = (data: SpuType) => {
export const createSpu = (data: Spu) => {
return request.post({ url: '/product/spu/create', data })
}
// 更新商品 Spu
export const updateSpu = (data: SpuType) => {
export const updateSpu = (data: Spu) => {
return request.put({ url: '/product/spu/update', data })
}
@ -81,6 +86,11 @@ export const getSpu = (id: number) => {
return request.get({ url: `/product/spu/get-detail?id=${id}` })
}
// 获得商品 Spu 详情列表
export const getSpuDetailList = (ids: number[]) => {
return request.get({ url: `/product/spu/list?spuIds=${ids}` })
}
// 删除商品 Spu
export const deleteSpu = (id: number) => {
return request.delete({ url: `/product/spu/delete?id=${id}` })
@ -90,3 +100,8 @@ export const deleteSpu = (id: number) => {
export const exportSpu = async (params) => {
return await request.download({ url: '/product/spu/export', params })
}
// 获得商品 SPU 精简列表
export const getSpuSimpleList = async () => {
return request.get({ url: '/product/spu/get-simple-list' })
}

View File

@ -0,0 +1,63 @@
import request from '@/config/axios'
import { Sku, Spu } from '@/api/mall/product/spu'
// TODO @puhui999: combinationActivity.ts
export interface CombinationActivityVO {
id?: number
name?: string
spuId?: number
totalLimitCount?: number
singleLimitCount?: number
startTime?: Date
endTime?: Date
userSize?: number
totalNum?: number
successNum?: number
orderUserCount?: number
virtualGroup?: number
status?: number
limitDuration?: number
products: CombinationProductVO[]
}
// 拼团活动所需属性
export interface CombinationProductVO {
spuId: number
skuId: number
activePrice: number // 拼团价格
}
// 扩展 Sku 配置
export type SkuExtension = Sku & {
productConfig: CombinationProductVO
}
export interface SpuExtension extends Spu {
skus: SkuExtension[] // 重写类型
}
// 查询拼团活动列表
export const getCombinationActivityPage = async (params) => {
return await request.get({ url: '/promotion/combination-activity/page', params })
}
// 查询拼团活动详情
export const getCombinationActivity = async (id: number) => {
return await request.get({ url: '/promotion/combination-activity/get?id=' + id })
}
// 新增拼团活动
export const createCombinationActivity = async (data: CombinationActivityVO) => {
return await request.post({ url: '/promotion/combination-activity/create', data })
}
// 修改拼团活动
export const updateCombinationActivity = async (data: CombinationActivityVO) => {
return await request.put({ url: '/promotion/combination-activity/update', data })
}
// 删除拼团活动
export const deleteCombinationActivity = async (id: number) => {
return await request.delete({ url: '/promotion/combination-activity/delete?id=' + id })
}

View File

@ -0,0 +1,18 @@
import request from '@/config/axios'
// TODO @dhb52vo 缺少
// 删除优惠劵
export const deleteCoupon = async (id: number) => {
return request.delete({
url: `/promotion/coupon/delete?id=${id}`
})
}
// 获得优惠劵分页
export const getCouponPage = async (params: PageParam) => {
return request.get({
url: '/promotion/coupon/page',
params: params
})
}

View File

@ -0,0 +1,83 @@
import request from '@/config/axios'
export interface CouponTemplateVO {
id: number
name: string
status: number
totalCount: number
takeLimitCount: number
takeType: number
usePrice: number
productScope: number
productSpuIds: string
validityType: number
validStartTime: Date
validEndTime: Date
fixedStartTerm: number
fixedEndTerm: number
discountType: number
discountPercent: number
discountPrice: number
discountLimitPrice: number
takeCount: number
useCount: number
}
// 创建优惠劵模板
export function createCouponTemplate(data: CouponTemplateVO) {
return request.post({
url: '/promotion/coupon-template/create',
data: data
})
}
// 更新优惠劵模板
export function updateCouponTemplate(data: CouponTemplateVO) {
return request.put({
url: '/promotion/coupon-template/update',
data: data
})
}
// 更新优惠劵模板的状态
export function updateCouponTemplateStatus(id: number, status: [0, 1]) {
const data = {
id,
status
}
return request.put({
url: '/promotion/coupon-template/update-status',
data: data
})
}
// 删除优惠劵模板
export function deleteCouponTemplate(id: number) {
return request.delete({
url: '/promotion/coupon-template/delete?id=' + id
})
}
// 获得优惠劵模板
export function getCouponTemplate(id: number) {
return request.get({
url: '/promotion/coupon-template/get?id=' + id
})
}
// 获得优惠劵模板分页
export function getCouponTemplatePage(params: PageParam) {
return request.get({
url: '/promotion/coupon-template/page',
params: params
})
}
// 导出优惠劵模板 Excel
export function exportCouponTemplateExcel(params: PageParam) {
return request.get({
url: '/promotion/coupon-template/export-excel',
params: params,
responseType: 'blob'
})
}

View File

@ -0,0 +1,63 @@
import request from '@/config/axios'
import { Sku, Spu } from '@/api/mall/product/spu'
export interface SeckillActivityVO {
id?: number
spuId?: number
name?: string
status?: number
remark?: string
startTime?: Date
endTime?: Date
sort?: number
configIds?: string
orderCount?: number
userCount?: number
totalPrice?: number
totalLimitCount?: number
singleLimitCount?: number
stock?: number
totalStock?: number
products?: SeckillProductVO[]
}
// 秒杀活动所需属性
export interface SeckillProductVO {
skuId: number
seckillPrice: number
stock: number
}
// 扩展 Sku 配置
export type SkuExtension = Sku & {
productConfig: SeckillProductVO
}
export interface SpuExtension extends Spu {
skus: SkuExtension[] // 重写类型
}
// 查询秒杀活动列表
export const getSeckillActivityPage = async (params) => {
return await request.get({ url: '/promotion/seckill-activity/page', params })
}
// 查询秒杀活动详情
export const getSeckillActivity = async (id: number) => {
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
}
// 新增秒杀活动
export const createSeckillActivity = async (data: SeckillActivityVO) => {
return await request.post({ url: '/promotion/seckill-activity/create', data })
}
// 修改秒杀活动
export const updateSeckillActivity = async (data: SeckillActivityVO) => {
return await request.put({ url: '/promotion/seckill-activity/update', data })
}
// 删除秒杀活动
export const deleteSeckillActivity = async (id: number) => {
return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id })
}

View File

@ -0,0 +1,49 @@
import request from '@/config/axios'
export interface SeckillConfigVO {
id: number
name: string
startTime: string
endTime: string
sliderPicUrls: string[]
status: number
}
// 查询秒杀时段配置列表
export const getSeckillConfigPage = async (params) => {
return await request.get({ url: '/promotion/seckill-config/page', params })
}
// 查询秒杀时段配置详情
export const getSeckillConfig = async (id: number) => {
return await request.get({ url: '/promotion/seckill-config/get?id=' + id })
}
// 获得所有开启状态的秒杀时段精简列表
export const getListAllSimple = async () => {
return await request.get({ url: '/promotion/seckill-config/list-all-simple' })
}
// 新增秒杀时段配置
export const createSeckillConfig = async (data: SeckillConfigVO) => {
return await request.post({ url: '/promotion/seckill-config/create', data })
}
// 修改秒杀时段配置
export const updateSeckillConfig = async (data: SeckillConfigVO) => {
return await request.put({ url: '/promotion/seckill-config/update', data })
}
// 修改时段配置状态
export const updateSeckillConfigStatus = (id: number, status: number) => {
const data = {
id,
status
}
return request.put({ url: '/promotion/seckill-config/update-status', data: data })
}
// 删除秒杀时段配置
export const deleteSeckillConfig = async (id: number) => {
return await request.delete({ url: '/promotion/seckill-config/delete?id=' + id })
}

View File

@ -33,6 +33,11 @@ export const getDeliveryExpressTemplate = async (id: number) => {
return await request.get({ url: '/trade/delivery/express-template/get?id=' + id })
}
// 查询快递运费模板详情
export const getSimpleTemplateList = async () => {
return await request.get({ url: '/trade/delivery/express-template/list-all-simple' })
}
// 新增快递运费模板
export const createDeliveryExpressTemplate = async (data: DeliveryExpressTemplateVO) => {
return await request.post({ url: '/trade/delivery/express-template/create', data })
@ -47,8 +52,3 @@ export const updateDeliveryExpressTemplate = async (data: DeliveryExpressTemplat
export const deleteDeliveryExpressTemplate = async (id: number) => {
return await request.delete({ url: '/trade/delivery/express-template/delete?id=' + id })
}
// 导出快递运费模板 Excel
export const exportDeliveryExpressTemplateApi = async (params) => {
return await request.download({ url: '/trade/delivery/express-template/export-excel', params })
}

View File

@ -0,0 +1,46 @@
import request from '@/config/axios'
export interface DeliveryPickUpStoreVO {
id: number
name: string
introduction: string
phone: string
areaId: number
detailAddress: string
logo: string
openingTime: string
closingTime: string
latitude: number
longitude: number
status: number
}
// 查询自提门店列表
export const getDeliveryPickUpStorePage = async (params: DeliveryPickUpStorePageReqVO) => {
return await request.get({ url: '/trade/delivery/pick-up-store/page', params })
}
// 查询自提门店详情
export const getDeliveryPickUpStore = async (id: number) => {
return await request.get({ url: '/trade/delivery/pick-up-store/get?id=' + id })
}
// 新增自提门店
export const createDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => {
return await request.post({ url: '/trade/delivery/pick-up-store/create', data })
}
// 修改自提门店
export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => {
return await request.put({ url: '/trade/delivery/pick-up-store/update', data })
}
// 删除自提门店
export const deleteDeliveryPickUpStore = async (id: number) => {
return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
}
// 导出自提门店 Excel
export const exportDeliveryPickUpStoreApi = async (params) => {
return await request.download({ url: '/trade/delivery/pick-up-store/export-excel', params })
}

View File

@ -0,0 +1,12 @@
import request from '@/config/axios'
// 获得交易订单分页
// TODO @xiaobai改成 getOrderPage
export const getOrderList = (params: PageParam) => {
return request.get({ url: '/trade/order/page', params })
}
// 获得交易订单详情
export const getOrderDetail = (id: number) => {
return request.get({ url: '/trade/order/get-detail?id=' + id })
}

View File

@ -0,0 +1,228 @@
// TODO @xiaobai这个放到 order/index.ts 里哈
// TODO @xiaobai注释放到变量后面这样简洁一点
// TODO @xiaobai这个改成 TradeOrderRespVO
export interface TradeOrderPageItemRespVO {
// 订单编号
id?: number
// 订单流水号
no?: string
// 下单时间
createTime?: Date
// 订单类型
type?: number
// 订单来源
terminal?: number
// 用户编号
userId?: number
// 用户 IP
userIp?: string
// 用户备注
userRemark?: string
// 订单状态
status?: number
// 购买的商品数量
productCount?: number
// 订单完成时间
finishTime?: Date
// 订单取消时间
cancelTime?: Date
// 取消类型
cancelType?: number
// 商家备注
remark?: string
// 支付订单编号
payOrderId: number
// 是否已支付
payed?: boolean
// 付款时间
payTime?: Date
// 支付渠道
payChannelCode?: string
// 商品原价(总)
originalPrice?: number
// 订单原价(总)
orderPrice?: number
// 订单优惠(总)
discountPrice?: number
// 运费金额
deliveryPrice?: number
// 订单调价(总)
adjustPrice?: number
// 应付金额(总)
payPrice?: number
// 配送模板编号
deliveryTemplateId?: number
// 发货物流公司编号
logisticsId?: number
// 发货物流单号
logisticsNo?: string
// 发货状态
deliveryStatus?: number
// 发货时间
deliveryTime?: Date
// 收货时间
receiveTime?: Date
// 收件人名称
receiverName?: string
// 收件人手机
receiverMobile?: string
// 收件人地区编号
receiverAreaId?: number
// 收件人邮编
receiverPostCode?: number
// 收件人详细地址
receiverDetailAddress?: string
// 售后状态
afterSaleStatus?: number
// 退款金额
refundPrice?: number
// 优惠劵编号
couponId?: number
// 优惠劵减免金额
couponPrice?: number
// 积分抵扣的金额
pointPrice?: number
//收件人地区名字
receiverAreaName?: string
// 订单项列表
items?: TradeOrderItemBaseVO[]
//用户信息
user?: MemberUserRespDTO
}
// TODO @xiaobai这个改成 TradeOrderItemRespVO
/**
* Base VO VO 使
* VO Swagger
*/
export interface TradeOrderItemBaseVO {
// ========== 订单项基本信息 ==========
/**
*
*/
id?: number
/**
*
*/
userId?: number
/**
*
*/
orderId?: number
// ========== 商品基本信息 ==========
/**
* SPU
*/
spuId?: number
/**
* SPU
*/
spuName?: string
/**
* SKU
*/
skuId?: number
/**
*
*/
picUrl?: string
/**
*
*/
count?: number
// ========== 价格 + 支付基本信息 ==========
/**
*
*/
originalPrice?: number
/**
*
*/
originalUnitPrice?: number
/**
*
*/
discountPrice?: number
/**
*
*/
payPrice?: number
/**
*
*/
orderPartPrice?: number
/**
*
*/
orderDividePrice?: number
// ========== 营销基本信息 ==========
// TODO 芋艿:在捉摸一下
// ========== 售后基本信息 ==========
/**
*
*/
afterSaleStatus?: number
//属性数组
properties?: ProductPropertyValueDetailRespVO[]
}
/**
* - Response VO
*/
export interface ProductPropertyValueDetailRespVO {
/**
*
*/
propertyId?: number
/**
*
*/
propertyName?: string
/**
*
*/
valueId?: number
/**
*
*/
valueName?: string
}
/**
*
*/
export interface TradeOrderPageReqVO {
pageNo: number
pageSize: number
no?: string
userId?: string
userNickname?: string
userMobile?: string
receiverName?: string
receiverMobile?: string
terminal?: string
type?: number
status?: number
payChannelCode?: string
createTime?: [Date, Date]
spuName?: string
itemCount?: string
all?: string
}
//用户信息
export interface MemberUserRespDTO {
id?: number
nickname?: string
status?: number
avatar?: string
mobile?: string
}
//订单详情选中type
export interface SelectType {
queryParams: TradeOrderPageReqVO
selectTotal: number //选中的数量
selectAllFlag: boolean //全选标识
selectData: Map<number, Set<string>> //存放涉及选中得页面以及每页选中得数据订单号 全选时根据条件查询 排除取消的list订单
unSelectList: Set<string> //登记取消的list 全选标识为true 时登记单独取消的list再次选中时排除 全选标识为false 时清空list
}

View File

@ -0,0 +1,19 @@
import request from '@/config/axios'
export interface ConfigVO {
id: number
tradeDeductEnable: number
tradeDeductUnitPrice: number
tradeDeductMaxPrice: number
tradeGivePoint: number
}
// 查询积分设置详情
export const getConfig = async () => {
return await request.get({ url: `/point/config/get` })
}
// 新增修改积分设置
export const saveConfig = async (data: ConfigVO) => {
return await request.put({ url: `/point/config/save`, data })
}

View File

@ -0,0 +1,47 @@
import request from '@/config/axios'
export interface RecordVO {
id: number
bizId: string
bizType: string
type: string
title: string
description: string
point: number
totalPoint: number
status: number
userId: number
freezingTime: Date
thawingTime: Date
createDate: Date
}
// 查询用户积分记录列表
export const getRecordPage = async (params) => {
return await request.get({ url: `/point/record/page`, params })
}
// 查询用户积分记录详情
export const getRecord = async (id: number) => {
return await request.get({ url: `/point/record/get?id=` + id })
}
// 新增用户积分记录
export const createRecord = async (data: RecordVO) => {
return await request.post({ url: `/point/record/create`, data })
}
// 修改用户积分记录
export const updateRecord = async (data: RecordVO) => {
return await request.put({ url: `/point/record/update`, data })
}
// 删除用户积分记录
export const deleteRecord = async (id: number) => {
return await request.delete({ url: `/point/record/delete?id=` + id })
}
// 导出用户积分记录 Excel
export const exportRecord = async (params) => {
return await request.download({ url: `/point/record/export-excel`, params })
}

View File

@ -0,0 +1,37 @@
import request from '@/config/axios'
export interface SignInConfigVO {
id: number
day: number
point: number
}
// 查询积分签到规则列表
export const getSignInConfigPage = async (params) => {
return await request.get({ url: `/point/sign-in-config/page`, params })
}
// 查询积分签到规则详情
export const getSignInConfig = async (id: number) => {
return await request.get({ url: `/point/sign-in-config/get?id=` + id })
}
// 新增积分签到规则
export const createSignInConfig = async (data: SignInConfigVO) => {
return await request.post({ url: `/point/sign-in-config/create`, data })
}
// 修改积分签到规则
export const updateSignInConfig = async (data: SignInConfigVO) => {
return await request.put({ url: `/point/sign-in-config/update`, data })
}
// 删除积分签到规则
export const deleteSignInConfig = async (id: number) => {
return await request.delete({ url: `/point/sign-in-config/delete?id=` + id })
}
// 导出积分签到规则 Excel
export const exportSignInConfig = async (params) => {
return await request.download({ url: `/point/sign-in-config/export-excel`, params })
}

View File

@ -0,0 +1,38 @@
import request from '@/config/axios'
export interface SignInRecordVO {
id: number
userId: number
day: number
point: number
}
// 查询用户签到积分列表
export const getSignInRecordPage = async (params) => {
return await request.get({ url: `/point/sign-in-record/page`, params })
}
// 查询用户签到积分详情
export const getSignInRecord = async (id: number) => {
return await request.get({ url: `/point/sign-in-record/get?id=` + id })
}
// 新增用户签到积分
export const createSignInRecord = async (data: SignInRecordVO) => {
return await request.post({ url: `/point/sign-in-record/create`, data })
}
// 修改用户签到积分
export const updateSignInRecord = async (data: SignInRecordVO) => {
return await request.put({ url: `/point/sign-in-record/update`, data })
}
// 删除用户签到积分
export const deleteSignInRecord = async (id: number) => {
return await request.delete({ url: `/point/sign-in-record/delete?id=` + id })
}
// 导出用户签到积分 Excel
export const exportSignInRecord = async (params) => {
return await request.download({ url: `/point/sign-in-record/export-excel`, params })
}

View File

@ -14,7 +14,7 @@ const props = defineProps({
})
const getBindValue = computed(() => {
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
const delArr: string[] = ['fullscreen', 'title', 'maxHeight', 'appendToBody']
const attrs = useAttrs()
const obj = { ...attrs, ...props }
for (const key in obj) {

View File

@ -1,16 +1,16 @@
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
import { computed, defineComponent, onMounted, PropType, ref, unref, watch } from 'vue'
import { ElCol, ElForm, ElFormItem, ElRow, ElTooltip } from 'element-plus'
import { componentMap } from './componentMap'
import { propTypes } from '@/utils/propTypes'
import { getSlot } from '@/utils/tsxHelper'
import {
setTextPlaceholder,
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel,
setFormItemSlots
setComponentProps,
setFormItemSlots,
setGridProp,
setItemComponentSlots,
setTextPlaceholder
} from './helper'
import { useRenderSelect } from './components/useRenderSelect'
import { useRenderRadio } from './components/useRenderRadio'
@ -27,6 +27,7 @@ const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('form')
export default defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Form',
props: {
// Form
@ -196,7 +197,7 @@ export default defineComponent({
<span>{item.label}</span>
<ElTooltip placement="right" raw-content>
{{
content: () => <span v-html={item.labelMessage}></span>,
content: () => <span v-dompurify-html={item.labelMessage}></span>,
default: () => (
<Icon
icon="ep:warning"

View File

@ -9,6 +9,7 @@ import { set } from 'lodash-es'
import { Pagination, TableColumn, TableSetPropsType, TableSlotDefault } from '@/types/table'
export default defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Table',
props: {
pageSize: propTypes.number.def(10),
@ -302,6 +303,7 @@ export default defineComponent({
margin-left: 0;
padding: 8px 4px;
}
:deep(.el-button.is-link) {
margin-left: 0;
padding: 8px 4px;

View File

@ -10,6 +10,7 @@
import BpmnViewer from 'bpmn-js/lib/Viewer'
import DefaultEmptyXML from './plugins/defaultEmpty'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
const props = defineProps({
value: {
// BPMN XML
@ -403,6 +404,7 @@ watch(
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
}
.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
fill: #1890ff !important;
stroke: #1890ff !important;
@ -414,8 +416,9 @@ watch(
stroke: #1890ff !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
marker-end: url(#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr);
marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
}
:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
fill: #1890ff !important;
stroke: #1890ff !important;
@ -429,14 +432,17 @@ watch(
stroke: green !important;
fill-opacity: 0.2 !important;
}
.highlight.djs-shape .djs-visual > :nth-child(2) {
fill: green !important;
}
.highlight.djs-shape .djs-visual > path {
fill: green !important;
fill-opacity: 0.2 !important;
stroke: green !important;
}
.highlight.djs-connection > .djs-visual > path {
stroke: green !important;
}
@ -450,14 +456,17 @@ watch(
stroke: green !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
fill: green !important;
}
:deep(.highlight.djs-shape .djs-visual > path) {
fill: green !important;
fill-opacity: 0.2 !important;
stroke: green !important;
}
:deep(.highlight.djs-connection > .djs-visual > path) {
stroke: green !important;
}
@ -468,14 +477,17 @@ watch(
stroke: red !important;
fill-opacity: 0.2 !important;
}
.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
fill: red !important;
}
.highlight-reject.djs-shape .djs-visual > path {
fill: red !important;
fill-opacity: 0.2 !important;
stroke: red !important;
}
.highlight-reject.djs-connection > .djs-visual > path {
stroke: red !important;
}
@ -489,14 +501,17 @@ watch(
stroke: red !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
fill: red !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > path) {
fill: red !important;
fill-opacity: 0.2 !important;
stroke: red !important;
}
:deep(.highlight-reject.djs-connection > .djs-visual > path) {
stroke: red !important;
}
@ -507,14 +522,17 @@ watch(
stroke: grey !important;
fill-opacity: 0.2 !important;
}
.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
fill: grey !important;
}
.highlight-cancel.djs-shape .djs-visual > path {
fill: grey !important;
fill-opacity: 0.2 !important;
stroke: grey !important;
}
.highlight-cancel.djs-connection > .djs-visual > path {
stroke: grey !important;
}
@ -528,14 +546,17 @@ watch(
stroke: grey !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
fill: grey !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > path) {
fill: grey !important;
fill-opacity: 0.2 !important;
stroke: grey !important;
}
:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
stroke: grey !important;
}
@ -543,7 +564,7 @@ watch(
.element-overlays {
box-sizing: border-box;
padding: 8px;
background: rgba(0, 0, 0, 0.6);
background: rgb(0 0 0 / 60%);
border-radius: 4px;
color: #fafafa;
width: 200px;

View File

@ -30,12 +30,13 @@ const addTask = (event, options: any = {}) => {
<style scoped lang="scss">
.my-process-palette {
box-sizing: border-box;
padding: 80px 20px 20px 20px;
padding: 80px 20px 20px;
.test-button {
box-sizing: border-box;
padding: 8px 16px;
border-radius: 4px;
border: 1px solid rgba(24, 144, 255, 0.8);
border: 1px solid rgb(24 144 255 / 80%);
cursor: pointer;
}
}

View File

@ -106,22 +106,22 @@ const elementBusinessObject = ref<any>({}) // 元素 businessObject 镜像,提
const conditionFormVisible = ref(false) //
const formVisible = ref(false) //
const bpmnElement = ref()
const timer = ref()
provide('prefix', props.prefix)
provide('width', props.width)
const bpmnInstances = () => (window as any)?.bpmnInstances
const initModels = () => {
// console.log(props, 'props')
// console.log(props.bpmnModeler, 'sakdjjaskdsajdkasdjkadsjk')
// modeler moddle
// nextTick(() => {
if (!props.bpmnModeler) {
// props.bpmnModeler initModels
const unwatchBpmn = watch(
() => props.bpmnModeler,
() => {
//
timer.value = setTimeout(() => initModels(), 10)
return
}
if (timer.value) {
clearTimeout(timer.value)
if (!props.bpmnModeler) {
console.log('缺少props.bpmnModeler')
return
}
console.log('props.bpmnModeler 有值了!!!')
const w = window as any
w.bpmnInstances = {
modeler: props.bpmnModeler,
@ -134,12 +134,16 @@ const initModels = () => {
replace: props.bpmnModeler.get('replace'),
selection: props.bpmnModeler.get('selection')
}
}
console.log(bpmnInstances(), 'window.bpmnInstances')
getActiveElement()
// })
}
console.log(bpmnInstances(), 'window.bpmnInstances')
getActiveElement()
unwatchBpmn()
},
{
immediate: true
}
)
const getActiveElement = () => {
// bpmn:Process
initFormOnChanged(null)
@ -187,11 +191,7 @@ const initFormOnChanged = (element) => {
)
formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
}
onMounted(() => {
setTimeout(() => {
initModels()
}, 100)
})
onBeforeUnmount(() => {
const w = window as any
w.bpmnInstances = null

View File

@ -1,7 +1,7 @@
import { reactive } from 'vue'
import { AxiosPromise } from 'axios'
import { findIndex } from '@/utils'
import { eachTree, treeMap, filter } from '@/utils/tree'
import { eachTree, filter, treeMap } from '@/utils/tree'
import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict'
import { FormSchema } from '@/types/form'
@ -36,8 +36,11 @@ type CrudSearchParams = {
type CrudTableParams = {
// 是否显示表头
show?: boolean
// 列宽配置
width?: number | string
// 列是否固定在左侧或者右侧
fixed?: 'left' | 'right'
} & Omit<FormSchema, 'field'>
type CrudFormParams = {
// 是否显示表单项
show?: boolean

View File

@ -136,9 +136,7 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
})
if (res) {
tableObject.tableList = (res as unknown as ResponseType).list
if ((res as unknown as ResponseType).total) {
tableObject.total = (res as unknown as ResponseType).total as unknown as number
}
tableObject.total = (res as unknown as ResponseType).total ?? 0
}
},
setProps: async (props: TableProps = {}) => {

View File

@ -2,6 +2,9 @@
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
// eslint-disable-next-line vue/no-reserved-component-names
defineOptions({ name: 'Footer' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('footer')

View File

@ -13,6 +13,7 @@ const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('menu')
export default defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Menu',
props: {
menuSelect: {

View File

@ -217,7 +217,7 @@ $prefix-cls: #{$namespace}-tab-menu;
.#{$prefix-cls} {
transition: all var(--transition-time-02);
&:after {
&::after {
position: absolute;
top: 0;
right: 0;

View File

@ -41,9 +41,10 @@ import App from './App.vue'
import './permission'
import '@/plugins/tongji' // 百度统计
import Logger from '@/utils/Logger'
import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
// 创建实例
const setupAll = async () => {
const app = createApp(App)
@ -66,6 +67,8 @@ const setupAll = async () => {
await router.isReady()
app.use(VueDOMPurifyHTML)
app.mount('#app')
}

View File

@ -195,6 +195,22 @@ const remainingRouter: AppRouteRecordRaw[] = [
noTagsView: true
}
},
{
path: '/trade/order',
component: Layout,
name: 'order',
meta: {
hidden: true
},
children: [
{
path: 'detail',
name: 'TradeOrderDetail',
component: () => import('@/views/mall/trade/order/tradeOrderDetail.vue'),
meta: { title: '订单详情', hidden: true }
}
]
},
{
path: '/403',
component: () => import('@/views/Error/403.vue'),
@ -355,7 +371,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
children: [
{
path: 'productSpuAdd', // TODO @puhui999最好拆成 add 和 edit 两个路由;添加商品;修改商品 fix
path: 'spu/add',
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'ProductSpuAdd',
meta: {
@ -368,7 +384,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
},
{
path: 'productSpuEdit/:spuId(\\d+)',
path: 'spu/edit/:spuId(\\d+)',
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'ProductSpuEdit',
meta: {
@ -379,6 +395,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
title: '编辑商品',
activeMenu: '/product/product-spu'
}
},
{
path: 'spu/detail/:spuId(\\d+)',
component: () => import('@/views/mall/product/spu/addForm.vue'),
name: 'ProductSpuDetail',
meta: {
noCache: true,
hidden: true,
canTo: true,
icon: 'ep:view',
title: '商品详情',
activeMenu: '/product/product-spu'
}
}
]
}

View File

@ -1,6 +1,8 @@
export type TableColumn = {
field: string
label?: string
width?: number | string
fixed?: 'left' | 'right'
children?: TableColumn[]
} & Recordable

View File

@ -114,6 +114,10 @@ export const PayChannelEnum = {
ALIPAY_QR: {
code: 'alipay_qr',
name: '支付宝扫码支付'
},
ALIPAY_BAR: {
code: 'alipay_bar',
name: '支付宝条码支付'
}
}
@ -218,7 +222,7 @@ export const PayRefundStatusEnum = {
}
/**
* SPU枚举类
* SPU
*/
export const ProductSpuStatusEnum = {
RECYCLE: {
@ -234,3 +238,59 @@ export const ProductSpuStatusEnum = {
name: '上架'
}
}
/**
*
*/
export const CouponTemplateValidityTypeEnum = {
DATE: {
type: 1,
name: '固定日期可用'
},
TERM: {
type: 2,
name: '领取之后可用'
}
}
/**
*
*/
export const PromotionProductScopeEnum = {
ALL: {
scope: 1,
name: '全部商品参与'
},
SPU: {
scope: 2,
name: '指定商品参与'
}
}
/**
*
*/
export const PromotionConditionTypeEnum = {
PRICE: {
type: 10,
name: '满 N 元'
},
COUNT: {
type: 20,
name: '满 N 件'
}
}
/**
*
*/
export const PromotionDiscountTypeEnum = {
PRICE: {
type: 1,
name: '满减'
},
PERCENT: {
type: 2,
name: '折扣'
}
}

View File

@ -33,7 +33,6 @@ export const getIntDictOptions = (dictType: string) => {
value: parseInt(dict.value + '')
})
})
return dictOption
}
@ -147,9 +146,30 @@ export enum DICT_TYPE {
MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
// ========== MALL 模块 ==========
// ========== MALL - 会员模块 ==========
MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
MEMBER_POINT_STATUS = 'member_point_status', // 积分的状态
// ========== MALL - 商品模块 ==========
PRODUCT_UNIT = 'product_unit', // 商品单位
PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
// ========== MALL 交易模块 ==========
EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode' //快递的计费方式
// ========== MALL - 交易模块 ==========
EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式
TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态
TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式
TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态
TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态
TERMINAL = 'terminal', // 终端
// ========== MALL - 营销模块 ==========
PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型
PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围
PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
PROMOTION_CONDITION_TYPE = 'promotion_condition_type' // 营销的条件类型枚举
}

View File

@ -70,6 +70,13 @@ export function parseTime(time: any, pattern?: string) {
return time_str
}
/**
* +
*/
export function getNowDateTime() {
return dayjs()
}
/**
*
* @param dateTime
@ -196,13 +203,28 @@ export const dateFormatter = (row, column, cellValue) => {
return formatDate(cellValue)
}
/**
* element plus Formatter 使 YYYY-MM-DD
*
* @param row
* @param column
* @param cellValue
*/
// @ts-ignore
export const dateFormatter2 = (row, column, cellValue) => {
if (!cellValue) {
return
}
return formatDate(cellValue, 'YYYY-MM-DD')
}
/**
* 时间为00:00:00
* @param param
* @returns 带时间00:00:00
*/
export function beginOfDay(param: Date) {
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0, 0)
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0)
}
/**
@ -211,7 +233,7 @@ export function beginOfDay(param: Date) {
* @returns 带时间23:59:59
*/
export function endOfDay(param: Date) {
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59, 999)
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59)
}
/**

View File

@ -3,6 +3,7 @@ interface TreeHelperConfig {
children: string
pid: string
}
const DEFAULT_CONFIG: TreeHelperConfig = {
id: 'id',
children: 'children',
@ -133,6 +134,7 @@ export const filter = <T = any>(
): T[] => {
config = getConfig(config)
const children = config.children as string
function listFilter(list: T[]) {
return list
.map((node: any) => ({ ...node }))
@ -141,6 +143,7 @@ export const filter = <T = any>(
return func(node) || (node[children] && node[children].length)
})
}
return listFilter(tree)
}
@ -264,6 +267,7 @@ export const handleTree = (data: any[], id?: string, parentId?: string, children
}
}
}
return tree
}
/**
@ -300,3 +304,94 @@ export const handleTree2 = (data, id, parentId, children, rootId) => {
})
return treeData !== '' ? treeData : data
}
/**
* level
*
* @param tree
* @param nodeId
* @param level ,
* @return true false
*/
export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => {
if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
console.warn('tree must be an array')
return false
}
// 校验是否是一级节点
if (tree.some((item) => item.id === nodeId)) {
return false
}
// 递归计数
let count = 1
// 深层次校验
function performAThoroughValidation(arr: any[]): boolean {
count += 1
for (const item of arr) {
if (item.id === nodeId) {
return true
} else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
if (performAThoroughValidation(item.children)) {
return true
}
}
}
return false
}
for (const item of tree) {
count = 1
if (performAThoroughValidation(item.children)) {
// 找到后对比是否是期望的层级
if (count >= level) {
return true
}
}
}
return false
}
/**
*
* @param tree
* @param nodeId id
*/
export const treeToString = (tree: any[], nodeId) => {
if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
console.warn('tree must be an array')
return ''
}
// 校验是否是一级节点
const node = tree.find((item) => item.id === nodeId)
if (typeof node !== 'undefined') {
return node.name
}
let str = ''
function performAThoroughValidation(arr) {
for (const item of arr) {
if (item.id === nodeId) {
str += `/${item.name}`
return true
} else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
str += `/${item.name}`
if (performAThoroughValidation(item.children)) {
return true
}
}
}
return false
}
for (const item of tree) {
str = `${item.name}`
if (performAThoroughValidation(item.children)) {
break
}
}
return str
}

View File

@ -9,7 +9,7 @@
label-width="120px"
size="large"
>
<el-row style="maring-left: -10px; maring-right: -10px">
<el-row style="margin-left: -10px; margin-right: -10px">
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />

View File

@ -183,12 +183,17 @@ const signIn = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
loginLoading.value = true
smsVO.loginSms.mobile = loginData.loginForm.mobileNumber
smsVO.loginSms.code = loginData.loginForm.code
await smsLoginApi(smsVO.loginSms)
.then(async (res) => {
setToken(res?.token)
setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
@ -197,6 +202,10 @@ const signIn = async () => {
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<el-row v-show="getShow" style="maring-left: -10px; maring-right: -10px">
<el-row v-show="getShow" style="margin-left: -10px; margin-right: -10px">
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<LoginFormTitle style="width: 100%" />
</el-col>

View File

@ -52,7 +52,14 @@ const client = ref({
name: '',
logo: ''
})
const queryParams = reactive({
interface queryType {
responseType: string
clientId: string
redirectUri: string
state: string
scopes: string[]
}
const queryParams = reactive<queryType>({
// URL client_idscope
responseType: '',
clientId: '',
@ -61,7 +68,10 @@ const queryParams = reactive({
scopes: [] // query
})
const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // SSO
const formData = reactive({
interface formType {
scopes: string[]
}
const formData = reactive<formType>({
scopes: [] // scope
})
const formLoading = ref(false) //

View File

@ -39,21 +39,25 @@ const activeName = ref('basicInfo')
<style scoped>
.user {
max-height: 960px;
padding: 15px 20px 20px 20px;
padding: 15px 20px 20px;
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
}
:deep(.el-card .el-card__header, .el-card .el-card__body) {
padding: 15px !important;
}
.profile-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-weight: 600;
}
.el-tabs--left .el-tabs__content {
height: 100%;
}

View File

@ -3,8 +3,6 @@
<!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px">
<template #handle>
<XButton type="primary" title="生成JSON" @click="showJson" />
<XButton type="primary" title="生成Options" @click="showOption" />
<XButton type="primary" :title="t('action.save')" @click="handleSave" />
</template>
</FcDesigner>
@ -13,7 +11,7 @@
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(formValue)" />
<el-scrollbar height="580">
<div>
<pre><code class="hljs" v-html="highlightedCode(formValue)"></code></pre>
<pre><code v-dompurify-html="highlightedCode(formValue)" class="hljs"></code></pre>
</div>
</el-scrollbar>
</div>
@ -68,7 +66,6 @@ const message = useMessage() // 消息
const { query } = useRoute() //
const designer = ref() //
const type = ref(-1)
const formValue = ref('')
const dialogTitle = ref('')
const dialogVisible = ref(false) //
@ -116,20 +113,7 @@ const submitForm = async () => {
dialogLoading.value = false
}
}
const showJson = () => {
openModel('生成JSON')
type.value = 0
formValue.value = designer.value.getRule()
}
const showOption = () => {
openModel('生成Options')
type.value = 1
formValue.value = designer.value.getOption()
}
const openModel = (title: string) => {
dialogVisible1.value = true
dialogTitle.value = title
}
/** 复制 **/
const copy = async (text: string) => {
// const { copy, copied, isSupported } = useClipboard({ source: JSON.stringify(text) })
@ -152,22 +136,6 @@ const copy = async (text: string) => {
oInput.remove()
}
/**
* 代码高亮
*/
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import java from 'highlight.js/lib/languages/java'
import xml from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import sql from 'highlight.js/lib/languages/sql'
import typescript from 'highlight.js/lib/languages/typescript'
const highlightedCode = (item) => {
const language = item.filePath.substring(item.filePath.lastIndexOf('.') + 1)
const result = hljs.highlight(language, item.code || '', true)
return result.value || '&nbsp;'
}
/** 初始化 **/
onMounted(async () => {
//
@ -180,13 +148,5 @@ onMounted(async () => {
formValues.value = data
setConfAndFields(designer, data.conf, data.fields)
})
//
hljs.registerLanguage('java', java)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('vue', xml)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('typescript', typescript)
})
</script>

View File

@ -203,6 +203,7 @@ import { setConfAndFields2 } from '@/utils/formCreate'
// import { OptionAttrs } from '@form-create/element-ui/types/config'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useUserStore } from '@/store/modules/user'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
const { query } = useRoute() //
const message = useMessage() //

View File

@ -3,8 +3,6 @@
<el-row>
<el-col>
<div class="mb-2 float-right">
<el-button size="small" @click="setJson"> 导入JSON</el-button>
<el-button size="small" @click="setOption"> 导入Options</el-button>
<el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
<el-button size="small" type="success" @click="showOption">生成 Options</el-button>
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
@ -18,18 +16,18 @@
</ContentWrap>
<!-- 弹窗表单预览 -->
<Dialog :title="dialogTitle" v-model="dialogVisible" max-height="600">
<div ref="editor" v-if="dialogVisible">
<Dialog v-model="dialogVisible" :title="dialogTitle" max-height="600">
<div v-if="dialogVisible" ref="editor">
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(formData)" />
<el-scrollbar height="580">
<div>
<pre><code class="hljs" v-html="highlightedCode(formData)"></code></pre>
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div>
</el-scrollbar>
</div>
</Dialog>
</template>
<script setup lang="ts" name="InfraBuild">
<script lang="ts" name="InfraBuild" setup>
import FcDesigner from '@form-create/designer'
// import { useClipboard } from '@vueuse/core'
import { isString } from '@/utils/is'
@ -54,12 +52,7 @@ const openModel = (title: string) => {
dialogTitle.value = title
}
const setJson = () => {
openModel('导入JSON--未实现')
}
const setOption = () => {
openModel('导入Options--未实现')
}
/** 生成 JSON */
const showJson = () => {
openModel('生成 JSON')
formType.value = 0

View File

@ -1,67 +1,83 @@
<template>
<ContentWrap>
<ContentDetailWrap :title="title" @back="push('/infra/codegen')">
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basicInfo">
<BasicInfoForm ref="basicInfoRef" :basicInfo="tableCurrentRow" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="cloum">
<CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
</el-tab-pane>
</el-tabs>
<template #right>
<XButton
type="primary"
:title="t('action.save')"
:loading="loading"
@click="submitForm()"
/>
</template>
</ContentDetailWrap>
<ContentWrap v-loading="formLoading">
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basicInfo">
<basic-info-form ref="basicInfoRef" :table="formData.table" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="column">
<column-info-form ref="columnInfoRef" :columns="formData.columns" />
</el-tab-pane>
<el-tab-pane label="生成信息" name="generateInfo">
<generate-info-form ref="generateInfoRef" :table="formData.table" />
</el-tab-pane>
</el-tabs>
<el-form>
<el-form-item style="float: right">
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="close">返回</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script setup lang="ts">
import { BasicInfoForm, CloumInfoForm } from './components'
import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { BasicInfoForm, ColumnInfoForm, GenerateInfoForm } from './components'
import * as CodegenApi from '@/api/infra/codegen'
defineOptions({ name: 'InfraCodegenEditTable' })
const { t } = useI18n() //
const message = useMessage() //
const { push } = useRouter()
const { query } = useRoute()
const loading = ref(false)
const title = ref('代码生成')
const activeName = ref('basicInfo')
const cloumInfoRef = ref(null)
const tableCurrentRow = ref<CodegenTableVO>()
const cloumCurrentRow = ref<CodegenColumnVO[]>([])
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
const getList = async () => {
const formLoading = ref(false) // 12
const activeName = ref('column') // Tag
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const columnInfoRef = ref<ComponentRef<typeof ColumnInfoForm>>()
const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
const formData = ref<CodegenApi.CodegenUpdateReqVO>({
table: {},
columns: []
})
/** 获得详情 */
const getDetail = async () => {
const id = query.id as unknown as number
if (id) {
//
const res = await getCodegenTableApi(id)
title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
tableCurrentRow.value = res.table
cloumCurrentRow.value = res.columns
if (!id) {
return
}
formLoading.value = true
try {
formData.value = await CodegenApi.getCodegenTableApi(id)
} finally {
formLoading.value = false
}
}
/** 提交按钮 */
const submitForm = async () => {
const basicInfo = unref(basicInfoRef)
const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
if (basicForm) {
const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
const genTable: CodegenUpdateReqVO = {
table: basicInfoData,
columns: cloumCurrentRow.value
}
await updateCodegenTableApi(genTable)
//
if (!unref(formData)) return
await unref(basicInfoRef)?.validate()
await unref(generateInfoRef)?.validate()
try {
//
await CodegenApi.updateCodegenTableApi(formData.value)
message.success(t('common.updateSuccess'))
push('/infra/codegen')
}
close()
} catch {}
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
push('/infra/codegen')
}
/** 初始化 */
onMounted(() => {
getList()
getDetail()
})
</script>

View File

@ -1,183 +1,87 @@
<template>
<Form :rules="rules" @register="register" />
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="表名称" prop="tableName">
<el-input v-model="formData.tableName" placeholder="请输入仓库名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表描述" prop="tableComment">
<el-input v-model="formData.tableComment" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
实体类名称
<el-tooltip
content="默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。"
placement="top"
>
<Icon class="" icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.className" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<el-input v-model="formData.author" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" :rows="3" type="textarea" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { useForm } from '@/hooks/web/useForm'
import { FormSchema } from '@/types/form'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { getIntDictOptions } from '@/utils/dict'
import { listSimpleMenusApi } from '@/api/system/menu'
import { handleTree, defaultProps } from '@/utils/tree'
<script lang="ts" setup>
import * as CodegenApi from '@/api/infra/codegen'
import { PropType } from 'vue'
defineOptions({ name: 'InfraCodegenBasicInfoForm' })
const props = defineProps({
basicInfo: {
type: Object as PropType<Nullable<CodegenTableVO>>,
table: {
type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
default: () => null
}
})
const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
const menuOptions = ref<any>([]) //
const getTree = async () => {
const res = await listSimpleMenusApi()
menuOptions.value = handleTree(res)
}
const formRef = ref()
const formData = ref({
tableName: '',
tableComment: '',
className: '',
author: '',
remark: ''
})
const rules = reactive({
tableName: [required],
tableComment: [required],
className: [required],
author: [required],
templateType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
classComment: [required]
})
const schema = reactive<FormSchema[]>([
{
label: '上级菜单',
field: 'parentMenuId',
component: 'TreeSelect',
componentProps: {
data: menuOptions,
props: defaultProps,
checkStrictly: true,
nodeKey: 'id'
},
labelMessage: '分配到指定菜单下,例如 系统管理',
colProps: {
span: 24
}
},
{
label: '表名称',
field: 'tableName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '表描述',
field: 'tableComment',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '实体类名称',
field: 'className',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '类名称',
field: 'className',
component: 'Input',
labelMessage: '类名称首字母大写例如SysUser、SysMenu、SysDictData 等等',
colProps: {
span: 12
}
},
{
label: '生成模板',
field: 'templateType',
component: 'Select',
componentProps: {
options: templateTypeOptions
},
colProps: {
span: 12
}
},
{
label: '生成场景',
field: 'scene',
component: 'Select',
componentProps: {
options: sceneOptions
},
colProps: {
span: 12
}
},
{
label: '模块名',
field: 'moduleName',
component: 'Input',
labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
colProps: {
span: 12
}
},
{
label: '业务名',
field: 'businessName',
component: 'Input',
labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
colProps: {
span: 12
}
},
{
label: '类描述',
field: 'classComment',
component: 'Input',
labelMessage: '用作类描述,例如 用户',
colProps: {
span: 12
}
},
{
label: '作者',
field: 'author',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '备注',
field: 'remark',
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
])
const { register, methods, elFormRef } = useForm({
schema
author: [required]
})
/** 监听 table 属性,复制给 formData 属性 */
watch(
() => props.basicInfo,
(basicInfo) => {
if (!basicInfo) return
const { setValues } = methods
setValues(basicInfo)
() => props.table,
(table) => {
if (!table) return
formData.value = table
},
{
deep: true,
immediate: true
}
)
// ========== ==========
onMounted(async () => {
await getTree()
})
defineExpose({
elFormRef,
getFormData: methods.getFormData
validate: async () => unref(formRef)?.validate()
})
</script>

View File

@ -1,137 +0,0 @@
<template>
<vxe-table
ref="dragTable"
border
:data="info"
max-height="600"
stripe
class="xtable-scrollbar"
:column-config="{ resizable: true }"
>
<vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
<vxe-colgroup title="基础属性">
<vxe-column title="字段描述" field="columnComment" width="10%">
<template #default="{ row }">
<vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
</template>
</vxe-column>
<vxe-column title="物理类型" field="dataType" width="10%" />
<vxe-column title="Java类型" width="10%" field="javaType">
<template #default="{ row }">
<vxe-select v-model="row.javaType" placeholder="请选择Java类型">
<vxe-option label="Long" value="Long" />
<vxe-option label="String" value="String" />
<vxe-option label="Integer" value="Integer" />
<vxe-option label="Double" value="Double" />
<vxe-option label="BigDecimal" value="BigDecimal" />
<vxe-option label="LocalDateTime" value="LocalDateTime" />
<vxe-option label="Boolean" value="Boolean" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="java属性" width="8%" field="javaField">
<template #default="{ row }">
<vxe-input v-model="row.javaField" placeholder="请输入java属性" />
</template>
</vxe-column>
</vxe-colgroup>
<vxe-colgroup title="增删改查">
<vxe-column title="插入" width="40px" field="createOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
</template>
</vxe-column>
<vxe-column title="编辑" width="40px" field="updateOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
</template>
</vxe-column>
<vxe-column title="列表" width="40px" field="listOperationResult">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
</template>
</vxe-column>
<vxe-column title="查询" width="40px" field="listOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
</template>
</vxe-column>
<vxe-column title="允许空" width="40px" field="nullable">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
</template>
</vxe-column>
<vxe-column title="查询方式" width="60px" field="listOperationCondition">
<template #default="{ row }">
<vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
<vxe-option label="=" value="=" />
<vxe-option label="!=" value="!=" />
<vxe-option label=">" value=">" />
<vxe-option label=">=" value=">=" />
<vxe-option label="<" value="<>" />
<vxe-option label="<=" value="<=" />
<vxe-option label="LIKE" value="LIKE" />
<vxe-option label="BETWEEN" value="BETWEEN" />
</vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
<vxe-column title="显示类型" width="10%" field="htmlType">
<template #default="{ row }">
<vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
<vxe-option label="文本框" value="input" />
<vxe-option label="文本域" value="textarea" />
<vxe-option label="下拉框" value="select" />
<vxe-option label="单选框" value="radio" />
<vxe-option label="复选框" value="checkbox" />
<vxe-option label="日期控件" value="datetime" />
<vxe-option label="图片上传" value="imageUpload" />
<vxe-option label="文件上传" value="fileUpload" />
<vxe-option label="富文本控件" value="editor" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="字典类型" width="10%" field="dictType">
<template #default="{ row }">
<vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
<vxe-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</vxe-select>
</template>
</vxe-column>
<vxe-column title="示例" field="example">
<template #default="{ row }">
<vxe-input v-model="row.example" placeholder="请输入示例" />
</template>
</vxe-column>
</vxe-table>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { DictTypeVO } from '@/api/system/dict/types'
import { CodegenColumnVO } from '@/api/infra/codegen/types'
import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'
const props = defineProps({
info: {
type: Array as unknown as PropType<CodegenColumnVO[]>,
default: () => null
}
})
/** 查询字典下拉列表 */
const dictOptions = ref<DictTypeVO[]>()
const getDictOptions = async () => {
const res = await listSimpleDictTypeApi()
dictOptions.value = res
}
onMounted(async () => {
await getDictOptions()
})
defineExpose({
info: props.info
})
</script>

View File

@ -0,0 +1,153 @@
<template>
<el-table ref="dragTable" :data="formData" :max-height="tableHeight" row-key="columnId">
<el-table-column
:show-overflow-tooltip="true"
label="字段列名"
min-width="10%"
prop="columnName"
/>
<el-table-column label="字段描述" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.columnComment" />
</template>
</el-table-column>
<el-table-column
:show-overflow-tooltip="true"
label="物理类型"
min-width="10%"
prop="dataType"
/>
<el-table-column label="Java类型" min-width="11%">
<template #default="scope">
<el-select v-model="scope.row.javaType">
<el-option label="Long" value="Long" />
<el-option label="String" value="String" />
<el-option label="Integer" value="Integer" />
<el-option label="Double" value="Double" />
<el-option label="BigDecimal" value="BigDecimal" />
<el-option label="LocalDateTime" value="LocalDateTime" />
<el-option label="Boolean" value="Boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="java属性" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.javaField" />
</template>
</el-table-column>
<el-table-column label="插入" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.createOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="编辑" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.updateOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="列表" min-width="4%">
<template #default="scope">
<el-checkbox
v-model="scope.row.listOperationResult"
false-label="false"
true-label="true"
/>
</template>
</el-table-column>
<el-table-column label="查询" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.listOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="查询方式" min-width="10%">
<template #default="scope">
<el-select v-model="scope.row.listOperationCondition">
<el-option label="=" value="=" />
<el-option label="!=" value="!=" />
<el-option label=">" value=">" />
<el-option label=">=" value=">=" />
<el-option label="<" value="<>" />
<el-option label="<=" value="<=" />
<el-option label="LIKE" value="LIKE" />
<el-option label="BETWEEN" value="BETWEEN" />
</el-select>
</template>
</el-table-column>
<el-table-column label="允许空" min-width="5%">
<template #default="scope">
<el-checkbox v-model="scope.row.nullable" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.htmlType">
<el-option label="文本框" value="input" />
<el-option label="文本域" value="textarea" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />
<el-option label="图片上传" value="imageUpload" />
<el-option label="文件上传" value="fileUpload" />
<el-option label="富文本控件" value="editor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
<el-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="示例" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.example" />
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import * as CodegenApi from '@/api/infra/codegen'
import * as DictDataApi from '@/api/system/dict/dict.type'
defineOptions({ name: 'InfraCodegenColumInfoForm' })
const props = defineProps({
columns: {
type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
default: () => null
}
})
const formData = ref<CodegenApi.CodegenColumnVO[]>([])
const tableHeight = document.documentElement.scrollHeight - 350 + 'px'
/** 查询字典下拉列表 */
const dictOptions = ref<DictDataApi.DictTypeVO[]>()
const getDictOptions = async () => {
dictOptions.value = await DictDataApi.getSimpleDictTypeList()
}
watch(
() => props.columns,
(columns) => {
if (!columns) return
formData.value = columns
},
{
deep: true,
immediate: true
}
)
onMounted(async () => {
await getDictOptions()
})
</script>

View File

@ -0,0 +1,391 @@
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item label="生成模板" prop="templateType">
<el-select v-model="formData.templateType" @change="tplSelectChange">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="前端类型" prop="frontType">
<el-select v-model="formData.frontType">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生成场景" prop="scene">
<el-select v-model="formData.scene">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
上级菜单
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-tree-select
v-model="formData.parentMenuId"
:data="menus"
:props="menuTreeProps"
check-strictly
node-key="id"
placeholder="请选择系统菜单"
/>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="packageName">-->
<!-- <span slot="label">-->
<!-- 生成包路径-->
<!-- <el-tooltip content="生成在哪个java包下例如 com.ruoyi.system" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.packageName" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="moduleName">
<template #label>
<span>
模块名
<el-tooltip
content="模块名,即一级目录,例如 system、infra、tool 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.moduleName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="businessName">
<template #label>
<span>
业务名
<el-tooltip
content="业务名,即二级目录,例如 user、permission、dict 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.businessName" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="businessPackage">-->
<!-- <span slot="label">-->
<!-- 业务包-->
<!-- <el-tooltip content="业务包,自定义二级目录。例如说,我们希望将 dictType 和 dictData 归类成 dict 业务" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.businessPackage" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
类名称
<el-tooltip
content="类名称首字母大写例如SysUser、SysMenu、SysDictData 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.className" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="classComment">
<template #label>
<span>
类描述
<el-tooltip content="用作类描述,例如 用户" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.classComment" />
</el-form-item>
</el-col>
<el-col v-if="formData.genType === '1'" :span="24">
<el-form-item prop="genPath">
<template #label>
<span>
自定义路径
<el-tooltip
content="填写磁盘绝对路径若不填写则生成到当前Web项目下"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.genPath">
<template #append>
<el-dropdown>
<el-button type="primary">
最近路径快速选择
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="formData.genPath = '/'">
恢复默认的生成基础路径
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'tree'">
<h4 class="form-header">其他信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树编码字段
<el-tooltip content="树显示的编码字段名, 如dept_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树父编码字段
<el-tooltip content="树显示的父编码字段名, 如parent_Id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeParentCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树名称字段
<el-tooltip content="树节点的显示名称字段名, 如dept_name" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeName" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'sub'">
<h4 class="form-header">关联信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
关联子表的表名
<el-tooltip content="关联子表的表名, 如sys_user" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableName" placeholder="请选择" @change="subSelectChange">
<el-option
v-for="(table0, index) in tables"
:key="index"
:label="table0.tableName + '' + table0.tableComment"
:value="table0.tableName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
子表关联的外键名
<el-tooltip content="子表关联的外键名, 如user_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableFkName" placeholder="请选择">
<el-option
v-for="(column, index) in subColumns"
:key="index"
:label="column.columnName + '' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { handleTree } from '@/utils/tree'
import * as CodegenApi from '@/api/infra/codegen'
import * as MenuApi from '@/api/system/menu'
import { PropType } from 'vue'
defineOptions({ name: 'InfraCodegenGenerateInfoForm' })
const message = useMessage() //
const props = defineProps({
table: {
type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
default: () => null
}
})
const formRef = ref()
const formData = ref({
templateType: null,
frontType: null,
scene: null,
moduleName: '',
businessName: '',
className: '',
classComment: '',
parentMenuId: null,
genPath: '',
treeCode: '',
treeParentCode: '',
treeName: '',
tplCategory: '',
subTableName: '',
subTableFkName: '',
genType: ''
})
const rules = reactive({
templateType: [required],
frontType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
className: [required],
classComment: [required]
})
const tables = ref([])
const subColumns = ref([])
const menus = ref<any[]>([])
const menuTreeProps = {
label: 'name'
}
/** 选择子表名触发 */
const subSelectChange = () => {
formData.value.subTableFkName = ''
}
/** 选择生成模板触发 */
const tplSelectChange = (value) => {
if (value !== 1) {
// TODO
message.error(
'暂时不考虑支持【树形】和【主子表】的代码生成。原因是:导致 vm 模板过于复杂,不利于胖友二次开发'
)
return false
}
if (value !== 'sub') {
formData.value.subTableName = ''
formData.value.subTableFkName = ''
}
}
watch(
() => props.table,
(table) => {
if (!table) return
formData.value = table as any
},
{
deep: true,
immediate: true
}
)
onMounted(async () => {
try {
const resp = await MenuApi.getSimpleMenusList()
menus.value = handleTree(resp)
} catch {}
})
defineExpose({
validate: async () => unref(formRef)?.validate()
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<XModal title="预览" v-model="preview.open" height="99%">
<XModal title="预览" v-model="preview.open" width="80%" height="99%">
<div class="flex">
<el-card class="w-1/4" :gutter="12" shadow="hover">
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
@ -22,7 +22,7 @@
:key="item.filePath"
>
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(item.code)" />
<pre>{{ item.code }}</pre>
<pre><code v-dompurify-html="highlightedCode(item)" class="hljs"></code></pre>
</el-tab-pane>
</el-tabs>
</el-card>
@ -35,6 +35,14 @@ import { handleTree2 } from '@/utils/tree'
import { previewCodegenApi } from '@/api/infra/codegen'
import { CodegenTableVO, CodegenPreviewVO } from '@/api/infra/codegen/types'
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import java from 'highlight.js/lib/languages/java'
import xml from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import sql from 'highlight.js/lib/languages/sql'
import typescript from 'highlight.js/lib/languages/typescript'
const { t } = useI18n() //
const message = useMessage() //
// ======== ========
@ -148,6 +156,28 @@ const copy = async (text: string) => {
message.success(t('common.copySuccess'))
oInput.remove()
}
/**
* 代码高亮
*/
const highlightedCode = (item) => {
const language = item.filePath.substring(item.filePath.lastIndexOf('.') + 1)
const result = hljs.highlight(language, item.code || '', true)
return result.value || '&nbsp;'
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('java', java)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('vue', xml)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('typescript', typescript)
})
defineExpose({
show
})

View File

@ -1,5 +1,6 @@
import BasicInfoForm from './BasicInfoForm.vue'
import CloumInfoForm from './CloumInfoForm.vue'
import ColumnInfoForm from './ColumnInfoForm.vue'
import GenerateInfoForm from './GenerateInfoForm.vue'
import ImportTable from './ImportTable.vue'
import Preview from './Preview.vue'
export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }
export { BasicInfoForm, ColumnInfoForm, GenerateInfoForm, ImportTable, Preview }

View File

@ -5,6 +5,7 @@
<BasicInfoForm
ref="basicInfoRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
@ -12,6 +13,7 @@
<DescriptionForm
ref="descriptionRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
@ -19,19 +21,22 @@
<OtherSettingsForm
ref="otherSettingsRef"
v-model:activeName="activeName"
:is-detail="isDetail"
:propFormData="formData"
/>
</el-tab-pane>
</el-tabs>
<el-form>
<el-form-item style="float: right">
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
保存
</el-button>
<el-button @click="close">返回</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" name="ProductSpuForm" setup>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
@ -39,19 +44,22 @@ import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { convertToInteger, formatToFraction } from '@/utils'
defineOptions({ name: 'ProductSpuForm' })
const { t } = useI18n() //
const message = useMessage() //
const { push, currentRoute } = useRouter() //
const { params } = useRoute() //
const { params, name } = useRoute() //
const { delView } = useTagsViewStore() //
const formLoading = ref(false) // 12
const activeName = ref('basicInfo') // Tag
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
const isDetail = ref(false) //
const basicInfoRef = ref() // Ref
const descriptionRef = ref() // Ref
const otherSettingsRef = ref() // Ref
// spu
const formData = ref<ProductSpuApi.SpuType>({
const formData = ref<ProductSpuApi.Spu>({
name: '', //
categoryId: null, //
keyword: '', //
@ -59,7 +67,7 @@ const formData = ref<ProductSpuApi.SpuType>({
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
deliveryTemplateId: null, //
brandId: null, //
specType: false, //
subCommissionType: false, //
@ -90,12 +98,15 @@ const formData = ref<ProductSpuApi.SpuType>({
/** 获得详情 */
const getDetail = async () => {
if ('ProductSpuDetail' === name) {
isDetail.value = true
}
const id = params.spuId as number
if (id) {
formLoading.value = true
try {
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.SpuType
res.skus.forEach((item) => {
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
res.skus?.forEach((item) => {
//
item.price = formatToFraction(item.price)
item.marketPrice = formatToFraction(item.marketPrice)
@ -120,9 +131,10 @@ const submitForm = async () => {
await unref(basicInfoRef)?.validate()
await unref(descriptionRef)?.validate()
await unref(otherSettingsRef)?.validate()
const deepCopyFormData = cloneDeep(unref(formData.value)) // fix: server
// TODO sku
formData.value.skus.forEach((sku) => {
// , server
const deepCopyFormData = cloneDeep(unref(formData.value))
// sku
formData.value.skus!.forEach((sku) => {
//
if (sku.barCode === '') {
const index = deepCopyFormData.skus.findIndex(
@ -150,7 +162,7 @@ const submitForm = async () => {
})
deepCopyFormData.sliderPicUrls = newSliderPicUrls
//
const data = deepCopyFormData as ProductSpuApi.SpuType
const data = deepCopyFormData as ProductSpuApi.Spu
const id = params.spuId as number
if (!id) {
await ProductSpuApi.createSpu(data)
@ -170,7 +182,6 @@ const close = () => {
delView(unref(currentRoute))
push('/product/product-spu')
}
/** 初始化 */
onMounted(async () => {
await getDetail()

View File

@ -1,5 +1,12 @@
<template>
<el-form ref="productSpuBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
<!-- 情况一添加/修改 -->
<el-form
v-if="!isDetail"
ref="productSpuBasicInfoRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row>
<el-col :span="12">
<el-form-item label="商品名称" prop="name">
@ -7,7 +14,6 @@
</el-form-item>
</el-col>
<el-col :span="12">
<!-- TODO @puhui999只能选根节点 -->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="formData.categoryId"
@ -17,6 +23,7 @@
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
@change="categoryNodeClick"
/>
</el-form-item>
</el-col>
@ -60,9 +67,15 @@
<el-col :span="12">
<el-form-item label="运费模板" prop="deliveryTemplateId">
<el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
<el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" />
<el-option
v-for="item in deliveryTemplateList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-button class="ml-20px">运费模板</el-button>
<!-- TODO 可能情况善品录入后选择运费发现下拉选择中没有对应的模版 这里需不需要做添加运费模版后选择的功能 -->
<!-- <el-button class="ml-20px">运费模板</el-button>-->
</el-form-item>
</el-col>
<el-col :span="12">
@ -95,6 +108,9 @@
</el-col>
<!-- 多规格添加-->
<el-col :span="24">
<el-form-item v-if="!formData.specType">
<SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
<el-form-item v-if="formData.specType" label="商品属性">
<el-button class="mr-15px mb-10px" @click="attributesAddFormRef.open">添加规格</el-button>
<ProductAttributes :propertyList="propertyList" @success="generateSkus" />
@ -107,34 +123,94 @@
<SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
</template>
<el-form-item v-if="!formData.specType">
<SkuList :prop-form-data="formData" :propertyList="propertyList" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
<!-- 情况二详情 -->
<Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
<template #categoryId="{ row }"> {{ categoryString(row.categoryId) }}</template>
<template #brandId="{ row }">
{{ brandList.find((item) => item.id === row.brandId)?.name }}
</template>
<template #deliveryTemplateId="{ row }">
{{ deliveryTemplateList.find((item) => item.id === row.deliveryTemplateId)?.name }}
</template>
<template #specType="{ row }">
{{ row.specType ? '多规格' : '单规格' }}
</template>
<template #subCommissionType="{ row }">
{{ row.subCommissionType ? '自行设置' : '默认设置' }}
</template>
<template #picUrl="{ row }">
<el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
</template>
<template #sliderPicUrls="{ row }">
<el-image
v-for="(item, index) in row.sliderPicUrls"
:key="index"
:src="item.url"
class="w-60px h-60px mr-10px"
@click="imagePreview(row.sliderPicUrls)"
/>
</template>
<template #skus>
<SkuList
ref="skuDetailListRef"
:is-detail="isDetail"
:prop-form-data="formData"
:propertyList="propertyList"
/>
</template>
</Descriptions>
<!-- 商品属性添加 Form 表单 -->
<ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
</template>
<script lang="ts" name="ProductSpuBasicInfoForm" setup>
<script lang="ts" setup>
import { PropType } from 'vue'
import { isArray } from '@/utils/is'
import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { defaultProps, handleTree } from '@/utils/tree'
import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
import { createImageViewer } from '@/components/ImageViewer'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import type { SpuType } from '@/api/mall/product/spu'
import { UploadImg, UploadImgs } from '@/components/UploadFile'
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
import { getPropertyList, ProductAttributes, ProductPropertyAddForm, SkuList } from './index'
import { basicInfoSchema } from './spu.data'
import type { Spu } from '@/api/mall/product/spu'
import * as ProductCategoryApi from '@/api/mall/product/category'
import { getSimpleBrandList } from '@/api/mall/product/brand'
import { getSimpleTemplateList } from '@/api/mall/trade/delivery/expressTemplate/index'
// ====== ======
const { allSchemas } = useCrudSchemas(basicInfoSchema)
/** 商品图预览 */
const imagePreview = (args) => {
const urlList = []
if (isArray(args)) {
args.forEach((item) => {
urlList.push(item.url)
})
} else {
urlList.push(args)
}
createImageViewer({
urlList
})
}
// ====== end ======
defineOptions({ name: 'ProductSpuBasicInfoForm' })
const message = useMessage() //
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const attributesAddFormRef = ref() //
const productSpuBasicInfoRef = ref() // Ref
@ -144,15 +220,15 @@ const skuListRef = ref() // 商品属性列表Ref
const generateSkus = (propertyList) => {
skuListRef.value.generateTableData(propertyList)
}
const formData = reactive<SpuType>({
const formData = reactive<Spu>({
name: '', //
categoryId: null, //
keyword: '', //
unit: '', //
unit: null, //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
deliveryTemplateId: null, //
brandId: null, //
specType: false, //
subCommissionType: false, //
@ -166,7 +242,7 @@ const rules = reactive({
introduction: [required],
picUrl: [required],
sliderPicUrls: [required],
// deliveryTemplateId: [required],
deliveryTemplateId: [required],
brandId: [required],
specType: [required],
subCommissionType: [required]
@ -182,29 +258,10 @@ watch(
return
}
copyValueToTarget(formData, data)
formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
url: item
}))
// TODO @puhui999if return
//
if (formData.specType) {
// skus propertyList
const properties = []
formData.skus.forEach((sku) => {
sku.properties.forEach(({ propertyId, propertyName, valueId, valueName }) => {
//
if (!properties.some((item) => item.id === propertyId)) {
properties.push({ id: propertyId, name: propertyName, values: [] })
}
//
const index = properties.findIndex((item) => item.id === propertyId)
if (!properties[index].values.some((value) => value.id === valueId)) {
properties[index].values.push({ id: valueId, name: valueName })
}
})
})
propertyList.value = properties
}
propertyList.value = getPropertyList(data)
},
{
immediate: true
@ -216,6 +273,8 @@ watch(
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
// sku
skuListRef.value.validateSku()
//
if (!productSpuBasicInfoRef) return
return await unref(productSpuBasicInfoRef).validate((valid) => {
@ -263,12 +322,32 @@ const onChangeSpec = () => {
}
const categoryList = ref([]) //
/**
* 选择分类时触发校验
*/
const categoryNodeClick = () => {
if (!checkSelectedNode(categoryList.value, formData.categoryId)) {
formData.categoryId = null
message.warning('必须选择二级及以下节点!!')
}
}
/**
* 获取分类的节点的完整结构
*
* @param categoryId 分类id
*/
const categoryString = (categoryId) => {
return treeToString(categoryList.value, categoryId)
}
const brandList = ref([]) //
const deliveryTemplateList = ref([]) //
onMounted(async () => {
//
const data = await ProductCategoryApi.getCategoryList({})
categoryList.value = handleTree(data, 'id', 'parentId')
//
brandList.value = await getSimpleBrandList()
//
deliveryTemplateList.value = await getSimpleTemplateList()
})
</script>

View File

@ -1,28 +1,54 @@
<template>
<el-form ref="descriptionFormRef" :model="formData" :rules="rules" label-width="120px">
<!-- 情况一添加/修改 -->
<el-form
v-if="!isDetail"
ref="descriptionFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<!--富文本编辑器组件-->
<el-form-item label="商品详情" prop="description">
<Editor v-model:modelValue="formData.description" />
</el-form-item>
</el-form>
<!-- 情况二详情 -->
<Descriptions
v-if="isDetail"
:data="formData"
:schema="allSchemas.detailSchema"
class="descriptionFormDescriptions"
>
<!-- 展示 HTML 内容 -->
<template #description="{ row }">
<div v-dompurify-html="row.description" style="width: 600px"></div>
</template>
</Descriptions>
</template>
<script lang="ts" name="DescriptionForm" setup>
import type { SpuType } from '@/api/mall/product/spu'
<script lang="ts" setup>
import type { Spu } from '@/api/mall/product/spu'
import { Editor } from '@/components/Editor'
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
import { descriptionSchema } from './spu.data'
defineOptions({ name: 'DescriptionForm' })
const message = useMessage() //
const { allSchemas } = useCrudSchemas(descriptionSchema)
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const descriptionFormRef = ref() // Ref
const formData = ref<SpuType>({
const formData = ref<Spu>({
description: '' //
})
//

View File

@ -1,5 +1,12 @@
<template>
<el-form ref="otherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
<!-- 情况一添加/修改 -->
<el-form
v-if="!isDetail"
ref="otherSettingsFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row>
<el-col :span="24">
<el-row :gutter="20">
@ -50,26 +57,57 @@
</el-col>
</el-row>
</el-form>
<!-- 情况二详情 -->
<Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
<template #recommendHot="{ row }">
{{ row.recommendHot ? '是' : '否' }}
</template>
<template #recommendBenefit="{ row }">
{{ row.recommendBenefit ? '是' : '否' }}
</template>
<template #recommendBest="{ row }">
{{ row.recommendBest ? '是' : '否' }}
</template>
<template #recommendNew="{ row }">
{{ row.recommendNew ? '是' : '否' }}
</template>
<template #recommendGood="{ row }">
{{ row.recommendGood ? '是' : '否' }}
</template>
<template #activityOrders>
<el-tag>默认</el-tag>
<el-tag class="ml-2" type="success">秒杀</el-tag>
<el-tag class="ml-2" type="info">砍价</el-tag>
<el-tag class="ml-2" type="warning">拼团</el-tag>
</template>
</Descriptions>
</template>
<script lang="ts" name="OtherSettingsForm" setup>
import type { SpuType } from '@/api/mall/product/spu'
<script lang="ts" setup>
import type { Spu } from '@/api/mall/product/spu'
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { copyValueToTarget } from '@/utils'
import { otherSettingsSchema } from './spu.data'
defineOptions({ name: 'OtherSettingsForm' })
const message = useMessage() //
const { allSchemas } = useCrudSchemas(otherSettingsSchema)
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
type: Object as PropType<Spu>,
default: () => {}
},
activeName: propTypes.string.def('')
activeName: propTypes.string.def(''),
isDetail: propTypes.bool.def(false) //
})
const otherSettingsFormRef = ref() // Ref
//
const formData = ref<SpuType>({
const formData = ref<Spu>({
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //

View File

@ -17,9 +17,11 @@
</template>
</Dialog>
</template>
<script lang="ts" name="ProductPropertyForm" setup>
<script lang="ts" setup>
import * as PropertyApi from '@/api/mall/product/property'
defineOptions({ name: 'ProductPropertyForm' })
const { t } = useI18n() //
const message = useMessage() //
@ -90,8 +92,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
name: '',
remark: ''
name: ''
}
formRef.value?.resetFields()
}

View File

@ -1,6 +1,8 @@
<template>
<!-- 情况一添加/修改 -->
<el-table
:data="isBatch ? skuList : formData.skus"
v-if="!isDetail && !isActivityComponent"
:data="isBatch ? skuList : formData!.skus!"
border
class="tabNumWidth"
max-height="500"
@ -11,7 +13,7 @@
<UploadImg v-model="row.picUrl" height="80px" width="100%" />
</template>
</el-table-column>
<template v-if="formData.specType && !isBatch">
<template v-if="formData!.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaders"
@ -21,8 +23,9 @@
min-width="120"
>
<template #default="{ row }">
<!-- TODO puhui999展示成蓝色有点区分度哈 -->
{{ row.properties[index]?.valueName }}
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
</el-table-column>
</template>
@ -73,7 +76,7 @@
<el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
</template>
</el-table-column>
<template v-if="formData.subCommissionType">
<template v-if="formData!.subCommissionType">
<el-table-column align="center" label="一级返佣(元)" min-width="168">
<template #default="{ row }">
<el-input-number
@ -97,7 +100,7 @@
</template>
</el-table-column>
</template>
<el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
<el-table-column v-if="formData?.specType" align="center" fixed="right" label="操作" width="80">
<template #default="{ row }">
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
批量添加
@ -106,27 +109,183 @@
</template>
</el-table-column>
</el-table>
<!-- 情况二详情 -->
<el-table
v-if="isDetail"
ref="activitySkuListRef"
:data="formData!.skus!"
border
max-height="500"
size="small"
style="width: 99%"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="isComponent" type="selection" width="45" />
<el-table-column align="center" label="图片" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<template v-if="formData!.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaders"
:key="index"
:label="item.label"
align="center"
min-width="80"
>
<template #default="{ row }">
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
</el-table-column>
</template>
<el-table-column align="center" label="商品条码" min-width="100">
<template #default="{ row }">
{{ row.barCode }}
</template>
</el-table-column>
<el-table-column align="center" label="销售价(元)" min-width="80">
<template #default="{ row }">
{{ row.price }}
</template>
</el-table-column>
<el-table-column align="center" label="市场价(元)" min-width="80">
<template #default="{ row }">
{{ row.marketPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="成本价(元)" min-width="80">
<template #default="{ row }">
{{ row.costPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="库存" min-width="80">
<template #default="{ row }">
{{ row.stock }}
</template>
</el-table-column>
<el-table-column align="center" label="重量(kg)" min-width="80">
<template #default="{ row }">
{{ row.weight }}
</template>
</el-table-column>
<el-table-column align="center" label="体积(m^3)" min-width="80">
<template #default="{ row }">
{{ row.volume }}
</template>
</el-table-column>
<template v-if="formData!.subCommissionType">
<el-table-column align="center" label="一级返佣(元)" min-width="80">
<template #default="{ row }">
{{ row.subCommissionFirstPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="二级返佣(元)" min-width="80">
<template #default="{ row }">
{{ row.subCommissionSecondPrice }}
</template>
</el-table-column>
</template>
</el-table>
<!-- 情况三作为活动组件 -->
<el-table
v-if="isActivityComponent"
:data="formData!.skus!"
border
max-height="500"
size="small"
style="width: 99%"
>
<el-table-column v-if="isComponent" type="selection" width="45" />
<el-table-column align="center" label="图片" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<template v-if="formData!.specType">
<!-- 根据商品属性动态添加 -->
<el-table-column
v-for="(item, index) in tableHeaders"
:key="index"
:label="item.label"
align="center"
min-width="80"
>
<template #default="{ row }">
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
</el-table-column>
</template>
<el-table-column align="center" label="商品条码" min-width="100">
<template #default="{ row }">
{{ row.barCode }}
</template>
</el-table-column>
<el-table-column align="center" label="销售价(元)" min-width="80">
<template #default="{ row }">
{{ row.price }}
</template>
</el-table-column>
<el-table-column align="center" label="市场价(元)" min-width="80">
<template #default="{ row }">
{{ row.marketPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="成本价(元)" min-width="80">
<template #default="{ row }">
{{ row.costPrice }}
</template>
</el-table-column>
<el-table-column align="center" label="库存" min-width="80">
<template #default="{ row }">
{{ row.stock }}
</template>
</el-table-column>
<!-- 方便扩展每个活动配置的属性不一样 -->
<slot name="extension"></slot>
</el-table>
</template>
<script lang="ts" name="SkuList" setup>
import { PropType } from 'vue'
<script lang="ts" setup>
import { PropType, Ref } from 'vue'
import { copyValueToTarget } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { UploadImg } from '@/components/UploadFile'
import type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
import type { Property, Sku, Spu } from '@/api/mall/product/spu'
import { createImageViewer } from '@/components/ImageViewer'
import { RuleConfig } from '@/views/mall/product/spu/components/index'
import { PropertyAndValues } from './index'
import { ElTable } from 'element-plus'
defineOptions({ name: 'SkuList' })
const message = useMessage() //
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
type: Object as PropType<Spu>,
default: () => {}
},
propertyList: {
type: Array,
type: Array as PropType<PropertyAndValues[]>,
default: () => []
},
isBatch: propTypes.bool.def(false) //
ruleConfig: {
type: Array as PropType<RuleConfig[]>,
default: () => []
},
isBatch: propTypes.bool.def(false), //
isDetail: propTypes.bool.def(false), // sku
isComponent: propTypes.bool.def(false), // sku
isActivityComponent: propTypes.bool.def(false) // sku
})
const formData = ref<SpuType>() //
const skuList = ref<SkuType[]>([
const formData: Ref<Spu | undefined> = ref<Spu>() //
const skuList = ref<Sku[]>([
{
price: 0, //
marketPrice: 0, //
@ -140,24 +299,87 @@ const skuList = ref<SkuType[]>([
subCommissionSecondPrice: 0 //
}
]) //
// TODO @puhui999 0.01
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
zIndex: 9999999,
urlList: [imgUrl]
})
}
/** 批量添加 */
const batchAdd = () => {
formData.value.skus.forEach((item) => {
formData.value!.skus!.forEach((item) => {
copyValueToTarget(item, skuList.value[0])
})
}
/** 删除 sku */
const deleteSku = (row) => {
const index = formData.value.skus.findIndex(
const index = formData.value!.skus!.findIndex(
//
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
formData.value.skus.splice(index, 1)
formData.value!.skus!.splice(index, 1)
}
const tableHeaders = ref<{ prop: string; label: string }[]>([]) //
/**
* 保存时每个商品规格的表单要校验下例如说销售金额最低是 0.01 这种
*/
const validateSku = () => {
const checks = ['price', 'marketPrice', 'costPrice']
let warningInfo = '请检查商品各行相关属性配置,'
let validate = true //
for (const sku of formData.value!.skus!) {
//
if (props.isActivityComponent) {
for (const rule of props.ruleConfig) {
const arg = getValue(sku, rule.name)
if (!rule.rule(arg)) {
validate = false //
warningInfo += rule.message
break
}
}
} else {
if (checks.some((check) => sku[check] < 0.01)) {
validate = false //
warningInfo = '商品相关价格不能低于 0.01 元!!'
break
}
}
//
if (!validate) {
message.warning(warningInfo)
throw new Error(warningInfo)
}
}
}
const getValue = (obj, arg) => {
const keys = arg.split('.')
let value = obj
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key]
} else {
value = undefined
break
}
}
return value
}
const emit = defineEmits<{
(e: 'selectionChange', value: Sku[]): void
}>()
/**
* 选择时触发
* @param Sku 传递过来的选中的 sku 是一个数组
*/
const handleSelectionChange = (val: Sku[]) => {
emit('selectionChange', val)
}
/**
* 将传进来的值赋值给 skuList
@ -185,14 +407,13 @@ const generateTableData = (propertyList: any[]) => {
valueName: v.name
}))
)
// TODO @puhui buildSkuListitem sku
const buildList = build(propertyValues)
const buildSkuList = build(propertyValues)
// sku skus
if (!validateData(propertyList)) {
// sku
formData.value!.skus = []
}
for (const item of buildList) {
for (const item of buildSkuList) {
const row = {
properties: Array.isArray(item) ? item : [item], // property
price: 0,
@ -207,13 +428,13 @@ const generateTableData = (propertyList: any[]) => {
subCommissionSecondPrice: 0
}
// sku
const index = formData.value!.skus.findIndex(
const index = formData.value!.skus!.findIndex(
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
)
if (index !== -1) {
continue
}
formData.value.skus.push(row)
formData.value!.skus!.push(row)
}
}
@ -221,13 +442,13 @@ const generateTableData = (propertyList: any[]) => {
* 生成 skus 前置校验
*/
const validateData = (propertyList: any[]) => {
const skuPropertyIds = []
formData.value.skus.forEach((sku) =>
const skuPropertyIds: number[] = []
formData.value!.skus!.forEach((sku) =>
sku.properties
?.map((property) => property.propertyId)
.forEach((propertyId) => {
if (skuPropertyIds.indexOf(propertyId) === -1) {
skuPropertyIds.push(propertyId)
?.forEach((propertyId) => {
if (skuPropertyIds.indexOf(propertyId!) === -1) {
skuPropertyIds.push(propertyId!)
}
})
)
@ -261,9 +482,9 @@ const build = (propertyValuesList: Property[][]) => {
/** 监听属性列表,生成相关参数和表头 */
watch(
() => props.propertyList,
(propertyList) => {
(propertyList: PropertyAndValues[]) => {
//
if (!formData.value.specType) {
if (!formData.value!.specType) {
return
}
// 使
@ -295,13 +516,12 @@ watch(
// nameindex
tableHeaders.value.push({ prop: `name${index}`, label: item.name })
})
// sku
if (validateData(propertyList)) {
return
}
//
if (propertyList.some((item) => item.values.length === 0)) {
if (propertyList.some((item) => item.values!.length === 0)) {
return
}
// table sku
@ -312,6 +532,10 @@ watch(
immediate: true
}
)
const activitySkuListRef = ref<InstanceType<typeof ElTable>>()
const clearSelection = () => {
activitySkuListRef.value.clearSelection()
}
// sku
defineExpose({ generateTableData })
defineExpose({ generateTableData, validateSku, clearSelection })
</script>

View File

@ -2,14 +2,70 @@ import BasicInfoForm from './BasicInfoForm.vue'
import DescriptionForm from './DescriptionForm.vue'
import OtherSettingsForm from './OtherSettingsForm.vue'
import ProductAttributes from './ProductAttributes.vue'
import ProductAttributesAddForm from './ProductAttributesAddForm.vue'
import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
import SkuList from './SkuList.vue'
import { Spu } from '@/api/mall/product/spu'
// TODO @puhui999Properties 改成 Property 更合适Property 在 Spu 中已存在避免冲突 PropertyAndValues
interface PropertyAndValues {
id: number
name: string
values?: PropertyAndValues[]
}
interface RuleConfig {
// 需要校验的字段
// 例name: 'name' 则表示校验 sku.name 的值
// 例name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
name: string
// 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
// 例需要校验价格必须大于0.01
// {
// name:'price',
// rule:(arg: number) => arg > 0.01
// }
rule: (arg: any) => boolean
// 校验不通过时的消息提示
message: string
}
/**
*
*
* @param spu
* @return PropertyAndValues
*/
const getPropertyList = (spu: Spu): PropertyAndValues[] => {
// 直接拿返回的 skus 属性逆向生成出 propertyList
const properties: PropertyAndValues[] = []
// 只有是多规格才处理
if (spu.specType) {
spu.skus?.forEach((sku) => {
sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => {
// 添加属性
if (!properties?.some((item) => item.id === propertyId)) {
properties.push({ id: propertyId!, name: propertyName!, values: [] })
}
// 添加属性值
const index = properties?.findIndex((item) => item.id === propertyId)
if (!properties[index].values?.some((value) => value.id === valueId)) {
properties[index].values?.push({ id: valueId!, name: valueName! })
}
})
})
}
return properties
}
export {
BasicInfoForm,
DescriptionForm,
OtherSettingsForm,
ProductAttributes,
ProductAttributesAddForm,
SkuList
ProductPropertyAddForm,
SkuList,
getPropertyList,
PropertyAndValues,
RuleConfig
}

View File

@ -0,0 +1,105 @@
import { CrudSchema } from '@/hooks/web/useCrudSchemas'
export const basicInfoSchema = reactive<CrudSchema[]>([
{
label: '商品名称',
field: 'name'
},
{
label: '关键字',
field: 'keyword'
},
{
label: '商品简介',
field: 'introduction'
},
{
label: '商品分类',
field: 'categoryId'
},
{
label: '商品品牌',
field: 'brandId'
},
{
label: '商品封面图',
field: 'picUrl'
},
{
label: '商品轮播图',
field: 'sliderPicUrls'
},
{
label: '商品视频',
field: 'videoUrl'
},
{
label: '单位',
field: 'unit',
dictType: DICT_TYPE.PRODUCT_UNIT
},
{
label: '规格类型',
field: 'specType'
},
{
label: '分销类型',
field: 'subCommissionType'
},
{
label: '物流模版',
field: 'deliveryTemplateId'
},
{
label: '商品属性列表',
field: 'skus'
}
])
export const descriptionSchema = reactive<CrudSchema[]>([
{
label: '商品详情',
field: 'description'
}
])
export const otherSettingsSchema = reactive<CrudSchema[]>([
{
label: '商品排序',
field: 'sort'
},
{
label: '赠送积分',
field: 'giveIntegral'
},
{
label: '虚拟销量',
field: 'virtualSalesCount'
},
{
label: '是否热卖推荐',
field: 'recommendHot'
},
{
label: '是否优惠推荐',
field: 'recommendBenefit'
},
{
label: '是否精品推荐',
field: 'recommendBest'
},
{
label: '是否新品推荐',
field: 'recommendNew'
},
{
label: '是否优品推荐',
field: 'recommendGood'
},
{
label: '赠送的优惠劵',
field: 'giveCouponTemplateIds'
},
{
label: '活动显示排序',
field: 'activityOrders'
}
])

View File

@ -8,18 +8,15 @@
class="-mb-15px"
label-width="68px"
>
<!-- TODO @puhui999品牌应该是数据下拉哈 -->
<el-form-item label="品牌名称" prop="name">
<el-form-item label="商品名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入名称"
placeholder="请输入品名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- TODO 分类只能选择二级分类目前还没做还是先以联调通顺为主 -->
<!-- TODO puhui999我们要不改成支持选择一级如果选择一级后端要递归查询下子分类然后去 in -->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="queryParams.categoryId"
@ -29,6 +26,7 @@
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
@change="nodeClick"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
@ -80,31 +78,54 @@
/>
</el-tabs>
<el-table v-loading="loading" :data="list">
<!-- TODO puhui这几个属性哈一行三个
商品分类服装鞋包/箱包
商品市场价格100.00
成本价0.00
收藏5
虚拟销量999 -->
<el-table-column type="expand" width="30">
<template #default="{ row }">
<el-form class="demo-table-expand" inline label-position="left">
<el-form-item label="市场价:">
<span>{{ formatToFraction(row.marketPrice) }}</span>
</el-form-item>
<el-form-item label="成本价:">
<span>{{ formatToFraction(row.costPrice) }}</span>
</el-form-item>
<el-form-item label="虚拟销量:">
<span>{{ row.virtualSalesCount }}</span>
</el-form-item>
<el-form class="demo-table-expand" label-position="left">
<el-row>
<el-col :span="24">
<el-row>
<el-col :span="8">
<el-form-item label="商品分类:">
<span>{{ categoryString(row.categoryId) }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="市场价:">
<span>{{ formatToFraction(row.marketPrice) }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="成本价:">
<span>{{ formatToFraction(row.costPrice) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-row>
<el-col :span="8">
<el-form-item label="收藏:">
<!-- TODO 没有这个属性暂时写死 5 -->
<span>5</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="虚拟销量:">
<span>{{ row.virtualSalesCount }}</span>
</el-form-item>
</el-col>
</el-row>
</el-col>
</el-row>
</el-form>
</template>
</el-table-column>
<el-table-column key="id" align="center" label="商品编号" prop="id" />
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" @click="imagePreview(row.picUrl)" class="w-30px h-30px" />
<el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
@ -143,8 +164,12 @@
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" min-width="200">
<template #default="{ row }">
<!-- TODO @puhui999详情可以后面点做哈 -->
<el-button v-hasPermi="['product:spu:update']" link type="primary" @click="openDetail">
<el-button
v-hasPermi="['product:spu:update']"
link
type="primary"
@click="openDetail(row.id)"
>
详情
</el-button>
<template v-if="queryParams.tabType === 4">
@ -197,18 +222,20 @@
/>
</ContentWrap>
</template>
<script lang="ts" name="ProductSpu" setup>
<script lang="ts" setup>
import { TabsPaneContext } from 'element-plus'
import { cloneDeep } from 'lodash-es'
import { createImageViewer } from '@/components/ImageViewer'
import { dateFormatter } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
import { ProductSpuStatusEnum } from '@/utils/constants'
import { formatToFraction } from '@/utils'
import download from '@/utils/download'
import * as ProductSpuApi from '@/api/mall/product/spu'
import * as ProductCategoryApi from '@/api/mall/product/category'
defineOptions({ name: 'ProductSpu' })
const message = useMessage() //
const { t } = useI18n() //
const { currentRoute, push } = useRouter() //
@ -256,12 +283,15 @@ const getTabsCount = async () => {
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0
tabType: 0,
name: '',
categoryId: null,
createTime: []
}) //
const queryFormRef = ref() // Ref
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName
queryParams.value.tabType = tab.paneName as number
getList()
}
@ -362,18 +392,18 @@ const resetQuery = () => {
const openForm = (id?: number) => {
//
if (typeof id === 'number') {
push('/product/productSpuEdit/' + id)
push('/product/spu/edit/' + id)
return
}
//
push('/product/productSpuAdd')
push({ name: 'ProductSpuAdd' })
}
/**
* 查看商品详情
*/
const openDetail = () => {
message.alert('查看详情未完善!!!')
const openDetail = (id?: number) => {
push('/product/spu/detail/' + id)
}
/** 导出按钮操作 */
@ -391,7 +421,7 @@ const handleExport = async () => {
}
}
// TODO @puhui999fix:
//
watch(
() => currentRoute.value,
() => {
@ -400,6 +430,24 @@ watch(
)
const categoryList = ref() //
/**
* 获取分类的节点的完整结构
* @param categoryId 分类id
*/
const categoryString = (categoryId) => {
return treeToString(categoryList.value, categoryId)
}
/**
* 校验所选是否为二级及以下节点
*/
const nodeClick = () => {
if (!checkSelectedNode(categoryList.value, queryParams.value.categoryId)) {
queryParams.value.categoryId = null
message.warning('必须选择二级及以下节点!!')
}
}
/** 初始化 **/
onMounted(async () => {
await getTabsCount()

View File

@ -0,0 +1,189 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
<Form
ref="formRef"
v-loading="formLoading"
:is-col="true"
:rules="rules"
:schema="allSchemas.formSchema"
>
<template #spuId>
<el-button @click="spuSelectRef.open()">选择商品</el-button>
<SpuAndSkuList
ref="spuAndSkuListRef"
:rule-config="ruleConfig"
:spu-list="spuList"
:spu-property-list-p="spuPropertyList"
>
<el-table-column align="center" label="拼团价格(元)" min-width="168">
<template #default="{ row: sku }">
<el-input-number
v-model="sku.productConfig.activePrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
/>
</template>
</el-table-column>
</SpuAndSkuList>
</template>
</Form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
</template>
<script lang="ts" setup>
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationactivity'
import { CombinationProductVO } from '@/api/mall/promotion/combination/combinationactivity'
import { allSchemas, rules } from './combinationActivity.data'
import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/components'
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { convertToInteger, formatToFraction } from '@/utils'
defineOptions({ name: 'PromotionCombinationActivityForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formRef = ref() // Ref
// ================= =================
const spuSelectRef = ref() // Ref
const spuAndSkuListRef = ref() // sku Ref
const spuList = ref<CombinationActivityApi.SpuExtension[]>([]) // spu
const spuPropertyList = ref<SpuProperty<CombinationActivityApi.SpuExtension>[]>([])
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.activePrice',
rule: (arg) => arg > 0.01,
message: '商品拼团价格不能小于0.01 '
}
]
const selectSpu = (spuId: number, skuIds: number[]) => {
formRef.value.setValues({ spuId })
getSpuDetails(spuId, skuIds)
}
/**
* 获取 SPU 详情
*/
const getSpuDetails = async (
spuId: number,
skuIds: number[] | undefined,
products?: CombinationProductVO[]
) => {
const spuProperties: SpuProperty<CombinationActivityApi.SpuExtension>[] = []
const res = (await ProductSpuApi.getSpuDetailList([
spuId
])) as CombinationActivityApi.SpuExtension[]
if (res.length == 0) {
return
}
spuList.value = []
//
const spu = res[0]
const selectSkus =
typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
selectSkus?.forEach((sku) => {
let config: CombinationProductVO = {
spuId: spu.id!,
skuId: sku.id!,
activePrice: 0
}
if (typeof products !== 'undefined') {
const product = products.find((item) => item.skuId === sku.id)
if (product) {
//
product.activePrice = formatToFraction(product.activePrice)
}
config = product || config
}
sku.productConfig = config
})
spu.skus = selectSkus as CombinationActivityApi.SkuExtension[]
spuProperties.push({
spuId: spu.id!,
spuDetail: spu,
propertyList: getPropertyList(spu)
})
spuList.value.push(spu)
spuPropertyList.value = spuProperties
}
// ================= end =================
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
await resetForm()
//
if (id) {
formLoading.value = true
try {
const data = (await CombinationActivityApi.getCombinationActivity(
id
)) as CombinationActivityApi.CombinationActivityVO
await getSpuDetails(
data.spuId!,
data.products?.map((sku) => sku.skuId),
data.products
)
formRef.value.setValues(data)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 重置表单 */
const resetForm = async () => {
spuList.value = []
spuPropertyList.value = []
await nextTick()
formRef.value.getElFormRef().resetFields()
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formRef.value.formModel as CombinationActivityApi.CombinationActivityVO
const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
products.forEach((item: CombinationProductVO) => {
//
item.activePrice = convertToInteger(item.activePrice)
})
data.products = products
if (formType.value === 'create') {
await CombinationActivityApi.createCombinationActivity(data)
message.success(t('common.createSuccess'))
} else {
await CombinationActivityApi.updateCombinationActivity(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
</script>

View File

@ -0,0 +1,151 @@
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter, getNowDateTime } from '@/utils/formatTime'
// 表单校验
export const rules = reactive({
name: [required],
totalLimitCount: [required],
singleLimitCount: [required],
startTime: [required],
endTime: [required],
userSize: [required],
totalNum: [required],
successNum: [required],
orderUserCount: [required],
virtualGroup: [required],
status: [required],
limitDuration: [required]
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
{
label: '拼团名称',
field: 'name',
isSearch: true,
isTable: false,
form: {
colProps: {
span: 24
}
}
},
{
label: '活动时间',
field: 'activityTime',
formatter: dateFormatter,
search: {
show: true,
component: 'DatePicker',
componentProps: {
valueFormat: 'x',
type: 'datetimerange',
rangeSeparator: '至'
}
},
form: {
component: 'DatePicker',
componentProps: {
valueFormat: 'x',
type: 'datetimerange',
rangeSeparator: '至'
},
value: [getNowDateTime().valueOf(), getNowDateTime().valueOf()],
colProps: {
span: 24
}
}
},
{
label: '参与人数',
field: 'orderUserCount',
isSearch: false,
form: {
component: 'InputNumber',
labelMessage: '参与人数不能少于两人',
value: 2
}
},
{
label: '限制时长',
field: 'limitDuration',
isSearch: false,
isTable: false,
form: {
component: 'InputNumber',
labelMessage: '限制时长(小时)',
componentProps: {
placeholder: '请输入限制时长(小时)'
}
}
},
{
label: '总限购数量',
field: 'totalLimitCount',
isSearch: false,
isTable: false,
form: {
component: 'InputNumber',
value: 0
}
},
{
label: '单次限购数量',
field: 'singleLimitCount',
isSearch: false,
isTable: false,
form: {
component: 'InputNumber',
value: 0
}
},
{
label: '购买人数',
field: 'userSize',
isSearch: false,
isForm: false
},
{
label: '开团组数',
field: 'totalNum',
isSearch: false,
isForm: false
},
{
label: '成团组数',
field: 'successNum',
isSearch: false,
isForm: false
},
{
label: '虚拟成团',
field: 'virtualGroup',
isSearch: false,
isTable: false,
isForm: false
},
{
label: '活动状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
isForm: false
},
{
label: '拼团商品',
field: 'spuId',
isSearch: false,
form: {
colProps: {
span: 24
}
}
},
{
label: '操作',
field: 'action',
isForm: false
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@ -0,0 +1,117 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
v-hasPermi="['promotion:combination-activity:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
</template>
</Search>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
v-model:currentPage="tableObject.currentPage"
v-model:pageSize="tableObject.pageSize"
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
>
<template #spuId="{ row }">
<el-image
:src="row.picUrl"
class="w-30px h-30px align-middle mr-5px"
@click="imagePreview(row.picUrl)"
/>
<span class="align-middle">{{ row.spuName }}</span>
</template>
<template #action="{ row }">
<el-button
v-hasPermi="['promotion:combination-activity:update']"
link
type="primary"
@click="openForm('update', row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['promotion:combination-activity:delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<CombinationActivityForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { allSchemas } from './combinationActivity.data'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationactivity'
import CombinationActivityForm from './CombinationActivityForm.vue'
import { cloneDeep } from 'lodash-es'
import { createImageViewer } from '@/components/ImageViewer'
defineOptions({ name: 'PromotionCombinationActivity' })
// tableObject
// tableMethods
// https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: CombinationActivityApi.getCombinationActivityPage, //
delListApi: CombinationActivityApi.deleteCombinationActivity //
})
//
const { getList, setSearchParams } = tableMethods
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
}
// TODO @puhui999使 element plus crud schema
/** 初始化 **/
onMounted(() => {
/**
TODO
后面准备封装成一个函数来操作 tableColumns 重新排列比如说需求是表单上商品选择是在后面的而列表展示的时候需要调到位置
封装效果支持批量操作给出 field 和需要插入的位置[{field:'spuId',index: 1}] 效果为把 field spuId column 移动到第一个位置
*/
//
const index = allSchemas.tableColumns.findIndex((item) => item.field === 'spuId')
const column = cloneDeep(allSchemas.tableColumns[index])
allSchemas.tableColumns.splice(index, 1)
//
allSchemas.tableColumns.unshift(column)
getList()
})
</script>

View File

@ -0,0 +1,112 @@
<template>
<el-table :data="spuData" :expand-row-keys="expandRowKeys" row-key="id">
<el-table-column type="expand" width="30">
<template #default="{ row }">
<SkuList
ref="skuListRef"
:is-activity-component="true"
:prop-form-data="spuPropertyList.find((item) => item.spuId === row.id)?.spuDetail"
:property-list="spuPropertyList.find((item) => item.spuId === row.id)?.propertyList"
:rule-config="ruleConfig"
>
<template #extension>
<slot></slot>
</template>
</SkuList>
</template>
</el-table-column>
<el-table-column key="id" align="center" label="商品编号" prop="id" />
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
<el-table-column align="center" label="商品售价" min-width="90" prop="price">
<template #default="{ row }">
{{ formatToFraction(row.price) }}
</template>
</el-table-column>
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
</el-table>
</template>
<script generic="T extends Spu" lang="ts" setup>
import { formatToFraction } from '@/utils'
import { createImageViewer } from '@/components/ImageViewer'
import { Spu } from '@/api/mall/product/spu'
import { RuleConfig, SkuList } from '@/views/mall/product/spu/components'
import { SpuProperty } from '@/views/mall/promotion/components/index'
defineOptions({ name: 'PromotionSpuAndSkuList' })
const props = defineProps<{
spuList: T[] // TODO 便 spu spu spu
ruleConfig: RuleConfig[]
spuPropertyListP: SpuProperty<T>[]
}>()
const spuData = ref<Spu[]>([]) // spu
const skuListRef = ref() // Ref
const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId sku
const expandRowKeys = ref<number[]>() // row-key 使 keys
/**
* 获取所有 sku 活动配置
*
* @param extendedAttribute sku 上扩展的属性秒杀活动 sku 扩展属性 productConfig 请参考 seckillActivity.ts
*/
const getSkuConfigs = (extendedAttribute: string) => {
skuListRef.value.validateSku()
const seckillProducts = []
spuPropertyList.value.forEach((item) => {
item.spuDetail.skus.forEach((sku) => {
seckillProducts.push(sku[extendedAttribute])
})
})
return seckillProducts
}
// 使
defineExpose({ getSkuConfigs })
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
zIndex: 99999999,
urlList: [imgUrl]
})
}
/**
* 将传进来的值赋值给 skuList
*/
watch(
() => props.spuList,
(data) => {
if (!data) return
spuData.value = data as Spu[]
},
{
deep: true,
immediate: true
}
)
/**
* 将传进来的值赋值给 skuList
*/
watch(
() => props.spuPropertyListP,
(data) => {
if (!data) return
spuPropertyList.value = data as SpuProperty<T>[]
// spu sku SkuList
setTimeout(() => {
expandRowKeys.value = data.map((item) => item.spuId)
}, 200)
},
{
deep: true,
immediate: true
}
)
</script>

View File

@ -0,0 +1,298 @@
<template>
<Dialog v-model="dialogVisible" :appendToBody="true" :title="dialogTitle" width="70%">
<ContentWrap>
<el-row :gutter="20" class="mb-10px">
<el-col :span="6">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入商品名称"
@keyup.enter="handleQuery"
/>
</el-col>
<el-col :span="6">
<el-tree-select
v-model="queryParams.categoryId"
:data="categoryList"
:props="defaultProps"
check-strictly
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
/>
</el-col>
<el-col :span="6">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-col>
<el-col :span="6">
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-col>
</el-row>
<el-table
ref="spuListRef"
v-loading="loading"
:data="list"
:expand-row-keys="expandRowKeys"
row-key="id"
@expand-change="expandChange"
@selection-change="selectSpu"
>
<el-table-column v-if="isSelectSku" type="expand" width="30">
<template #default>
<SkuList
v-if="isExpand"
ref="skuListRef"
:isComponent="true"
:isDetail="true"
:prop-form-data="spuData"
:property-list="propertyList"
@selection-change="selectSku"
/>
</template>
</el-table-column>
<el-table-column type="selection" width="55" />
<el-table-column key="id" align="center" label="商品编号" prop="id" />
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
</template>
</el-table-column>
<el-table-column
:show-overflow-tooltip="true"
label="商品名称"
min-width="300"
prop="name"
/>
<el-table-column align="center" label="商品售价" min-width="90" prop="price">
<template #default="{ row }">
{{ formatToFraction(row.price) }}
</template>
</el-table-column>
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer>
<el-button type="primary" @click="confirm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/product/spu/components'
import { ElTable } from 'element-plus'
import { dateFormatter } from '@/utils/formatTime'
import { createImageViewer } from '@/components/ImageViewer'
import { formatToFraction } from '@/utils'
import { defaultProps, handleTree } from '@/utils/tree'
import * as ProductCategoryApi from '@/api/mall/product/category'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { propTypes } from '@/utils/propTypes'
defineOptions({ name: 'PromotionSpuSelect' })
const props = defineProps({
// spu spu sku
// :isSelectSku='true'
isSelectSku: propTypes.bool.def(false) // sku
})
const message = useMessage() //
const total = ref(0) //
const list = ref<any[]>([]) //
const loading = ref(false) //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0, //
name: '',
categoryId: null,
createTime: []
}) //
const propertyList = ref<PropertyAndValues[]>([]) //
const spuListRef = ref<InstanceType<typeof ElTable>>()
const skuListRef = ref() // Ref
const spuData = ref<ProductSpuApi.Spu>() //
const isExpand = ref(false) // SKU
const expandRowKeys = ref<number[]>() // row-key 使 keys
//============ ============
const selectedSpuId = ref<number>(0) // spuId
const selectedSkuIds = ref<number[]>([]) // skuIds
const selectSku = (val: ProductSpuApi.Sku[]) => {
if (selectedSpuId.value === 0) {
message.warning('请先选择商品再选择相应的规格!!!')
skuListRef.value.clearSelection()
return
}
selectedSkuIds.value = val.map((sku) => sku.id!)
}
const selectSpu = (val: ProductSpuApi.Spu[]) => {
if (val.length === 0) {
selectedSpuId.value = 0
return
}
//
selectedSpuId.value = val.map((spu) => spu.id!)[0]
// spu sku , sku spu
if (selectedSkuIds.value.length > 0) {
selectedSkuIds.value = []
}
// 1
if (val.length > 1) {
//
spuListRef.value.clearSelection()
//
spuListRef.value.toggleRowSelection(val.pop(), true)
return
}
expandChange(val[0], val)
}
//
const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi.Spu[]) => {
// spuId === spuId A A skuList A B
// sku
if (selectedSpuId.value !== 0) {
if (row.id !== selectedSpuId.value) {
message.warning('你已选择商品请先取消')
expandRowKeys.value = [selectedSpuId.value]
return
}
// skuList spu skuList
if (isExpand.value && spuData.value?.id === row.id) {
return
}
}
spuData.value = {}
propertyList.value = []
isExpand.value = false
if (expandedRows?.length === 0) {
// 0
expandRowKeys.value = []
return
}
// SPU
const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
propertyList.value = getPropertyList(res)
spuData.value = res
isExpand.value = true
expandRowKeys.value = [row.id!]
}
//
const emits = defineEmits<{
(e: 'confirm', spuId: number, skuIds?: number[]): void
}>()
/**
* 确认选择返回选中的 spu sku (如果需要选择sku的话)
*/
const confirm = () => {
if (selectedSpuId.value === 0) {
message.warning('没有选择任何商品')
return
}
if (props.isSelectSku && selectedSkuIds.value.length === 0) {
message.warning('没有选择任何商品属性')
return
}
// id
props.isSelectSku
? emits('confirm', selectedSpuId.value, selectedSkuIds.value)
: emits('confirm', selectedSpuId.value)
//
dialogVisible.value = false
selectedSpuId.value = 0
selectedSkuIds.value = []
}
/** 打开弹窗 */
const open = () => {
dialogTitle.value = '商品选择'
dialogVisible.value = true
}
defineExpose({ open }) // open
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ProductSpuApi.getSpuPage(queryParams.value)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
tabType: 0, //
name: '',
categoryId: null,
createTime: []
}
getList()
}
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
zIndex: 99999999,
urlList: [imgUrl]
})
}
const categoryList = ref() //
/** 初始化 **/
onMounted(async () => {
await getList()
//
const data = await ProductCategoryApi.getCategoryList({})
categoryList.value = handleTree(data, 'id', 'parentId')
})
</script>

View File

@ -0,0 +1,14 @@
import SpuSelect from './SpuSelect.vue'
import SpuAndSkuList from './SpuAndSkuList.vue'
import { PropertyAndValues } from '@/views/mall/product/spu/components'
type SpuProperty<T> = {
spuId: number
spuDetail: T
propertyList: PropertyAndValues[]
}
/**
*
*/
export { SpuSelect, SpuAndSkuList, SpuProperty }

View File

@ -0,0 +1,200 @@
<template>
<doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="会员昵称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入会员昵称"
clearable
@keyup="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
style="width: 240px"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<!-- <el-row :gutter="10" class="mb8">
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row> -->
</ContentWrap>
<ContentWrap>
<!-- Tab 选项真正的内容在 Lab -->
<el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
<el-tab-pane
v-for="tab in statusTabs"
:key="tab.value"
:label="tab.label"
:name="tab.value"
/>
</el-tabs>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="会员信息" align="center" prop="nickname" />
<!-- TODO 芋艿以后支持头像支持跳转 -->
<el-table-column label="优惠劵" align="center" prop="name" />
<el-table-column label="优惠券类型" align="center" prop="discountType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
</template>
</el-table-column>
<el-table-column label="领取方式" align="center" prop="takeType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="领取时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column
label="使用时间"
align="center"
prop="useTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
size="small"
type="primary"
link
@click="handleDelete(scope.row)"
v-hasPermi="['promotion:coupon:delete']"
><Icon icon="ep:delete" :size="12" class="mr-1px" />回收</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script setup lang="ts" name="PromotionCoupon">
import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { FormInstance } from 'element-plus'
//
const message = useMessage()
//
const loading = ref(true)
//
const total = ref(0)
//
const list = ref([])
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
createTime: [],
status: undefined
})
// Tab
const activeTab = ref('all')
const statusTabs = reactive([
{
label: '全部',
value: 'all'
}
])
const queryFormRef = ref<FormInstance | null>(null)
/** 查询列表 */
const getList = async () => {
loading.value = true
//
try {
const data = await getCouponPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 删除按钮操作 */
const handleDelete = async (row) => {
const id = row.id
try {
await message.confirm(
'回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
)
await deleteCoupon(id)
getList()
message.notifySuccess('回收成功')
} catch {}
}
/** tab 切换 */
const onTabChange = (tabName) => {
queryParams.status = tabName === 'all' ? undefined : tabName
getList()
}
onMounted(() => {
getList()
// statuses
for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
statusTabs.push({
label: dict.label,
value: dict.value as string
})
}
})
</script>

View File

@ -0,0 +1,614 @@
<template>
<doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
:model="queryParams"
ref="queryFormRef"
:inline="true"
v-show="showSearch"
label-width="82px"
>
<el-form-item label="优惠券名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入优惠劵名"
clearable
@keyup="handleQuery"
/>
</el-form-item>
<el-form-item label="优惠券类型" prop="discountType">
<el-select v-model="queryParams.discountType" placeholder="请选择优惠券类型" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="优惠券状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择优惠券状态" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
style="width: 240px"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
</el-form-item>
</el-form>
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
@click="handleAdd"
v-hasPermi="['promotion:coupon-template:create']"
>
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
<el-button
type="info"
plain
@click="$router.push('/promotion/coupon')"
v-hasPermi="['promotion:coupon:query']"
>
<Icon icon="ep:operation" class="mr-5px" />会员优惠劵
</el-button>
</el-col>
<!-- <right-toolbar v-model:showSearch="showSearch" @query-table="getList" /> -->
</el-row>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="优惠券名称" align="center" prop="name" />
<el-table-column label="优惠券类型" align="center" prop="discountType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
</template>
</el-table-column>
<el-table-column
label="优惠金额 / 折扣"
align="center"
prop="discount"
:formatter="discountFormat"
/>
<el-table-column label="发放数量" align="center" prop="totalCount" />
<el-table-column
label="剩余数量"
align="center"
prop="totalCount"
:formatter="(row) => row.totalCount - row.takeCount"
/>
<el-table-column
label="领取上限"
align="center"
prop="takeLimitCount"
:formatter="takeLimitCountFormat"
/>
<el-table-column
label="有效期限"
align="center"
prop="validityType"
width="180"
:formatter="validityTypeFormat"
/>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
size="small"
type="primary"
link
@click="handleUpdate(scope.row)"
v-hasPermi="['promotion:coupon-template:update']"
>
<Icon icon="ep:edit" :size="12" class="mr-1px" />
修改
</el-button>
<el-button
size="small"
type="primary"
link
@click="handleDelete(scope.row)"
v-hasPermi="['promotion:coupon-template:delete']"
>
<Icon icon="ep:delete" :size="12" class="mr-1px" />
删除
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-form-item label="优惠券名称" prop="name">
<el-input v-model="form.name" placeholder="请输入优惠券名称" />
</el-form-item>
<el-form-item label="优惠券类型" prop="discountType">
<el-radio-group v-model="form.discountType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
:key="dict.value"
:label="parseInt(dict.value)"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="form.discountType === PromotionDiscountTypeEnum.PRICE.type"
label="优惠券面额"
prop="discountPrice"
>
<el-input-number
v-model="form.discountPrice"
placeholder="请输入优惠金额,单位:元"
style="width: 400px"
:precision="2"
:min="0"
/>
</el-form-item>
<el-form-item
v-if="form.discountType === PromotionDiscountTypeEnum.PERCENT.type"
label="优惠券折扣"
prop="discountPercent"
>
<el-input-number
v-model="form.discountPercent"
placeholder="优惠券折扣不能小于 1 折,且不可大于 9.9 折"
style="width: 400px"
:precision="1"
:min="1"
:max="9.9"
/>
</el-form-item>
<el-form-item
v-if="form.discountType === PromotionDiscountTypeEnum.PERCENT.type"
label="最多优惠"
prop="discountLimitPrice"
>
<el-input-number
v-model="form.discountLimitPrice"
placeholder="请输入最多优惠"
style="width: 400px"
:precision="2"
:min="0"
/>
</el-form-item>
<el-form-item label="满多少元可以使用" prop="usePrice">
<el-input-number
v-model="form.usePrice"
placeholder="无门槛请设为 0"
style="width: 400px"
:precision="2"
:min="0"
/>
</el-form-item>
<el-form-item label="领取方式" prop="takeType">
<el-radio-group v-model="form.takeType">
<el-radio :key="1" :label="1">直接领取</el-radio>
<el-radio :key="2" :label="2">指定发放</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.takeType === 1" label="发放数量" prop="totalCount">
<el-input-number
v-model="form.totalCount"
placeholder="发放数量,没有之后不能领取或发放,-1 为不限制"
style="width: 400px"
:precision="0"
:min="-1"
/>
</el-form-item>
<el-form-item v-if="form.takeType === 1" label="每人限领个数" prop="takeLimitCount">
<el-input-number
v-model="form.takeLimitCount"
placeholder="设置为 -1 时,可无限领取"
style="width: 400px"
:precision="0"
:min="-1"
/>
</el-form-item>
<el-form-item label="有效期类型" prop="validityType">
<el-radio-group v-model="form.validityType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE)"
:key="dict.value"
:label="parseInt(dict.value)"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="form.validityType === CouponTemplateValidityTypeEnum.DATE.type"
label="固定日期"
prop="validTimes"
>
<el-date-picker
v-model="form.validTimes"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
/>
</el-form-item>
<el-form-item
v-if="form.validityType === CouponTemplateValidityTypeEnum.TERM.type"
label="领取日期"
prop="fixedStartTerm"
>
<el-input-number
v-model="form.fixedStartTerm"
placeholder="0 为今天生效"
style="width: 165px"
:precision="0"
:min="0"
/>
<el-input-number
v-model="form.fixedEndTerm"
placeholder="请输入结束天数"
style="width: 165px"
:precision="0"
:min="0"
/>
天有效
</el-form-item>
<el-form-item label="活动商品" prop="productScope">
<el-radio-group v-model="form.productScope">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
:key="dict.value"
:label="parseInt(dict.value)"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="form.productScope === PromotionProductScopeEnum.SPU.scope"
prop="productSpuIds"
>
<el-select
v-model="form.productSpuIds"
placeholder="请选择活动商品"
clearable
size="small"
multiple
filterable
style="width: 400px"
>
<el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px"
>{{ (item.minPrice / 100.0).toFixed(2) }}</span
>
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="PromotionCouponTemplate">
import {
createCouponTemplate,
updateCouponTemplate,
deleteCouponTemplate,
getCouponTemplate,
getCouponTemplatePage,
updateCouponTemplateStatus
} from '@/api/mall/promotion/couponTemplate'
import {
CommonStatusEnum,
CouponTemplateValidityTypeEnum,
PromotionDiscountTypeEnum,
PromotionProductScopeEnum
} from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { getSpuSimpleList } from '@/api/mall/product/spu'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { FormInstance } from 'element-plus'
//
const message = useMessage()
//
const loading = ref(true)
//
const showSearch = ref(true)
//
const total = ref(0)
//
const list = ref([])
//
const title = ref('')
//
const open = ref(false)
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
status: null,
type: null,
createTime: []
})
//
const form = ref<any>({})
//
const rules = {
name: [{ required: true, message: '优惠券名称不能为空', trigger: 'blur' }],
discountType: [{ required: true, message: '优惠券类型不能为空', trigger: 'change' }],
discountPrice: [{ required: true, message: '优惠券面额不能为空', trigger: 'blur' }],
discountPercent: [{ required: true, message: '优惠券折扣不能为空', trigger: 'blur' }],
discountLimitPrice: [{ required: true, message: '最多优惠不能为空', trigger: 'blur' }],
usePrice: [{ required: true, message: '满多少元可以使用不能为空', trigger: 'blur' }],
takeType: [{ required: true, message: '领取方式不能为空', trigger: 'change' }],
totalCount: [{ required: true, message: '发放数量不能为空', trigger: 'blur' }],
takeLimitCount: [{ required: true, message: '每人限领个数不能为空', trigger: 'blur' }],
validityType: [{ required: true, message: '有效期类型不能为空', trigger: 'change' }],
validTimes: [{ required: true, message: '固定日期不能为空', trigger: 'change' }],
fixedStartTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
fixedEndTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
productSpuIds: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }]
}
//
const productSpus = ref([])
const queryFormRef = ref<FormInstance | null>(null)
const formRef = ref<FormInstance | null>(null)
onMounted(() => {
getList()
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
//
const data = await getCouponTemplatePage(queryParams)
list.value = data.list
total.value = data.total
//
productSpus.value = await getSpuSimpleList()
} finally {
loading.value = false
}
}
/** 取消按钮 */
const cancel = () => {
open.value = false
reset()
}
/** 表单重置 */
const reset = () => {
form.value = {
id: undefined,
name: undefined,
discountType: PromotionDiscountTypeEnum.PRICE.type,
discountPrice: undefined,
discountPercent: undefined,
discountLimitPrice: undefined,
usePrice: undefined,
takeType: 1,
totalCount: undefined,
takeLimitCount: undefined,
validityType: CouponTemplateValidityTypeEnum.DATE.type,
validTimes: [],
validStartTime: undefined,
validEndTime: undefined,
fixedStartTerm: undefined,
fixedEndTerm: undefined,
productScope: PromotionProductScopeEnum.ALL.scope,
productSpuIds: []
}
formRef.value?.resetFields()
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef?.value?.resetFields()
handleQuery()
}
/** 新增按钮操作 */
const handleAdd = () => {
reset()
open.value = true
title.value = '添加优惠劵'
}
/** 修改按钮操作 */
const handleUpdate = async (row: any) => {
reset()
const id = row.id
try {
const data = await getCouponTemplate(id)
form.value = {
...data,
discountPrice: data.discountPrice !== undefined ? data.discountPrice / 100.0 : undefined,
discountPercent: data.discountPercent !== undefined ? data.discountPercent / 10.0 : undefined,
discountLimitPrice:
data.discountLimitPrice !== undefined ? data.discountLimitPrice / 100.0 : undefined,
usePrice: data.usePrice !== undefined ? data.usePrice / 100.0 : undefined,
validTimes: [data.validStartTime, data.validEndTime]
}
open.value = true
title.value = '修改优惠劵'
} catch {}
}
/** 提交按钮 */
const submitForm = async () => {
const valid = await formRef.value?.validate()
if (!valid) {
return
}
//
let data = {
...form.value,
discountPrice:
form.value.discountPrice !== undefined ? form.value.discountPrice * 100 : undefined,
discountPercent:
form.value.discountPercent !== undefined ? form.value.discountPercent * 10 : undefined,
discountLimitPrice:
form.value.discountLimitPrice !== undefined ? form.value.discountLimitPrice * 100 : undefined,
usePrice: form.value.usePrice !== undefined ? form.value.usePrice * 100 : undefined,
validStartTime:
form.value.validTimes && form.value.validTimes.length === 2
? form.value.validTimes[0]
: undefined,
validEndTime:
form.value.validTimes && form.value.validTimes.length === 2
? form.value.validTimes[1]
: undefined
}
//
if (form.value.id != null) {
try {
await updateCouponTemplate(data)
message.success('修改成功')
open.value = false
getList()
} catch {}
return
}
try {
await createCouponTemplate(data)
message.success('新增成功')
open.value = false
getList()
} catch {}
}
/** 优惠劵模板状态修改 */
const handleStatusChange = async (row: any) => {
// row
let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
try {
await message.confirm('确认要"' + text + '""' + row.name + '"优惠劵吗?')
await updateCouponTemplateStatus(row.id, row.status)
message.success(text + '成功')
} catch {
// row.status
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
const id = row.id
try {
await message.confirm('是否确认删除优惠劵编号为"' + id + '"的数据项?')
await deleteCouponTemplate(id)
} catch {}
}
// /
const discountFormat = (row: any) => {
if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
return `${(row.discountPrice / 100.0).toFixed(2)}`
}
if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
return `${(row.discountPrice / 100.0).toFixed(2)}`
}
return '未知【' + row.discountType + '】'
}
//
const takeLimitCountFormat = (row: any) => {
if (row.takeLimitCount === -1) {
return '无领取限制'
}
return `${row.takeLimitCount} 张/人`
}
//
const validityTypeFormat = (row: any) => {
if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
return `${formatDate(row.validStartTime)}${formatDate(row.validEndTime)}`
}
if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
}
return '未知【' + row.validityType + '】'
}
</script>

View File

@ -0,0 +1,210 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
<Form
ref="formRef"
v-loading="formLoading"
:isCol="true"
:rules="rules"
:schema="allSchemas.formSchema"
>
<!-- 先选择 -->
<template #spuId>
<el-button @click="spuSelectRef.open()">选择商品</el-button>
<SpuAndSkuList
ref="spuAndSkuListRef"
:rule-config="ruleConfig"
:spu-list="spuList"
:spu-property-list-p="spuPropertyList"
>
<el-table-column align="center" label="秒杀库存" min-width="168">
<template #default="{ row: sku }">
<el-input-number v-model="sku.productConfig.stock" :min="0" class="w-100%" />
</template>
</el-table-column>
<el-table-column align="center" label="秒杀价格(元)" min-width="168">
<template #default="{ row: sku }">
<el-input-number
v-model="sku.productConfig.seckillPrice"
:min="0"
:precision="2"
:step="0.1"
class="w-100%"
/>
</template>
</el-table-column>
</SpuAndSkuList>
</template>
</Form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
</template>
<script lang="ts" setup>
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
import { allSchemas, rules } from './seckillActivity.data'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
import { convertToInteger, formatToFraction } from '@/utils'
defineOptions({ name: 'PromotionSeckillActivityForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formRef = ref() // Ref
// ================= =================
const spuSelectRef = ref() // Ref
const spuAndSkuListRef = ref() // sku Ref
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.stock',
rule: (arg) => arg > 1,
message: '商品秒杀库存必须大于 1 '
},
{
name: 'productConfig.seckillPrice',
rule: (arg) => arg > 0.01,
message: '商品秒杀价格必须大于 0.01 '
}
]
const spuList = ref<SeckillActivityApi.SpuExtension[]>([]) // spu
const spuPropertyList = ref<SpuProperty<SeckillActivityApi.SpuExtension>[]>([])
const selectSpu = (spuId: number, skuIds: number[]) => {
formRef.value.setValues({ spuId })
getSpuDetails(spuId, skuIds)
}
/**
* 获取 SPU 详情
*/
const getSpuDetails = async (
spuId: number,
skuIds: number[] | undefined,
products?: SeckillProductVO[]
) => {
const spuProperties: SpuProperty<SeckillActivityApi.SpuExtension>[] = []
const res = (await ProductSpuApi.getSpuDetailList([spuId])) as SeckillActivityApi.SpuExtension[]
if (res.length == 0) {
return
}
spuList.value = []
//
const spu = res[0]
const selectSkus =
typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
selectSkus?.forEach((sku) => {
let config: SeckillActivityApi.SeckillProductVO = {
skuId: sku.id!,
stock: 0,
seckillPrice: 0
}
if (typeof products !== 'undefined') {
const product = products.find((item) => item.skuId === sku.id)
if (product) {
//
product.seckillPrice = formatToFraction(product.seckillPrice)
}
config = product || config
}
sku.productConfig = config
})
spu.skus = selectSkus as SeckillActivityApi.SkuExtension[]
spuProperties.push({
spuId: spu.id!,
spuDetail: spu,
propertyList: getPropertyList(spu)
})
spuList.value.push(spu)
spuPropertyList.value = spuProperties
}
// ================= end =================
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
await resetForm()
//
if (id) {
formLoading.value = true
try {
const data = (await SeckillActivityApi.getSeckillActivity(
id
)) as SeckillActivityApi.SeckillActivityVO
await getSpuDetails(
data.spuId!,
data.products?.map((sku) => sku.skuId),
data.products
)
formRef.value.setValues(data)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 重置表单 */
const resetForm = async () => {
spuList.value = []
spuPropertyList.value = []
await nextTick()
formRef.value.getElFormRef().resetFields()
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
products.forEach((item: SeckillProductVO) => {
//
item.seckillPrice = convertToInteger(item.seckillPrice)
})
//
data.products = products
if (formType.value === 'create') {
await SeckillActivityApi.createSeckillActivity(data)
message.success(t('common.createSuccess'))
} else {
await SeckillActivityApi.updateSeckillActivity(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.demo-table-expand {
padding-left: 42px;
:deep(.el-form-item__label) {
width: 82px;
font-weight: bold;
color: #99a9bf;
}
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
v-hasPermi="['promotion:seckill-activity:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
</template>
</Search>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
v-model:currentPage="tableObject.currentPage"
v-model:pageSize="tableObject.pageSize"
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:expand="true"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
@expand-change="expandChange"
>
<template #expand> 展示活动商品和商品相关属性活动配置</template>
<template #spuId="{ row }">
<el-image
:src="row.picUrl"
class="w-30px h-30px align-middle mr-5px"
@click="imagePreview(row.picUrl)"
/>
<span class="align-middle">{{ row.spuName }}</span>
</template>
<template #configIds="{ row }">
<el-tag v-for="(name, index) in convertSeckillConfigNames(row)" :key="index" class="mr-5px">
{{ name }}
</el-tag>
</template>
<template #action="{ row }">
<el-button
v-hasPermi="['promotion:seckill-activity:update']"
link
type="primary"
@click="openForm('update', row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['promotion:seckill-activity:delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<SeckillActivityForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { allSchemas } from './seckillActivity.data'
import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import SeckillActivityForm from './SeckillActivityForm.vue'
import { cloneDeep } from 'lodash-es'
import { createImageViewer } from '@/components/ImageViewer'
defineOptions({ name: 'PromotionSeckillActivity' })
// tableObject
// tableMethods
// https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: SeckillActivityApi.getSeckillActivityPage, //
delListApi: SeckillActivityApi.deleteSeckillActivity //
})
//
const { getList, setSearchParams } = tableMethods
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
}
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
const configList = ref([]) //
const convertSeckillConfigNames = computed(
() => (row) =>
configList.value
?.filter((item) => row.configIds.includes(item.id))
?.map((config) => config.name)
)
const expandChange = (row, expandedRows) => {
// TODO puhui CRUD
console.log(row, expandedRows)
}
/** 初始化 **/
onMounted(async () => {
/*
TODO
后面准备封装成一个函数来操作 tableColumns 重新排列比如说需求是表单上商品选择是在后面的而列表展示的时候需要调到位置
封装效果支持批量操作给出 field 和需要插入的位置[{field:'spuId',index: 1}] 效果为把 field spuId column 移动到第一个位置
*/
//
const index = allSchemas.tableColumns.findIndex((item) => item.field === 'spuId')
const column = cloneDeep(allSchemas.tableColumns[index])
allSchemas.tableColumns.splice(index, 1)
//
allSchemas.tableColumns.unshift(column)
await getList()
configList.value = await getListAllSimple()
})
</script>

View File

@ -0,0 +1,259 @@
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
// 表单校验
export const rules = reactive({
spuId: [required],
name: [required],
startTime: [required],
endTime: [required],
sort: [required],
configIds: [required],
totalLimitCount: [required],
singleLimitCount: [required],
totalStock: [required]
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
{
label: '秒杀活动名称',
field: 'name',
isSearch: true,
form: {
colProps: {
span: 24
}
},
table: {
width: 120
}
},
{
label: '活动开始时间',
field: 'startTime',
formatter: dateFormatter2,
isSearch: true,
search: {
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
type: 'daterange'
}
},
form: {
component: 'DatePicker',
componentProps: {
type: 'date',
valueFormat: 'x'
}
},
table: {
width: 120
}
},
{
label: '活动结束时间',
field: 'endTime',
formatter: dateFormatter2,
isSearch: true,
search: {
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
type: 'daterange'
}
},
form: {
component: 'DatePicker',
componentProps: {
type: 'date',
valueFormat: 'x'
}
},
table: {
width: 120
}
},
{
label: '秒杀时段',
field: 'configIds',
form: {
component: 'Select',
componentProps: {
multiple: true,
optionsAlias: {
labelField: 'name',
valueField: 'id'
}
},
api: getListAllSimple
},
table: {
width: 300
}
},
{
label: '新增订单数',
field: 'orderCount',
isForm: false,
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '付款人数',
field: 'userCount',
isForm: false,
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '订单实付金额',
field: 'totalPrice',
isForm: false,
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '总限购数量',
field: 'totalLimitCount',
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '单次限够数量',
field: 'singleLimitCount',
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '排序',
field: 'sort',
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 80
}
},
{
label: '秒杀库存',
field: 'stock',
isForm: false,
form: {
component: 'InputNumber',
value: 0
},
table: {
width: 120
}
},
{
label: '秒杀总库存',
field: 'totalStock',
isForm: false,
table: {
width: 120
}
},
{
label: '秒杀活动商品',
field: 'spuId',
isTable: true,
isSearch: false,
form: {
colProps: {
span: 24
}
},
table: {
width: 300
}
},
{
label: '创建时间',
field: 'createTime',
formatter: dateFormatter,
search: {
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD HH:mm:ss',
type: 'daterange',
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
}
},
isForm: false,
table: {
width: 120
}
},
{
label: '状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isForm: false,
isSearch: true,
form: {
component: 'Radio'
},
table: {
width: 80
}
},
{
label: '备注',
field: 'remark',
isSearch: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
table: {
width: 300
}
},
{
label: '操作',
field: 'action',
isForm: false,
table: {
width: 120,
fixed: 'right'
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@ -0,0 +1,78 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Form ref="formRef" v-loading="formLoading" :rules="rules" :schema="allSchemas.formSchema" />
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="SeckillConfigForm" setup>
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
import { allSchemas, rules } from './seckillConfig.data'
import { cloneDeep } from 'lodash-es'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
//
if (id) {
formLoading.value = true
try {
const data = await SeckillConfigApi.getSeckillConfig(id)
data.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
url: item
}))
formRef.value.setValues(data)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
//
formLoading.value = true
try {
//
const data = formRef.value.formModel as SeckillConfigApi.SeckillConfigVO
const cloneData = cloneDeep(data)
const newSliderPicUrls = []
cloneData.sliderPicUrls.forEach((item) => {
//
typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
})
cloneData.sliderPicUrls = newSliderPicUrls
if (formType.value === 'create') {
await SeckillConfigApi.createSeckillConfig(cloneData)
message.success(t('common.createSuccess'))
} else {
await SeckillConfigApi.updateSeckillConfig(cloneData)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
</script>

View File

@ -0,0 +1,138 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
v-hasPermi="['promotion:seckill-config:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
</template>
</Search>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
v-model:currentPage="tableObject.currentPage"
v-model:pageSize="tableObject.pageSize"
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
>
<template #sliderPicUrls="{ row }">
<el-image
v-for="(item, index) in row.sliderPicUrls"
:key="index"
:src="item"
class="w-60px h-60px mr-10px"
@click="imagePreview(row.sliderPicUrls)"
/>
</template>
<template #status="{ row }">
<el-switch
v-model="row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(row)"
/>
</template>
<template #action="{ row }">
<el-button
v-hasPermi="['promotion:seckill-config:update']"
link
type="primary"
@click="openForm('update', row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['promotion:seckill-config:delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<SeckillConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" name="PromotionSeckillConfig" setup>
import { allSchemas } from './seckillConfig.data'
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
import SeckillConfigForm from './SeckillConfigForm.vue'
import { createImageViewer } from '@/components/ImageViewer'
import { CommonStatusEnum } from '@/utils/constants'
import { isArray } from '@/utils/is'
const message = useMessage() //
// tableObject
// tableMethods
// https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: SeckillConfigApi.getSeckillConfigPage, //
delListApi: SeckillConfigApi.deleteSeckillConfig //
})
//
const { getList, setSearchParams } = tableMethods
/** 轮播图预览预览 */
const imagePreview = (args) => {
const urlList = []
if (isArray(args)) {
args.forEach((item) => {
urlList.push(item)
})
} else {
urlList.push(args)
}
createImageViewer({
urlList
})
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
}
/** 修改用户状态 */
const handleStatusChange = async (row: SeckillConfigApi.SeckillConfigVO) => {
try {
//
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
await message.confirm('确认要"' + text + '""' + row.name + '?')
//
await SeckillConfigApi.updateSeckillConfigStatus(row.id, row.status)
//
await getList()
} catch {
//
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,82 @@
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter } from '@/utils/formatTime'
// 表单校验
export const rules = reactive({
name: [required],
startTime: [required],
endTime: [required],
picUrl: [required],
status: [required]
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
{
label: '秒杀时段名称',
field: 'name',
isSearch: true
},
{
label: '开始时间点',
field: 'startTime',
isSearch: false,
search: {
component: 'TimePicker'
},
form: {
component: 'TimePicker',
componentProps: {
valueFormat: 'HH:mm:ss'
}
}
},
{
label: '结束时间点',
field: 'endTime',
isSearch: false,
search: {
component: 'TimePicker'
},
form: {
component: 'TimePicker',
componentProps: {
valueFormat: 'HH:mm:ss'
}
}
},
{
label: '秒杀轮播图',
field: 'sliderPicUrls',
isSearch: false,
form: {
component: 'UploadImgs'
},
table: {
width: 300
}
},
{
label: '状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
form: {
component: 'Radio'
}
},
{
label: '创建时间',
field: 'createTime',
isForm: false,
isSearch: false,
formatter: dateFormatter
},
{
label: '操作',
field: 'action',
isForm: false
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@ -89,7 +89,7 @@
<el-table border style="width: 100%" :data="formData.templateFree">
<el-table-column align="center" label="区域">
<template #default="{ row }">
<!-- 区域数据太多用赖加载方式要不然性能有问题 -->
<!-- 区域数据太多用赖加载方式要不然性能有问题 -->
<el-tree-select
v-model="row.areaIds"
multiple
@ -137,7 +137,7 @@
</template>
</Dialog>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
import { defaultProps } from '@/utils/tree'
@ -171,7 +171,10 @@ const formRules = reactive({
sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const areaCache = ref([]) //
const areaCache = ref([]) //
// TODO @jason
// TODO @jaosn
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -204,9 +207,9 @@ const open = async (type: string, id?: number) => {
}
item.freePrice = fenToYuan(item.freePrice)
})
//
//
const areaIds = chargeAreaIds.concat(freeAreaIds)
//
//
areaCache.value = await getAreaListByIds(areaIds.join(','))
}
} finally {
@ -226,8 +229,9 @@ const submitForm = async () => {
formLoading.value = true
try {
const data = formData.value as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
//
// TODO @jason
data.templateCharge.forEach((item) => {
//
item.startPrice = yuanToFen(item.startPrice)
item.extraPrice = yuanToFen(item.extraPrice)
})
@ -248,6 +252,7 @@ const submitForm = async () => {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
@ -269,6 +274,7 @@ const resetForm = () => {
columnTitle.value = columnTitleMap.get(1)
formRef.value?.resetFields()
}
/** 配送计费方法改变 */
const changeChargeMode = (chargeMode: number) => {
columnTitle.value = columnTitleMap.get(chargeMode)
@ -276,6 +282,24 @@ const changeChargeMode = (chargeMode: number) => {
const defaultArea = [{ id: 1, name: '全国', disabled: false }]
/** 初始化数据 */
// TODO @jasoncolumnTitleMap
// const columnTitleMap = {
// '1': {
// startCountTitle: '',
// extraCountTitle: '',
// freeCountTitle: ''
// },
// '2': {
// startCountTitle: '(kg)',
// extraCountTitle: '(kg)',
// freeCountTitle: '(kg)'
// },
// '3': {
// startCountTitle: '(m³)',
// extraCountTitle: '(m³)',
// freeCountTitle: '(m³)'
// }
// }
const initData = async () => {
// TODO
// formLoading.value = true
@ -286,7 +310,7 @@ const initData = async () => {
// } finally {
// formLoading.value = false
// }
//
//
columnTitleMap.set(1, {
startCountTitle: '首件',
extraCountTitle: '续件',
@ -320,6 +344,7 @@ const loadChargeArea = async (node, resolve) => {
const item = data[0]
if (areaIds.includes(item.id)) {
// TODO @
// TODO @jason
//item.disabled = true
}
resolve(data)
@ -357,10 +382,11 @@ const loadFreeArea = async (node, resolve) => {
} else {
const id = node.data.id
const data = await getChildrenArea(id)
//
//
data.forEach((item) => {
if (areaIds.includes(item.id)) {
// TODO @
// TODO @jason
//item.disabled = true
}
})
@ -378,11 +404,13 @@ const addChargeArea = () => {
extraPrice: 1
})
}
/** 删除计费区域 */
const deleteChargeArea = (index) => {
const data = formData.value
data.templateCharge.splice(index, 1)
}
/** 添加包邮区域 */
const addFreeArea = () => {
const data = formData.value
@ -392,6 +420,7 @@ const addFreeArea = () => {
freePrice: 1
})
}
/** 删除包邮区域 */
const deleteFreeArea = (index) => {
const data = formData.value

View File

@ -92,12 +92,14 @@
<!-- 表单弹窗添加/修改 -->
<ExpressTemplateForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="DeliveryExpressTemplate">
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
import ExpressTemplateForm from './ExpressTemplateForm.vue'
defineOptions({ name: 'DeliveryExpressTemplate' })
const message = useMessage() //
const { t } = useI18n() //
const total = ref(0) //
@ -110,6 +112,7 @@ const queryParams = reactive({
chargeMode: undefined
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true

View File

@ -0,0 +1,287 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-row>
<el-col :span="12">
<el-form-item label="门店 logo" prop="logo">
<UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
<div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门店状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="门店名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入门店名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门店手机" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入门店手机" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="门店简介" prop="introduction">
<el-input
v-model="formData.introduction"
:rows="3"
type="textarea"
placeholder="请输入门店简介"
/>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="门店所在地区" prop="areaId">
<el-cascader v-model="formData.areaId" :options="areaList" :props="areaTreeProps" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门店详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入门店详细地址" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="营业开始时间" prop="openingTime">
<el-time-select
v-model="formData.openingTime"
:max-time="formData.closingTime"
placeholder="开始时间"
start="08:30"
step="00:15"
end="23:30"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="营业结束时间" prop="closingTime">
<el-time-select
v-model="formData.closingTime"
:min-time="formData.openingTime"
placeholder="结束时间"
start="08:30"
step="00:15"
end="23:30"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="经度" prop="longitude">
<el-input v-model="formData.longitude" placeholder="请输入门店经度" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="纬度" prop="latitude">
<el-input v-model="formData.latitude" placeholder="请输入门店纬度" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="获取经纬度">
<el-button type="primary" @click="mapDialogVisible.value = true">获取</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
<el-dialog
v-model="mapDialogVisible"
title="获取经纬度"
append-to-body
width="500px"
class="mapBox"
>
<iframe id="mapPage" width="100%" height="100%" frameborder="0" :src="tencentLbsUrl"></iframe>
</el-dialog>
</Dialog>
</template>
<script setup lang="ts">
import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { getAreaTree } from '@/api/system/area'
import * as ConfigApi from '@/api/infra/config'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const mapDialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: '',
phone: '',
logo: '',
detailAddress: '',
introduction: '',
areaId: 0,
openingTime: undefined,
closingTime: undefined,
latitude: undefined,
longitude: undefined,
status: CommonStatusEnum.ENABLE
})
const formRules = reactive({
name: [{ required: true, message: '门店名称不能为空', trigger: 'blur' }],
logo: [{ required: true, message: '门店 logo 不能为空', trigger: 'blur' }],
phone: [
{ required: true, message: '门店手机不能为空', trigger: 'blur' },
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
areaId: [{ required: true, message: '门店所在区域不能为空', trigger: 'blur' }],
detailAddress: [{ required: true, message: '门店详细地址不能为空', trigger: 'blur' }],
openingTime: [{ required: true, message: '营业开始时间不能为空', trigger: 'blur' }],
closingTime: [{ required: true, message: '营业结束时间不能为空', trigger: 'blur' }],
latitude: [{ required: true, message: '纬度不能为空', trigger: 'blur' }],
longitude: [{ required: true, message: '经度不能为空', trigger: 'blur' }],
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const areaTreeProps = {
children: 'children',
label: 'name',
value: 'id',
emitPath: false
}
const areaList = ref() //
const tencentLbsUrl = ref('') // url
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as DeliveryPickUpStoreApi.DeliveryPickUpStoreVO
if (formType.value === 'create') {
await DeliveryPickUpStoreApi.createDeliveryPickUpStore(data)
message.success(t('common.createSuccess'))
} else {
await DeliveryPickUpStoreApi.updateDeliveryPickUpStore(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
phone: '',
logo: '',
detailAddress: '',
introduction: '',
areaId: undefined,
openingTime: undefined,
closingTime: undefined,
latitude: undefined,
longitude: undefined,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 选择经纬度 */
const selectAddress = function (loc: any): void {
if (loc.latlng && loc.latlng.lat) {
formData.value.latitude = loc.latlng.lat
}
if (loc.latlng && loc.latlng.lng) {
formData.value.longitude = loc.latlng.lng
}
mapDialogVisible.value = false
}
/** 初始化数据 */
const initData = async () => {
formLoading.value = true
try {
const data = await getAreaTree()
areaList.value = data
} finally {
formLoading.value = false
}
// TODO @jason initTencentLbsMap
window.selectAddress = selectAddress
window.addEventListener(
'message',
function (event) {
//
let loc = event.data
if (loc && loc.module === 'locationPicker') {
// post module 'locationPicker'
window.parent.selectAddress(loc)
}
},
false
)
const data = await ConfigApi.getConfigKey('tencent.lbs.key')
let key = ''
if (data && data.length > 0) {
key = data
}
tencentLbsUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`
}
/** 初始化 **/
onMounted(() => {
initData()
})
</script>
<style lang="scss">
.mapBox .el-dialog__body {
height: 640px !important;
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="门店手机" prop="phone">
<el-input
v-model="queryParams.phone"
placeholder="请输门店手机"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="门店名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输门店名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="门店状态" prop="status">
<el-select v-model="queryParams.status" placeholder="门店状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['trade:delivery:pick-up-store:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['trade:delivery:pick-up-store:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" prop="id" />
<el-table-column label="门店 logo" prop="logo">
<template #default="scope">
<img v-if="scope.row.logo" :src="scope.row.logo" alt="门店 logo" class="h-100px" />
</template>
</el-table-column>
<el-table-column label="门店名称" prop="name" />
<el-table-column label="门店手机" prop="phone" />
<el-table-column label="门店详细地址" align="center" prop="detailAddress" />
<el-table-column label="开启状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['trade:delivery:pick-up-store:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['trade:delivery:pick-up-store:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeliveryPickUpStoreForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="DeliveryPickUpStore">
import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
const message = useMessage() //
const { t } = useI18n() //
const total = ref(0) //
const loading = ref(true) //
const exportLoading = ref(false) //
const list = ref<any[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
status: undefined,
phone: undefined,
name: undefined,
createTime: []
})
const queryFormRef = ref() //
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await DeliveryPickUpStoreApi.deleteDeliveryPickUpStore(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeliveryPickUpStoreApi.getDeliveryPickUpStorePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await DeliveryPickUpStoreApi.exportDeliveryPickUpStoreApi(queryParams)
download.excel(data, '自提门店.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,572 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:model="queryParams"
class="-mb-15px"
label-width="68px"
:inline="true"
>
<el-form-item label="订单状态" prop="status">
<el-select class="!w-280px" v-model="queryParams.status" clearable placeholder="全部">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
:key="(dict.value as string)"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="支付方式" prop="payChannelCode">
<el-select
v-model="queryParams.payChannelCode"
class="!w-280px"
clearable
placeholder="全部"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE_TYPE)"
:key="(dict.value as string)"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-280px"
start-placeholder="自定义时间"
end-placeholder="自定义时间"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="订单来源" prop="terminal">
<el-select class="!w-280px" v-model="queryParams.terminal" clearable placeholder="全部">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.TERMINAL)"
:key="(dict.value as string)"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="订单类型" prop="type">
<el-select class="!w-280px" v-model="queryParams.type" clearable placeholder="全部">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
:key="(dict.value as string)"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="订单搜索">
<el-input
v-show="true"
class="!w-280px"
v-model="queryType.v"
clearable
placeholder="请输入"
>
<template #prepend>
<el-select style="width: 110px" v-model="queryType.k" clearable placeholder="全部">
<el-option
v-for="dict in searchList"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery" v-hasPermi="['trade:order:query']">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery" v-hasPermi="['trade:order:query']">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
<!-- v-hasPermi="['trade:order:export']" -->
<Icon icon="ep:download" class="mr-5px" /> 导出TODO
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 表格 -->
<ContentWrap>
<!-- 表单 -->
<el-table v-loading="loading" :data="list">
<el-table-column type="expand" fixed="left">
<template #default="scope">
<el-descriptions class="mx-40">
<el-descriptions-item label="商品原价(总): ">{{
'¥ ' +
parseFloat((scope.row.originalPrice / 100) as unknown as string).toFixed(2) +
' 元'
}}</el-descriptions-item>
<el-descriptions-item label="下单时间: ">
{{ formatDate(scope.row.createTime) }}</el-descriptions-item
>
<el-descriptions-item label="推广人: ">TODO</el-descriptions-item>
<el-descriptions-item label="用户备注: ">{{
scope.row.userRemark
}}</el-descriptions-item>
<el-descriptions-item label="商家备注: ">{{ scope.row.remark }}</el-descriptions-item>
</el-descriptions>
</template>
</el-table-column>
<el-table-column width="100" fixed="left">
<template #header>
<el-dropdown icon="eq:search" @command="handleDropType">
<el-button link type="primary">全选({{ orderSelect.selectTotal }}) </el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="1">当前页</el-dropdown-item>
<el-dropdown-item command="2">所有页</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #default="scope">
<el-checkbox v-model="scope.row.itemSelect" @change="handcheckclick(scope.row)" />
</template>
</el-table-column>
<el-table-column label="订单号" align="center" min-width="110">
<template #default="scope">
<el-button link type="primary" @click="showOrderDetail(scope.row)">{{
scope.row.no
}}</el-button>
</template>
</el-table-column>
<el-table-column label="订单类型" align="center" min-width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="用户信息" align="center" min-width="100">
<template #default="scope">
<el-button link type="primary" @click="goUserDetail(scope.row)"
>{{ scope.row.userId }}{{ '[' + scope.row.user.nickname + ']' }}</el-button
>
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
min-width="180"
/>
<el-table-column label="订单来源" align="center" min-width="100">
<template #default="scope">
<dict-tag
v-if="scope.row.terminal"
:type="DICT_TYPE.TERMINAL"
:value="scope.row.terminal"
/>
<span v-else>{{ scope.terminal }}</span>
</template>
</el-table-column>
<el-table-column label="商品信息" align="left" min-width="200" prop="items">
<template #default="scope">
<el-popover
ref="popover"
placement="bottom"
:title="'订单:' + scope.row.no"
:width="400"
trigger="hover"
>
<template #reference>
<div>
<div v-for="item in scope.row.items" :key="item">
<el-image
style="width: 36px; height: 36px"
:src="item.picUrl"
:preview-src-list="[item.picUrl]"
fit="cover"
@click="imagePreview(item.picUrl)"
/>
<span class="m-2">{{ item.spuName }}</span>
</div>
</div>
</template>
<div v-for="item in scope.row.items" :key="item">
<div>
<p>{{ item.spuName }}</p>
<!-- TODO xiaobai: 是不是 (item.payPrice / 100.0).toFixed(2) -->
<p>{{
'¥ ' +
parseFloat((item.payPrice / 100) as unknown as string).toFixed(2) +
'元 x ' +
item.count
}}</p>
</div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="实际支付(元)" align="center" prop="payPrice" min-width="100">
<template #default="scope">
{{ '¥ ' + parseFloat((scope.row.payPrice / 100) as unknown as string).toFixed(2) }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="支付时间"
prop="payTime"
min-width="180"
/>
<el-table-column label="支付类型" align="center" min-width="100" prop="payChannelCode">
<template #default="scope">
<dict-tag
v-if="scope.row.payChannelCode"
:type="DICT_TYPE.PAY_CHANNEL_CODE_TYPE"
:value="scope.row.payChannelCode"
/>
</template>
</el-table-column>
<el-table-column label="订单状态" align="center" prop="status" min-width="100">
<template #default="scope">
<dict-tag
v-if="scope.row.status !== ''"
:type="DICT_TYPE.TRADE_ORDER_STATUS"
:value="scope.row.status"
/>
<span v-else>{{ scope.status }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" min-width="150">
<template #default="scope">
<!-- <el-button v-if="scope.row.status == '0'" link type="primary" @click="sendXX(scope.row)"
>待支付</el-button> -->
<el-button v-if="scope.row.status == '10'" link type="primary" @click="sendXX(scope.row)"
>发货</el-button
>
<el-button link type="primary" @click="showOrderDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<el-image-viewer
v-if="imgViewVisible"
:url-list="imageViewerList"
@close="imgViewVisible = false"
/>
</template>
<script setup lang="ts" name="OrderList">
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import * as TradeOrderApi from '@/api/mall/trade/order'
import {
TradeOrderPageReqVO,
SelectType,
TradeOrderPageItemRespVO
} from '@/api/mall/trade/order/type/orderType'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
const message = useMessage()
const { push } = useRouter()
const imgViewVisible = ref(false) //
const imageViewerList = ref<string[]>([]) //
const queryFormRef = ref()
const loading = ref(false)
const exportLoading = ref(false)
const total = ref(0) //
const list = ref<Array<TradeOrderPageItemRespVO | any>>([]) //
//
const orderSelect: SelectType = reactive({
queryParams: {} as TradeOrderPageReqVO,
selectTotal: 0,
selectAllFlag: false,
selectData: new Map<number, Set<string>>(),
unSelectList: new Set<string>()
})
//
const queryParams: TradeOrderPageReqVO = reactive({
pageNo: 1, //
pageSize: 10 //
})
const queryType = reactive({ k: '', v: '' }) // kv
/*
* 订单搜索
* 商品名称 商品件数 全部 需要后端支持TODO
*/
const searchList = ref([
{ value: 'no', label: '订单号' },
{ value: 'userId', label: '用户UID' },
{ value: 'userNickname', label: '用户昵称' },
{ value: 'userMobile', label: '用户电话' },
{ value: 'spuName', label: '商品名称TODO' },
{ value: 'itemCount', label: '商品件数TODO' }
])
/**
当前页/ 如果pageNo存在则将但前数据全部按照单个选中模式取消 ,不存在则新增全页 增加 Map.pageNo Map.roderNoList
单个选中 如果pagelist存在订单号选中状态取反并对总数按选中状态加减如果pagelist不存在订单号选中状态取反并对总数按选中状态加减增加 Map.pageNo
如果当前Map.pageNo 所对应list 为空 清除pageNo
* @param command ===1 当前页 选中 ===2 所有页面选中
*/
const handleDropType = (command: string) => {
let i = 0
//
if (command === '1') {
//
if (orderSelect.selectData && orderSelect.selectData.has(queryParams.pageNo)) {
for (i = 0; i < list.value.length; i++) {
if (orderSelect.selectData.get(queryParams.pageNo)!.has(list.value[i].id)) {
//
orderSelect.selectTotal -= 1
// orderSelect.unSelectList
unSelectListRecord(list.value[i].id, 'add')
}
list.value[i]['itemSelect'] = false
}
orderSelect.selectData.delete(queryParams.pageNo) //
} else {
//
orderSelect.selectData.set(queryParams.pageNo, new Set<string>())
for (i = 0; i < list.value.length; i++) {
list.value[i]['itemSelect'] = true
orderSelect.selectData.get(queryParams.pageNo)!.add(list.value[i].id)
//
orderSelect.selectTotal += 1
//
unSelectListRecord(list.value[i].id, 'del')
}
}
}
//
if (command === '2') {
orderSelect.selectAllFlag = !orderSelect.selectAllFlag
if (orderSelect.selectAllFlag) {
// //
orderSelect.selectData?.set(queryParams.pageNo, new Set<string>())
for (i = 0; i < list.value.length; i++) {
list.value[i]['itemSelect'] = true
orderSelect.selectData?.get(queryParams.pageNo)?.add(list.value[i].id) //id
}
orderSelect.selectTotal = total.value
} else {
//
for (i; i < list.value.length; i++) {
list.value[i]['itemSelect'] = false
}
initSelect() //
}
}
}
//
const unSelectListRecord = (id: string, op: string) => {
if (!orderSelect.selectAllFlag) {
return
}
if (op == 'add') {
orderSelect.unSelectList.add(id)
} else {
orderSelect.unSelectList.delete(id)
}
}
/***复选框选中 */
const handcheckclick = (row: any) => {
if (row.itemSelect) {
orderSelect.selectTotal += 1
if (!orderSelect.selectData.has(queryParams.pageNo)) {
orderSelect.selectData?.set(queryParams.pageNo, new Set<string>())
}
orderSelect.selectData?.get(queryParams.pageNo)?.add(row.id)
unSelectListRecord(row.id, 'del')
} else {
orderSelect.selectTotal -= 1
orderSelect.selectData.get(queryParams.pageNo)?.delete(row.id)
unSelectListRecord(row.id, 'add')
}
}
/**
* 导出数据
*/
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
orderSelect.queryParams = queryParams
//
exportLoading.value = true
// unseleectList
// selectData
console.log(orderSelect)
download.excel(orderSelect as any, '订单信息.xls') //?
} catch {
} finally {
exportLoading.value = false
}
//TODO
exportLoading.value = false
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
queryType.v = '' //
queryType.k = ''
//0.1s watch
setTimeout(() => {
initSelect() //
handleQuery()
}, 100)
}
/**选中状态初始化**/
const initSelect = () => {
orderSelect.queryParams = {} as TradeOrderPageReqVO
orderSelect.selectTotal = 0
orderSelect.selectAllFlag = false
orderSelect.selectData?.clear()
orderSelect.unSelectList?.clear()
}
const getList = async () => {
loading.value = true
try {
const data = await TradeOrderApi.getOrderList(queryParams)
list.value = data.list
total.value = data.total
let i = 0
if (orderSelect.selectData && orderSelect.selectData.has(queryParams.pageNo)) {
//
for (i = 0; i < list.value.length; i++) {
if (orderSelect.selectData.get(queryParams.pageNo)!.has(list.value[i].id)) {
list.value[i]['itemSelect'] = true //
} else {
list.value[i]['itemSelect'] = false
}
}
} else if (orderSelect.selectAllFlag) {
//
orderSelect.selectData.set(queryParams.pageNo, new Set<string>())
for (i = 0; i < list.value.length; i++) {
list.value[i]['itemSelect'] = true
orderSelect.selectData.get(queryParams.pageNo)!.add(list.value[i].id)
}
} else {
//
for (i; i < list.value.length; i++) {
list.value[i]['itemSelect'] = false //
}
}
} finally {
loading.value = false
}
}
/**
* 跳转订单详情
*/
const showOrderDetail = (row: any) => {
push({ name: 'TradeOrderDetail', query: { id: row.id } })
}
/**
* 跳转用户详情
*/
const goUserDetail = (row: any) => {
console.log('TODO User Detail: ' + row.userId)
}
/**
* 发货
*/
const sendXX = (row: any) => {
console.log('TODO Send XX: ' + row.no)
}
/**
* 商品图预览
* @param imgUrl
*/
const imagePreview = (imgUrl: string) => {
imageViewerList.value = [imgUrl]
imgViewVisible.value = true
}
// 使
watch(
() => [queryType.k, queryType.v],
([newK, newV], [oldK]) => {
//oldKvalue
if (oldK != newK) {
if (oldK == 'no' && queryParams.no != '') {
queryParams.no = ''
} else if (oldK == 'userId' && queryParams.userId != '') {
queryParams.userId = ''
} else if (oldK == 'userNickname' && queryParams.userNickname != '') {
queryParams.userNickname = ''
} else if (oldK == 'userMobile' && queryParams.userMobile !== '') {
queryParams.userMobile = ''
} else if (oldK == 'spuName' && queryParams.spuName !== '') {
queryParams.spuName = ''
} else if (oldK == 'itemCount' && queryParams.itemCount !== '') {
queryParams.itemCount = ''
} else if (oldK == '' && queryParams.all !== '') {
queryParams.all = ''
}
}
// kValue
if (newK == 'no') {
queryParams.no = newV
} else if (newK == 'userId') {
queryParams.userId = newV
} else if (newK == 'userNickname') {
queryParams.userNickname = newV
} else if (newK == 'userMobile') {
queryParams.userMobile = newV
} else if (newK == 'spuName') {
queryParams.spuName = newV
} else if (newK == 'itemCount') {
queryParams.itemCount = newV
} else if (newK == '') {
queryParams.all = newV
}
}
)
/** 初始化 **/
onMounted(() => {
initSelect()
getList()
})
</script>

View File

@ -0,0 +1,365 @@
<template>
<ContentWrap>
<!-- 订单信息 -->
<el-descriptions title="订单信息">
<el-descriptions-item label="订单号: ">{{ order.no }}</el-descriptions-item>
<el-descriptions-item label="配送方式: ">物流配送</el-descriptions-item>
<!-- TODO 芋艿待实现 -->
<el-descriptions-item label="营销活动: ">物流配送</el-descriptions-item>
<!-- TODO 芋艿待实现 -->
<el-descriptions-item label="订单类型: ">
<dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="order.type" />
</el-descriptions-item>
<el-descriptions-item label="收货人: ">{{ order.receiverName }}</el-descriptions-item>
<el-descriptions-item label="买家留言: ">{{ order.userRemark }}</el-descriptions-item>
<el-descriptions-item label="订单来源: ">
<dict-tag :type="DICT_TYPE.TERMINAL" :value="order.terminal" />
</el-descriptions-item>
<el-descriptions-item label="联系电话: ">{{ order.receiverMobile }}</el-descriptions-item>
<el-descriptions-item label="商家备注: ">{{ order.remark }}</el-descriptions-item>
<el-descriptions-item label="支付单号: ">{{ order.payOrderId }}</el-descriptions-item>
<el-descriptions-item label="付款方式: ">
<dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE_TYPE" :value="order.payChannelCode" />
</el-descriptions-item>
<!-- <el-descriptions-item label="买家: ">{{ order.user.nickname }}</el-descriptions-item> -->
<!-- TODO 芋艿待实现跳转会员 -->
<el-descriptions-item label="收货地址: ">
{{ order.receiverAreaName }} {{ order.receiverDetailAddress }}
<el-link
v-clipboard:copy="order.receiverAreaName + ' ' + order.receiverDetailAddress"
v-clipboard:success="clipboardSuccess"
icon="ep:document-copy"
type="primary"
/>
</el-descriptions-item>
</el-descriptions>
<!-- 订单状态 -->
<el-descriptions title="订单状态" :column="1">
<el-descriptions-item label="订单状态: ">
<!-- TODO xiaobaistatus 一定有值哈不用判断 -->
<dict-tag
v-if="order.status !== ''"
:type="DICT_TYPE.TRADE_ORDER_STATUS"
:value="order.status"
/>
</el-descriptions-item>
<el-descriptions-item label-class-name="no-colon">
<el-button type="primary" size="small">调整价格</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">备注</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">发货</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">关闭订单</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">修改地址</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">打印电子面单</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">打印发货单</el-button>
<!-- TODO 芋艿待实现 -->
<el-button type="primary" size="small">确认收货</el-button>
<!-- TODO 芋艿待实现 -->
</el-descriptions-item>
<el-descriptions-item>
<template #label><span style="color: red">提醒: </span></template>
买家付款成功后货款将直接进入您的商户号微信支付宝<br />
请及时关注你发出的包裹状态确保可以配送至买家手中 <br />
如果买家表示没收到货或货物有问题请及时联系买家处理友好协商
</el-descriptions-item>
</el-descriptions>
<!-- 物流信息 TODO -->
<!-- 商品信息 -->
<el-descriptions title="商品信息">
<el-descriptions-item labelClassName="no-colon">
<el-row :gutter="20">
<el-col :span="15">
<el-table :data="order.items" border>
<el-table-column prop="spuName" label="商品" width="auto">
<template #default="{ row }">
{{ row.spuName }}
<el-tag
size="medium"
v-for="property in row.properties"
:key="property.propertyId"
>
{{ property.propertyName }}: {{ property.valueName }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="price" label="商品原价(元)" width="150">
<template #default="{ row }"> {{ (row.price / 100.0).toFixed(2) }} </template>
</el-table-column>
<el-table-column prop="count" label="数量" width="100" />
<el-table-column prop="payPrice" label="合计(元)" width="150">
<template #default="{ row }"> {{ (row.payPrice / 100.0).toFixed(2) }} </template>
</el-table-column>
<el-table-column prop="afterSaleStatus" label="售后状态" width="auto">
<template #default="{ row }">
<dict-tag
:type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
:value="row.afterSaleStatus"
/>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col :span="10" />
</el-row>
</el-descriptions-item>
<!-- 占位 -->
<!-- <el-descriptions-item v-for="item in 5" label-class-name="no-colon" :key="item" /> -->
</el-descriptions>
<el-descriptions column="6">
<el-descriptions-item label="商品总额: ">
<!-- TODO xiaobai: 是不是 (item.payPrice / 100.0).toFixed(2) -->
{{ parseFloat((order.totalPrice / 100.0) as unknown as string).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="运费金额: ">
{{ parseFloat((order.deliveryPrice / 100.0) as unknown as string).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="订单调价: ">
{{
parseFloat((order.adjustPrice / 100.0) as unknown as string).toFixed(2)
}}</el-descriptions-item
>
<el-descriptions-item>
<template #label><span style="color: red">商品优惠: </span></template>
<!-- 没理解TODO order.totalPrice - order.totalPrice -->
{{
parseFloat(((order.totalPrice - order.totalPrice) / 100.0) as unknown as string).toFixed(
2
)
}}
</el-descriptions-item>
<el-descriptions-item>
<template #label><span style="color: red">订单优惠: </span></template>
{{ parseFloat((order.discountPrice / 100.0) as unknown as string).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item>
<template #label><span style="color: red">积分抵扣: </span></template>
{{ parseFloat((order.pointPrice / 100.0) as unknown as string).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item v-for="item in 5" label-class-name="no-colon" :key="item" />
<!-- 占位 -->
<el-descriptions-item label="应付金额: ">
{{ (order.payPrice / 100.0).toFixed(2) }}
</el-descriptions-item>
</el-descriptions>
<!-- TODO 芋艿需要改改 -->
<div v-for="group in detailGroups" :key="group.title">
<el-descriptions v-bind="group.groupProps" :title="group.title">
<!-- 订单操作日志 -->
<el-descriptions-item v-if="group.key === 'orderLog'" labelClassName="no-colon">
<el-timeline>
<el-timeline-item
v-for="activity in detailInfo[group.key]"
:key="activity.timestamp"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-descriptions-item>
<!-- 物流信息 -->
<!-- TODO @xiaobai改成一个包裹哈目前只允许发货一次 -->
<el-descriptions-item v-if="group.key === 'expressInfo'" labelClassName="no-colon">
<!-- 循环包裹物流信息 -->
<div v-show="(pkgInfo = detailInfo[group.key]) !== null" style="border: 1px dashed">
<!-- 包裹详情 -->
<el-descriptions class="m-5">
<el-descriptions-item
v-for="(pkgChild, pkgCIdx) in group.children"
v-bind="pkgChild.childProps"
:key="`pkgChild_${pkgCIdx}`"
:label="pkgChild.label"
>
<!-- 包裹商品列表 -->
<template v-if="pkgChild.valueKey === 'goodsList' && pkgInfo[pkgChild.valueKey]">
<div
v-for="(goodInfo, goodInfoIdx) in pkgInfo[pkgChild.valueKey]"
:key="`goodInfo_${goodInfoIdx}`"
style="display: flex"
>
<el-image
style="width: 100px; height: 100px; flex: none"
:src="goodInfo.imgUrl"
/>
<el-descriptions :column="1">
<el-descriptions-item labelClassName="no-colon">{{
goodInfo.name
}}</el-descriptions-item>
<el-descriptions-item label="数量">{{ goodInfo.count }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<!-- 包裹物流详情 -->
<template v-else-if="pkgChild.valueKey === 'wlxq'">
<el-row :gutter="10">
<el-col :span="6" :offset="1">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in pkgInfo[pkgChild.valueKey]"
:key="index"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-col>
</el-row>
</template>
<template v-else>
{{ pkgInfo[pkgChild.valueKey] }}
</template>
</el-descriptions-item>
</el-descriptions>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</ContentWrap>
</template>
<script lang="ts" name="TradeOrderDetail" setup>
// TODO @xiaobai order order/detail index.vue
import { DICT_TYPE } from '@/utils/dict'
import * as TradeOrderApi from '@/api/mall/trade/order'
const message = useMessage() //
const { query } = useRoute()
const queryParams = reactive({
id: query.id
})
const dialogVisible = ref(false)
const loading = ref(false)
const order = ref<any>({
items: [],
user: {}
}) //
const detailGroups = ref([
{
title: '物流信息',
key: 'expressInfo',
children: [
{ label: '发货时间: ', valueKey: 'fhsj' },
{ label: '物流公司: ', valueKey: 'wlgs' },
{ label: '运单号: ', valueKey: 'ydh' },
{ label: '物流状态: ', valueKey: 'wlzt', childProps: { span: 3 } },
{ label: '物流详情: ', valueKey: 'wlxq' }
]
},
{
title: '订单操作日志',
key: 'orderLog'
}
])
const detailInfo = ref({
expressInfo:
//
{
label: '包裹1',
name: 'bg1',
fhsj: '2022-11-03 16:50:45',
wlgs: '极兔',
ydh: '2132123',
wlzt: '不支持此快递公司',
wlxq: [
{
content: '正在派送途中,请您准备签收(派件人:王涛,电话:13854563814)',
timestamp: '2018-04-15 15:00:16'
},
{
content: '快件到达 【烟台龙口东江村委营业点】',
timestamp: '2018-04-13 14:54:19'
},
{
content: '快件已发车',
timestamp: '2018-04-11 12:55:52'
},
{
content: '快件已发车',
timestamp: '2018-04-11 12:55:52'
},
{
content: '快件已发车',
timestamp: '2018-04-11 12:55:52'
}
]
},
orderLog: [
//
{
content: '买家【乌鸦】关闭了订单',
timestamp: '2018-04-15 15:00:16'
},
{
content: '买家【乌鸦】下单了',
timestamp: '2018-04-15 15:00:16'
}
],
goodsInfo: [] // tableData
})
// TODO
const getlist = async () => {
dialogVisible.value = true
loading.value = true
try {
const res = await TradeOrderApi.getOrderDetail(queryParams.id as unknown as number)
order.value = res
console.log(order)
} catch {
message.error('获取详情数据失败')
} finally {
loading.value = false
}
}
onMounted(async () => {
await getlist()
})
const clipboardSuccess = () => {
message.success('复制成功')
}
</script>
<style lang="scss" scoped>
:deep(.el-descriptions) {
&:not(:nth-child(1)) {
margin-top: 20px;
}
.el-descriptions__title {
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
margin-right: 10px;
width: 3px;
height: 20px;
background-color: #409eff;
}
}
.el-descriptions-item__container {
margin: 0 10px;
.no-colon {
margin: 0;
&::after {
content: '';
}
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<ContentWrap>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="hideId" v-show="false">
<el-input v-model="formData.id" />
</el-form-item>
<!-- TODO @xiaqing展示给用户的字段名可以和 crmeb 保持一直然后每一个表单都有类似 crmeb tip例如说积分抵用比例(1积分抵多少金额)单位 -->
<el-form-item label="积分抵扣" prop="tradeDeductEnable">
<el-switch v-model="formData.tradeDeductEnable" />
</el-form-item>
<!-- TODO @xiaqing用户看到的是元最多 2 分是后端的存储哈 -->
<el-form-item label="抵扣单位(分)" prop="tradeDeductUnitPrice">
<el-input-number
v-model="formData.tradeDeductUnitPrice"
placeholder="请输入抵扣单位(分)"
style="width: 300px"
/>
</el-form-item>
<el-form-item label="积分抵扣最大值" prop="tradeDeductMaxPrice">
<el-input-number
v-model="formData.tradeDeductMaxPrice"
placeholder="请输入积分抵扣最大值"
style="width: 300px"
/>
</el-form-item>
<el-form-item label="1 元赠送多少分" prop="tradeGivePoint">
<el-input-number
v-model="formData.tradeGivePoint"
placeholder="请输入 1 元赠送多少积分"
style="width: 300px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/point/config'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) // 12
const formData = ref({
id: undefined,
tradeDeductEnable: undefined,
tradeDeductUnitPrice: undefined,
tradeDeductMaxPrice: undefined,
tradeGivePoint: undefined
})
const formRules = reactive({})
const formRef = ref() // Ref
/** 修改积分配置 */
const onSubmit = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as ConfigApi.ConfigVO
await ConfigApi.saveConfig(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
/** 获得积分配置 */
const getConfig = async () => {
try {
const data = await ConfigApi.getConfig()
formData.value = data
} finally {
}
}
onMounted(() => {
getConfig()
})
</script>

View File

@ -0,0 +1,180 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="业务编码" prop="bizId">
<el-input v-model="formData.bizId" placeholder="请输入业务编码" />
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-select v-model="formData.bizType" placeholder="请选择业务类型">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作类型" prop="type">
<el-select v-model="formData.type" placeholder="操作类型">
<el-option label="增加" value="1" />
<el-option label="扣减" value="0" />
</el-select>
</el-form-item>
<el-form-item label="积分标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入积分标题" />
</el-form-item>
<el-form-item label="积分描述">
<Editor :model-value="formData.description" height="150px" />
</el-form-item>
<el-form-item label="积分" prop="point">
<el-input v-model="formData.point" placeholder="请输入积分" />
</el-form-item>
<el-form-item label="变动后的积分" prop="totalPoint">
<el-input v-model="formData.totalPoint" placeholder="请输入变动后的积分" />
</el-form-item>
<el-form-item label="积分状态" prop="status">
<el-select v-model="formData.status" placeholder="积分状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="用户id" prop="userId">
<el-input v-model="formData.userId" placeholder="请输入用户id" />
</el-form-item>
<el-form-item label="冻结时间" prop="freezingTime">
<el-date-picker
v-model="formData.freezingTime"
type="date"
value-format="x"
placeholder="选择冻结时间"
/>
</el-form-item>
<el-form-item label="解冻时间" prop="thawingTime">
<el-date-picker
v-model="formData.thawingTime"
type="date"
value-format="x"
placeholder="选择解冻时间"
/>
</el-form-item>
<el-form-item label="发生时间" prop="createDate">
<el-date-picker
v-model="formData.createDate"
type="date"
value-format="x"
placeholder="选择发生时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getStrDictOptions, getIntDictOptions } from '@/utils/dict'
import * as RecordApi from '@/api/point/record'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
bizId: undefined,
bizType: undefined,
type: undefined,
title: undefined,
description: undefined,
point: undefined,
totalPoint: undefined,
status: undefined,
userId: undefined,
freezingTime: undefined,
thawingTime: undefined,
createDate: undefined
})
const formRules = reactive({
totalPoint: [{ required: true, message: '变动后的积分不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await RecordApi.getRecord(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as RecordApi.RecordVO
if (formType.value === 'create') {
await RecordApi.createRecord(data)
message.success(t('common.createSuccess'))
} else {
await RecordApi.updateRecord(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
// TODO @xiaqing
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
bizId: undefined,
bizType: undefined,
type: undefined,
title: undefined,
description: undefined,
point: undefined,
totalPoint: undefined,
status: undefined,
userId: undefined,
freezingTime: undefined,
thawingTime: undefined,
createDate: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,193 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="业务编码" prop="bizId">
<el-input
v-model="queryParams.bizId"
placeholder="请输入业务编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-select
v-model="queryParams.bizType"
placeholder="请选择业务类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作类型" prop="type">
<el-select v-model="queryParams.type" placeholder="操作类型" clearable class="!w-240px">
<el-option label="增加" value="1" />
<el-option label="扣减" value="0" />
</el-select>
</el-form-item>
<el-form-item label="积分标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入积分标题"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="积分状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="获得时间" prop="createDate">
<el-date-picker
v-model="queryParams.createDate"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<!-- TODO @xiaqing展示用户的昵称哈 -->
<el-table-column label="用户" align="center" prop="userId" />
<el-table-column label="积分标题" align="center" prop="title" />
<el-table-column label="积分描述" align="center" prop="description" />
<el-table-column
label="获得时间"
align="center"
prop="createDate"
:formatter="dateFormatter"
/>
<!-- todo @xiaqing可以参考 crmeb 的展示把积分和增加减少放一起用红色和绿色展示 -->
<el-table-column
label="操作类型"
align="center"
prop="type"
:formatter="
(a, b, c) => {
return c === '1' ? '增加' : '扣减'
}
"
/>
<el-table-column label="积分" align="center" prop="point" />
<el-table-column label="变动后的积分" align="center" prop="totalPoint" />
<el-table-column label="业务编码" align="center" prop="bizId" />
<el-table-column label="业务类型" align="center" prop="bizType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MEMBER_POINT_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="冻结时间"
align="center"
prop="freezingTime"
:formatter="dateFormatter"
/>
<el-table-column
label="解冻时间"
align="center"
prop="thawingTime"
:formatter="dateFormatter"
/>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<RecordForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getStrDictOptions, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as RecordApi from '@/api/point/record'
import RecordForm from './RecordForm.vue'
defineOptions({ name: 'PointRecord' })
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
bizId: null,
bizType: null,
type: null,
title: null,
status: null,
createDate: []
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await RecordApi.getRecordPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,97 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="签到天数" prop="day">
<el-input-number v-model="formData.day" :min="1" :max="7" :precision="0" />
<el-text class="mx-1" style="margin-left: 10px" type="danger">
只允许设置1-7默认签到7天为一个周期</el-text
>
</el-form-item>
<el-form-item label="签到分数" prop="point">
<el-input-number v-model="formData.point" :precision="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as SignInConfigApi from '@/api/point/signInConfig'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
day: undefined,
point: undefined
})
const formRules = reactive({})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await SignInConfigApi.getSignInConfig(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as SignInConfigApi.SignInConfigVO
if (formType.value === 'create') {
await SignInConfigApi.createSignInConfig(data)
message.success(t('common.createSuccess'))
} else {
await SignInConfigApi.updateSignInConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
day: undefined,
point: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,171 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<!-- TODO @xiaqing搜索可以去掉因为一共就没几条配置哈 -->
<el-form-item label="签到天数" prop="day">
<el-input
v-model="queryParams.day"
placeholder="请输入签到天数"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['point:sign-in-config:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['point:sign-in-config:export']"
>
<!-- TODO @xiaqing四个功能的导出都可以去掉 -->
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<!-- TODO @xiaqing展示优化下改成第 1 2 天这种 -->
<el-table-column label="签到天数" align="center" prop="day" />
<el-table-column label="获得积分" align="center" prop="point" />
<!-- TODO @xiaqing展示一个是否开启 -->
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['point:sign-in-config:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['point:sign-in-config:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<SignInConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import download from '@/utils/download'
import * as SignInConfigApi from '@/api/point/signInConfig'
import SignInConfigForm from './SignInConfigForm.vue'
defineOptions({ name: 'SignInConfig' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
day: null
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
// TODO @xiaqing
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SignInConfigApi.getSignInConfigPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await SignInConfigApi.deleteSignInConfig(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await SignInConfigApi.exportSignInConfig(queryParams)
download.excel(data, '积分签到规则.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,99 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="签到用户" prop="userId">
<el-input v-model="formData.userId" placeholder="请输入签到用户" />
</el-form-item>
<el-form-item label="签到天数" prop="day">
<el-input v-model="formData.day" placeholder="请输入签到天数" />
</el-form-item>
<el-form-item label="签到的分数" prop="point">
<el-input v-model="formData.point" placeholder="请输入签到的分数" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as SignInRecordApi from '@/api/point/signInRecord'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
userId: undefined,
day: undefined,
point: undefined
})
const formRules = reactive({})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await SignInRecordApi.getSignInRecord(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as SignInRecordApi.SignInRecordVO
if (formType.value === 'create') {
await SignInRecordApi.createSignInRecord(data)
message.success(t('common.createSuccess'))
} else {
await SignInRecordApi.updateSignInRecord(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
userId: undefined,
day: undefined,
point: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,179 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="签到用户" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入签到用户"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="签到天数" prop="day">
<el-input
v-model="queryParams.day"
placeholder="请输入签到天数"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="签到时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['point:sign-in-record:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<!-- TODO @xiaqing展示用户昵称 -->
<el-table-column label="签到用户" align="center" prop="userId" />
<el-table-column label="签到天数" align="center" prop="day" />
<el-table-column label="获得积分" align="center" prop="point" />
<el-table-column
label="签到时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['point:sign-in-record:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<SignInRecordForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="SignInRecord">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SignInRecordApi from '@/api/point/signInRecord'
import SignInRecordForm from './SignInRecordForm.vue'
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
day: null,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SignInRecordApi.getSignInRecordPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
// const formRef = ref()
// const openForm = (type: string, id?: number) => {
// formRef.value.open(type, id)
// }
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await SignInRecordApi.deleteSignInRecord(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await SignInRecordApi.exportSignInRecord(queryParams)
download.excel(data, '用户签到积分.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -227,7 +227,7 @@ onMounted(async () => {
})
</script>
<style lang="scss" scoped>
@media (min-width: 992px) and (max-width: 1300px) {
@media (width >= 992px) and (width <= 1300px) {
.waterfall {
column-count: 3;
}
@ -237,7 +237,7 @@ onMounted(async () => {
}
}
@media (min-width: 768px) and (max-width: 991px) {
@media (width >= 768px) and (width <= 991px) {
.waterfall {
column-count: 2;
}
@ -247,7 +247,7 @@ onMounted(async () => {
}
}
@media (max-width: 767px) {
@media (width <= 767px) {
.waterfall {
column-count: 1;
}

View File

@ -67,19 +67,19 @@ const emit = defineEmits<{
}
}
@media (min-width: 992px) and (max-width: 1300px) {
@media (width >= 992px) and (width <= 1300px) {
.waterfall {
column-count: 3;
}
}
@media (min-width: 768px) and (max-width: 991px) {
@media (width >= 768px) and (width <= 991px) {
.waterfall {
column-count: 2;
}
}
@media (max-width: 767px) {
@media (width <= 767px) {
.waterfall {
column-count: 1;
}

View File

@ -31,7 +31,7 @@ const emit = defineEmits<{
</script>
<style lang="scss" scoped>
@media (min-width: 992px) and (max-width: 1300px) {
@media (width >= 992px) and (width <= 1300px) {
.waterfall {
column-count: 3;
}
@ -41,7 +41,7 @@ const emit = defineEmits<{
}
}
@media (min-width: 768px) and (max-width: 991px) {
@media (width >= 768px) and (width <= 991px) {
.waterfall {
column-count: 2;
}
@ -51,7 +51,7 @@ const emit = defineEmits<{
}
}
@media (max-width: 767px) {
@media (width <= 767px) {
.waterfall {
column-count: 1;
}

View File

@ -37,7 +37,12 @@
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
>
<!-- 展示 HTML 内容 -->
<template #templateContent="{ row }">
<div v-dompurify-html="row.templateContent"></div>
</template>
</Descriptions>
<template #footer>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />

View File

@ -1,41 +1,42 @@
module.exports = {
root: true,
plugins: ['stylelint-order'],
customSyntax: 'postcss-html',
extends: ['stylelint-config-standard'],
customSyntax: 'postcss-html',
rules: {
'function-no-unknown': null,
'selector-class-pattern': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'deep']
ignorePseudoClasses: ['global']
}
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep']
}
],
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin']
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen', 'function', 'if', 'each', 'include', 'mixin']
}
],
'no-empty-source': null,
'import-notation': null,
'named-grid-areas-no-invalid': null,
'unicode-bom': 'never',
'no-descending-specificity': null,
'font-family-no-missing-generic-family-keyword': null,
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
'declaration-block-trailing-semicolon': null,
// 'declaration-block-trailing-semicolon': 'always',
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'first-nested']
}
],
'unit-no-unknown': [
true,
{
ignoreUnits: ['rpx']
}
],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'order/order': [
[
'dollar-variables',
@ -52,165 +53,14 @@ module.exports = {
},
'rules'
],
{
severity: 'warning'
}
],
// Specify the alphabetical order of the attributes in the declaration block
'order/properties-order': [
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height',
'max-width',
'max-height',
'min-width',
'min-height',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-collapse',
'margin-top-collapse',
'margin-right-collapse',
'margin-bottom-collapse',
'margin-left-collapse',
'overflow',
'overflow-x',
'overflow-y',
'clip',
'clear',
'font',
'font-family',
'font-size',
'font-smoothing',
'osx-font-smoothing',
'font-style',
'font-weight',
'hyphens',
'src',
'line-height',
'letter-spacing',
'word-spacing',
'color',
'text-align',
'text-decoration',
'text-indent',
'text-overflow',
'text-rendering',
'text-size-adjust',
'text-shadow',
'text-transform',
'word-break',
'word-wrap',
'white-space',
'vertical-align',
'list-style',
'list-style-type',
'list-style-position',
'list-style-image',
'pointer-events',
'cursor',
'background',
'background-attachment',
'background-color',
'background-image',
'background-position',
'background-repeat',
'background-size',
'border',
'border-collapse',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-color',
'border-image',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-spacing',
'border-style',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-width',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-radius',
'border-top-right-radius',
'border-bottom-right-radius',
'border-bottom-left-radius',
'border-top-left-radius',
'border-radius-topright',
'border-radius-bottomright',
'border-radius-bottomleft',
'border-radius-topleft',
'content',
'quotes',
'outline',
'outline-offset',
'opacity',
'filter',
'visibility',
'size',
'zoom',
'transform',
'box-align',
'box-flex',
'box-orient',
'box-pack',
'box-shadow',
'box-sizing',
'table-layout',
'animation',
'animation-delay',
'animation-duration',
'animation-iteration-count',
'animation-name',
'animation-play-state',
'animation-timing-function',
'animation-fill-mode',
'transition',
'transition-delay',
'transition-duration',
'transition-property',
'transition-timing-function',
'background-clip',
'backface-visibility',
'resize',
'appearance',
'user-select',
'interpolation-mode',
'direction',
'marks',
'page',
'set-link-source',
'unicode-bidi',
'speak'
{ severity: 'warning' }
]
},
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
overrides: [
{
files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
extends: ['stylelint-config-recommended', 'stylelint-config-html'],
extends: ['stylelint-config-recommended'],
rules: {
'keyframes-name-pattern': null,
'selector-pseudo-class-no-unknown': [
@ -226,6 +76,11 @@ module.exports = {
}
]
}
},
{
files: ['*.less', '**/*.less'],
customSyntax: 'postcss-less',
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue']
}
]
}