diff --git a/src/api/mall/promotion/coupon/coupon.ts b/src/api/mall/promotion/coupon/coupon.ts index 565b86f7..2ebff5da 100755 --- a/src/api/mall/promotion/coupon/coupon.ts +++ b/src/api/mall/promotion/coupon/coupon.ts @@ -16,3 +16,11 @@ export const getCouponPage = async (params: PageParam) => { params: params }) } + +// 发送优惠券 +export const sendCoupon = async (data: any) => { + return request.post({ + url: '/promotion/coupon/send', + data: data + }) +} diff --git a/src/api/mall/promotion/coupon/couponTemplate.ts b/src/api/mall/promotion/coupon/couponTemplate.ts index 6a58876e..243e22ee 100755 --- a/src/api/mall/promotion/coupon/couponTemplate.ts +++ b/src/api/mall/promotion/coupon/couponTemplate.ts @@ -9,7 +9,7 @@ export interface CouponTemplateVO { takeType: number usePrice: number productScope: number - productSpuIds: string + productScopeValues: number[] validityType: number validStartTime: Date validEndTime: Date diff --git a/src/api/mall/trade/order/index.ts b/src/api/mall/trade/order/index.ts index 9413b73c..d4a22944 100644 --- a/src/api/mall/trade/order/index.ts +++ b/src/api/mall/trade/order/index.ts @@ -15,11 +15,11 @@ export interface OrderVO { cancelTime?: Date | null // 订单取消时间 cancelType?: number | null // 取消类型 remark?: string // 商家备注 - payOrderId: number | null // 支付订单编号 + payOrderId?: number | null // 支付订单编号 payed?: boolean // 是否已支付 payTime?: Date | null // 付款时间 payChannelCode?: string // 支付渠道 - originalPrice?: number | null // 商品原价(总) + totalPrice?: number | null // 商品原价(总) orderPrice?: number | null // 订单原价(总) discountPrice?: number | null // 订单优惠(总) deliveryPrice?: number | null // 运费金额 @@ -44,12 +44,19 @@ export interface OrderVO { pointPrice?: number | null // 积分抵扣的金额 receiverAreaName?: string //收件人地区名字 items?: OrderItemRespVO[] // 订单项列表 - //用户信息 + // 用户信息 user?: { id?: number | null nickname?: string avatar?: string } + // 订单操作日志 + orderLog: orderLog[] +} + +export interface orderLog { + content?: string + createTime?: Date } export interface OrderItemRespVO { @@ -94,6 +101,11 @@ export const getOrder = async (id: number | null) => { return await request.get({ url: `/trade/order/get-detail?id=` + id }) } +// 查询交易订单物流详情 +export const getExpressTrackList = async (id: number | null) => { + return await request.get({ url: `/trade/order/get-express-track-list?id=` + id }) +} + export interface DeliveryVO { id: number // 订单编号 logisticsId: number | null // 物流公司编号 diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cd346a5c..8d783635 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -209,17 +209,39 @@ export const CouponTemplateValidityTypeEnum = { } } +/** + * 优惠劵模板的领取方式的枚举 + */ +export const CouponTemplateTakeTypeEnum = { + USER: { + type: 1, + name: '直接领取' + }, + ADMIN: { + type: 2, + name: '指定发放' + }, + REGISTER: { + type: 3, + name: '新人券' + } +} + /** * 营销的商品范围枚举 */ export const PromotionProductScopeEnum = { ALL: { scope: 1, - name: '全部商品参与' + name: '通用劵' }, SPU: { scope: 2, - name: '指定商品参与' + name: '商品劵' + }, + CATEGORY: { + scope: 3, + name: '品类劵' } } diff --git a/src/views/mall/product/brand/index.vue b/src/views/mall/product/brand/index.vue index 9250befd..c89fe520 100644 --- a/src/views/mall/product/brand/index.vue +++ b/src/views/mall/product/brand/index.vue @@ -59,7 +59,7 @@ <el-table-column label="品牌名称" prop="name" sortable /> <el-table-column label="品牌图片" align="center" prop="picUrl"> <template #default="scope"> - <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="品牌图片" class="h-100px" /> + <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="品牌图片" class="h-30px" /> </template> </el-table-column> <el-table-column label="品牌排序" align="center" prop="sort" /> diff --git a/src/views/mall/product/category/components/ProductCategorySelect.vue b/src/views/mall/product/category/components/ProductCategorySelect.vue new file mode 100644 index 00000000..179a6a3b --- /dev/null +++ b/src/views/mall/product/category/components/ProductCategorySelect.vue @@ -0,0 +1,47 @@ +<template> + <el-tree-select + v-model="selectCategoryId" + :data="categoryList" + :props="defaultProps" + :multiple="multiple" + :show-checkbox="multiple" + class="w-1/1" + node-key="id" + placeholder="请选择商品分类" + /> +</template> +<script lang="ts" setup> +import { defaultProps, handleTree } from '@/utils/tree' +import * as ProductCategoryApi from '@/api/mall/product/category' +import { oneOfType } from 'vue-types' +import { propTypes } from '@/utils/propTypes' + +/** 商品分类选择组件 */ +defineOptions({ name: 'ProductCategorySelect' }) + +const props = defineProps({ + modelValue: oneOfType([propTypes.number.def(undefined), propTypes.array.def([])]).def(undefined), // 选中的ID + multiple: propTypes.bool.def(false) // 是否多选 +}) + +/** 选中的分类 ID */ +const selectCategoryId = computed({ + get: () => { + return props.modelValue + }, + set: (val: number | number[]) => { + emit('update:modelValue', val) + } +}) + +/** 分类选择 */ +const emit = defineEmits(['update:modelValue']) + +/** 初始化 **/ +const categoryList = ref([]) // 分类树 +onMounted(async () => { + // 获得分类树 + const data = await ProductCategoryApi.getCategoryList({}) + categoryList.value = handleTree(data, 'id', 'parentId') +}) +</script> diff --git a/src/views/mall/product/category/index.vue b/src/views/mall/product/category/index.vue index a2adfe7f..27ea5368 100644 --- a/src/views/mall/product/category/index.vue +++ b/src/views/mall/product/category/index.vue @@ -38,7 +38,7 @@ <el-table-column label="分类名称" prop="name" sortable /> <el-table-column label="移动端分类图" align="center" prop="picUrl"> <template #default="scope"> - <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-100px" /> + <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-30px" /> </template> </el-table-column> <el-table-column label="分类排序" align="center" prop="sort" /> diff --git a/src/views/mall/product/comment/index.vue b/src/views/mall/product/comment/index.vue index 1b0745ba..7c0737f5 100644 --- a/src/views/mall/product/comment/index.vue +++ b/src/views/mall/product/comment/index.vue @@ -115,7 +115,7 @@ :formatter="dateFormatter" width="170" /> - <el-table-column label="状态" align="center" width="65px"> + <el-table-column label="是否展示" align="center" width="80px"> <template #default="scope"> <el-switch v-model="scope.row.visible" diff --git a/src/views/mall/product/property/value/index.vue b/src/views/mall/product/property/value/index.vue index b8be9e3c..0d30fd74 100644 --- a/src/views/mall/product/property/value/index.vue +++ b/src/views/mall/product/property/value/index.vue @@ -147,7 +147,7 @@ const handleDelete = async (id: number) => { // 删除的二次确认 await message.delConfirm() // 发起删除 - await PropertyApi.deleteProperty(id) + await PropertyApi.deletePropertyValue(id) message.success(t('common.delSuccess')) // 刷新列表 await getList() diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index 1cc70298..81e2347d 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -328,24 +328,15 @@ 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)) { + for (const rule of props?.ruleConfig) { + const arg = getValue(sku, rule.name) + if (!rule.rule(arg)) { validate = false // 只要有一个不通过则直接不通过 - warningInfo = '商品相关价格不能低于 0.01 元!!' + warningInfo += rule.message break } } diff --git a/src/views/mall/product/spu/components/SpuTableSelect.vue b/src/views/mall/product/spu/components/SpuTableSelect.vue index f8560aa3..9e193257 100644 --- a/src/views/mall/product/spu/components/SpuTableSelect.vue +++ b/src/views/mall/product/spu/components/SpuTableSelect.vue @@ -1,81 +1,103 @@ <template> <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商品" width="70%"> - <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="categoryTreeList" - :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" show-overflow-tooltip> - <el-table-column label="#" width="55"> - <template #default="{ row }"> - <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSelected(row)" - > </el-radio - > - </template> - </el-table-column> - <el-table-column key="id" align="center" label="商品编号" prop="id" min-width="60" /> - <el-table-column label="商品图" min-width="80"> - <template #default="{ row }"> - <el-image - :src="row.picUrl" - class="w-30px h-30px" - :preview-src-list="[row.picUrl]" - preview-teleported + <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" /> - </template> - </el-table-column> - <el-table-column label="商品名称" min-width="200" prop="name" /> - <el-table-column label="商品分类" min-width="100" prop="categoryId"> - <template #default="{ row }"> - <span>{{ categoryList.find((c) => c.id === row.categoryId)?.name }}</span> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - v-model:limit="queryParams.pageSize" - v-model:page="queryParams.pageNo" - :total="total" - @pagination="getList" - /> + </el-col> + <el-col :span="6"> + <el-tree-select + v-model="queryParams.categoryId" + :data="categoryTreeList" + :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 v-loading="loading" :data="list" show-overflow-tooltip> + <!-- 多选模式 --> + <el-table-column key="2" type="selection" width="55" v-if="multiple"> + <template #header> + <el-checkbox + :value="allChecked && checkedPageNos.indexOf(queryParams.pageNo) > -1" + @change="handleCheckAll" + /> + </template> + <template #default="{ row }"> + <el-checkbox + :value="checkedSpuIds.indexOf(row.id) > -1" + @change="(checked: boolean) => handleCheckOne(checked, row)" + /> + </template> + </el-table-column> + <!-- 单选模式 --> + <el-table-column label="#" width="55" v-else> + <template #default="{ row }"> + <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)" + > </el-radio + > + </template> + </el-table-column> + <el-table-column key="id" align="center" label="商品编号" prop="id" min-width="60" /> + <el-table-column label="商品图" min-width="80"> + <template #default="{ row }"> + <el-image + :src="row.picUrl" + class="w-30px h-30px" + :preview-src-list="[row.picUrl]" + preview-teleported + /> + </template> + </el-table-column> + <el-table-column label="商品名称" min-width="200" prop="name" /> + <el-table-column label="商品分类" min-width="100" prop="categoryId"> + <template #default="{ row }"> + <span>{{ categoryList?.find((c) => c.id === row.categoryId)?.name }}</span> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + </ContentWrap> + <template #footer v-if="multiple"> + <el-button type="primary" @click="handleEmitChange">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> </Dialog> </template> @@ -85,12 +107,19 @@ 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' + +type Spu = Required<ProductSpuApi.Spu> defineOptions({ name: 'SpuTableSelect' }) -const message = useMessage() // 消息弹窗 +const props = defineProps({ + // 多选 + multiple: propTypes.bool.def(false) +}) + const total = ref(0) // 列表的总页数 -const list = ref<any[]>([]) // 列表的数据 +const list = ref<Spu[]>([]) // 列表的数据 const loading = ref(false) // 列表的加载中 const dialogVisible = ref(false) // 弹窗的是否展示 const queryParams = ref({ @@ -101,26 +130,24 @@ const queryParams = ref({ categoryId: null, createTime: [] }) // 查询参数 -const spuListRef = ref<InstanceType<typeof ElTable>>() const selectedSpuId = ref() // 选中的商品 spuId -/** 选中时触发 */ -const handleSelected = (row: ProductSpuApi.Spu) => { - emits('change', row) - // 关闭弹窗 - dialogVisible.value = false - selectedSpuId.value = undefined -} - -// 确认选择时的触发事件 -const emits = defineEmits<{ - (e: 'change', spu: ProductSpuApi.Spu): void -}>() - /** 打开弹窗 */ -const open = () => { +const open = (spus?: Spu[]) => { + if (spus && spus.length > 0) { + // todo check-box不显示选中? + checkedSpus.value = [...spus] + checkedSpuIds.value = spus.map((spu) => spu.id) + } else { + checkedSpus.value = [] + checkedSpuIds.value = [] + } + allChecked.value = false + checkedPageNos.value = [] + dialogVisible.value = true + resetQuery() } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -138,6 +165,7 @@ const getList = async () => { /** 搜索按钮操作 */ const handleQuery = () => { + queryParams.value.pageNo = 1 getList() } @@ -154,9 +182,65 @@ const resetQuery = () => { getList() } +const allChecked = ref(false) //是否全选 +const checkedPageNos = ref<number[]>([]) //选中的页码 +const checkedSpuIds = ref<number[]>([]) //选中的商品ID +const checkedSpus = ref<Spu[]>([]) //选中的商品 + +/** 单选中时触发 */ +const handleSingleSelected = (row: Spu) => { + emits('change', row) + // 关闭弹窗 + dialogVisible.value = false + // 记住上次选择的ID + selectedSpuId.value = row.id +} + +/** 多选完成 */ +const handleEmitChange = () => { + // 关闭弹窗 + dialogVisible.value = false + emits('change', [...checkedSpus.value]) +} + +/** 确认选择时的触发事件 */ +const emits = defineEmits<{ + (e: 'change', spu: Spu | Spu[] | any): void +}>() + +/** 全选 */ +const handleCheckAll = (checked: boolean) => { + debugger + console.log('checkAll', checked) + allChecked.value = checked + const index = checkedPageNos.value.indexOf(queryParams.value.pageNo) + checkedPageNos.value.push(queryParams.value.pageNo) + if (index > -1) { + checkedPageNos.value.splice(index, 1) + } + + list.value.forEach((item) => handleCheckOne(checked, item)) +} + +/** 选中一行 */ +const handleCheckOne = (checked: boolean, spu: Spu) => { + if (checked) { + const index = checkedSpuIds.value.indexOf(spu.id) + if (index === -1) { + checkedSpuIds.value.push(spu.id) + checkedSpus.value.push(spu) + } + } else { + const index = checkedSpuIds.value.indexOf(spu.id) + if (index > -1) { + checkedSpuIds.value.splice(index, 1) + checkedSpus.value.splice(index, 1) + } + } +} + const categoryList = ref() // 分类列表 const categoryTreeList = ref() // 分类树 - /** 初始化 **/ onMounted(async () => { await getList() diff --git a/src/views/mall/product/spu/components/index.ts b/src/views/mall/product/spu/components/index.ts index 8f793c51..e2cbe73d 100644 --- a/src/views/mall/product/spu/components/index.ts +++ b/src/views/mall/product/spu/components/index.ts @@ -1,4 +1,5 @@ import SkuList from './SkuList.vue' +import { Spu } from '@/api/mall/product/spu' interface PropertyAndValues { id: number @@ -22,4 +23,32 @@ interface RuleConfig { message: string } -export { SkuList, PropertyAndValues, RuleConfig } +/** + * 获得商品的规格列表 - 商品相关的公共函数 + * + * @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 { SkuList, PropertyAndValues, RuleConfig, getPropertyList } diff --git a/src/views/mall/product/spu/form/BasicInfoForm.vue b/src/views/mall/product/spu/form/BasicInfoForm.vue index 678b564d..c6426090 100644 --- a/src/views/mall/product/spu/form/BasicInfoForm.vue +++ b/src/views/mall/product/spu/form/BasicInfoForm.vue @@ -109,7 +109,12 @@ <!-- 多规格添加--> <el-col :span="24"> <el-form-item v-if="!formData.specType"> - <SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" /> + <SkuList + ref="skuListRef" + :prop-form-data="formData" + :propertyList="propertyList" + :rule-config="ruleConfig" + /> </el-form-item> <el-form-item v-if="formData.specType" label="商品属性"> <el-button class="mr-15px mb-10px" @click="attributesAddFormRef.open">添加规格</el-button> @@ -120,7 +125,12 @@ <SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" /> </el-form-item> <el-form-item label="属性列表"> - <SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" /> + <SkuList + ref="skuListRef" + :prop-form-data="formData" + :propertyList="propertyList" + :rule-config="ruleConfig" + /> </el-form-item> </template> </el-col> @@ -175,7 +185,7 @@ import { propTypes } from '@/utils/propTypes' import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree' import { createImageViewer } from '@/components/ImageViewer' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { PropertyAndValues, SkuList } from '@/views/mall/product/spu/components/index.ts' +import { getPropertyList, RuleConfig, SkuList } from '@/views/mall/product/spu/components/index.ts' import ProductAttributes from './ProductAttributes.vue' import ProductPropertyAddForm from './ProductPropertyAddForm.vue' import { basicInfoSchema } from './spu.data' @@ -186,6 +196,30 @@ import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate' defineOptions({ name: 'ProductSpuBasicInfoForm' }) +// sku 相关属性校验规则 +const ruleConfig: RuleConfig[] = [ + { + name: 'stock', + rule: (arg) => arg >= 1, + message: '商品库存必须大于等于 1 !!!' + }, + { + name: 'price', + rule: (arg) => arg >= 0.01, + message: '商品销售价格必须大于等于 0.01 !!!' + }, + { + name: 'marketPrice', + rule: (arg) => arg >= 0.01, + message: '商品市场价格必须大于等于 0.01 !!!' + }, + { + name: 'costPrice', + rule: (arg) => arg >= 0.01, + message: '商品成本价格必须大于等于 0.01 !!!' + } +] + // ====== 商品详情相关操作 ====== const { allSchemas } = useCrudSchemas(basicInfoSchema) /** 商品图预览 */ @@ -203,34 +237,6 @@ const imagePreview = (args) => { }) } -/** - * 获得商品的规格列表 - * - * @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 -} - // ====== end ====== const message = useMessage() // 消息弹窗 diff --git a/src/views/mall/product/spu/form/index.vue b/src/views/mall/product/spu/form/index.vue index db5b0445..0ba7a6d8 100644 --- a/src/views/mall/product/spu/form/index.vue +++ b/src/views/mall/product/spu/form/index.vue @@ -142,17 +142,6 @@ const submitForm = async () => { await unref(otherSettingsRef)?.validate() // 深拷贝一份, 这样最终 server 端不满足,不需要恢复, const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu - // 兜底处理 sku 空数据 - formData.value.skus!.forEach((sku) => { - // 因为是空数据这里判断一下商品条码是否为空就行 - if (sku.barCode === '') { - const index = deepCopyFormData.skus!.findIndex( - (item) => JSON.stringify(item.properties) === JSON.stringify(sku.properties) - ) - // 删除这条 sku - deepCopyFormData.skus!.splice(index, 1) - } - }) deepCopyFormData.skus!.forEach((item) => { // 给sku name赋值 item.name = deepCopyFormData.name @@ -189,7 +178,7 @@ const submitForm = async () => { /** 关闭按钮 */ const close = () => { delView(unref(currentRoute)) - push('/product/product-spu') + push({ name: 'ProductSpu' }) } /** 初始化 */ onMounted(async () => { diff --git a/src/views/mall/product/spu/index.vue b/src/views/mall/product/spu/index.vue index 9ea06cd5..2ed6d3ad 100644 --- a/src/views/mall/product/spu/index.vue +++ b/src/views/mall/product/spu/index.vue @@ -170,6 +170,14 @@ > 详情 </el-button> + <el-button + v-hasPermi="['product:spu:update']" + link + type="primary" + @click="openForm(row.id)" + > + 修改 + </el-button> <template v-if="queryParams.tabType === 4"> <el-button v-hasPermi="['product:spu:delete']" @@ -189,16 +197,6 @@ </el-button> </template> <template v-else> - <!-- 只有不是上架和回收站的商品可以编辑 --> - <el-button - v-if="queryParams.tabType !== 0" - v-hasPermi="['product:spu:update']" - link - type="primary" - @click="openForm(row.id)" - > - 修改 - </el-button> <el-button v-hasPermi="['product:spu:update']" link diff --git a/src/views/mall/promotion/coupon/components/CouponSendForm.vue b/src/views/mall/promotion/coupon/components/CouponSendForm.vue new file mode 100644 index 00000000..be0223a0 --- /dev/null +++ b/src/views/mall/promotion/coupon/components/CouponSendForm.vue @@ -0,0 +1,162 @@ +<template> + <Dialog v-model="dialogVisible" :appendToBody="true" title="发送优惠券" width="70%"> + <!-- 搜索工作栏 --> + <el-form + ref="queryFormRef" + :inline="true" + :model="queryParams" + class="-mb-15px" + label-width="82px" + > + <el-form-item label="优惠券名称" prop="name"> + <el-input + v-model="queryParams.name" + class="!w-240px" + placeholder="请输入优惠劵名" + clearable + @keyup="handleQuery" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> + </el-form-item> + </el-form> + + <!-- 列表 --> + <el-table v-loading="loading" :data="list" show-overflow-tooltip> + <el-table-column align="center" label="优惠券名称" prop="name" min-width="60" /> + <el-table-column + label="优惠金额 / 折扣" + align="center" + prop="discount" + :formatter="discountFormat" + min-width="60" + /> + <el-table-column + align="center" + label="最低消费" + prop="usePrice" + min-width="60" + :formatter="usePriceFormat" + /> + <el-table-column + align="center" + label="有效期限" + prop="validityType" + min-width="140" + :formatter="validityTypeFormat" + /> + <el-table-column + align="center" + label="剩余数量" + min-width="60" + :formatter="remainedCountFormat" + /> + <el-table-column label="操作" align="center" min-width="60px" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + :disabled="sendLoading" + :loading="sendLoading" + @click="handleSendCoupon(scope.row.id)" + v-hasPermi="['member:level:update']" + > + 发送 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + <div class="clear-both"></div> + </Dialog> +</template> +<script lang="ts" setup> +import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' +import * as CouponApi from '@/api/mall/promotion/coupon/coupon' +import { + discountFormat, + remainedCountFormat, + usePriceFormat, + validityTypeFormat +} from '@/views/mall/promotion/coupon/formatter' +import { CouponTemplateTakeTypeEnum } from '@/utils/constants' + +defineOptions({ name: 'PromotionCouponSendForm' }) + +const message = useMessage() // 消息弹窗 +const total = ref(0) // 列表的总页数 +const list = ref<any[]>([]) // 列表的数据 +const loading = ref(false) // 列表的加载中 +const sendLoading = ref(false) // 发送按钮的加载中 +const dialogVisible = ref(false) // 弹窗的是否展示 +const queryParams = ref({ + pageNo: 1, + pageSize: 10, + name: null, + canTakeTypes: [CouponTemplateTakeTypeEnum.ADMIN.type] +}) // 查询参数 +const queryFormRef = ref() // 搜索的表单 +// 领取人的编号列表 +let userIds: number[] = [] + +/** 打开弹窗 */ +const open = (ids: number[]) => { + userIds = ids + // 打开时重置查询,防止发送列表剩余数量未更新的问题 + resetQuery() + + dialogVisible.value = true +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await CouponTemplateApi.getCouponTemplatePage(queryParams.value) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.value.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef?.value?.resetFields() + handleQuery() +} + +/** 发送操作 **/ +const handleSendCoupon = async (templateId: number) => { + try { + sendLoading.value = true + await CouponApi.sendCoupon({ templateId, userIds }) + // 提示 + message.success('发送成功') + dialogVisible.value = false + } finally { + sendLoading.value = false + } +} +</script> diff --git a/src/views/mall/promotion/coupon/formatter.ts b/src/views/mall/promotion/coupon/formatter.ts new file mode 100644 index 00000000..2b0ba7c6 --- /dev/null +++ b/src/views/mall/promotion/coupon/formatter.ts @@ -0,0 +1,44 @@ +import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants' +import { formatDate } from '@/utils/formatTime' +import { CouponTemplateVO } from '@/api/mall/promotion/coupon/couponTemplate' +import { floatToFixed2 } from '@/utils' + +// 格式化【优惠金额/折扣】 +export const discountFormat = (row: CouponTemplateVO) => { + if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) { + return `¥${floatToFixed2(row.discountPrice)}` + } + if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) { + return `${row.discountPrice}%` + } + return '未知【' + row.discountType + '】' +} + +// 格式化【领取上限】 +export const takeLimitCountFormat = (row: CouponTemplateVO) => { + if (row.takeLimitCount === -1) { + return '无领取限制' + } + return `${row.takeLimitCount} 张/人` +} + +// 格式化【有效期限】 +export const validityTypeFormat = (row: CouponTemplateVO) => { + 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 + '】' +} + +// 格式化【剩余数量】 +export const remainedCountFormat = (row: CouponTemplateVO) => { + return row.totalCount - row.takeCount +} + +// 格式化【最低消费】 +export const usePriceFormat = (row: CouponTemplateVO) => { + return `¥${floatToFixed2(row.usePrice)}` +} diff --git a/src/views/mall/promotion/coupon/index.vue b/src/views/mall/promotion/coupon/index.vue index acfccece..9f81edf2 100755 --- a/src/views/mall/promotion/coupon/index.vue +++ b/src/views/mall/promotion/coupon/index.vue @@ -122,7 +122,8 @@ const queryParams = reactive({ pageNo: 1, pageSize: 10, createTime: [], - status: undefined + status: undefined, + nickname: undefined }) const queryFormRef = ref() // 搜索的表单 diff --git a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue index e0173c00..0c4238ac 100644 --- a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue +++ b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue @@ -10,7 +10,40 @@ <el-form-item label="优惠券名称" prop="name"> <el-input v-model="formData.name" placeholder="请输入优惠券名称" /> </el-form-item> - <el-form-item label="优惠券类型" prop="discountType"> + <el-form-item label="优惠劵类型" prop="productScope"> + <el-radio-group v-model="formData.productScope"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item + label="商品" + v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope" + prop="productSpuIds" + > + <div class="flex items-center gap-1 flex-wrap"> + <div class="select-box spu-pic" v-for="(spu, index) in productSpus" :key="spu.id"> + <el-image :src="spu.picUrl" /> + <Icon icon="ep:circle-close-filled" class="del-icon" @click="handleRemoveSpu(index)" /> + </div> + <div class="select-box" @click="openSpuTableSelect"> + <Icon icon="ep:plus" /> + </div> + </div> + </el-form-item> + <el-form-item + label="分类" + v-if="formData.productScope === PromotionProductScopeEnum.CATEGORY.scope" + prop="productCategoryIds" + > + <ProductCategorySelect v-model="formData.productCategoryIds" /> + </el-form-item> + <el-form-item label="优惠类型" prop="discountType"> <el-radio-group v-model="formData.discountType"> <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)" @@ -29,7 +62,7 @@ <el-input-number v-model="formData.discountPrice" placeholder="请输入优惠金额,单位:元" - style="width: 400px" + class="!w-400px mr-2" :precision="2" :min="0" /> @@ -43,7 +76,7 @@ <el-input-number v-model="formData.discountPercent" placeholder="优惠券折扣不能小于 1 折,且不可大于 9.9 折" - style="width: 400px" + class="!w-400px mr-2" :precision="1" :min="1" :max="9.9" @@ -58,7 +91,7 @@ <el-input-number v-model="formData.discountLimitPrice" placeholder="请输入最多优惠" - style="width: 400px" + class="!w-400px mr-2" :precision="2" :min="0" /> @@ -68,7 +101,7 @@ <el-input-number v-model="formData.usePrice" placeholder="无门槛请设为 0" - style="width: 400px" + class="!w-400px mr-2" :precision="2" :min="0" /> @@ -84,7 +117,7 @@ <el-input-number v-model="formData.totalCount" placeholder="发放数量,没有之后不能领取或发放,-1 为不限制" - style="width: 400px" + class="!w-400px mr-2" :precision="0" :min="-1" /> @@ -94,7 +127,7 @@ <el-input-number v-model="formData.takeLimitCount" placeholder="设置为 -1 时,可无限领取" - style="width: 400px" + class="!w-400px mr-2" :precision="0" :min="-1" /> @@ -119,7 +152,7 @@ <el-date-picker v-model="formData.validTimes" style="width: 240px" - value-format="YYYY-MM-DD HH:mm:ss" + value-format="x" type="datetimerange" :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]" /> @@ -133,7 +166,7 @@ <el-input-number v-model="formData.fixedStartTerm" placeholder="0 为今天生效" - style="width: 165px" + class="mx-2" :precision="0" :min="0" /> @@ -141,49 +174,19 @@ <el-input-number v-model="formData.fixedEndTerm" placeholder="请输入结束天数" - style="width: 165px" + class="mx-2" :precision="0" :min="0" /> 天有效 </el-form-item> - <el-form-item label="活动商品" prop="productScope"> - <el-radio-group v-model="formData.productScope"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item - v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope" - prop="productSpuIds" - > - <el-select - v-model="formData.productSpuIds" - placeholder="请选择活动商品" - clearable - 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; font-size: 13px; color: #8492a6"> - ¥{{ (item.minPrice / 100.0).toFixed(2) }} - </span> - </el-option> - </el-select> - </el-form-item> </el-form> <template #footer> <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> <el-button @click="dialogVisible = false">取 消</el-button> </template> </Dialog> + <SpuTableSelect ref="spuTableSelectRef" multiple @change="handleSpuSelected" /> </template> <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' @@ -194,6 +197,8 @@ import { PromotionDiscountTypeEnum, PromotionProductScopeEnum } from '@/utils/constants' +import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue' +import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue' defineOptions({ name: 'CouponTemplateForm' }) @@ -222,7 +227,9 @@ const formData = ref({ fixedStartTerm: undefined, fixedEndTerm: undefined, productScope: PromotionProductScopeEnum.ALL.scope, - productSpuIds: [] + productScopeValues: [], // 商品范围:值为 品类编号列表 或 商品编号列表 ,用于提交 + productCategoryIds: [], // 仅用于表单,不提交 + productSpuIds: [] // 仅用于表单,不提交 }) const formRules = reactive({ name: [{ required: true, message: '优惠券名称不能为空', trigger: 'blur' }], @@ -239,10 +246,11 @@ const formRules = reactive({ fixedStartTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }], fixedEndTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }], productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }], - productSpuIds: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }] + productSpuIds: [{ required: true, message: '商品不能为空', trigger: 'blur' }], + productCategoryIds: [{ required: true, message: '分类不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const productSpus = ref([]) // 商品列表 +const productSpus = ref<ProductSpuApi.Spu[]>([]) // 商品列表 /** 打开弹窗 */ const open = async (type: string, id?: number) => { @@ -265,12 +273,12 @@ const open = async (type: string, id?: number) => { usePrice: data.usePrice !== undefined ? data.usePrice / 100.0 : undefined, validTimes: [data.validStartTime, data.validEndTime] } + // 获得商品范围 + await getProductScope() } finally { formLoading.value = false } } - // 获得商品列表 - productSpus.value = await ProductSpuApi.getSpuSimpleList() } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -305,7 +313,11 @@ const submitForm = async () => { formData.value.validTimes && formData.value.validTimes.length === 2 ? formData.value.validTimes[1] : undefined - } as CouponTemplateApi.CouponTemplateVO + } as unknown as CouponTemplateApi.CouponTemplateVO + + // 设置商品范围 + setProductScopeValues(data) + if (formType.value === 'create') { await CouponTemplateApi.createCouponTemplate(data) message.success(t('common.createSuccess')) @@ -341,8 +353,92 @@ const resetForm = () => { fixedStartTerm: undefined, fixedEndTerm: undefined, productScope: PromotionProductScopeEnum.ALL.scope, - productSpuIds: [] + productScopeValues: [], + productSpuIds: [], + productCategoryIds: [] } formRef.value?.resetFields() + productSpus.value = [] +} + +/** 获得商品范围 */ +const getProductScope = async () => { + switch (formData.value.productScope) { + case PromotionProductScopeEnum.SPU.scope: + // 设置商品编号 + formData.value.productSpuIds = formData.value.productScopeValues + // 获得商品列表 + productSpus.value = await ProductSpuApi.getSpuDetailList(formData.value.productScopeValues) + break + case PromotionProductScopeEnum.CATEGORY.scope: + await nextTick(() => { + let productCategoryIds = formData.value.productScopeValues + if (Array.isArray(productCategoryIds) && productCategoryIds.length > 0) { + // 单选时使用数组不能反显 + productCategoryIds = productCategoryIds[0] + } + // 设置品类编号 + formData.value.productCategoryIds = productCategoryIds + }) + break + default: + break + } +} +/** 设置商品范围 */ +function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) { + switch (formData.value.productScope) { + case PromotionProductScopeEnum.SPU.scope: + data.productScopeValues = formData.value.productSpuIds + break + case PromotionProductScopeEnum.CATEGORY.scope: + data.productScopeValues = Array.isArray(formData.value.productCategoryIds) + ? formData.value.productCategoryIds + : [formData.value.productCategoryIds] + break + default: + break + } +} + +/** 活动商品的按钮 */ +const spuTableSelectRef = ref() +const openSpuTableSelect = () => { + spuTableSelectRef.value.open(productSpus.value) +} + +/** 选择商品后触发 */ +const handleSpuSelected = (spus: ProductSpuApi.Spu[]) => { + productSpus.value = spus + formData.value.productSpuIds = spus.map((spu) => spu.id) as [] +} + +/** 选择商品后触发 */ +const handleRemoveSpu = (index: number) => { + productSpus.value.splice(index, 1) + formData.value.productSpuIds.splice(index, 1) } </script> + +<style scoped lang="scss"> +.select-box { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed var(--el-border-color-darker); + border-radius: 8px; + width: 60px; + height: 60px; +} +.spu-pic { + position: relative; +} +.del-icon { + position: absolute; + z-index: 1; + width: 20px !important; + height: 20px !important; + right: -10px; + top: -10px; +} +</style> diff --git a/src/views/mall/promotion/coupon/template/index.vue b/src/views/mall/promotion/coupon/template/index.vue index ee8dad46..ad0077ce 100755 --- a/src/views/mall/promotion/coupon/template/index.vue +++ b/src/views/mall/promotion/coupon/template/index.vue @@ -103,7 +103,7 @@ label="剩余数量" align="center" prop="totalCount" - :formatter="(row) => row.totalCount - row.takeCount" + :formatter="remainedCountFormat" /> <el-table-column label="领取上限" @@ -171,14 +171,16 @@ <script lang="ts" setup> import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' -import { - CommonStatusEnum, - CouponTemplateValidityTypeEnum, - PromotionDiscountTypeEnum -} from '@/utils/constants' +import { CommonStatusEnum } from '@/utils/constants' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter, formatDate } from '@/utils/formatTime' +import { dateFormatter } from '@/utils/formatTime' import CouponTemplateForm from './CouponTemplateForm.vue' +import { + discountFormat, + remainedCountFormat, + takeLimitCountFormat, + validityTypeFormat +} from '@/views/mall/promotion/coupon/formatter' defineOptions({ name: 'PromotionCouponTemplate' }) @@ -193,6 +195,7 @@ const queryParams = reactive({ pageSize: 10, name: null, status: null, + discountType: null, type: null, createTime: [] }) @@ -258,36 +261,6 @@ const handleDelete = async (id: number) => { } 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 + '】' -} - /** 初始化 **/ onMounted(() => { getList() diff --git a/src/views/mall/trade/afterSale/detail/index.vue b/src/views/mall/trade/afterSale/detail/index.vue index 7802a805..190a4b33 100644 --- a/src/views/mall/trade/afterSale/detail/index.vue +++ b/src/views/mall/trade/afterSale/detail/index.vue @@ -122,8 +122,36 @@ </el-row> </el-descriptions-item> </el-descriptions> - <!-- 售后信息 TODO @puhui999:需要接入 --> - <el-descriptions title="售后日志" /> + <el-descriptions title="售后日志"> + <el-descriptions-item labelClassName="no-colon"> + <el-timeline> + <el-timeline-item + v-for="saleLog in formData.afterSaleLog" + :key="saleLog.id" + :timestamp="formatDate(saleLog.createTime)" + placement="top" + > + <el-card> + <span>用户类型:</span> + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="saleLog.userType" class="mr-10px" /> + <span>售后状态(之前):</span> + <dict-tag + :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" + :value="saleLog.beforeStatus" + class="mr-10px" + /> + <span>售后状态(之后):</span> + <dict-tag + :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" + :value="saleLog.afterStatus" + class="mr-10px" + /> + <span>操作明细:{{ saleLog.content }}</span> + </el-card> + </el-timeline-item> + </el-timeline> + </el-descriptions-item> + </el-descriptions> </ContentWrap> <!-- 各种操作的弹窗 --> @@ -138,12 +166,14 @@ import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDi import { createImageViewer } from '@/components/ImageViewer' import { isArray } from '@/utils/is' -defineOptions({ name: 'TradeOrderDetailForm' }) +defineOptions({ name: 'TradeAfterSaleDetail' }) +const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const { params } = useRoute() // 查询参数 const formData = ref({ - order: {} + order: {}, + afterSaleLog: [] }) const updateAuditReasonFormRef = ref() // 拒绝售后表单 Ref @@ -154,44 +184,48 @@ const getDetail = async () => { formData.value = await AfterSaleApi.getAfterSale(id) } } -/** - * 同意售后 - */ + +/** 同意售后 */ const agree = () => { message.confirm('是否同意售后?').then(() => { AfterSaleApi.agree(formData.value.id) + message.success(t('common.success')) + getDetail() }) } -/** - * 拒绝售后 - */ + +/** 拒绝售后 */ const disagree = () => { updateAuditReasonFormRef.value?.open(formData.value) } -/** - * 确认收货 - */ + +/** 确认收货 */ const receive = () => { message.confirm('是否确认收货?').then(() => { AfterSaleApi.receive(formData.value.id) + message.success(t('common.success')) + getDetail() }) } -/** - * 拒绝收货 - */ + +/** 拒绝收货 */ const refuse = () => { message.confirm('是否拒绝收货?').then(() => { AfterSaleApi.refuse(formData.value.id) + message.success(t('common.success')) + getDetail() }) } -/** - * 确认退款 - */ + +/** 确认退款 */ const refund = () => { message.confirm('是否确认退款?').then(() => { AfterSaleApi.refund(formData.value.id) + message.success(t('common.success')) + getDetail() }) } + /** 图片预览 */ const imagePreview = (args) => { const urlList = [] @@ -206,6 +240,7 @@ const imagePreview = (args) => { urlList }) } + onMounted(async () => { await getDetail() }) diff --git a/src/views/mall/trade/order/detail/index.vue b/src/views/mall/trade/order/detail/index.vue index ef2d9958..a823fef5 100644 --- a/src/views/mall/trade/order/detail/index.vue +++ b/src/views/mall/trade/order/detail/index.vue @@ -2,32 +2,32 @@ <ContentWrap> <!-- 订单信息 --> <el-descriptions title="订单信息"> - <el-descriptions-item label="订单号: ">{{ orderInfo.no }}</el-descriptions-item> + <el-descriptions-item label="订单号: ">{{ formData.no }}</el-descriptions-item> <el-descriptions-item label="配送方式: "> - <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="orderInfo.deliveryType" /> + <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.deliveryType!" /> </el-descriptions-item> <!-- TODO 营销活动待实现 --> <el-descriptions-item label="营销活动: ">秒杀活动</el-descriptions-item> <el-descriptions-item label="订单类型: "> - <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="orderInfo.type" /> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.type!" /> </el-descriptions-item> - <el-descriptions-item label="收货人: ">{{ orderInfo.receiverName }}</el-descriptions-item> - <el-descriptions-item label="买家留言: ">{{ orderInfo.userRemark }}</el-descriptions-item> + <el-descriptions-item label="收货人: ">{{ formData.receiverName }}</el-descriptions-item> + <el-descriptions-item label="买家留言: ">{{ formData.userRemark }}</el-descriptions-item> <el-descriptions-item label="订单来源: "> - <dict-tag :type="DICT_TYPE.TERMINAL" :value="orderInfo.terminal" /> + <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.terminal!" /> </el-descriptions-item> - <el-descriptions-item label="联系电话: ">{{ orderInfo.receiverMobile }}</el-descriptions-item> - <el-descriptions-item label="商家备注: ">{{ orderInfo.remark }}</el-descriptions-item> - <el-descriptions-item label="支付单号: ">{{ orderInfo.payOrderId }}</el-descriptions-item> + <el-descriptions-item label="联系电话: ">{{ formData.receiverMobile }}</el-descriptions-item> + <el-descriptions-item label="商家备注: ">{{ formData.remark }}</el-descriptions-item> + <el-descriptions-item label="支付单号: ">{{ formData.payOrderId }}</el-descriptions-item> <el-descriptions-item label="付款方式: "> - <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="orderInfo.payChannelCode" /> + <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.payChannelCode!" /> </el-descriptions-item> - <!-- <el-descriptions-item label="买家: ">{{ orderInfo.user.nickname }}</el-descriptions-item> --> - <!-- TODO 芋艿:待实现:跳转会员 --> + <!-- <el-descriptions-item label="买家: ">{{ formData.user.nickname }}</el-descriptions-item> --> + <!-- TODO @puhui999:待实现:跳转会员 --> <el-descriptions-item label="收货地址: "> - {{ orderInfo.receiverAreaName }} {{ orderInfo.receiverDetailAddress }} + {{ formData.receiverAreaName }} {{ formData.receiverDetailAddress }} <el-link - v-clipboard:copy="orderInfo.receiverAreaName + ' ' + orderInfo.receiverDetailAddress" + v-clipboard:copy="formData.receiverAreaName + ' ' + formData.receiverDetailAddress" v-clipboard:success="clipboardSuccess" icon="ep:document-copy" type="primary" @@ -38,8 +38,9 @@ <!-- 订单状态 --> <el-descriptions :column="1" title="订单状态"> <el-descriptions-item label="订单状态: "> - <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="orderInfo.status" /> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.status!" /> </el-descriptions-item> + <!-- TODO @puhui999:根据状态,进行展示按钮 --> <el-descriptions-item label-class-name="no-colon"> <el-button type="primary" @click="openForm('updatePrice')">调整价格</el-button> <el-button type="primary" @click="openForm('remark')">备注</el-button> @@ -59,7 +60,7 @@ <el-descriptions-item labelClassName="no-colon"> <el-row :gutter="20"> <el-col :span="15"> - <el-table :data="orderInfo.items" border> + <el-table :data="formData.items" border> <el-table-column label="商品" prop="spuName" width="auto"> <template #default="{ row }"> {{ row.spuName }} @@ -91,111 +92,75 @@ </el-descriptions> <el-descriptions :column="6"> <el-descriptions-item label="商品总额: "> - {{ floatToFixed2(orderInfo.totalPrice) }}元 + {{ floatToFixed2(formData.totalPrice!) }}元 </el-descriptions-item> <el-descriptions-item label="运费金额: "> - {{ floatToFixed2(orderInfo.deliveryPrice) }}元 + {{ floatToFixed2(formData.deliveryPrice!) }}元 </el-descriptions-item> <el-descriptions-item label="订单调价: "> - {{ floatToFixed2(orderInfo.updatePrice) }}元 + {{ floatToFixed2(formData.adjustPrice!) }}元 </el-descriptions-item> <el-descriptions-item> <template #label><span style="color: red">商品优惠: </span></template> - {{ floatToFixed2(orderInfo.couponPrice) }}元 + {{ floatToFixed2(formData.couponPrice!) }}元 </el-descriptions-item> <el-descriptions-item> <template #label><span style="color: red">订单优惠: </span></template> - {{ floatToFixed2(orderInfo.discountPrice) }}元 + {{ floatToFixed2(formData.discountPrice!) }}元 </el-descriptions-item> <el-descriptions-item> <template #label><span style="color: red">积分抵扣: </span></template> - {{ floatToFixed2(orderInfo.pointPrice) }}元 + {{ floatToFixed2(formData.pointPrice!) }}元 </el-descriptions-item> <el-descriptions-item v-for="item in 5" :key="item" label-class-name="no-colon" /> <!-- 占位 --> <el-descriptions-item label="应付金额: "> - {{ floatToFixed2(orderInfo.payPrice) }}元 + {{ floatToFixed2(formData.payPrice!) }}元 </el-descriptions-item> </el-descriptions> <!-- TODO 芋艿:需要改改 --> - <div v-for="group in detailGroups" :key="group.title"> - <el-descriptions :title="group.title" v-bind="group.groupProps"> - <!-- 订单操作日志 --> - <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 等物流接口搞定重构一下 --> - <!-- 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" - :key="`pkgChild_${pkgCIdx}`" - :label="pkgChild.label" - v-bind="pkgChild.childProps" - > - <!-- 包裹商品列表 --> - <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 - :src="goodInfo.imgUrl" - style="width: 100px; height: 100px; flex: none" - /> - <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 :offset="1" :span="6"> - <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> + <el-descriptions :column="4" title="物流信息"> + <el-descriptions-item label="物流公司: "> + {{ deliveryExpressList.find((item) => item.id === formData.logisticsId)?.name }} + </el-descriptions-item> + <el-descriptions-item label="运单号: ">{{ formData.logisticsNo }}</el-descriptions-item> + <el-descriptions-item label="发货时间: "> + {{ formatDate(formData.deliveryTime!) }} + </el-descriptions-item> + <el-descriptions-item label="物流状态: "> + <!-- TODO 物流状态怎么获取? --> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.deliveryStatus!" /> + </el-descriptions-item> + <!-- 占位 4 --> + <el-descriptions-item v-for="item in 4" :key="item" label-class-name="no-colon" /> + <el-descriptions-item label="物流详情: "> + <el-timeline> + <el-timeline-item + v-for="(express, index) in expressTrackList" + :key="index" + :timestamp="formatDate(express.time)" + > + {{ express.content }} + </el-timeline-item> + </el-timeline> + </el-descriptions-item> + </el-descriptions> + <el-descriptions title="订单操作日志"> + <el-descriptions-item labelClassName="no-colon"> + <el-timeline> + <el-timeline-item + v-for="(log, index) in formData.orderLog" + :key="index" + :timestamp="formatDate(log.createTime!)" + > + {{ log.content }} + </el-timeline-item> + </el-timeline> + </el-descriptions-item> + </el-descriptions> </ContentWrap> <!-- 各种操作的弹窗 --> @@ -207,84 +172,24 @@ <script lang="ts" setup> import * as TradeOrderApi from '@/api/mall/trade/order' import { floatToFixed2 } from '@/utils' +import { formatDate } from '@/utils/formatTime' import { DICT_TYPE } from '@/utils/dict' import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue' import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue' import OrderUpdateAddressForm from '@/views/mall/trade/order/form/OrderUpdateAddressForm.vue' import OrderUpdatePriceForm from '@/views/mall/trade/order/form/OrderUpdatePriceForm.vue' +import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express' -defineOptions({ name: 'TradeOrderDetailForm' }) +defineOptions({ name: 'TradeOrderDetail' }) const message = useMessage() // 消息弹窗 -const { params } = useRoute() // 查询参数 -const orderInfo = ref<TradeOrderApi.OrderVO>({}) -// TODO @puhui999:这个改成直接读属性,不用按照这种写法;后续再改 -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' - } -]) - -// TODO @puhui999:从后台读数据哈。后续再改 -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 +// 订单详情 +const formData = ref<TradeOrderApi.OrderVO>({ + orderLog: [] // TODO @puhui999:orderLogs }) +// TODO @puhui999:这个最好也拆掉哈 const deliveryFormRef = ref() // 发货表单 Ref const updateRemarkForm = ref() // 订单备注表单 Ref const updateAddressFormRef = ref() // 收货地址表单 Ref @@ -292,36 +197,43 @@ const updatePriceFormRef = ref() // 订单调价表单 Ref const openForm = (type: string) => { switch (type) { case 'remark': - updateRemarkForm.value?.open(orderInfo.value) + updateRemarkForm.value?.open(formData.value) break case 'delivery': - deliveryFormRef.value?.open(orderInfo.value) + deliveryFormRef.value?.open(formData.value) break case 'updateAddress': - updateAddressFormRef.value?.open(orderInfo.value) + updateAddressFormRef.value?.open(formData.value) break case 'updatePrice': - updatePriceFormRef.value?.open(orderInfo.value) + updatePriceFormRef.value?.open(formData.value) break } } /** 获得详情 */ +const { params } = useRoute() // 查询参数 const getDetail = async () => { const id = params.orderId as unknown as number if (id) { const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO - orderInfo.value = res + formData.value = res } } -onMounted(async () => { - await getDetail() -}) - +/** 复制 */ const clipboardSuccess = () => { message.success('复制成功') } + +/** 初始化 **/ +const deliveryExpressList = ref([]) // 物流公司 +const expressTrackList = ref([]) // 物流详情 +onMounted(async () => { + await getDetail() + deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList() + expressTrackList.value = await TradeOrderApi.getExpressTrackList(formData.value.id!) +}) </script> <style lang="scss" scoped> :deep(.el-descriptions) { diff --git a/src/views/member/user/index.vue b/src/views/member/user/index.vue index bae5c048..fa62dea5 100644 --- a/src/views/member/user/index.vue +++ b/src/views/member/user/index.vue @@ -60,13 +60,21 @@ <el-form-item> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button @click="openCoupon" v-hasPermi="['promotion:coupon:send']">发送优惠券</el-button> </el-form-item> </el-form> </ContentWrap> <!-- 列表 --> <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table + v-loading="loading" + :data="list" + :stripe="true" + :show-overflow-tooltip="true" + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" /> <el-table-column label="用户编号" align="center" prop="id" width="120px" /> <el-table-column label="头像" align="center" prop="avatar" width="80px"> <template #default="scope"> @@ -145,6 +153,8 @@ <UserForm ref="formRef" @success="getList" /> <!-- 修改用户等级弹窗 --> <UpdateLevelForm ref="updateLevelFormRef" @success="getList" /> + <!-- 发送优惠券弹窗 --> + <CouponSendForm ref="couponSendFormRef" /> </template> <script setup lang="ts"> import { dateFormatter } from '@/utils/formatTime' @@ -154,9 +164,12 @@ import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue' import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue' import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue' import UpdateLevelForm from '@/views/member/user/UpdateLevelForm.vue' +import CouponSendForm from '@/views/mall/promotion/coupon/components/CouponSendForm.vue' defineOptions({ name: 'MemberUser' }) +const message = useMessage() // 消息弹窗 + const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref([]) // 列表的数据 @@ -173,6 +186,7 @@ const queryParams = reactive({ }) const queryFormRef = ref() // 搜索的表单 const updateLevelFormRef = ref() // 修改会员等级表单 +const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组 /** 查询列表 */ const getList = async () => { @@ -204,6 +218,21 @@ const openDetail = (id: number) => { push({ name: 'MemberUserDetail', params: { id } }) } +/** 表格选中事件 */ +const handleSelectionChange = (rows: UserApi.UserVO[]) => { + selectedIds.value = rows.map((row) => row.id) +} + +/** 发送优惠券 */ +const couponSendFormRef = ref() +const openCoupon = () => { + if (selectedIds.value.length === 0) { + message.warning('请选择要发送优惠券的用户') + return + } + couponSendFormRef.value.open(selectedIds.value) +} + /** 初始化 **/ onMounted(() => { getList()