diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts index 2ad9bc60..6c476b8c 100644 --- a/src/api/mall/product/spu.ts +++ b/src/api/mall/product/spu.ts @@ -20,8 +20,8 @@ export interface Sku { stock?: number // 库存 weight?: number // 商品重量,单位:kg 千克 volume?: number // 商品体积,单位:m^3 平米 - subCommissionFirstPrice?: number | string // 一级分销的佣金 - subCommissionSecondPrice?: number | string // 二级分销的佣金 + firstBrokerageRecord?: number | string // 一级分销的佣金 + secondBrokerageRecord?: number | string // 二级分销的佣金 salesCount?: number // 商品销量 } diff --git a/src/api/mall/trade/brokerage/record/index.ts b/src/api/mall/trade/brokerage/record/index.ts new file mode 100644 index 00000000..7df9a225 --- /dev/null +++ b/src/api/mall/trade/brokerage/record/index.ts @@ -0,0 +1,11 @@ +import request from '@/config/axios' + +// 查询佣金记录列表 +export const getBrokerageRecordPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-record/page`, params }) +} + +// 查询佣金记录详情 +export const getBrokerageRecord = async (id: number) => { + return await request.get({ url: `/trade/brokerage-record/get?id=` + id }) +} diff --git a/src/api/mall/trade/brokerage/user/index.ts b/src/api/mall/trade/brokerage/user/index.ts new file mode 100644 index 00000000..1fed3bfa --- /dev/null +++ b/src/api/mall/trade/brokerage/user/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface BrokerageUserVO { + id: number + bindUserId: number + bindUserTime: Date + brokerageEnabled: boolean + brokerageTime: Date + price: number + frozenPrice: number + + nickname: string + avatar: string +} + +// 查询分销用户列表 +export const getBrokerageUserPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-user/page`, params }) +} + +// 查询分销用户详情 +export const getBrokerageUser = async (id: number) => { + return await request.get({ url: `/trade/brokerage-user/get?id=` + id }) +} + +// 修改推广员 +export const updateBindUser = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/update-bind-user`, data }) +} + +// 清除推广员 +export const clearBindUser = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/clear-bind-user`, data }) +} + +// 修改推广资格 +export const updateBrokerageEnabled = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/update-brokerage-enable`, data }) +} diff --git a/src/api/mall/trade/brokerage/withdraw/index.ts b/src/api/mall/trade/brokerage/withdraw/index.ts new file mode 100644 index 00000000..c93286a9 --- /dev/null +++ b/src/api/mall/trade/brokerage/withdraw/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface BrokerageWithdrawVO { + id: number + userId: number + price: number + feePrice: number + totalPrice: number + type: number + name: string + accountNo: string + bankName: string + bankAddress: string + accountQrCodeUrl: string + status: number + auditReason: string + auditTime: Date + remark: string +} + +// 查询佣金提现列表 +export const getBrokerageWithdrawPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-withdraw/page`, params }) +} + +// 查询佣金提现详情 +export const getBrokerageWithdraw = async (id: number) => { + return await request.get({ url: `/trade/brokerage-withdraw/get?id=` + id }) +} + +// 佣金提现 - 通过申请 +export const approveBrokerageWithdraw = async (id: number) => { + return await request.put({ url: `/trade/brokerage-withdraw/approve?id=` + id }) +} + +// 审核佣金提现 - 驳回申请 +export const rejectBrokerageWithdraw = async (data: BrokerageWithdrawVO) => { + return await request.put({ url: `/trade/brokerage-withdraw/reject`, data }) +} diff --git a/src/api/mall/trade/config/index.ts b/src/api/mall/trade/config/index.ts new file mode 100644 index 00000000..3a1771d8 --- /dev/null +++ b/src/api/mall/trade/config/index.ts @@ -0,0 +1,24 @@ +import request from '@/config/axios' + +export interface ConfigVO { + brokerageEnabled: boolean + brokerageEnabledCondition: number + brokerageBindMode: number + brokeragePostUrls: string + brokerageFirstPercent: number + brokerageSecondPercent: number + brokerageWithdrawMinPrice: number + brokerageBankNames: string + brokerageFrozenDays: number + brokerageWithdrawType: string +} + +// 查询交易中心配置详情 +export const getTradeConfig = async () => { + return await request.get({ url: `/trade/config/get` }) +} + +// 保存交易中心配置 +export const saveTradeConfig = async (data: ConfigVO) => { + return await request.put({ url: `/trade/config/save`, data }) +} diff --git a/src/api/mall/trade/order/index.ts b/src/api/mall/trade/order/index.ts index d4a22944..80669c57 100644 --- a/src/api/mall/trade/order/index.ts +++ b/src/api/mall/trade/order/index.ts @@ -51,12 +51,13 @@ export interface OrderVO { avatar?: string } // 订单操作日志 - orderLog: orderLog[] + logs?: OrderLogRespVO[] } -export interface orderLog { +export interface OrderLogRespVO { content?: string createTime?: Date + userType?: number } export interface OrderItemRespVO { diff --git a/src/api/member/experience-record/index.ts b/src/api/member/experience-record/index.ts new file mode 100644 index 00000000..6d40a48d --- /dev/null +++ b/src/api/member/experience-record/index.ts @@ -0,0 +1,22 @@ +import request from '@/config/axios' + +export interface ExperienceRecordVO { + id: number + userId: number + bizId: string + bizType: number + title: string + description: string + experience: number + totalExperience: number +} + +// 查询会员经验记录列表 +export const getExperienceRecordPage = async (params) => { + return await request.get({ url: `/member/experience-record/page`, params }) +} + +// 查询会员经验记录详情 +export const getExperienceRecord = async (id: number) => { + return await request.get({ url: `/member/experience-record/get?id=` + id }) +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8d783635..6f3ded5c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -272,3 +272,89 @@ export const PromotionDiscountTypeEnum = { name: '折扣' } } + +/** + * 分销关系绑定模式枚举 + */ +export const BrokerageBindModeEnum = { + ANYTIME: { + mode: 0, + name: '没有推广人' + }, + REGISTER: { + mode: 1, + name: '新用户' + } +} +/** + * 分佣模式枚举 + */ +export const BrokerageEnabledConditionEnum = { + ALL: { + condition: 0, + name: '人人分销' + }, + ADMIN: { + condition: 1, + name: '指定分销' + } +} +/** + * 佣金记录业务类型枚举 + */ +export const BrokerageRecordBizTypeEnum = { + ORDER: { + type: 1, + name: '获得推广佣金' + }, + WITHDRAW: { + type: 2, + name: '提现申请' + } +} +/** + * 佣金提现状态枚举 + */ +export const BrokerageWithdrawStatusEnum = { + AUDITING: { + status: 0, + name: '审核中' + }, + AUDIT_SUCCESS: { + status: 10, + name: '审核通过' + }, + AUDIT_FAIL: { + status: 20, + name: '审核不通过' + }, + WITHDRAW_SUCCESS: { + status: 11, + name: '提现成功' + }, + WITHDRAW_FAIL: { + status: 21, + name: '提现失败' + } +} +/** + * 佣金提现类型枚举 + */ +export const BrokerageWithdrawTypeEnum = { + WALLET: { + type: 1, + name: '钱包' + }, + BANK: { + type: 2, + name: '银行卡' + }, + WECHAT: { + type: 3, + name: '微信' + }, + ALIPAY: { + type: 4, + name: '支付宝' + } +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 6b163628..aa948594 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -60,13 +60,19 @@ export const getBoolDictOptions = (dictType: string) => { return dictOption } -export const getDictObj = (dictType: string, value: any) => { +/** + * 获取指定字典类型的指定值对应的字典对象 + * @param dictType 字典类型 + * @param value 字典值 + * @return DictDataType 字典对象 + */ +export const getDictObj = (dictType: string, value: any): DictDataType | undefined => { const dictOptions: DictDataType[] = getDictOptions(dictType) - dictOptions.forEach((dict: DictDataType) => { - if (dict.value === value.toString()) { + for (const dict of dictOptions) { + if (dict.value === value + '') { return dict } - }) + } } /** @@ -74,12 +80,13 @@ export const getDictObj = (dictType: string, value: any) => { * * @param dictType 字典类型 * @param value 字典数据的值 + * @return 字典名称 */ -export const getDictLabel = (dictType: string, value: any) => { +export const getDictLabel = (dictType: string, value: any): string => { const dictOptions: DictDataType[] = getDictOptions(dictType) const dictLabel = ref('') dictOptions.forEach((dict: DictDataType) => { - if (dict.value === value) { + if (dict.value === value + '') { dictLabel.value = dict.label } }) @@ -131,7 +138,7 @@ export enum DICT_TYPE { BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', // ========== PAY 模块 ========== - PAY_CHANNEL_CODE = 'pay_channel_code_type', // 支付渠道编码类型 + PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 @@ -143,6 +150,7 @@ export enum DICT_TYPE { // ========== MALL - 会员模块 ========== MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 + MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 // ========== MALL - 商品模块 ========== PRODUCT_UNIT = 'product_unit', // 商品单位 @@ -157,6 +165,13 @@ export enum DICT_TYPE { TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 + BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 + BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 + BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 + BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 + BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 + BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 + BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 // ========== MALL - 营销模块 ========== PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts new file mode 100644 index 00000000..7c6e39ff --- /dev/null +++ b/src/utils/formatter.ts @@ -0,0 +1,12 @@ +import { fenToYuan } from '@/utils' +import { TableColumnCtx } from 'element-plus' + +// 格式化金额【分转元】 +export const fenToYuanFormat = ( + row: any, + column: TableColumnCtx<any>, + cellValue: any, + index: number +) => { + return `¥${fenToYuan(cellValue)}` +} diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index 49742f00..a4eb0b92 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -230,6 +230,7 @@ const getCookie = () => { } } } +const loading = ref() // ElLoading.service 返回的实例 // 登录 const handleLogin = async (params) => { loginLoading.value = true @@ -244,7 +245,7 @@ const handleLogin = async (params) => { if (!res) { return } - ElLoading.service({ + loading.value = ElLoading.service({ lock: true, text: '正在加载系统中...', background: 'rgba(0, 0, 0, 0.7)' @@ -264,13 +265,9 @@ const handleLogin = async (params) => { } else { push({ path: redirect.value || permissionStore.addRouters[0].path }) } - } catch { - loginLoading.value = false } finally { - setTimeout(() => { - const loadingInstance = ElLoading.service() - loadingInstance.close() - }, 400) + loginLoading.value = false + loading.value.close() } } diff --git a/src/views/infra/server/index.vue b/src/views/infra/server/index.vue index 06c35d97..b9a157a5 100644 --- a/src/views/infra/server/index.vue +++ b/src/views/infra/server/index.vue @@ -16,6 +16,9 @@ const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications') /** 初始化 */ onMounted(async () => { try { + // 友情提示:如果访问出现 404 问题: + // 1)boot 参考 https://doc.iocoder.cn/server-monitor/ 解决; + // 2)cloud 参考 https://cloud.iocoder.cn/server-monitor/ 解决 const data = await ConfigApi.getConfigKey('url.spring-boot-admin') if (data && data.length > 0) { src.value = data diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index f64e4a98..7a4605c6 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -80,7 +80,7 @@ <el-table-column align="center" label="一级返佣(元)" min-width="168"> <template #default="{ row }"> <el-input-number - v-model="row.subCommissionFirstPrice" + v-model="row.firstBrokerageRecord" :min="0" :precision="2" :step="0.1" @@ -91,7 +91,7 @@ <el-table-column align="center" label="二级返佣(元)" min-width="168"> <template #default="{ row }"> <el-input-number - v-model="row.subCommissionSecondPrice" + v-model="row.secondBrokerageRecord" :min="0" :precision="2" :step="0.1" @@ -181,12 +181,12 @@ <template v-if="formData!.subCommissionType"> <el-table-column align="center" label="一级返佣(元)" min-width="80"> <template #default="{ row }"> - {{ row.subCommissionFirstPrice }} + {{ row.firstBrokerageRecord }} </template> </el-table-column> <el-table-column align="center" label="二级返佣(元)" min-width="80"> <template #default="{ row }"> - {{ row.subCommissionSecondPrice }} + {{ row.secondBrokerageRecord }} </template> </el-table-column> </template> @@ -295,8 +295,8 @@ const skuList = ref<Sku[]>([ stock: 0, // 库存 weight: 0, // 商品重量 volume: 0, // 商品体积 - subCommissionFirstPrice: 0, // 一级分销的佣金 - subCommissionSecondPrice: 0 // 二级分销的佣金 + firstBrokerageRecord: 0, // 一级分销的佣金 + secondBrokerageRecord: 0 // 二级分销的佣金 } ]) // 批量添加时的临时数据 @@ -415,8 +415,8 @@ const generateTableData = (propertyList: any[]) => { stock: 0, weight: 0, volume: 0, - subCommissionFirstPrice: 0, - subCommissionSecondPrice: 0 + firstBrokerageRecord: 0, + secondBrokerageRecord: 0 } // 如果存在属性相同的 sku 则不做处理 const index = formData.value!.skus!.findIndex( @@ -491,8 +491,8 @@ watch( stock: 0, weight: 0, volume: 0, - subCommissionFirstPrice: 0, - subCommissionSecondPrice: 0 + firstBrokerageRecord: 0, + secondBrokerageRecord: 0 } ] } diff --git a/src/views/mall/product/spu/form/BasicInfoForm.vue b/src/views/mall/product/spu/form/BasicInfoForm.vue index b41300cc..66523432 100644 --- a/src/views/mall/product/spu/form/BasicInfoForm.vue +++ b/src/views/mall/product/spu/form/BasicInfoForm.vue @@ -332,8 +332,8 @@ defineExpose({ validate }) const changeSubCommissionType = () => { // 默认为零,类型切换后也要重置为零 for (const item of formData.skus) { - item.subCommissionFirstPrice = 0 - item.subCommissionSecondPrice = 0 + item.firstBrokerageRecord = 0 + item.secondBrokerageRecord = 0 } } @@ -352,8 +352,8 @@ const onChangeSpec = () => { stock: 0, weight: 0, volume: 0, - subCommissionFirstPrice: 0, - subCommissionSecondPrice: 0 + firstBrokerageRecord: 0, + secondBrokerageRecord: 0 } ] } diff --git a/src/views/mall/product/spu/form/index.vue b/src/views/mall/product/spu/form/index.vue index 0ba7a6d8..e6364f5d 100644 --- a/src/views/mall/product/spu/form/index.vue +++ b/src/views/mall/product/spu/form/index.vue @@ -82,8 +82,8 @@ const formData = ref<ProductSpuApi.Spu>({ stock: 0, // 库存 weight: 0, // 商品重量 volume: 0, // 商品体积 - subCommissionFirstPrice: 0, // 一级分销的佣金 - subCommissionSecondPrice: 0 // 二级分销的佣金 + firstBrokerageRecord: 0, // 一级分销的佣金 + secondBrokerageRecord: 0 // 二级分销的佣金 } ], description: '', // 商品详情 @@ -112,15 +112,15 @@ const getDetail = async () => { item.price = floatToFixed2(item.price) item.marketPrice = floatToFixed2(item.marketPrice) item.costPrice = floatToFixed2(item.costPrice) - item.subCommissionFirstPrice = floatToFixed2(item.subCommissionFirstPrice) - item.subCommissionSecondPrice = floatToFixed2(item.subCommissionSecondPrice) + item.firstBrokerageRecord = floatToFixed2(item.firstBrokerageRecord) + item.secondBrokerageRecord = floatToFixed2(item.secondBrokerageRecord) } else { // 回显价格分转元 item.price = formatToFraction(item.price) item.marketPrice = formatToFraction(item.marketPrice) item.costPrice = formatToFraction(item.costPrice) - item.subCommissionFirstPrice = formatToFraction(item.subCommissionFirstPrice) - item.subCommissionSecondPrice = formatToFraction(item.subCommissionSecondPrice) + item.firstBrokerageRecord = formatToFraction(item.firstBrokerageRecord) + item.secondBrokerageRecord = formatToFraction(item.secondBrokerageRecord) } }) formData.value = res @@ -149,8 +149,8 @@ const submitForm = async () => { item.price = convertToInteger(item.price) item.marketPrice = convertToInteger(item.marketPrice) item.costPrice = convertToInteger(item.costPrice) - item.subCommissionFirstPrice = convertToInteger(item.subCommissionFirstPrice) - item.subCommissionSecondPrice = convertToInteger(item.subCommissionSecondPrice) + item.firstBrokerageRecord = convertToInteger(item.firstBrokerageRecord) + item.secondBrokerageRecord = convertToInteger(item.secondBrokerageRecord) }) // 处理轮播图列表 const newSliderPicUrls: any[] = [] diff --git a/src/views/mall/promotion/components/SpuAndSkuList.vue b/src/views/mall/promotion/components/SpuAndSkuList.vue index e09b34dd..facc6cf3 100644 --- a/src/views/mall/promotion/components/SpuAndSkuList.vue +++ b/src/views/mall/promotion/components/SpuAndSkuList.vue @@ -41,7 +41,7 @@ import { SpuProperty } from '@/views/mall/promotion/components/index' defineOptions({ name: 'PromotionSpuAndSkuList' }) const props = defineProps<{ - spuList: T[] // TODO 为了方便兼容后续可能有需要展示多个 spu 的情况暂时保持,如果后续都是只操作一个 spu 的话则可更改为接受一个 spu 或保持 + spuList: T[] ruleConfig: RuleConfig[] spuPropertyListP: SpuProperty<T>[] }>() diff --git a/src/views/mall/trade/afterSale/detail/index.vue b/src/views/mall/trade/afterSale/detail/index.vue index dbe9cdf3..557e6b6e 100644 --- a/src/views/mall/trade/afterSale/detail/index.vue +++ b/src/views/mall/trade/afterSale/detail/index.vue @@ -126,28 +126,22 @@ <el-descriptions-item labelClassName="no-colon"> <el-timeline> <el-timeline-item - v-for="saleLog in formData.afterSaleLog" + v-for="saleLog in formData.logs" :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> + <div class="el-timeline-right-content"> + <span>{{ saleLog.content }}</span> + </div> + <template #dot> + <span + :style="{ backgroundColor: getUserTypeColor(saleLog.userType) }" + class="dot-node-style" + > + {{ getDictLabel(DICT_TYPE.USER_TYPE, saleLog.userType)[0] || '系' }} + </span> + </template> </el-timeline-item> </el-timeline> </el-descriptions-item> @@ -160,28 +154,52 @@ <script lang="ts" setup> import * as AfterSaleApi from '@/api/mall/trade/afterSale/index' import { floatToFixed2 } from '@/utils' -import { DICT_TYPE } from '@/utils/dict' +import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict' import { formatDate } from '@/utils/formatTime' import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue' import { createImageViewer } from '@/components/ImageViewer' import { isArray } from '@/utils/is' +import { useTagsViewStore } from '@/store/modules/tagsView' defineOptions({ name: 'TradeAfterSaleDetail' }) const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const { params } = useRoute() // 查询参数 +const { push, currentRoute } = useRouter() // 路由 const formData = ref({ order: {}, - afterSaleLog: [] + logs: [] }) const updateAuditReasonFormRef = ref() // 拒绝售后表单 Ref +/** 获得 userType 颜色 */ +const getUserTypeColor = (type: number) => { + const dict = getDictObj(DICT_TYPE.USER_TYPE, type) + switch (dict?.colorType) { + case 'success': + return '#67C23A' + case 'info': + return '#909399' + case 'warning': + return '#E6A23C' + case 'danger': + return '#F56C6C' + } + return '#409EFF' +} + /** 获得详情 */ const getDetail = async () => { const id = params.orderId as unknown as number if (id) { - formData.value = await AfterSaleApi.getAfterSale(id) + const res = await AfterSaleApi.getAfterSale(id) + // 没有表单信息则关闭页面返回 + if (res == null) { + message.notifyError('售后订单不存在') + close() + } + formData.value = res } } @@ -240,7 +258,12 @@ const imagePreview = (args) => { urlList }) } - +const { delView } = useTagsViewStore() // 视图操作 +/** 关闭 tag */ +const close = () => { + delView(unref(currentRoute)) + push({ name: 'TradeAfterSale' }) +} onMounted(async () => { await getDetail() }) @@ -277,4 +300,51 @@ onMounted(async () => { } } } + +// 时间线样式调整 +:deep(.el-timeline) { + margin: 10px 0px 0px 160px; + + .el-timeline-item__wrapper { + position: relative; + top: -20px; + + .el-timeline-item__timestamp { + position: absolute !important; + top: 10px; + left: -150px; + } + } + + .el-timeline-right-content { + display: flex; + align-items: center; + min-height: 30px; + padding: 10px; + background-color: #f7f8fa; + + &::before { + content: ''; + position: absolute; + top: 10px; + left: 13px; + border-width: 8px; /* 调整尖角大小 */ + border-style: solid; + border-color: transparent #f7f8fa transparent transparent; /* 尖角颜色,左侧朝向 */ + } + } + + .dot-node-style { + width: 20px; + height: 20px; + position: absolute; + left: -5px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + color: #fff; + font-size: 10px; + } +} </style> diff --git a/src/views/mall/trade/brokerage/record/index.vue b/src/views/mall/trade/brokerage/record/index.vue new file mode 100644 index 00000000..f9cae4e1 --- /dev/null +++ b/src/views/mall/trade/brokerage/record/index.vue @@ -0,0 +1,169 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="用户编号" prop="userId"> + <el-input + v-model="queryParams.userId" + placeholder="请输入用户编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="业务类型" prop="bizType"> + <el-select + v-model="queryParams.bizType" + placeholder="请选择业务类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_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="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="编号" align="center" prop="id" min-width="60" /> + <el-table-column label="用户编号" align="center" prop="userId" min-width="80" /> + <el-table-column label="头像" align="center" prop="userAvatar" width="70px"> + <template #default="scope"> + <el-avatar :src="scope.row.userAvatar" /> + </template> + </el-table-column> + <el-table-column label="昵称" align="center" prop="userNickname" min-width="80px" /> + <el-table-column label="业务类型" align="center" prop="bizType" min-width="85"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE" :value="scope.row.bizType" /> + </template> + </el-table-column> + <el-table-column label="业务编号" align="center" prop="bizId" min-width="80" /> + <el-table-column label="标题" align="center" prop="title" min-width="110" /> + <el-table-column + label="金额" + align="center" + prop="price" + min-width="60" + :formatter="fenToYuanFormat" + /> + <el-table-column label="说明" align="center" prop="description" min-width="120" /> + <el-table-column label="状态" align="center" prop="status" min-width="85"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="解冻时间" + align="center" + prop="unfreezeTime" + :formatter="dateFormatter" + width="170px" + /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="170px" + /> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> +</template> + +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record' +import { fenToYuanFormat } from '@/utils/formatter' + +defineOptions({ name: 'TradeBrokerageRecord' }) + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + bizType: null, + price: null, + status: null, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await BrokerageRecordApi.getBrokerageRecordPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue b/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue new file mode 100644 index 00000000..d2c0568f --- /dev/null +++ b/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue @@ -0,0 +1,152 @@ +<template> + <Dialog v-model="dialogVisible" title="推广人列表" width="75%"> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="85px" + > + <el-form-item label="用户类型" prop="level"> + <el-radio-group v-model="queryParams.level" @change="handleQuery"> + <el-radio-button checked>全部</el-radio-button> + <el-radio-button label="1">一级推广人</el-radio-button> + <el-radio-button label="2">二级推广人</el-radio-button> + </el-radio-group> + </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.BROKERAGE_RECORD_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="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="订单编号" align="center" prop="bizId" min-width="80px" /> + <el-table-column label="用户编号" align="center" prop="sourceUserId" min-width="80px" /> + <el-table-column label="头像" align="center" prop="sourceUserAvatar" width="70px"> + <template #default="scope"> + <el-avatar :src="scope.row.sourceUserAvatar" /> + </template> + </el-table-column> + <el-table-column label="昵称" align="center" prop="sourceUserNickname" min-width="80px" /> + <el-table-column + label="佣金" + align="center" + prop="price" + min-width="100px" + :formatter="fenToYuanFormat" + /> + <el-table-column label="状态" align="center" prop="status" min-width="85"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="170px" + /> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + </Dialog> +</template> + +<script setup lang="ts"> +import { dateFormatter } from '@/utils/formatTime' +import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record' +import { BrokerageRecordBizTypeEnum } from '@/utils/constants' +import { fenToYuanFormat } from '@/utils/formatter' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' + +/** 推广订单列表 */ +defineOptions({ name: 'BrokerageOrderListDialog' }) + +const message = useMessage() // 消息弹窗 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + bizType: BrokerageRecordBizTypeEnum.ORDER.type, + level: '', + createTime: [], + status: null +}) +const queryFormRef = ref() // 搜索的表单 + +/** 打开弹窗 */ +const dialogVisible = ref(false) // 弹窗的是否展示 +const open = async (userId: any) => { + dialogVisible.value = true + queryParams.userId = userId + resetQuery() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await BrokerageRecordApi.getBrokerageRecordPage(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() +} +</script> diff --git a/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue b/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue new file mode 100644 index 00000000..2277cc01 --- /dev/null +++ b/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue @@ -0,0 +1,137 @@ +<template> + <Dialog v-model="dialogVisible" title="推广人列表" width="75%"> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="85px" + > + <el-form-item label="用户类型" prop="level"> + <el-radio-group v-model="queryParams.level" @change="handleQuery"> + <el-radio-button checked>全部</el-radio-button> + <el-radio-button label="1">一级推广人</el-radio-button> + <el-radio-button label="2">二级推广人</el-radio-button> + </el-radio-group> + </el-form-item> + <el-form-item label="绑定时间" prop="bindUserTime"> + <el-date-picker + v-model="queryParams.bindUserTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="用户编号" align="center" prop="id" min-width="80px" /> + <el-table-column label="头像" align="center" prop="avatar" width="70px"> + <template #default="scope"> + <el-avatar :src="scope.row.avatar" /> + </template> + </el-table-column> + <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" /> + <el-table-column + label="推广人数" + align="center" + prop="brokerageUserCount" + min-width="80px" + /> + <el-table-column + label="推广订单数量" + align="center" + prop="brokerageOrderCount" + min-width="110px" + /> + <el-table-column label="推广资格" align="center" prop="brokerageEnabled" min-width="80px"> + <template #default="scope"> + <el-tag v-if="scope.row.brokerageEnabled">有</el-tag> + <el-tag v-else type="info">无</el-tag> + </template> + </el-table-column> + <el-table-column + label="绑定时间" + align="center" + prop="bindUserTime" + :formatter="dateFormatter" + width="170px" + /> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + </Dialog> +</template> + +<script setup lang="ts"> +import { dateFormatter } from '@/utils/formatTime' +import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' + +/** 推广人列表 */ +defineOptions({ name: 'BrokerageUserListDialog' }) + +const message = useMessage() // 消息弹窗 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + bindUserId: null, + level: '', + bindUserTime: [] +}) +const queryFormRef = ref() // 搜索的表单 + +/** 打开弹窗 */ +const dialogVisible = ref(false) // 弹窗的是否展示 +const open = async (bindUserId: any) => { + dialogVisible.value = true + queryParams.bindUserId = bindUserId + resetQuery() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await BrokerageUserApi.getBrokerageUserPage(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() +} +</script> diff --git a/src/views/mall/trade/brokerage/user/UpdateBindUserForm.vue b/src/views/mall/trade/brokerage/user/UpdateBindUserForm.vue new file mode 100644 index 00000000..b4c04780 --- /dev/null +++ b/src/views/mall/trade/brokerage/user/UpdateBindUserForm.vue @@ -0,0 +1,127 @@ +<template> + <Dialog v-model="dialogVisible" title="修改上级推广人" width="500"> + <el-form + ref="formRef" + v-loading="formLoading" + :model="formData" + :rules="formRules" + label-width="80px" + > + <el-form-item label="推广人" prop="bindUserId"> + <el-input + v-model="formData.bindUserId" + placeholder="请输入推广员编号" + v-loading="formLoading" + > + <template #append> + <el-button @click="handleGetUser"><Icon icon="ep:search" class="mr-5px" /></el-button> + </template> + </el-input> + </el-form-item> + </el-form> + <el-descriptions v-if="bindUser" :column="1" border> + <el-descriptions-item label="头像"> + <el-avatar :src="bindUser.avatar" /> + </el-descriptions-item> + <el-descriptions-item label="昵称">{{ bindUser.nickname }}</el-descriptions-item> + <el-descriptions-item label="推广资格"> + <el-tag v-if="bindUser.brokerageEnabled">有</el-tag> + <el-tag v-else type="info">无</el-tag> + </el-descriptions-item> + <el-descriptions-item label="成为推广员的时间"> + {{ formatDate(bindUser.brokerageTime) }} + </el-descriptions-item> + </el-descriptions> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" setup> +import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' +import { formatDate } from '@/utils/formatTime' + +/** 修改上级推广人表单 */ +defineOptions({ name: 'UpdateBindUserForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref() +const formRef = ref() // 表单 Ref +const formRules = reactive({ + bindUserId: [{ required: true, message: '推广员人不能为空', trigger: 'blur' }] +}) + +/** 打开弹窗 */ +const open = async (row: BrokerageUserApi.BrokerageUserVO) => { + resetForm() + // 设置数据 + formData.value.id = row.id + formData.value.bindUserId = row.bindUserId + // 反显上级推广人 + if (row.bindUserId) { + await handleGetUser() + } + dialogVisible.value = true +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +/** 修改上级推广人 */ +const submitForm = async () => { + if (formLoading.value) return + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + + if (!bindUser.value) { + message.error('请先查询并确认推广人') + return + } + + // 提交请求 + formLoading.value = true + try { + // 发起修改 + await BrokerageUserApi.updateBindUser(formData.value) + message.success(t('common.updateSuccess')) + dialogVisible.value = false + // 发送操作成功的事件 + emit('success', true) + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: 0, + bindUserId: undefined + } + formRef.value?.resetFields() + bindUser.value = undefined +} + +/** 查询推广员 */ +const bindUser = ref<BrokerageUserApi.BrokerageUserVO>() +const handleGetUser = async () => { + if (formData.value.bindUserId == formData.value.id) { + message.error('不能绑定自己为推广人') + return + } + + formLoading.value = true + bindUser.value = await BrokerageUserApi.getBrokerageUser(formData.value.bindUserId) + if (!bindUser.value) { + message.warning('推广员不存在') + } + formLoading.value = false +} +</script> diff --git a/src/views/mall/trade/brokerage/user/index.vue b/src/views/mall/trade/brokerage/user/index.vue new file mode 100644 index 00000000..285c49ee --- /dev/null +++ b/src/views/mall/trade/brokerage/user/index.vue @@ -0,0 +1,305 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="85px" + > + <el-form-item label="推广员编号" prop="bindUserId"> + <el-input + v-model="queryParams.bindUserId" + placeholder="请输入推广员编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="推广资格" prop="brokerageEnabled"> + <el-select + v-model="queryParams.brokerageEnabled" + class="!w-240px" + clearable + placeholder="请选择推广资格" + > + <el-option label="有" :value="true" /> + <el-option label="无" :value="false" /> + </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="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="用户编号" align="center" prop="id" min-width="80px" /> + <el-table-column label="头像" align="center" prop="avatar" width="70px"> + <template #default="scope"> + <el-avatar :src="scope.row.avatar" /> + </template> + </el-table-column> + <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" /> + <el-table-column label="推广人数" align="center" prop="brokerageUserCount" width="80px" /> + <el-table-column + label="推广订单数量" + align="center" + prop="brokerageOrderCount" + min-width="110px" + /> + <el-table-column + label="推广订单金额" + align="center" + prop="brokerageOrderPrice" + min-width="110px" + :formatter="fenToYuanFormat" + /> + <el-table-column + label="已提现金额" + align="center" + prop="withdrawPrice" + min-width="100px" + :formatter="fenToYuanFormat" + /> + <el-table-column label="已提现次数" align="center" prop="withdrawCount" min-width="100px" /> + <el-table-column + label="未提现金额" + align="center" + prop="price" + min-width="100px" + :formatter="fenToYuanFormat" + /> + <el-table-column + label="冻结中佣金" + align="center" + prop="frozenPrice" + min-width="100px" + :formatter="fenToYuanFormat" + /> + <el-table-column label="推广资格" align="center" prop="brokerageEnabled" min-width="80px"> + <template #default="scope"> + <el-switch + v-model="scope.row.brokerageEnabled" + active-text="有" + inactive-text="无" + inline-prompt + :disabled="!checkPermi(['trade:brokerage-user:update-bind-user'])" + @change="handleBrokerageEnabledChange(scope.row)" + /> + </template> + </el-table-column> + <el-table-column + label="成为推广员时间" + align="center" + prop="brokerageTime" + :formatter="dateFormatter" + width="170px" + /> + <el-table-column label="上级推广员编号" align="center" prop="bindUserId" width="150px" /> + <el-table-column + label="推广员绑定时间" + align="center" + prop="bindUserTime" + :formatter="dateFormatter" + width="170px" + /> + <el-table-column label="操作" align="center" width="150px" fixed="right"> + <template #default="scope"> + <el-dropdown + @command="(command) => handleCommand(command, scope.row)" + v-hasPermi="[ + 'trade:brokerage-user:user-query', + 'trade:brokerage-user:order-query', + 'trade:brokerage-user:update-bind-user', + 'trade:brokerage-user:clear-bind-user' + ]" + > + <el-button link type="primary"> + <Icon icon="ep:d-arrow-right" /> + 更多 + </el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + command="openBrokerageUserTable" + v-if="checkPermi(['trade:brokerage-user:user-query'])" + > + 推广人 + </el-dropdown-item> + <el-dropdown-item + command="openBrokerageOrderTable" + v-if="checkPermi(['trade:brokerage-user:order-query'])" + > + 推广订单 + </el-dropdown-item> + <el-dropdown-item + command="openUpdateBindUserForm" + v-if="checkPermi(['trade:brokerage-user:update-bind-user'])" + > + 修改上级推广人 + </el-dropdown-item> + <el-dropdown-item + command="handleClearBindUser" + v-if=" + scope.row.bindUserId && checkPermi(['trade:brokerage-user:clear-bind-user']) + " + > + 清除上级推广人 + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + <!-- 修改上级推广人表单 --> + <UpdateBindUserForm ref="updateBindUserFormRef" @success="getList" /> + <!-- 推广人列表 --> + <BrokerageUserListDialog ref="brokerageUserListDialogRef" /> + <!-- 推广订单列表 --> + <BrokerageOrderListDialog ref="brokerageOrderListDialogRef" /> +</template> + +<script setup lang="ts"> +import { dateFormatter } from '@/utils/formatTime' +import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' +import { checkPermi } from '@/utils/permission' +import { fenToYuanFormat } from '@/utils/formatter' +import UpdateBindUserForm from '@/views/mall/trade/brokerage/user/UpdateBindUserForm.vue' +import BrokerageUserListDialog from '@/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue' +import BrokerageOrderListDialog from '@/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue' + +defineOptions({ name: 'TradeBrokerageUser' }) + +const message = useMessage() // 消息弹窗 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + bindUserId: null, + brokerageEnabled: null, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await BrokerageUserApi.getBrokerageUserPage(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 handleCommand = (command: string, row: BrokerageUserApi.BrokerageUserVO) => { + switch (command) { + case 'openBrokerageUserTable': + openBrokerageUserTable(row.id) + break + case 'openBrokerageOrderTable': + openBrokerageOrderTable(row.id) + break + case 'openUpdateBindUserForm': + openUpdateBindUserForm(row) + break + case 'handleClearBindUser': + handleClearBindUser(row) + break + } +} + +/** 打开推广人列表 */ +const brokerageUserListDialogRef = ref() +const openBrokerageUserTable = (id: number) => { + brokerageUserListDialogRef.value.open(id) +} + +/** 打开推广订单列表 */ +const brokerageOrderListDialogRef = ref() +const openBrokerageOrderTable = (id: number) => { + brokerageOrderListDialogRef.value.open(id) +} + +/** 打开表单:修改上级推广人 */ +const updateBindUserFormRef = ref() +const openUpdateBindUserForm = (row: BrokerageUserApi.BrokerageUserVO) => { + updateBindUserFormRef.value.open(row) +} + +/** 清除上级推广人 */ +const handleClearBindUser = async (row: BrokerageUserApi.BrokerageUserVO) => { + try { + // 二次确认 + await message.confirm(`确认要清除"${row.nickname}"的上级推广人吗?`) + // 发起修改 + await BrokerageUserApi.clearBindUser({ id: row.id }) + message.success('清除成功') + // 刷新列表 + await getList() + } catch {} +} + +/** 推广资格 开通/关闭 */ +const handleBrokerageEnabledChange = async (row: BrokerageUserApi.BrokerageUserVO) => { + try { + // 二次确认 + const text = row.brokerageEnabled ? '开通' : '关闭' + await message.confirm(`确认要${text}"${row.nickname}"的推广资格吗?`) + // 发起修改 + await BrokerageUserApi.updateBrokerageEnabled({ id: row.id, enabled: row.brokerageEnabled }) + message.success(text + '成功') + // 刷新列表 + await getList() + } catch { + // 异常时,需要重置回之前的值 + row.brokerageEnabled = !row.brokerageEnabled + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue b/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue new file mode 100644 index 00000000..2a69b5b0 --- /dev/null +++ b/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue @@ -0,0 +1,73 @@ +<template> + <Dialog title="审核" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="驳回原因" prop="auditReason"> + <el-input v-model="formData.auditReason" type="textarea" placeholder="请输入驳回原因" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw' + +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref({ + id: undefined, + auditReason: undefined +}) +const formRules = reactive({ + auditReason: [{ required: true, message: '驳回原因不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (id: number) => { + dialogVisible.value = true + resetForm() + formData.value.id = id +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as BrokerageWithdrawApi.BrokerageWithdrawVO + await BrokerageWithdrawApi.rejectBrokerageWithdraw(data) + message.success('驳回成功') + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + auditReason: undefined + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/mall/trade/brokerage/withdraw/index.vue b/src/views/mall/trade/brokerage/withdraw/index.vue new file mode 100644 index 00000000..535387e9 --- /dev/null +++ b/src/views/mall/trade/brokerage/withdraw/index.vue @@ -0,0 +1,264 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="用户编号" prop="userId"> + <el-input + v-model="queryParams.userId" + placeholder="请输入用户编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="提现类型" prop="type"> + <el-select + v-model="queryParams.type" + placeholder="请选择提现类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="账号" prop="accountNo"> + <el-input + v-model="queryParams.accountNo" + placeholder="请输入账号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="提现银行" prop="bankName"> + <el-select + v-model="queryParams.bankName" + placeholder="请选择提现银行" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_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="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="编号" align="left" prop="id" min-width="60px" /> + <el-table-column label="用户信息" align="left" min-width="120px"> + <template #default="scope"> + <div>编号:{{ scope.row.userId }}</div> + <div>昵称:{{ scope.row.userNickname }}</div> + </template> + </el-table-column> + <el-table-column label="提现金额" align="left" prop="price" min-width="80px"> + <template #default="scope"> + <div>金 额:¥{{ fenToYuan(scope.row.price) }}</div> + <div>手续费:¥{{ fenToYuan(scope.row.feePrice) }}</div> + </template> + </el-table-column> + <el-table-column label="提现方式" align="left" prop="type" min-width="120px"> + <template #default="scope"> + <div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type"> 余额 </div> + <div v-else> + {{ getDictLabel(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, scope.row.type) }}账号: + {{ scope.row.accountNo }} + </div> + <template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type"> + <div>真实姓名:{{ scope.row.name }}</div> + <div> + 银行名称: + <dict-tag :type="DICT_TYPE.BROKERAGE_BANK_NAME" :value="scope.row.bankName" /> + </div> + <div>开户地址:{{ scope.row.bankAddress }}</div> + </template> + </template> + </el-table-column> + <el-table-column label="收款码" align="left" prop="accountQrCodeUrl" width="70px"> + <template #default="scope"> + <el-image + :src="scope.row.accountQrCodeUrl" + class="w-40px h-40px" + :preview-src-list="[scope.row.accountQrCodeUrl]" + preview-teleported + /> + </template> + </el-table-column> + <el-table-column + label="申请时间" + align="left" + prop="createTime" + :formatter="dateFormatter" + width="170px" + /> + <el-table-column label="备注" align="left" prop="remark" /> + <el-table-column label="状态" align="left" prop="status" min-width="120px"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS" :value="scope.row.status" /> + <div v-if="scope.row.auditTime" class="text-xs"> + 时间:{{ formatDate(scope.row.auditTime) }} + </div> + <div v-if="scope.row.auditReason" class="text-xs"> + 原因:{{ scope.row.auditReason }} + </div> + </template> + </el-table-column> + <el-table-column label="操作" align="left" width="110px" fixed="right"> + <template #default="scope"> + <template v-if="scope.row.status === BrokerageWithdrawStatusEnum.AUDITING.status"> + <el-button + link + type="primary" + @click="handleApprove(scope.row.id)" + v-hasPermi="['trade:brokerage-withdraw:audit']" + > + 通过 + </el-button> + <el-button + link + type="danger" + @click="openForm(scope.row.id)" + v-hasPermi="['trade:brokerage-withdraw:audit']" + > + 驳回 + </el-button> + </template> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <BrokerageWithdrawRejectForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { DICT_TYPE, getDictLabel, getIntDictOptions, getStrDictOptions } from '@/utils/dict' +import { dateFormatter, formatDate } from '@/utils/formatTime' +import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw' +import BrokerageWithdrawRejectForm from './BrokerageWithdrawRejectForm.vue' +import { BrokerageWithdrawStatusEnum, BrokerageWithdrawTypeEnum } from '@/utils/constants' +import { fenToYuanFormat } from '@/utils/formatter' +import { fenToYuan } from '@/utils' + +defineOptions({ name: 'BrokerageWithdraw' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + type: null, + name: null, + accountNo: null, + bankName: null, + status: null, + auditReason: null, + auditTime: [], + remark: null, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await BrokerageWithdrawApi.getBrokerageWithdrawPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (id: number) => { + formRef.value.open(id) +} + +/** 审核通过 */ +const handleApprove = async (id: number) => { + try { + loading.value = true + await message.confirm('确定要审核通过吗?') + await BrokerageWithdrawApi.approveBrokerageWithdraw(id) + await message.success(t('common.success')) + await getList() + } finally { + loading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/mall/trade/config/index.vue b/src/views/mall/trade/config/index.vue new file mode 100644 index 00000000..6fa41cad --- /dev/null +++ b/src/views/mall/trade/config/index.vue @@ -0,0 +1,243 @@ +<template> + <ContentWrap> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="120px" + v-loading="formLoading" + > + <el-form-item label="hideId" v-show="false"> + <el-input v-model="formData.id" /> + </el-form-item> + + <el-tabs> + <el-tab-pane label="配送"> + <el-form-item label="启用包邮" prop="deliveryExpressFreeEnabled"> + <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" /> + <el-text class="w-full" size="small" type="info"> 商城是否启用全场包邮 </el-text> + </el-form-item> + <el-form-item label="满额包邮" prop="deliveryExpressFreePrice"> + <el-input-number + v-model="formData.deliveryExpressFreePrice" + placeholder="请输入满额包邮" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> 商城商品满多少金额即可包邮 </el-text> + </el-form-item> + </el-tab-pane> + <el-tab-pane label="分销"> + <el-form-item label="分佣启用" prop="brokerageEnabled"> + <el-switch v-model="formData.brokerageEnabled" style="user-select: none" /> + <el-text class="w-full" size="small" type="info"> 商城是否开启分销模式 </el-text> + </el-form-item> + <el-form-item label="分佣模式" prop="brokerageEnabledCondition"> + <el-radio-group v-model="formData.brokerageEnabledCondition"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_ENABLED_CONDITION)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + <el-text class="w-full" size="small" type="info"> + 人人分销:每个用户都可以成为推广员 + </el-text> + <el-text class="w-full" size="small" type="info"> + 指定分销:仅可在后台手动设置推广员 + </el-text> + </el-form-item> + <el-form-item label="分销关系绑定" prop="brokerageBindMode"> + <el-radio-group v-model="formData.brokerageBindMode"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + <el-text class="w-full" size="small" type="info"> + 没有推广人:只要用户没有推广人,随时都可以绑定推广关系 + </el-text> + <el-text class="w-full" size="small" type="info"> + 新用户:只有新用户注册时或首次进入系统时才可以绑定推广关系 + </el-text> + </el-form-item> + <el-form-item label="分销海报图"> + <UploadImgs v-model="formData.brokeragePostUrls" width="75px" height="125px" /> + <el-text class="w-full" size="small" type="info"> + 个人中心分销海报图片,建议尺寸600x1000 + </el-text> + </el-form-item> + <el-form-item label="一级返佣比例" prop="brokerageFirstPercent"> + <el-input-number + v-model="formData.brokerageFirstPercent" + placeholder="请输入一级返佣比例" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> + 订单交易成功后给推广人返佣的百分比 + </el-text> + </el-form-item> + <el-form-item label="二级返佣比例" prop="brokerageSecondPercent"> + <el-input-number + v-model="formData.brokerageSecondPercent" + placeholder="请输入二级返佣比例" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> + 订单交易成功后给推广人的推荐人返佣的百分比 + </el-text> + </el-form-item> + <el-form-item label="佣金冻结天数" prop="brokerageFrozenDays"> + <el-input-number + v-model="formData.brokerageFrozenDays" + placeholder="请输入佣金冻结天数" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> + 防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天 + </el-text> + </el-form-item> + <el-form-item label="提现最低金额" prop="brokerageWithdrawMinPrice"> + <el-input-number + v-model="formData.brokerageWithdrawMinPrice" + placeholder="请输入提现最低金额" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> + 用户提现最低金额限制,单位:元 + </el-text> + </el-form-item> + <el-form-item label="提现手续费" prop="brokerageWithdrawFeePercent"> + <el-input-number + v-model="formData.brokerageWithdrawFeePercent" + placeholder="请输入提现手续费" + class="!w-xs" + /> + <el-text class="w-full" size="small" type="info"> + 提现手续费百分比,范围0-100,0为无提现手续费,例:设置10,即收取10%手续费,提现100元,到账90元,10元手续费 + </el-text> + </el-form-item> + <el-form-item label="提现方式" prop="brokerageWithdrawType"> + <el-checkbox-group v-model="formData.brokerageWithdrawType"> + <el-checkbox + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-checkbox> + </el-checkbox-group> + <el-text class="w-full" size="small" type="info"> 商城开通提现的付款方式 </el-text> + </el-form-item> + <el-form-item label="提现银行" prop="brokerageBankNames"> + <el-select v-model="formData.brokerageBankNames" placeholder="请选择提现银行" multiple> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + <el-text class="w-full" size="small" type="info"> 商城开通提现的银行列表 </el-text> + </el-form-item> + </el-tab-pane> + </el-tabs> + + <el-form-item> + <el-button type="primary" @click="submitForm" :loading="formLoading"> 保存 </el-button> + </el-form-item> + </el-form> + </ContentWrap> +</template> + +<script setup lang="ts"> +import * as ConfigApi from '@/api/mall/trade/config' +import { BrokerageBindModeEnum, BrokerageEnabledConditionEnum } from '@/utils/constants' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' + +defineOptions({ name: 'TradeConfig' }) + +const message = useMessage() // 消息弹窗 + +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formRef = ref() +const formData = ref({ + id: null, + deliveryExpressFreeEnabled: true, + deliveryExpressFreePrice: 0, + brokerageEnabled: true, + brokerageEnabledCondition: BrokerageEnabledConditionEnum.ALL.condition, + brokerageBindMode: BrokerageBindModeEnum.ANYTIME.mode, + brokeragePostUrls: [], + brokerageFirstPercent: 0, + brokerageSecondPercent: 0, + brokerageWithdrawMinPrice: 0, + brokerageWithdrawFeePercent: 0, + brokerageBankNames: [], + brokerageFrozenDays: 0, + brokerageWithdrawType: [] +}) +const formRules = reactive({ + deliveryExpressFreePrice: [{ required: true, message: '满额包邮不能为空', trigger: 'blur' }], + brokerageEnabledCondition: [{ required: true, message: '分佣模式不能为空', trigger: 'blur' }], + brokerageBindMode: [{ required: true, message: '分销关系绑定模式不能为空', trigger: 'blur' }], + brokerageFirstPercent: [{ required: true, message: '一级返佣比例不能为空', trigger: 'blur' }], + brokerageSecondPercent: [{ required: true, message: '二级返佣比例不能为空', trigger: 'blur' }], + brokerageWithdrawMinPrice: [ + { required: true, message: '用户提现最低金额不能为空', trigger: 'blur' } + ], + brokerageWithdrawFeePercent: [{ required: true, message: '提现手续费不能为空', trigger: 'blur' }], + brokerageBankNames: [{ required: true, message: '提现银行不能为空', trigger: 'blur' }], + brokerageFrozenDays: [{ required: true, message: '佣金冻结时间不能为空', trigger: 'blur' }], + brokerageWithdrawType: [ + { + required: true, + message: '提现方式不能为空', + trigger: 'change' + } + ] +}) + +const submitForm = async () => { + if (formLoading.value) return + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ConfigApi.ConfigVO + data.brokeragePostUrls = formData.value.brokeragePostUrls.map((item: any) => { + return item?.url ? item.url : item + }) + await ConfigApi.saveTradeConfig(data) + message.success('保存成功') + } finally { + formLoading.value = false + } +} + +/** 查询交易中心配置 */ +const getConfig = async () => { + formLoading.value = true + try { + const data = await ConfigApi.getTradeConfig() + if (data != null) { + data.brokeragePostUrls = data.brokeragePostUrls.map((url) => ({ url })) + formData.value = data + } + } finally { + formLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getConfig() +}) +</script> diff --git a/src/views/mall/trade/order/detail/index.vue b/src/views/mall/trade/order/detail/index.vue index a823fef5..e92fa82c 100644 --- a/src/views/mall/trade/order/detail/index.vue +++ b/src/views/mall/trade/order/detail/index.vue @@ -40,12 +40,17 @@ <el-descriptions-item label="订单状态: "> <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> - <el-button type="primary" @click="openForm('delivery')">发货</el-button> - <el-button type="primary" @click="openForm('updateAddress')">修改地址</el-button> + <el-button v-if="formData.status! === 0" type="primary" @click="updatePrice"> + 调整价格 + </el-button> + <el-button type="primary" @click="remark">备注</el-button> + <el-button v-if="formData.status! === 10" type="primary" @click="delivery"> + 发货 + </el-button> + <el-button v-if="formData.status! === 10" type="primary" @click="updateAddress"> + 修改地址 + </el-button> </el-descriptions-item> <el-descriptions-item> <template #label><span style="color: red">提醒: </span></template> @@ -152,11 +157,22 @@ <el-descriptions-item labelClassName="no-colon"> <el-timeline> <el-timeline-item - v-for="(log, index) in formData.orderLog" + v-for="(log, index) in formData.logs" :key="index" :timestamp="formatDate(log.createTime!)" + placement="top" > - {{ log.content }} + <div class="el-timeline-right-content"> + {{ log.content }} + </div> + <template #dot> + <span + :style="{ backgroundColor: getUserTypeColor(log.userType!) }" + class="dot-node-style" + > + {{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }} + </span> + </template> </el-timeline-item> </el-timeline> </el-descriptions-item> @@ -173,42 +189,55 @@ import * as TradeOrderApi from '@/api/mall/trade/order' import { floatToFixed2 } from '@/utils' import { formatDate } from '@/utils/formatTime' -import { DICT_TYPE } from '@/utils/dict' +import { DICT_TYPE, getDictLabel, getDictObj } 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' +import { useTagsViewStore } from '@/store/modules/tagsView' defineOptions({ name: 'TradeOrderDetail' }) const message = useMessage() // 消息弹窗 +/** 获得 userType 颜色 */ +const getUserTypeColor = (type: number) => { + const dict = getDictObj(DICT_TYPE.USER_TYPE, type) + switch (dict?.colorType) { + case 'success': + return '#67C23A' + case 'info': + return '#909399' + case 'warning': + return '#E6A23C' + case 'danger': + return '#F56C6C' + } + return '#409EFF' +} + // 订单详情 const formData = ref<TradeOrderApi.OrderVO>({ - orderLog: [] // TODO @puhui999:orderLogs + logs: [] }) -// TODO @puhui999:这个最好也拆掉哈 -const deliveryFormRef = ref() // 发货表单 Ref +/** 各种操作 */ const updateRemarkForm = ref() // 订单备注表单 Ref +const remark = () => { + updateRemarkForm.value?.open(formData.value) +} +const deliveryFormRef = ref() // 发货表单 Ref +const delivery = () => { + deliveryFormRef.value?.open(formData.value) +} const updateAddressFormRef = ref() // 收货地址表单 Ref +const updateAddress = () => { + updateAddressFormRef.value?.open(formData.value) +} const updatePriceFormRef = ref() // 订单调价表单 Ref -const openForm = (type: string) => { - switch (type) { - case 'remark': - updateRemarkForm.value?.open(formData.value) - break - case 'delivery': - deliveryFormRef.value?.open(formData.value) - break - case 'updateAddress': - updateAddressFormRef.value?.open(formData.value) - break - case 'updatePrice': - updatePriceFormRef.value?.open(formData.value) - break - } +const updatePrice = () => { + updatePriceFormRef.value?.open(formData.value) } /** 获得详情 */ @@ -217,10 +246,23 @@ const getDetail = async () => { const id = params.orderId as unknown as number if (id) { const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO + // 没有表单信息则关闭页面返回 + if (res === null) { + message.error('交易订单不存在') + close() + } formData.value = res } } +/** 关闭 tag */ +const { delView } = useTagsViewStore() // 视图操作 +const { push, currentRoute } = useRouter() // 路由 +const close = () => { + delView(unref(currentRoute)) + push({ name: 'TradeOrder' }) +} + /** 复制 */ const clipboardSuccess = () => { message.success('复制成功') @@ -267,4 +309,51 @@ onMounted(async () => { } } } + +// 时间线样式调整 +:deep(.el-timeline) { + margin: 10px 0px 0px 160px; + + .el-timeline-item__wrapper { + position: relative; + top: -20px; + + .el-timeline-item__timestamp { + position: absolute !important; + top: 10px; + left: -150px; + } + } + + .el-timeline-right-content { + display: flex; + align-items: center; + min-height: 30px; + padding: 10px; + background-color: #f7f8fa; + + &::before { + content: ''; /* 必须设置 content 属性 */ + position: absolute; + top: 10px; + left: 13px; /* 将伪元素水平居中 */ + border-width: 8px; /* 调整尖角大小 */ + border-style: solid; + border-color: transparent #f7f8fa transparent transparent; /* 尖角颜色,左侧朝向 */ + } + } + + .dot-node-style { + width: 20px; + height: 20px; + position: absolute; + left: -5px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + color: #fff; + font-size: 10px; + } +} </style> diff --git a/src/views/mall/trade/order/index.vue b/src/views/mall/trade/order/index.vue index 4b8d295f..0970f6bc 100644 --- a/src/views/mall/trade/order/index.vue +++ b/src/views/mall/trade/order/index.vue @@ -100,17 +100,24 @@ /> </el-select> </el-form-item> - <!-- TODO 聚合搜索等售后结束后实现--> + <!-- TODO puhui 聚合搜索等售后结束后实现--> + <!-- TODO puhui999:尽量不要用 .k 这样的参数,完整拼写,有完整的业务含义 --> <el-form-item label="聚合搜索"> <el-input v-show="true" - v-model="queryType.v" + v-model="queryParams[queryType.k]" class="!w-280px" clearable placeholder="请输入" > <template #prepend> - <el-select v-model="queryType.k" class="!w-110px" clearable placeholder="全部"> + <el-select + v-model="queryType.k" + class="!w-110px" + clearable + placeholder="全部" + @change="inputChangeSelect" + > <el-option v-for="dict in searchList" :key="dict.value" @@ -139,7 +146,7 @@ <el-table v-loading="loading" :data="list"> <el-table-column class-name="order-table-col"> <template #header> - <!-- TODO @phui999:小屏幕下,会有偏移,后续看看 --> + <!-- TODO @puhui999:小屏幕下,会有偏移,后续看看 --> <div class="flex items-center" style="width: 100%"> <div class="ml-100px mr-200px">商品信息</div> <div class="mr-60px">单价(元)/数量</div> @@ -266,8 +273,8 @@ <el-table-column align="center" fixed="right" label="操作" width="160"> <template #default> <!-- TODO 权限后续补齐 --> - <div class="flex items-center justify-center"> - <el-button link type="primary" @click="openForm(scope.row.id)"> + <div class="flex justify-center items-center"> + <el-button link type="primary" @click="openDetail(scope.row.id)"> <Icon icon="ep:notification" /> 详情 </el-button> @@ -335,41 +342,42 @@ const total = ref(2) // 列表的总页数 const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据 const queryFormRef = ref<FormInstance>() // 搜索的表单 // 表单搜索 -const queryParams = reactive({ - pageNo: 1, //首页 - pageSize: 10, //页面大小 - no: '', - userId: '', - userNickname: '', - userMobile: '', - receiverName: '', - receiverMobile: '', - - terminal: '', - type: null, - status: null, - payChannelCode: '', - createTime: [], - deliveryType: null, - spuName: '', - itemCount: '', - pickUpStoreId: [], - logisticsId: null, - all: '' +const queryParams = ref({ + pageNo: 1, // 页数 + pageSize: 10, // 每页显示数量 + status: null, // 订单状态 + payChannelCode: null, // 支付方式 + createTime: null, // 创建时间 + terminal: null, // 订单来源 + type: null, // 订单类型 + deliveryType: null, // 配送方式 + logisticsId: null, // 快递公司 + pickUpStoreId: null // 自提门店 }) -const queryType = reactive({ k: '', v: '' }) // 订单搜索类型kv -/** - * 订单聚合搜索 - * 商品名称、商品件数、全部 - * - * 需要后端支持 TODO - */ +const queryType = reactive({ k: '' }) // 订单搜索类型 k + +// 订单聚合搜索 select 类型配置 +// TODO @puhui999:dynamicSearchList,动态搜索;其它相关的变量和方法,都可以朝着这个变量靠哈;这样更容易理解; const searchList = ref([ { value: 'no', label: '订单号' }, { value: 'userId', label: '用户UID' }, { value: 'userNickname', label: '用户昵称' }, { value: 'userMobile', label: '用户电话' } ]) +/** + * 聚合搜索切换查询对象时触发 + * @param val + */ +const inputChangeSelect = (val: string) => { + searchList.value + .filter((item) => item.value !== val) + ?.forEach((item1) => { + // 清除集合搜索无用属性 + if (queryParams.value.hasOwnProperty(item1.value)) { + delete queryParams.value[item1.value] + } + }) +} const headerStyle = ({ row, columnIndex }: any) => { // 表头第一行第一列占 8 @@ -417,7 +425,7 @@ const spanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => { const getList = async () => { loading.value = true try { - const data = await TradeOrderApi.getOrderPage(queryParams) + const data = await TradeOrderApi.getOrderPage(unref(queryParams)) list.value = data.list total.value = data.total } finally { @@ -427,13 +435,25 @@ const getList = async () => { /** 搜索按钮操作 */ const handleQuery = async () => { - queryParams.pageNo = 1 + queryParams.value.pageNo = 1 await getList() } /** 重置按钮操作 */ const resetQuery = () => { queryFormRef.value?.resetFields() + queryParams.value = { + pageNo: 1, // 页数 + pageSize: 10, // 每页显示数量 + status: null, // 订单状态 + payChannelCode: null, // 支付方式 + createTime: null, // 创建时间 + terminal: null, // 订单来源 + type: null, // 订单类型 + deliveryType: null, // 配送方式 + logisticsId: null, // 快递公司 + pickUpStoreId: null // 自提门店 + } handleQuery() } @@ -445,7 +465,7 @@ const imagePreview = (imgUrl: string) => { } /** 查看订单详情 */ -const openForm = (id: number) => { +const openDetail = (id: number) => { push({ name: 'TradeOrderDetail', params: { orderId: id } }) } diff --git a/src/views/member/point/config/index.vue b/src/views/member/point/config/index.vue index 27c52085..ed000537 100644 --- a/src/views/member/point/config/index.vue +++ b/src/views/member/point/config/index.vue @@ -10,47 +10,44 @@ <el-form-item label="hideId" v-show="false"> <el-input v-model="formData.id" /> </el-form-item> - <el-form-item label="积分抵扣" prop="tradeDeductEnable" class="item-bottom"> - <el-switch v-model="formData.tradeDeductEnable" style="user-select: none" /> - </el-form-item> - <el-form-item> - <el-text class="mx-1" size="small" type="info">下单积分是否抵用订单金额</el-text> - </el-form-item> - <el-form-item label="积分抵扣" prop="tradeDeductUnitPrice" class="item-bottom"> - <el-input-number - v-model="computedTradeDeductUnitPrice" - placeholder="请输入积分抵扣金额" - style="width: 300px" - :precision="2" - /> - </el-form-item> - <el-form-item> - <el-text class="mx-1" size="small" type="info"> - 积分抵用比例(1 积分抵多少金额),单位:元 - </el-text> - </el-form-item> - <el-form-item label="积分抵扣最大值" prop="tradeDeductMaxPrice" class="item-bottom"> - <el-input-number - v-model="formData.tradeDeductMaxPrice" - placeholder="请输入积分抵扣最大值" - style="width: 300px" - /> - </el-form-item> - <el-form-item> - <el-text class="mx-1" size="small" type="info">单次下单积分使用上限,0 不限制</el-text> - </el-form-item> - <el-form-item label="1 元赠送多少分" prop="tradeGivePoint" class="item-bottom"> - <el-input-number - v-model="formData.tradeGivePoint" - placeholder="请输入 1 元赠送多少积分" - style="width: 300px" - /> - </el-form-item> - <el-form-item> - <el-text class="mx-1" size="small" type="info"> - 下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分) - </el-text> - </el-form-item> + + <el-tabs> + <el-tab-pane label="积分"> + <el-form-item label="积分抵扣" prop="tradeDeductEnable"> + <el-switch v-model="formData.tradeDeductEnable" style="user-select: none" /> + <el-text class="w-full" size="small" type="info">下单积分是否抵用订单金额</el-text> + </el-form-item> + <el-form-item label="积分抵扣" prop="tradeDeductUnitPrice"> + <el-input-number + v-model="computedTradeDeductUnitPrice" + placeholder="请输入积分抵扣金额" + :precision="2" + /> + <el-text class="w-full" size="small" type="info"> + 积分抵用比例(1 积分抵多少金额),单位:元 + </el-text> + </el-form-item> + <el-form-item label="积分抵扣最大值" prop="tradeDeductMaxPrice"> + <el-input-number + v-model="formData.tradeDeductMaxPrice" + placeholder="请输入积分抵扣最大值" + /> + <el-text class="w-full" size="small" type="info"> + 单次下单积分使用上限,0 不限制 + </el-text> + </el-form-item> + <el-form-item label="1 元赠送多少分" prop="tradeGivePoint"> + <el-input-number + v-model="formData.tradeGivePoint" + placeholder="请输入 1 元赠送多少积分" + /> + <el-text class="w-full" size="small" type="info"> + 下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分) + </el-text> + </el-form-item> + </el-tab-pane> + </el-tabs> + <el-form-item> <el-button type="primary" @click="onSubmit">保存</el-button> </el-form-item> @@ -68,6 +65,7 @@ const message = useMessage() // 消息弹窗 const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formData = ref({ + id: undefined, tradeDeductEnable: true, tradeDeductUnitPrice: 0, tradeDeductMaxPrice: 0, @@ -119,9 +117,3 @@ onMounted(() => { getConfig() }) </script> - -<style scoped> -.item-bottom { - margin-bottom: 0; -} -</style> diff --git a/src/views/member/user/detail/UserExperienceRecordList.vue b/src/views/member/user/detail/UserExperienceRecordList.vue new file mode 100644 index 00000000..64414ad1 --- /dev/null +++ b/src/views/member/user/detail/UserExperienceRecordList.vue @@ -0,0 +1,158 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="业务类型" prop="bizType"> + <el-select + v-model="queryParams.bizType" + placeholder="请选择业务类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="标题" prop="title"> + <el-input + v-model="queryParams.title" + placeholder="请输入标题" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="编号" align="center" prop="id" width="150px" /> + <el-table-column + label="获得时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + /> + <el-table-column label="经验" align="center" prop="experience" width="150px"> + <template #default="scope"> + <el-tag v-if="scope.row.experience > 0" class="ml-2" type="success" effect="dark"> + +{{ scope.row.experience }} + </el-tag> + <el-tag v-else class="ml-2" type="danger" effect="dark"> + {{ scope.row.experience }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="总经验" align="center" prop="totalExperience" width="150px"> + <template #default="scope"> + <el-tag class="ml-2" effect="dark"> + {{ scope.row.totalExperience }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="标题" align="center" prop="title" width="150px" /> + <el-table-column label="描述" align="center" prop="description" /> + <el-table-column label="业务编号" align="center" prop="bizId" width="150px" /> + <el-table-column label="业务类型" align="center" prop="bizType" width="150px"> + <!-- TODO 芋艿:此处应创建对应的字典 --> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE" :value="scope.row.bizType" /> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> +</template> + +<script setup lang="ts"> +import { dateFormatter } from '@/utils/formatTime' +import * as ExperienceRecordApi from '@/api/member/experience-record/index' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' + +defineOptions({ name: 'UserExperienceRecordList' }) + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + bizId: null, + bizType: null, + title: null, + description: null, + experience: null, + totalExperience: null, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ExperienceRecordApi.getExperienceRecordPage(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 { userId } = defineProps({ + userId: { + type: Number, + required: true + } +}) +/** 初始化 **/ +onMounted(() => { + queryParams.userId = userId + getList() +}) +</script> diff --git a/src/views/member/user/detail/UserOrderList.vue b/src/views/member/user/detail/UserOrderList.vue new file mode 100644 index 00000000..86c121bc --- /dev/null +++ b/src/views/member/user/detail/UserOrderList.vue @@ -0,0 +1,403 @@ +<template> + <!-- 搜索 --> + <ContentWrap> + <el-form + ref="queryFormRef" + :inline="true" + :model="queryParams" + class="-mb-15px" + label-width="68px" + > + <el-form-item label="订单状态" prop="status"> + <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="支付方式" prop="payChannelCode"> + <el-select + v-model="queryParams.payChannelCode" + class="!w-280px" + clearable + placeholder="全部" + > + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)" + :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" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-280px" + end-placeholder="自定义时间" + start-placeholder="自定义时间" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" + /> + </el-form-item> + <el-form-item label="订单来源" prop="terminal"> + <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="订单类型" prop="type"> + <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="配送方式" prop="deliveryType"> + <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item v-if="queryParams.deliveryType === 1" label="快递公司"> + <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="item in deliveryExpressList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item v-if="queryParams.deliveryType === 2" label="自提门店"> + <el-select + v-model="queryParams.pickUpStoreId" + class="!w-280px" + clearable + multiple + placeholder="全部" + > + <el-option + v-for="item in pickUpStoreList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <!-- TODO 聚合搜索等售后结束后实现--> + <!-- <el-form-item label="聚合搜索">--> + <!-- <el-input--> + <!-- v-show="true"--> + <!-- v-model="queryType.v"--> + <!-- class="!w-280px"--> + <!-- clearable--> + <!-- placeholder="请输入"--> + <!-- >--> + <!-- <template #prepend>--> + <!-- <el-select v-model="queryType.k" class="!w-110px" clearable placeholder="全部">--> + <!-- <el-option--> + <!-- v-for="dict in searchList"--> + <!-- :key="dict.value"--> + <!-- :label="dict.label"--> + <!-- :value="dict.value"--> + <!-- />--> + <!-- </el-select>--> + <!-- </template>--> + <!-- </el-input>--> + <!-- </el-form-item>--> + <el-form-item> + <el-button @click="handleQuery"> + <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> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column class-name="order-table-col"> + <template #header> + <!-- TODO @puhui999:小屏幕下,会有偏移,后续看看 --> + <div class="flex items-center" style="width: 100%"> + <div class="ml-100px mr-200px">商品信息</div> + <div class="mr-60px">单价(元)/数量</div> + <div class="mr-60px">售后状态</div> + <div class="mr-60px">实付金额(元)</div> + <div class="mr-60px">买家/收货人</div> + <div class="mr-60px">配送方式</div> + <div class="mr-60px">订单状态</div> + <div class="mr-60px">操作</div> + </div> + </template> + <template #default="scope"> + <el-table + :border="true" + :data="scope.row.items" + :header-cell-style="headerStyle" + :span-method="spanMethod" + style="width: 100%" + > + <el-table-column min-width="300" prop="spuName"> + <template #header> + <div + class="flex items-center" + style="width: 100%; height: 35px; background-color: #f7f7f7" + > + <span class="mr-20px">订单号:{{ scope.row.no }} </span> + <span class="mr-20px">下单时间:{{ formatDate(scope.row.createTime) }}</span> + <span>订单来源:</span> + <dict-tag + :type="DICT_TYPE.TERMINAL" + :value="scope.row.terminal" + class="mr-20px" + /> + <span>支付方式:</span> + <dict-tag + v-if="scope.row.payChannelCode" + :type="DICT_TYPE.PAY_CHANNEL_CODE" + :value="scope.row.payChannelCode" + class="mr-20px" + /> + <v-else v-else class="mr-20px">未支付</v-else> + <span v-if="scope.row.payTime" class="mr-20px"> + 支付时间:{{ formatDate(scope.row.payTime) }} + </span> + <span>订单类型:</span> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="scope.row.type" /> + </div> + </template> + <template #default="{ row }"> + <div class="flex items-center"> + <el-image + :src="row.picUrl" + class="w-30px h-30px mr-10px" + @click="imagePreview(row.picUrl)" + /> + <span class="mr-10px">{{ row.spuName }}</span> + <el-tag + v-for="property in row.properties" + :key="property.propertyId" + class="mr-10px" + > + {{ property.propertyName }}: {{ property.valueName }} + </el-tag> + </div> + </template> + </el-table-column> + <el-table-column label="商品原价*数量" prop="price" width="150"> + <template #default="{ row }"> + {{ floatToFixed2(row.price) }} 元 / {{ row.count }} + </template> + </el-table-column> + <el-table-column label="售后状态" prop="afterSaleStatus" width="120"> + <template #default="{ row }"> + <dict-tag + :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS" + :value="row.afterSaleStatus" + /> + </template> + </el-table-column> + <el-table-column align="center" label="实际支付" min-width="120" prop="payPrice"> + <template #default> + {{ floatToFixed2(scope.row.payPrice) + '元' }} + </template> + </el-table-column> + <el-table-column label="买家/收货人" min-width="160"> + <template #default> + <!-- 快递发货 --> + <div v-if="scope.row.deliveryType === 1" class="flex flex-col"> + <span>买家:{{ scope.row.user.nickname }}</span> + <span> + 收货人:{{ scope.row.receiverName }} {{ scope.row.receiverMobile }} + {{ scope.row.receiverAreaName }} {{ scope.row.receiverDetailAddress }} + </span> + </div> + <!-- 自提 --> + <div v-if="scope.row.deliveryType === 2" class="flex flex-col"> + <span> + 门店名称: + {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.name }} + </span> + <span> + 门店手机: + {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.phone }} + </span> + <span> + 自提门店: + {{ + pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.detailAddress + }} + </span> + </div> + </template> + </el-table-column> + <el-table-column align="center" label="配送方式" width="120"> + <template #default> + <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="scope.row.deliveryType" /> + </template> + </el-table-column> + <el-table-column align="center" label="订单状态" width="120"> + <template #default> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column align="center" fixed="right" label="操作" width="160"> + <template #default> + <el-button link type="primary" @click="openDetail(scope.row.id)"> + <Icon icon="ep:notification" /> + 详情 + </el-button> + </template> + </el-table-column> + </el-table> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + </ContentWrap> +</template> +<script setup lang="ts"> +import * as OrderApi from '@/api/mall/trade/order/index' +import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import { floatToFixed2 } from '@/utils' +import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore' +import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express' +import { createImageViewer } from '@/components/ImageViewer' +import * as TradeOrderApi from '@/api/mall/trade/order' +import { FormInstance, TableColumnCtx } from 'element-plus' + +const { push } = useRouter() // 路由跳转 + +const { userId }: { userId: number } = defineProps({ + userId: { + type: Number, + required: true + } +}) +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const pickUpStoreList = ref([]) // 自提门店精简列表 +const deliveryExpressList = ref([]) // 物流公司 +const queryFormRef = ref<FormInstance>() // 搜索的表单 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + createDate: [], + userId: NaN +}) +const headerStyle = ({ row, columnIndex }: any) => { + // 表头第一行第一列占 8 + if (columnIndex === 0) { + row[columnIndex].colSpan = 8 + } else { + // 其余的不要 + row[columnIndex].colSpan = 0 + return { + display: 'none' + } + } +} +/** 搜索按钮操作 */ +const handleQuery = async () => { + queryParams.pageNo = 1 + await getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value?.resetFields() + handleQuery() +} +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await OrderApi.getOrderPage(queryParams) + console.log(data) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 商品图预览 */ +const imagePreview = (imgUrl: string) => { + createImageViewer({ + urlList: [imgUrl] + }) +} + +interface SpanMethodProps { + row: TradeOrderApi.OrderItemRespVO + column: TableColumnCtx<TradeOrderApi.OrderItemRespVO> + rowIndex: number + columnIndex: number +} + +const spanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => { + const len = list.value.find( + (order) => order.items?.findIndex((item) => item.id === row.id) !== -1 + )?.items?.length + // 要合并的列,从零开始 + const colIndex = [3, 4, 5, 6, 7] + if (colIndex.includes(columnIndex)) { + // 除了第一行其余的不要 + if (rowIndex !== 0) { + return { + rowspan: 0, + colspan: 0 + } + } + // 动态合并行 + return { + rowspan: len, + colspan: 1 + } + } +} + +/** 查看订单详情 */ +const openDetail = (id: number) => { + push({ name: 'TradeOrderDetail', params: { orderId: id } }) +} + +/** 初始化 **/ +onMounted(async () => { + queryParams.userId = userId + await getList() + pickUpStoreList.value = await PickUpStoreApi.getListAllSimple() + deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList() +}) +</script> +<style scoped lang="scss"></style> diff --git a/src/views/member/user/detail/index.vue b/src/views/member/user/detail/index.vue index bd084687..d9fa844e 100644 --- a/src/views/member/user/detail/index.vue +++ b/src/views/member/user/detail/index.vue @@ -36,12 +36,16 @@ <el-tab-pane label="签到" name="sign" lazy> <UserSignList :user-id="id" /> </el-tab-pane> - <el-tab-pane label="成长值" name="third">成长值(WIP)</el-tab-pane> + <el-tab-pane label="成长值" name="experience" lazy> + <UserExperienceRecordList :user-id="id" + /></el-tab-pane> <el-tab-pane label="余额" name="fourth">余额(WIP)</el-tab-pane> <el-tab-pane label="收货地址" name="address" lazy> <UserAddressList :user-id="id" /> </el-tab-pane> - <el-tab-pane label="订单管理" name="fourth">订单管理(WIP)</el-tab-pane> + <el-tab-pane label="订单管理" name="order" lazy> + <UserOrderList :user-id="id" /> + </el-tab-pane> <el-tab-pane label="售后管理" name="fourth">售后管理(WIP)</el-tab-pane> <el-tab-pane label="收藏记录" name="fourth">收藏记录(WIP)</el-tab-pane> <el-tab-pane label="优惠劵" name="fourth">优惠劵(WIP)</el-tab-pane> @@ -62,14 +66,16 @@ import UserAccountInfo from './UserAccountInfo.vue' import UserAddressList from './UserAddressList.vue' import UserPointList from './UserPointList.vue' import UserSignList from './UserSignList.vue' +import UserExperienceRecordList from './UserExperienceRecordList.vue' import { CardTitle } from '@/components/Card/index' +import UserOrderList from '@/views/member/user/detail/UserOrderList.vue' import { ElMessage } from 'element-plus' defineOptions({ name: 'MemberDetail' }) const activeName = ref('point') // 账户明细 选中的 tabs const loading = ref(true) // 加载中 -const user = ref<UserApi.UserVO>() +const user = ref<UserApi.UserVO>({}) /** 添加/修改操作 */ const formRef = ref() diff --git a/src/views/member/user/index.vue b/src/views/member/user/index.vue index fa62dea5..8350f743 100644 --- a/src/views/member/user/index.vue +++ b/src/views/member/user/index.vue @@ -160,6 +160,7 @@ import { dateFormatter } from '@/utils/formatTime' import * as UserApi from '@/api/member/user' import { DICT_TYPE } from '@/utils/dict' +import UserForm from './UserForm.vue' 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' @@ -218,6 +219,12 @@ const openDetail = (id: number) => { push({ name: 'MemberUserDetail', params: { id } }) } +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + /** 表格选中事件 */ const handleSelectionChange = (rows: UserApi.UserVO[]) => { selectedIds.value = rows.map((row) => row.id)