add trade order 20230606

This commit is contained in:
xiaobai 2023-06-06 01:36:22 +08:00
commit e2d795f49d
30 changed files with 2230 additions and 150 deletions

View File

@ -19,13 +19,13 @@ VITE_API_URL=/admin-api
VITE_BASE_PATH=/
# 是否删除debugger
VITE_DROP_DEBUGGER=false
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
VITE_SOURCEMAP=false
# 输出路径
VITE_OUT_DIR=dist-dev

View File

@ -65,6 +65,7 @@
"url": "^0.11.0",
"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",

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 // 是否热卖
@ -62,12 +60,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 })
}
@ -90,3 +88,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,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

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

@ -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'
@ -196,7 +196,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

@ -38,9 +38,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)
@ -61,6 +62,8 @@ const setupAll = async () => {
await router.isReady()
app.use(VueDOMPurifyHTML)
app.mount('#app')
}

View File

@ -395,6 +395,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
title: '编辑商品',
activeMenu: '/product/product-spu'
}
},
{
path: 'productSpuDetail/: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

@ -222,7 +222,7 @@ export const PayRefundStatusEnum = {
}
/**
* SPU枚举类
* SPU
*/
export const ProductSpuStatusEnum = {
RECYCLE: {
@ -238,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

@ -145,12 +145,14 @@ export enum DICT_TYPE {
MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
// ========== MALL 模块 ==========
PRODUCT_UNIT = 'product_unit', // 商品单位
PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
// ========== MALL 交易模块 ==========
EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式
// ========== MALL - PROMOTION 模块 ==========
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', // 营销的条件类型枚举
//===add by 20230530====
// ========== MALL - ORDER 模块 ==========

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
}
@ -302,3 +306,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

@ -16,20 +16,20 @@
</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">
<el-button style="float: right" @click="copy(formData)">
{{ t('common.copy') }}
</el-button>
<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'

View File

@ -46,7 +46,7 @@
{{ t('common.copy') }}
</el-button>
<div>
<pre><code class="hljs" v-html="highlightedCode(item)"></code></pre>
<pre><code v-dompurify-html="highlightedCode(item)" class="hljs"></code></pre>
</div>
</el-tab-pane>
</el-tabs>

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,13 +21,16 @@
<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>
@ -42,16 +47,17 @@ import { convertToInteger, formatToFraction } from '@/utils'
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 isDetail = ref(false) //
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // Ref
const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // Ref
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // Ref
// spu
const formData = ref<ProductSpuApi.SpuType>({
const formData = ref<ProductSpuApi.Spu>({
name: '', //
categoryId: null, //
keyword: '', //
@ -59,7 +65,7 @@ const formData = ref<ProductSpuApi.SpuType>({
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTemplateId: 1, //
deliveryTemplateId: null, //
brandId: null, //
specType: false, //
subCommissionType: false, //
@ -90,12 +96,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 +129,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 +160,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 +180,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,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<!-- TODO @puhui999只能选根节点 -->
<!-- TODO @puhui999只能选根节点 fix: 已完善-->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="formData.categoryId"
@ -17,6 +24,7 @@
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
@change="categoryNodeClick"
/>
</el-form-item>
</el-col>
@ -60,9 +68,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 +109,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 +124,93 @@
<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>
<!-- 情况二详情 -->
<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 表单 -->
<!-- TODO @puhui999: ProductPropertyAddForm 是不是更合适呀 -->
<ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
</template>
<script lang="ts" name="ProductSpuBasicInfoForm" 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 { 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 ======
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, //
@ -185,9 +261,10 @@ watch(
formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
url: item
}))
// TODO @puhui999if return
//
if (formData.specType) {
if (!formData.specType) {
return
}
// skus propertyList
const properties = []
formData.skus.forEach((sku) => {
@ -204,7 +281,6 @@ watch(
})
})
propertyList.value = properties
}
},
{
immediate: true
@ -216,6 +292,11 @@ watch(
*/
const emit = defineEmits(['update:activeName'])
const validate = async () => {
// sku
if (!skuListRef.value.validateSku()) {
message.warning('商品相关价格不能低于 0.01 元!!')
throw new Error('商品相关价格不能低于 0.01 元!!')
}
//
if (!productSpuBasicInfoRef) return
return await unref(productSpuBasicInfoRef).validate((valid) => {
@ -263,12 +344,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,51 @@
<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'
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'
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,55 @@
</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'
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'
const { allSchemas } = useCrudSchemas(otherSettingsSchema)
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 otherSettingsFormRef = ref() // Ref
//
const formData = ref<SpuType>({
const formData = ref<Spu>({
sort: 1, //
giveIntegral: 1, //
virtualSalesCount: 1, //

View File

@ -90,8 +90,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"
: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,10 @@
min-width="120"
>
<template #default="{ row }">
<!-- TODO puhui999展示成蓝色有点区分度哈 -->
<!-- TODO puhui999展示成蓝色有点区分度哈 fix-->
<span style="font-weight: bold; color: #40aaff">
{{ row.properties[index]?.valueName }}
</span>
</template>
</el-table-column>
</template>
@ -73,7 +77,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 +101,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 +110,108 @@
</template>
</el-table-column>
</el-table>
<!-- 情况二详情 -->
<el-table
v-if="isDetail"
:data="formData!.skus"
border
max-height="500"
size="small"
style="width: 99%"
>
<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>
</template>
<script lang="ts" name="SkuList" setup>
import { PropType } from 'vue'
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'
const props = defineProps({
propFormData: {
type: Object as PropType<SpuType>,
type: Object as PropType<Spu>,
default: () => {}
},
propertyList: {
type: Array,
default: () => []
},
isBatch: propTypes.bool.def(false) //
isBatch: propTypes.bool.def(false), //
isDetail: 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 +225,44 @@ const skuList = ref<SkuType[]>([
subCommissionSecondPrice: 0 //
}
]) //
// TODO @puhui999 0.01
/** 商品图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
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 = (): boolean => {
const checks = ['price', 'marketPrice', 'costPrice']
let validate = true //
for (const sku of formData.value!.skus) {
if (checks.some((check) => sku[check] < 0.01)) {
validate = false //
break
}
}
return validate
}
/**
* 将传进来的值赋值给 skuList
@ -185,14 +290,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 +311,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)
}
}
@ -222,7 +326,7 @@ const generateTableData = (propertyList: any[]) => {
*/
const validateData = (propertyList: any[]) => {
const skuPropertyIds = []
formData.value.skus.forEach((sku) =>
formData.value!.skus!.forEach((sku) =>
sku.properties
?.map((property) => property.propertyId)
.forEach((propertyId) => {
@ -263,7 +367,7 @@ watch(
() => props.propertyList,
(propertyList) => {
//
if (!formData.value.specType) {
if (!formData.value!.specType) {
return
}
// 使
@ -313,5 +417,5 @@ watch(
}
)
// sku
defineExpose({ generateTableData })
defineExpose({ generateTableData, validateSku })
</script>

View File

@ -0,0 +1,106 @@
import { CrudSchema } from '@/hooks/web/useCrudSchemas'
// TODO @puhui999如果只要 detail可以不用 CrudSchema只要描述的 Schema
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,16 @@
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 -->
<!-- TODO 分类只能选择二级分类目前还没做还是先以联调通顺为主 fixL: 已完善 -->
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="queryParams.categoryId"
@ -29,6 +27,7 @@
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
@change="nodeClick"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
@ -80,7 +79,7 @@
/>
</el-tabs>
<el-table v-loading="loading" :data="list">
<!-- TODO puhui这几个属性哈一行三个
<!-- TODO puhui这几个属性哈一行三个 fix
商品分类服装鞋包/箱包
商品市场价格100.00
成本价0.00
@ -88,23 +87,52 @@
虚拟销量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="市场价:">
<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-form-item label="成本价:">
</el-col>
<el-col :span="8">
<el-form-item label="成本价:">
<span>{{ formatToFraction(row.costPrice) }}</span>
</el-form-item>
<el-form-item label="虚拟销量:">
</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 +171,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">
@ -202,7 +234,7 @@ 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'
@ -256,12 +288,14 @@ const getTabsCount = async () => {
const queryParams = ref({
pageNo: 1,
pageSize: 10,
tabType: 0
tabType: 0,
name: '',
categoryId: null
}) //
const queryFormRef = ref() // Ref
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName
queryParams.value.tabType = tab.paneName as number
getList()
}
@ -372,8 +406,8 @@ const openForm = (id?: number) => {
/**
* 查看商品详情
*/
const openDetail = () => {
message.alert('查看详情未完善!!!')
const openDetail = (id?: number) => {
push('/product/productSpuDetail/' + id)
}
/** 导出按钮操作 */
@ -391,7 +425,7 @@ const handleExport = async () => {
}
}
// TODO @puhui999fix:
//
watch(
() => currentRoute.value,
() => {
@ -400,6 +434,22 @@ 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,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

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

@ -110,6 +110,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

@ -3,7 +3,7 @@
<Descriptions :data="detailData" :schema="allSchemas.detailSchema">
<!-- 展示 HTML 内容 -->
<template #templateContent="{ row }">
<div v-html="row.templateContent"></div>
<div v-dompurify-html="row.templateContent"></div>
</template>
</Descriptions>
</Dialog>