完成积分模块

This commit is contained in:
gyq
2025-12-11 09:15:30 +08:00
parent 5b2d2ab8c3
commit b860fa0009
19 changed files with 1813 additions and 309 deletions

159
src/api/points/index.js Normal file
View File

@@ -0,0 +1,159 @@
import request from "@/utils/request";
const market_baseURL = "/market";
const order_baseURL = "/order";
/**
* 积分:配置:详情
* @param {*} params
* @returns
*/
export function pointsConfigGet(params) {
return request({
url: `${market_baseURL}/admin/points/config`,
method: "get",
params,
});
}
/**
* 积分:配置:新增/更新
* @param {*} params
* @returns
*/
export function pointsConfigPost(data) {
return request({
url: `${market_baseURL}/admin/points/config`,
method: "post",
data,
});
}
/**
* 积分:商品:新增/修改
* @param {*} params
* @returns
*/
export function pointsGoodsPost(data) {
return request({
url: `${market_baseURL}/admin/pointsGoods`,
method: "post",
data,
});
}
/**
* 积分:商品:列表
* @param {*} params
* @returns
*/
export function pointsGoodsPage(params) {
return request({
url: `${market_baseURL}/admin/pointsGoods/page`,
method: "get",
params,
});
}
/**
* 积分-商品-删除
* @param {*} id
* @returns
*/
export function pointsGoodsDel(id) {
return request({
url: `${market_baseURL}/admin/pointsGoods/${id}`,
method: "delete"
});
}
/**
* 积分:积分商品:兑换记录
* @param {*} params
* @returns
*/
export function goodsRecordPage(params) {
return request({
url: `${order_baseURL}/admin/points/goodsRecord/page`,
method: "get",
params,
});
}
/**
* 积分:积分商品:兑换统计
* @param {*} params
* @returns
*/
export function goodsRecordTotal(params) {
return request({
url: `${order_baseURL}/admin/points/goodsRecord/total`,
method: "get",
params,
});
}
/**
* 积分:积分商品:兑换统计
* @param {*} params
* @returns
*/
export function pointsUserPage(params) {
return request({
url: `${market_baseURL}/admin/points/userPage`,
method: "get",
params,
});
}
/**
* 积分:用户:积分详情
* @param {*} params
* @returns
*/
export function pointsUserRecord(params) {
return request({
url: `${market_baseURL}/admin/points/userRecord`,
method: "get",
params,
});
}
/**
* 积分:商品:商家核销
* @param {*} params
* @returns
*/
export function goodsRecordCheckout(data) {
return request({
url: `${order_baseURL}/admin/points/goodsRecord/checkout`,
method: "post",
data,
});
}
/**
* 积分:商家 退单/同意退单
* @param {*} params
* @returns
*/
export function goodsRecordAgreeRefund(data) {
return request({
url: `${order_baseURL}/admin/points/goodsRecord/agreeRefund`,
method: "post",
data,
});
}
/**
* 积分:商家 驳回退单
* @param {*} params
* @returns
*/
export function goodsRecordRejectRefund(data) {
return request({
url: `${order_baseURL}/admin/points/goodsRecord/rejectRefund`,
method: "post",
data,
});
}

View File

@@ -2,7 +2,7 @@
<template>
<el-upload v-model:file-list="fileList" list-type="picture-card" :before-upload="handleBeforeUpload"
:http-request="handleUpload" :on-success="handleSuccess" :on-progress="handleProgress" :on-error="handleError"
:on-exceed="handleExceed" :accept="props.accept" :limit="props.limit" multiple>
:on-exceed="handleExceed" :accept="props.accept" :limit="props.limit" multiple :auto-upload="true">
<el-icon>
<Plus />
</el-icon>
@@ -12,7 +12,9 @@
<span class="el-upload-list__item-actions">
<!-- 预览 -->
<span @click="handlePreviewImage(file.url!)">
<el-icon><zoom-in /></el-icon>
<el-icon>
<ZoomIn />
</el-icon>
</span>
<!-- 删除 -->
<span @click="handleRemove(file.url!)">
@@ -28,104 +30,86 @@
<el-image-viewer v-if="previewVisible" :zoom-rate="1.2" :initial-index="previewImageIndex" :url-list="modelValue"
@close="handlePreviewClose" />
</template>
<script setup lang="ts">
import { ref, watch, onMounted, PropType, computed } from "vue";
import { ElMessage } from "element-plus";
import { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import FileAPI, { FileInfo } from "@/api/account/common";
// 定义Props
const props = defineProps({
/**
* 请求携带的额外参数
*/
data: {
type: Object,
default: () => {
return {};
},
default: () => ({}),
},
/**
* 上传文件的参数名
*/
name: {
type: String,
default: "file",
},
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 10,
},
/**
* 单个文件的最大允许大小
*/
maxFileSize: {
type: Number,
default: 10,
default: 10, // 单位M
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "image/*", // 默认支持所有图片格式 ,如果需要指定格式,格式如下:'.png,.jpg,.jpeg,.gif,.bmp'
default: "image/*",
},
});
const emit = defineEmits(['upDataEvent'])
const previewVisible = ref(false); // 是否显示预览
const previewImageIndex = ref(0); // 预览图片的索引
// 定义Emits
const emit = defineEmits(['upDataEvent', 'uploadAllSuccess']); // 新增:所有图片上传完成事件
// 响应式数据
const previewVisible = ref(false);
const previewImageIndex = ref(0);
const progress = ref(0);
const modelValue = defineModel("modelValue", {
type: [Array] as PropType<string[]>,
// 修复defineModel 类型定义
const modelValue = defineModel<string[]>("modelValue", {
type: Array as PropType<string[]>,
required: true,
default: () => [],
});
const fileList = ref<UploadUserFile[]>([]);
// 新增:批量上传计数
const pendingUploadCount = ref(0); // 待上传文件数
const completedUploadCount = ref(0); // 已完成上传数
/**
* 删除图片
*/
function handleRemove(imageUrl: string) {
// FileAPI.delete(imageUrl).then(() => {
// const index = modelValue.value.indexOf(imageUrl);
// if (index !== -1) {
// // 直接修改数组避免触发整体更新
// modelValue.value.splice(index, 1);
// fileList.value.splice(index, 1); // 同步更新 fileList
// }
// });
const index = modelValue.value.indexOf(imageUrl);
if (index !== -1) {
// 直接修改数组避免触发整体更新
modelValue.value.splice(index, 1);
fileList.value.splice(index, 1); // 同步更新 fileList
fileList.value.splice(index, 1);
}
}
function handleProgress(event: any, file: any, fileList: any) {
// console.log("handleProgress", evt, file, fileList);
// this.progress = Math.round(event.percent); // Element UI 已计算好百分比
// console.log(`文件 ${file.name} 上传进度: ${this.progress}%`);
// progress.value = Math.round(event.percent);
/**
* 上传进度
*/
function handleProgress(event: any) {
console.log("文件上传进度", Math.round(event.percent));
}
/**
* 上传前校验
* 上传前校验 & 初始化待上传文件
*/
function handleBeforeUpload(file: UploadRawFile) {
// 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则
// 1. 文件类型校验
const acceptTypes = props.accept.split(",").map((type) => type.trim());
// 检查文件格式是否符合 accept
const isValidType = acceptTypes.some((type) => {
if (type === "image/*") {
// 如果是 image/*,检查 MIME 类型是否以 "image/" 开头
return file.type.startsWith("image/");
} else if (type.startsWith(".")) {
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
return file.name.toLowerCase().endsWith(type);
} else {
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配
return file.type === type;
}
});
@@ -135,29 +119,42 @@ function handleBeforeUpload(file: UploadRawFile) {
return false;
}
// 限制文件大小
// 2. 文件大小校验
if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M");
ElMessage.warning(`上传图片不能大于${props.maxFileSize}M`);
return false;
}
emit('upDataEvent', true)
// 3. 新增初始化待上传文件到fileList确保uid匹配
const uploadFile: UploadUserFile = {
uid: file.uid, // 关键复用文件原生uid
name: file.name,
status: "ready",
raw: file,
};
fileList.value.push(uploadFile);
// 4. 新增:增加待上传计数
pendingUploadCount.value++;
emit('upDataEvent', true); // 开始上传触发loading
return true;
}
/*
* 上传文件
/**
* 自定义上传逻辑
*/
function handleUpload(options: UploadRequestOptions) {
return new Promise((resolve, reject) => {
const file = options.file;
const formData = new FormData();
formData.append(props.name, file);
// 处理附加参数
// 附加参数
Object.keys(props.data).forEach((key) => {
formData.append(key, props.data[key]);
});
FileAPI.upload(formData)
.then((data) => {
resolve(data);
@@ -169,23 +166,38 @@ function handleUpload(options: UploadRequestOptions) {
}
/**
* 上传文件超出限制
* 超出数量限制
*/
function handleExceed(files: File[], uploadFiles: UploadUserFile[]) {
ElMessage.warning("最多只能上传" + props.limit + "张图片");
ElMessage.warning(`最多只能上传${props.limit}张图片`);
}
/**
* 上传成功回调
* 单文件上传成功回调
*/
const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => {
ElMessage.success("上传成功");
emit('upDataEvent', false)
// 1. 修复通过uid精准匹配文件
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid);
if (index !== -1) {
fileList.value[index].url = fileInfo;
fileList.value[index].url = fileInfo as string; // 确保fileInfo是字符串类型
fileList.value[index].status = "success";
modelValue.value[index] = fileInfo;
// 同步更新modelValue若index超出当前长度push否则替换
if (index >= modelValue.value.length) {
modelValue.value.push(fileInfo as string);
} else {
modelValue.value[index] = fileInfo as string;
}
}
// 2. 新增:完成计数+判断所有上传完成
completedUploadCount.value++;
if (completedUploadCount.value === pendingUploadCount.value) {
ElMessage.success("所有图片上传成功!");
emit('uploadAllSuccess', modelValue.value); // 所有图片上传完成emit成功事件
// 重置计数
pendingUploadCount.value = 0;
completedUploadCount.value = 0;
emit('upDataEvent', false); // 结束loading
}
};
@@ -193,9 +205,12 @@ const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => {
* 上传失败回调
*/
const handleError = (error: any) => {
console.log("handleError");
emit('upDataEvent', false)
ElMessage.error("上传失败: " + error.message);
console.error("上传失败:", error);
ElMessage.error("上传失败: " + (error.message || "未知错误"));
// 重置计数+结束loading
pendingUploadCount.value = 0;
completedUploadCount.value = 0;
emit('upDataEvent', false);
};
/**
@@ -212,12 +227,24 @@ const handlePreviewImage = (imageUrl: string) => {
const handlePreviewClose = () => {
previewVisible.value = false;
};
// 监听modelValue同步fileList
watch(modelValue, (newValue) => {
fileList.value = newValue.map((url) => ({ url }) as UploadUserFile);
});
fileList.value = newValue.map((url) => ({
url,
uid: Date.now() + Math.random(), // 为已有图片生成唯一uid
status: "success"
}) as UploadUserFile);
}, { immediate: true });
onMounted(() => {
fileList.value = modelValue.value.map((url) => ({ url }) as UploadUserFile);
// 初始化fileList
fileList.value = modelValue.value.map((url) => ({
url,
uid: Date.now() + Math.random(),
status: "success"
}) as UploadUserFile);
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog title="选择优惠" top="5vh" :visible.sync="dialogVisible">
<el-dialog title="选择优惠" top="5vh" :visible.sync="dialogVisible">
<div class="head-container">
<el-table ref="table" :data="tableData.list" height="500" v-loading="tableData.loading">
<!-- <el-table-column type="selection" width="55" align="center" v-if="!radio"></el-table-column> -->

View File

@@ -259,4 +259,44 @@ export const multiplyAndFormat = (num1: any, num2: any = 1): string => {
console.error('计算错误:', error);
return '0.00'; // 出错时返回默认值
}
};
};
/**
* 判断目标值是否包含指定字符串
* @param {string|number|array} target - 目标值(字符串/数字/数组)
* @param {string} searchStr - 要查找的子字符串
* @param {object} [options] - 可选配置
* @param {boolean} [options.ignoreCase=false] - 是否忽略大小写
* @param {boolean} [options.allowEmpty=false] - 当 searchStr 为空时,是否返回 true默认 false
* @returns {boolean} 是否包含指定字符串
*/
export function includesString(target, searchStr, options = {}) {
// 解构配置,设置默认值
const { ignoreCase = false, allowEmpty = false } = options;
// 1. 处理 searchStr 为空的情况
if (searchStr === '' || searchStr === null || searchStr === undefined) {
return allowEmpty;
}
// 2. 统一将目标值转为字符串(兼容数字/数组等)
let targetStr = '';
if (typeof target === 'string') {
targetStr = target;
} else if (typeof target === 'number') {
targetStr = String(target);
} else if (Array.isArray(target)) {
// 数组:拼接为字符串(也可改为 "数组中某一项包含",根据需求调整)
targetStr = target.join(',');
} else {
// 其他类型(对象/布尔等):转为字符串或返回 false
targetStr = String(target);
}
// 3. 处理大小写忽略
const processedTarget = ignoreCase ? targetStr.toLowerCase() : targetStr;
const processedSearch = ignoreCase ? searchStr.toLowerCase() : searchStr;
// 4. 执行包含判断
return processedTarget.includes(processedSearch);
}

View File

@@ -3,12 +3,7 @@
<div class="search-bar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="关键字" prop="title">
<el-input
v-model="queryParams.title"
placeholder="菜单名称"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.title" placeholder="菜单名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
@@ -19,27 +14,15 @@
<el-card shadow="never">
<div class="mb-10px">
<el-button
v-hasPerm="['sys:menu:add']"
type="success"
icon="plus"
@click="handleOpenDialog('0')"
>
<el-button v-hasPerm="['sys:menu:add']" type="success" icon="plus" @click="handleOpenDialog('0')">
新增
</el-button>
</div>
<el-table
v-loading="loading"
:data="menuTableData"
highlight-current-row
row-key="menuId"
:tree-props="{
children: 'children',
hasChildren: 'hasChildren',
}"
@row-click="handleRowClick"
>
<el-table v-loading="loading" :data="menuTableData" highlight-current-row row-key="menuId" :tree-props="{
children: 'children',
hasChildren: 'hasChildren',
}" @row-click="handleRowClick">
<el-table-column label="菜单名称" min-width="140">
<template #default="scope">
{{ scope.row.title }}
@@ -60,20 +43,14 @@
</el-table-column>
<el-table-column label="类型" align="center" width="80">
<template #default="scope">
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && scope.row.path.startsWith('/')"
type="warning"
>
<el-tag v-if="scope.row.type === MenuTypeEnum.MENU && scope.row.path.startsWith('/')" type="warning">
目录
</el-tag>
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && !scope.row.path.startsWith('/')"
type="success"
>
<el-tag v-if="scope.row.type === MenuTypeEnum.MENU && !scope.row.path.startsWith('/')" type="success">
菜单
</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">接口</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="left" width="150" prop="menuSort" />
@@ -101,36 +78,17 @@
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="220">
<template #default="scope">
<el-button
v-if="scope.row.type == 0"
v-hasPerm="['sys:menu:add']"
type="primary"
link
size="small"
icon="plus"
@click.stop="handleOpenDialog(scope.row.menuId)"
>
<el-button v-if="scope.row.type == 0" v-hasPerm="['sys:menu:add']" type="primary" link size="small"
icon="plus" @click.stop="handleOpenDialog(scope.row.menuId)">
新增
</el-button>
<el-button
v-hasPerm="['sys:menu:edit']"
type="primary"
link
size="small"
icon="edit"
@click.stop="handleOpenDialog(undefined, scope.row.menuId)"
>
<el-button v-hasPerm="['sys:menu:edit']" type="primary" link size="small" icon="edit"
@click.stop="handleOpenDialog(undefined, scope.row.menuId)">
编辑
</el-button>
<el-button
v-hasPerm="['sys:menu:delete']"
type="danger"
link
size="small"
icon="delete"
@click.stop="handleDelete(scope.row.menuId)"
>
<el-button v-hasPerm="['sys:menu:delete']" type="danger" link size="small" icon="delete"
@click.stop="handleDelete(scope.row.menuId)">
删除
</el-button>
</template>
@@ -141,14 +99,8 @@
<el-drawer v-model="dialog.visible" :title="dialog.title" size="50%" @close="handleCloseDialog">
<el-form ref="editRequestRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="父级菜单" prop="pid">
<el-tree-select
v-model="formData.pid"
placeholder="选择上级菜单"
:data="menuOptions"
filterable
check-strictly
:render-after-expand="false"
/>
<el-tree-select v-model="formData.pid" placeholder="选择上级菜单" :data="menuOptions" filterable check-strictly
:render-after-expand="false" />
</el-form-item>
<el-form-item label="菜单名称" prop="title">
@@ -162,14 +114,9 @@
<el-radio :value="2">接口</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否外链" >
<el-switch
v-model="formData.iFrame"
:active-value="1"
:inactive-value="0"
active-text=""
inactive-text=""
/>
<el-form-item label="是否外链">
<el-switch v-model="formData.iFrame" :active-value="1" :inactive-value="0" active-text="是"
inactive-text="" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="name">
@@ -204,11 +151,7 @@
</el-tooltip>
</div>
</template>
<el-input
v-if="formData.type == MenuTypeEnum.MENU"
v-model="formData.path"
placeholder="system"
/>
<el-input v-if="formData.type == MenuTypeEnum.MENU" v-model="formData.path" placeholder="system" />
<el-input v-else v-model="formData.path" placeholder="user" />
</el-form-item>
@@ -240,10 +183,7 @@
<el-input v-model="formData.miniComponent" placeholder="小程序组件名称" />
</el-form-item>
<el-form-item label="小程序图标">
<single-image-upload
style="width: 100px; height: 100px"
v-model="formData.miniIcon"
></single-image-upload>
<single-image-upload style="width: 100px; height: 100px" v-model="formData.miniIcon"></single-image-upload>
</el-form-item>
<el-form-item label="接口路径" prop="miniPage">
<div class="w-full">
@@ -253,11 +193,7 @@
<div class="u-flex u-m-t-10" v-for="(item, index) in formData.apiInfo" :key="index">
<el-form-item label="请求方式" label-position="left">
<el-select v-model="item.method" style="width: 100px">
<el-option
v-for="(item, index) in apiMethodOptions"
:key="index"
:value="item.value"
>
<el-option v-for="(item, index) in apiMethodOptions" :key="index" :value="item.value">
{{ item.label }}
</el-option>
</el-select>
@@ -265,12 +201,7 @@
<el-form-item label="接口地址" style="flex: 1">
<el-input v-model="item.url" placeholder="支持通配符*和?" />
</el-form-item>
<el-icon
style="margin-left: 10px"
size="20"
color="#F56c6c"
@click="formData.apiInfo.splice(index, 1)"
>
<el-icon style="margin-left: 10px" size="20" color="#F56c6c" @click="formData.apiInfo.splice(index, 1)">
<RemoveFilled />
</el-icon>
</div>
@@ -291,20 +222,11 @@
</el-form-item>
<el-form-item label="排序" prop="menuSort">
<el-input-number
v-model="formData.menuSort"
style="width: 100px"
controls-position="right"
:min="0"
/>
<el-input-number v-model="formData.menuSort" style="width: 100px" controls-position="right" :min="0" />
</el-form-item>
<!-- 权限标识 -->
<el-form-item
v-if="formData.type == MenuTypeEnum.BUTTON"
label="权限标识"
prop="permission"
>
<el-form-item v-if="formData.type == MenuTypeEnum.BUTTON" label="权限标识" prop="permission">
<el-input v-model="formData.permission" placeholder="sys:user:add" />
</el-form-item>

View File

@@ -1,17 +1,17 @@
<template>
<div style="padding: 15px;">
<el-tabs type="border-card" v-model="datas.activeName" @tab-click="handleClick">
<el-tab-pane label="基本设置" name="BasicSettings">
<BasicSettings v-if="datas.activeName == 'BasicSettings'" />
<el-tab-pane label="基本设置" name="BasicSettings" key="BasicSettings">
<BasicSettings />
</el-tab-pane>
<el-tab-pane label="商品设置" name="ProductSettings">
<ProductSettings v-if="datas.activeName == 'ProductSettings'" />
<el-tab-pane label="商品设置" name="ProductSettings" key="ProductSettings">
<ProductSettings />
</el-tab-pane>
<el-tab-pane label="兑换记录" name="Exchangerecords">
<Exchangerecords v-if="datas.activeName == 'Exchangerecords'" />
<el-tab-pane label="兑换记录" name="Exchangerecords" key="Exchangerecords">
<Exchangerecords />
</el-tab-pane>
<el-tab-pane label="会员积分" name="Memberpoints">
<Memberpoints v-if="datas.activeName == 'Memberpoints'" />
<el-tab-pane label="用户积分" name="Memberpoints" key="Memberpoints">
<Memberpoints />
</el-tab-pane>
</el-tabs>
</div>
@@ -23,7 +23,6 @@ import Memberpoints from './pointsconfig/Memberpoints.vue'
import ProductSettings from './pointsconfig/ProductSettings.vue'
let datas = reactive({
activeName: "BasicSettings",
})
function handleClick(tab) {
datas.activeName = tab.props.name;

View File

@@ -1,72 +1,64 @@
<template>
<!-- 基本设置 -->
<el-form :model="Elform" label-width="150px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
<el-form-item label="是否消费赠送积分">
<el-switch v-model="Elform.enableRewards" :active-value="1" :inactive-value="0" />
<div class="center">
<el-switch v-model="form.enableRewards" :active-value="1" :inactive-value="0" />
<span class="tips">开启后所有用户可通过消费获得积分及可用积分抵扣支付</span>
</div>
</el-form-item>
<el-form-item label="适用群体">
<el-radio-group v-model="Elform.rewardsGroup">
<!-- <el-form-item label="适用群体">
<el-radio-group v-model="form.rewardsGroup">
<el-radio label="全部" value="all" />
<el-radio label="仅会员" value="vip" />
</el-radio-group>
</el-form-item>
<el-form-item label="消费送积分">
</el-form-item> -->
<el-form-item label="消费送积分" prop="consumeAmount">
<el-col :span="7">
<el-input v-model="Elform.consumeAmount" type="number" placeholder="">
<el-input v-model="form.consumeAmount" :maxlength="8" @input="e => form.consumeAmount = filterNumberInput(e)"
:input-style="{ textAlign: 'center' }">
<template #prepend>每消费</template>
<template #append>元赠送1积分</template>
</el-input>
</el-col>
</el-form-item>
<el-form-item label="开启下单积分抵扣">
<el-switch v-model="Elform.enableDeduction" :active-value="1" :inactive-value="0" />
<!-- <el-form-item label="开启下单积分抵扣">
<el-switch v-model="form.enableDeduction" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="适用群体">
<el-radio-group v-model="Elform.deductionGroup">
<el-radio-group v-model="form.deductionGroup">
<el-radio label="全部" value="all" />
<el-radio label="仅会员" value="vip" />
</el-radio-group>
</el-form-item>
<el-form-item label="下单实付抵扣门槛">
</el-form-item> -->
<el-form-item label="下单实付抵扣门槛" prop="minPaymentAmount">
<el-col :span="3">
<el-input v-model="Elform.minPaymentAmount" type="number" placeholder="">
<el-input v-model="form.minPaymentAmount" :maxlength="8"
@input="e => form.minPaymentAmount = filterNumberInput(e)">
<template #append></template>
</el-input>
</el-col>
</el-form-item>
<el-form-item label="下单最高抵扣比例">
<el-input-number
v-model="Elform.maxDeductionRatio"
type="number"
placeholder=""
:min="0"
:max="100"
></el-input-number>
<el-input-number v-model="form.maxDeductionRatio" type="number" placeholder="" :min="1"
:max="100"></el-input-number>
<span class="u-m-l-10 color-999">%</span>
</el-form-item>
<el-form-item label="下单抵扣积分比例">
<el-col :span="7">
<span class="color-999">1元等于</span>
<el-input-number
class="u-m-l-10 u-m-r-10"
v-model="Elform.equivalentPoints"
type="number"
placeholder=""
min="1"
></el-input-number>
<el-input-number class="u-m-l-10 u-m-r-10" v-model="form.equivalentPoints" type="number" placeholder=""
min="1"></el-input-number>
<span class="color-999">积分</span>
</el-col>
</el-form-item>
<div class="form_header">开启积分商城</div>
<el-form-item label="开启积分商城">
<el-switch v-model="Elform.enablePointsMall" :active-value="1" :inactive-value="0" />
<el-switch v-model="form.enablePointsMall" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="显示样式">
<!-- <el-form-item label="显示样式">
<div class="style_wrap">
<div
class="item style1"
:class="{ active: Elform.browseMode == 'list' }"
@click="Elform.browseMode = 'list'"
>
<div class="item style1" :class="{ active: form.browseMode == 'list' }" @click="form.browseMode = 'list'">
<div class="row">
<div class="cover"></div>
<div class="info">
@@ -84,11 +76,7 @@
</div>
</div>
</div>
<div
class="item style2"
:class="{ active: Elform.browseMode == 'grid' }"
@click="Elform.browseMode = 'grid'"
>
<div class="item style2" :class="{ active: form.browseMode == 'grid' }" @click="form.browseMode = 'grid'">
<div class="row">
<div class="cover"></div>
<div class="line"></div>
@@ -107,48 +95,122 @@
</div>
</div>
</div>
</el-form-item>
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="onSubmit">确定</el-button>
<el-button type="primary" :loading="loading" @click="onSubmit">确定</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive } from "vue";
import API from "../index";
// do not use same name with ref
const Elform = reactive({
enableRewards: 0,
rewardsGroup: "all",
consumeAmount: 0,
enableDeduction: 0,
deductionGroup: "all",
minPaymentAmount: 0,
maxDeductionRatio: 0,
equivalentPoints: 0,
enablePointsMall: 0,
browseMode: "list",
import { ref } from "vue";
import { pointsConfigGet, pointsConfigPost } from '@/api/points'
import { filterNumberInput } from '@/utils'
const form = ref({
enableRewards: 0, // 开启消费赠送积分 1-开启 0-关闭 开启后,所有用户可通过消费获得积分及可用积分抵扣支付
consumeAmount: '', // 每消费xx元赠送1积分
minPaymentAmount: '', // 下单实付抵扣门槛
maxDeductionRatio: '', // 下单最高抵扣比例
equivalentPoints: '', // 下单抵扣积分比例 1元=?积分
enablePointsMall: '', // 开启积分商城
});
onMounted(() => {
getList();
});
async function getList() {
const res = await API.getBasicSetting({});
Object.assign(Elform, res);
}
async function onSubmit() {
const res = await API.BasicSettingadd(Elform);
if (res.code == 200) {
ElMessage({
message: "成功",
type: "success",
});
const rules = ref({
consumeAmount: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.consumeAmount === '') {
callback(new Error('请输入'))
} else {
callback()
}
},
triiger: 'blur'
}
],
minPaymentAmount: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.minPaymentAmount === '') {
callback(new Error('请输入'))
} else {
callback()
}
},
triiger: 'blur'
}
],
maxDeductionRatio: [
{
required: true,
message: ' ',
triiger: 'blur'
}
]
})
// 积分:配置:详情
async function pointsConfigGetAjax() {
const res = await pointsConfigGet();
if (res) {
form.value = res
formRef.value.clearValidate('consumeAmount');
formRef.value.clearValidate('minPaymentAmount');
}
}
const formRef = ref(null)
const loading = ref(false)
function onSubmit() {
formRef.value.validate(async vaild => {
try {
if (vaild) {
loading.value = true
await pointsConfigPost(form.value);
ElMessage({
message: "保存成功",
type: "success",
});
}
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false
}, 500);
})
}
onMounted(() => {
pointsConfigGetAjax();
});
</script>
<style scoped lang="scss">
.center {
display: flex;
align-items: center;
gap: 10px;
.tips {
color: #666;
font-size: 14px;
}
}
.form_header {
padding: 14px;
display: flex;
align-items: center;
font-size: 16px;
color: #333;
background-color: #F8F8F8;
margin-bottom: 14px;
}
.style_wrap {
display: flex;
width: 350px;

View File

@@ -1,13 +1,206 @@
<template>
<!-- 会员积分模块 -->
<div style="padding: 15px;">
<!-- 搜索 -->
<Search></Search>
<!-- 表格 -->
<Content></Content>
<div>
<div class="row">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column label="用户ID" prop="id" width="100"></el-table-column>
<el-table-column label="用户" prop="nickName">
<template v-slot="scope">
<div class="user_info">
<el-avatar :src="scope.row.headImg" :size="35"></el-avatar>
<span class="name">{{ scope.row.nickName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="手机号" prop="phone"></el-table-column>
<el-table-column label="账户积分" prop="pointBalance"></el-table-column>
<el-table-column label="操作" width="100">
<template v-slot="scope">
<el-button link type="primary" @click="showRecord(scope.row)">查看明细</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="row mt14">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[10, 30, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<el-dialog title="积分明细" width="800px" v-model="visible" @closed="selectUserTableDataReset">
<div class="content">
<div class="header">
<div class="item">
<span class="t1">用户</span>
<span class="t2">{{ selectRow.nickName }} {{ selectRow.phone }}</span>
</div>
<div class="item">
<span class="t1">账户积分</span>
<span class="t2">{{ selectRow.pointBalance }}</span>
</div>
</div>
<div class="row mt14">
<el-table :data="selectUserTableData.list" border stripe v-loading="selectUserTableData.loading"
height="50vh">
<el-table-column label="变动类型" prop="floatType" width="100">
<template v-slot="scope">
<span v-if="scope.row.floatType == 'add'">累加</span>
<span v-if="scope.row.floatType == 'subtract'">扣减</span>
</template>
</el-table-column>
<el-table-column label="变动积分" prop="floatPoints" width="100"></el-table-column>
<el-table-column label="变动原因" prop="content"></el-table-column>
<el-table-column label="变动时间" prop="createTime" width="200"></el-table-column>
<el-table-column label="变动后积分" prop="balancePoints" width="100"></el-table-column>
</el-table>
</div>
<div class="row mt14">
<el-pagination v-model:current-page="selectUserTableData.page" v-model:page-size="selectUserTableData.size"
:page-sizes="[10, 30, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="selectUserTableData.total" @size-change="selectUserTableDataSizeChange"
@current-change="selectUserTableDataCurrentChange" />
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import Search from './Memberpointsconfig/Search.vue'
import Content from './Memberpointsconfig/Content.vue'
</script>
import { ref, reactive, onMounted } from 'vue'
import { pointsUserPage, pointsUserRecord } from '@/api/points'
const tableData = reactive({
loading: false,
list: [],
page: 1,
size: 10,
total: 0
})
// 分页大小发生变化
function handleSizeChange(e) {
tableData.size = e;
goodsRecordPageAjax();
goodsRecordTotalAjax()
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
goodsRecordPageAjax();
goodsRecordTotalAjax()
}
// 积分:用户:列表
async function pointsUserPageAjax() {
try {
tableData.loading = true
const res = await pointsUserPage({
page: tableData.page,
size: tableData.size
})
tableData.list = res.records
tableData.total = res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
const visible = ref(false)
const selectRow = ref({})
function showRecord(row) {
selectRow.value = row
visible.value = true
pointsUserRecordAjax()
}
// 积分:用户:积分详情
const selectUserTableData = reactive({
loading: false,
page: 1,
size: 10,
total: 0,
list: []
})
// 分页大小发生变化
function selectUserTableDataSizeChange(e) {
selectUserTableData.size = e;
pointsUserRecordAjax()
}
// 分页发生变化
function selectUserTableDataCurrentChange(e) {
selectUserTableData.page = e;
pointsUserRecordAjax()
}
function selectUserTableDataReset() {
selectUserTableData.page = 1
selectUserTableData.list = []
}
async function pointsUserRecordAjax() {
try {
selectUserTableData.loading = true
const res = await pointsUserRecord({
page: 1,
size: 10,
id: selectRow.value.id
})
selectUserTableData.list = res.records
selectUserTableData.total = res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
selectUserTableData.loading = false
}, 500);
}
onMounted(() => {
pointsUserPageAjax()
})
</script>
<style scoped lang="scss">
.row {
&.mt14 {
margin-top: 14px;
}
}
.user_info {
display: flex;
gap: 10px;
align-items: center;
.name {
font-size: 14px;
color: #333;
}
}
.content {
.header {
display: flex;
.item {
flex: 1;
display: flex;
align-items: center;
gap: 14px;
.t1 {
font-size: 16px;
color: #333;
}
.t2 {
font-size: 16px;
color: #666;
}
}
}
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<!-- 会员积分模块 -->
<div style="padding: 15px;">
<!-- 搜索 -->
<Search></Search>
<!-- 表格 -->
<Content></Content>
</div>
</template>
<script setup>
import Search from './Memberpointsconfig/Search.vue'
import Content from './Memberpointsconfig/Content.vue'
</script>

View File

@@ -1,10 +1,170 @@
<template>
<!-- 会员商品设置模块 -->
<div style="padding: 15px;">
<!-- 表格 -->
<Content></Content>
<div>
<div class="row">
<el-button type="primary" @click="addProduceDialogRef.show()">添加</el-button>
</div>
<div class="row mt14">
<el-table :data="tableData.list" border stripe v-loading="tableData.loading">
<el-table-column label="商品" width="250">
<template v-slot="scope">
<div class="goods_info" v-if="includesString(scope.row.goodsCategory, '其它商品')">
<el-image class="icon" :src="scope.row.goodsImageUrl" fit="cover"></el-image>
<div class="info">
<div class="name">{{ scope.row.goodsName }}</div>
</div>
</div>
<div class="coupon_info" v-if="includesString(scope.row.goodsCategory, '优惠券')">
<div class="icon">
<CouponIcon :item="scope.row.couponInfo" />
</div>
<div class="info">
<div class="name">
{{ scope.row.goodsName }}
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="所需积分" prop="requiredPoints"></el-table-column>
<el-table-column label="额外价格(元)" prop="extraPrice" width="130"></el-table-column>
<el-table-column label="累计兑换次数" prop="totalExchangeCount"></el-table-column>
<el-table-column label="库存" prop="quantity"></el-table-column>
<el-table-column label="状态" prop="status" width="100">
<template v-slot="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"
@change="statusChange($event, scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180"></el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template v-slot="scope">
<el-button type="primary" link @click="addProduceDialogRef.show(scope.row)">编辑</el-button>
<el-popconfirm title="确认要删除吗?" @confirm="deleteHandle(scope.row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<div class="row mt14">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[10, 30, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<addProduceDialog ref="addProduceDialogRef" @success="pointsGoodsPageAjax" />
</div>
</template>
<script setup>
import Content from './ProductSettingsconfig/Content.vue'
</script>
import { ref, reactive, onMounted } from 'vue'
import { pointsGoodsPage, pointsGoodsDel, pointsGoodsPost } from '@/api/points'
import addProduceDialog from './addProduceDialog.vue';
import CouponIcon from './coupon-icon.vue';
import { includesString } from '@/utils'
const addProduceDialogRef = ref(null)
const tableData = reactive({
page: 1,
size: 10,
total: 0,
list: [],
loading: false
})
// 分页大小发生变化
function handleSizeChange(e) {
tableData.size = e;
pointsGoodsPageAjax();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
pointsGoodsPageAjax();
}
// 积分:商品:列表
async function pointsGoodsPageAjax() {
try {
tableData.loading = true
const res = await pointsGoodsPage({
page: tableData.page,
size: tableData.size
})
tableData.list = res.records
tableData.total = res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
// 删除
async function deleteHandle(row) {
try {
await pointsGoodsDel(row.id)
ElNotification({
title: '注意',
message: '已删除',
type: 'success'
})
pointsGoodsPageAjax()
} catch (error) {
console.log(error);
}
}
// 修改状态
async function statusChange(e, row) {
try {
await pointsGoodsPost(row)
pointsGoodsPageAjax()
} catch (error) {
console.log(error);
}
}
onMounted(() => {
pointsGoodsPageAjax()
})
</script>
<style scoped lang="scss">
.row {
&.mt14 {
margin-top: 14px;
}
}
.goods_info,
.coupon_info {
display: flex;
align-items: center;
gap: 14px;
.icon {
$size: 70px;
width: $size;
height: $size;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ececec;
border-radius: 8px;
}
.info {
display: flex;
align-items: center;
.name {
font-size: 14px;
color: #666;
}
}
}
</style>

View File

@@ -13,7 +13,7 @@
<el-form-item label="奖品类型">
<el-radio-group v-model="datas.DialogForm.goodsCategory">
<el-radio label="实物" value="physical" />
<el-radio label="优惠" value="coupon" />
<el-radio label="优惠" value="coupon" />
</el-radio-group>
</el-form-item>
<el-form-item label="商品图片" required prop="goodsImageUrl">

View File

@@ -0,0 +1,272 @@
<!-- 添加商品 -->
<template>
<el-dialog :title="form.id ? '编辑商品' : '添加商品'" top="4vh" width="600px" v-model="visible" @closed="reset">
<div class="content">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="left">
<el-form-item label="商品类型">
<el-radio-group v-model="form.goodsCategory">
<el-radio label="优惠券" value="优惠券"></el-radio>
<el-radio label="其它商品" value="其它商品"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赠送优惠券" v-if="form.goodsCategory == '优惠券'" prop="couponId">
<el-select v-model="form.couponId" placeholder="请选择优惠券" style="width: 200px;" @change="selectCoupon">
<el-option v-for="item in couponList" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</el-form-item>
<div v-if="form.goodsCategory == '其它商品'">
<el-form-item label="商品名称" prop="goodsName">
<el-input v-model="form.goodsName" placeholder="请输入商品名称" :maxlength="30" clearable
style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="商品图片" prop="goodsImageUrl">
<SingleImageUpload v-model="form.goodsImageUrl" />
</el-form-item>
</div>
<el-form-item label="所需积分" prop="requiredPoints">
<el-input v-model="form.requiredPoints" :maxlength="8"
@input="e => form.requiredPoints = filterNumberInput(e, 1)" style="width: 200px;">
<template #append>积分</template>
</el-input>
</el-form-item>
<el-form-item label="额外价格">
<el-input v-model="form.extraPrice" :maxlength="8" @input="e => form.extraPrice = filterNumberInput(e)"
style="width: 200px;">
<template #append></template>
</el-input>
</el-form-item>
<el-form-item label="数量" prop="quantity">
<el-input v-model="form.quantity" :maxlength="8" @input="e => form.quantity = filterNumberInput(e, 1)"
style="width: 200px;">
</el-input>
</el-form-item>
<el-form-item label="排序">
<el-input v-model="form.sort" :maxlength="8" @input="e => form.sort = filterNumberInput(e, 0)"
style="width: 200px;">
</el-input>
</el-form-item>
<el-form-item label="是否上架">
<div class="center">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
<!-- <span class="tips">开启后所有用户可通过消费获得积分及可用积分抵扣支付</span> -->
</div>
</el-form-item>
<el-form-item label="发放方式">
<div class="center">
<span class="tips">系统发放</span>
</div>
</el-form-item>
<el-form-item label="每人限购" prop="limitQuota">
<div class="center">
<el-switch v-model="limitQuotaSwitch" @change="limitQuotaSwitchChange" />
<el-input v-model="form.limitQuota" placeholder="请输入限购数量"
@input="e => form.limitQuota = filterNumberInput(e, 1)" v-if="limitQuotaSwitch"></el-input>
</div>
</el-form-item>
<el-form-item label="商品详情">
<MultiImageUpload v-model="goodsDescription" @up-data-event="multiImgSuccess" />
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="submitHandle" :loading="confirmLoading"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { pointsGoodsPost } from '@/api/points'
import couponApi from "@/api/market/coupon";
import { filterNumberInput } from '@/utils'
// 单图上传
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue';
// 多图上传
import MultiImageUpload from '@/components/Upload/MultiImageUpload.vue';
const visible = ref(false)
const formRef = ref(null)
const limitQuotaSwitch = ref(false)
const confirmLoading = ref(false)
const goodsDescription = ref([])
const formObj = {
id: '',
goodsCategory: '优惠券', // 商品类型 优惠券 其它商品
couponId: '', // 优惠券id
goodsName: '', // 商品名称/优惠券名称
goodsImageUrl: '', // 商品图片URL
requiredPoints: '', // 所需积分
extraPrice: '', // 额外价格
quantity: '', // 数量
sort: 0,
status: 1, // 是否上架 1-是 0-否
receiveType: '', // 领取方式 店内自取、系统发放
limitQuota: '', // 限购数量
goodsDescription: '', // 商品详情
}
const form = ref({ ...formObj })
function reset() {
limitQuotaSwitch.value = false
goodsDescription.value = []
form.value = { ...formObj }
}
const rules = ref({
couponId: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.couponId === '') {
return callback(new Error('请选择优惠券'))
} else {
return callback()
}
},
blur: ['change']
}
],
goodsName: [
{
required: true,
message: '请输入商品名称',
blur: ['change']
}
],
goodsImageUrl: [
{
required: true,
message: '请选择商品图片',
blur: ['change']
}
],
requiredPoints: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.requiredPoints === '') {
return callback(new Error('请输入所需积分'))
} else {
return callback()
}
},
blur: ['blur']
}
],
quantity: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.quantity === '') {
return callback(new Error('请输入数量'))
} else {
return callback()
}
},
blur: ['blur']
}
],
limitQuota: [
{
validator: (rule, value, callback) => {
if (limitQuotaSwitch.value && form.value.limitQuota === '') {
return callback(new Error('请输入限购数量'))
} else {
return callback()
}
},
blur: ['blur', 'change']
}
]
})
const couponList = ref([])
// 选择优惠券
function selectCoupon(e) {
let obj = couponList.value.find(item => item.id == e)
form.value.goodsName = obj.title
}
function limitQuotaSwitchChange() {
form.value.limitQuota = ''
}
// 多图片上传成功
async function multiImgSuccess(response) {
if (!response && goodsDescription.value.length > 0) {
await nextTick()
form.value.goodsDescription = JSON.stringify(goodsDescription.value)
console.log('onSuccess.selectItem.value', form.value);
}
}
const emits = defineEmits(['success'])
function submitHandle() {
try {
formRef.value.validate(async vaild => {
if (vaild) {
confirmLoading.value = true
await pointsGoodsPost(form.value)
ElNotification({
title: '注意',
message: '保存成功',
type: 'success'
})
visible.value = false
emits('success')
}
})
} catch (error) {
console.log(error);
}
setTimeout(() => {
confirmLoading.value = false
}, 500);
}
function show(obj) {
visible.value = true
if (obj && obj.id) {
form.value = { ...obj }
if (obj.goodsDescription !== '') {
goodsDescription.value = JSON.parse(obj.goodsDescription)
}
if (obj.limitQuota !== '' && obj.limitQuota !== null) {
limitQuotaSwitch.value = true
} else {
limitQuotaSwitch.value = false
}
}
}
defineExpose({
show
})
onMounted(() => {
couponApi.getList({ size: 999 }).then((res) => {
if (res) {
couponList.value = res.records || [];
}
});
});
</script>
<style scoped lang="scss">
.center {
display: flex;
align-items: center;
gap: 10px;
.tips {
color: #666;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<!-- 优惠券图标 -->
<template>
<div class="container">
<div class="icon icon1" v-if="props.item.couponType == 1">
<div class="top">
<span class="i"></span>
<span class="num">{{ props.item.discountAmount }}</span>
</div>
<div class="intro">
<span class="t">{{ props.item.fullAmount }}可用</span>
</div>
</div>
<div class="icon icon2" v-if="props.item.couponType == 2">
<div class="top">
<span class="i">{{ props.item.discountNum }}</span>
<span class="num">商品兑换</span>
</div>
<div class="intro">
<span class="t">{{ props.item.fullAmount }}可用</span>
</div>
</div>
<div class="icon icon3" v-if="props.item.couponType == 3">
<div class="top">
<span class="num">{{ props.item.discountRate / 10 }}</span>
</div>
<div class="intro">
<span class="t">{{ props.item.fullAmount }}可用</span>
</div>
</div>
<div class="icon icon2" v-if="props.item.couponType == 4">
<div class="top">
<span class="i">第二件</span>
<span class="num">半价券</span>
</div>
</div>
<div class="icon icon2" v-if="props.item.couponType == 6">
<div class="top">
<span class="i">买一送</span>
<span class="num">一券</span>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
item: {
type: Object,
default: {}
}
});
</script>
<style scoped lang="scss">
$color: #ff1c1c;
.container {
width: 100%;
height: 100%;
.icon {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.icon1 {
.top {
.i {
color: $color;
font-size: 24upx;
font-weight: bold;
}
.num {
color: $color;
font-size: 72upx;
font-weight: bold;
}
}
}
&.icon2 {
.top {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.i {
color: $color;
font-size: 34upx;
font-weight: bold;
}
.num {
color: $color;
font-size: 34upx;
font-weight: bold;
}
}
}
&.icon3 {
.top {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.i {
color: $color;
font-size: 34upx;
font-weight: bold;
}
.num {
color: $color;
font-size: 52upx;
font-weight: bold;
}
}
}
.intro {
display: flex;
justify-content: center;
.t {
font-size: 22upx;
color: #999;
}
}
}
}
</style>

View File

@@ -1,16 +1,408 @@
<template>
<!-- 兑换记录模块 -->
<div style="padding: 15px;">
<!-- 搜索 -->
<Search></Search>
<!-- 数据统计 -->
<DataStatistics></DataStatistics>
<!-- 表格 -->
<Content></Content>
<div>
<div class="row">
<el-form :mode="queryForm" inline>
<el-form-item label="商品类型">
<el-select v-model="queryForm.goodsCategory" style="width: 150px;">
<el-option label="优惠券" value="优惠券"></el-option>
<el-option label="其它商品" value="其它商品"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryForm.status" style="width: 150px;">
<el-option v-for="item in statusList" :key="item.label" :label="item.label" :value="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="搜索">
<el-input v-model="queryForm.pointsGoodsName" placeholder="搜索关键词"></el-input>
</el-form-item>
<el-form-item>
<el-date-picker v-model="time" type="daterange" range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" @change="timeChange" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchHandle">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="row">
<div class="total_info">
<div class="item">
<img class="icon" src="@/assets/applocation/dds.png">
<div class="info">
<span class="t1">总订单数</span>
<span class="t2">{{ totalInfo.count }}</span>
</div>
</div>
<div class="item">
<img class="icon" src="@/assets/applocation/ddje.png">
<div class="info">
<span class="t1">已支付金额</span>
<span class="t2">¥ {{ multiplyAndFormat(totalInfo.totalAmount) }}</span>
</div>
</div>
</div>
</div>
<div class="row mt14">
<el-table :data="tableData.list" border stripe v-loading="tableData.loading">
<el-table-column label="订单号" prop="orderNo" width="200"></el-table-column>
<el-table-column label="用户" prop="orderNo" width="200">
<template v-slot="scope">
<div class="column">
<span class="t1">{{ scope.row.nickName }}</span>
<span class="t2">{{ scope.row.phone }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="商品" prop="pointsGoodsName" width="200">
<template v-slot="scope">
<div class="goods_info" v-if="includesString(scope.row.goodsCategory, '其它商品')">
<el-image class="icon" :src="scope.row.goodsImageUrl" fit="cover"></el-image>
<div class="info">
<div class="name">{{ scope.row.pointsGoodsName }}</div>
</div>
</div>
<div class="coupon_info" v-if="includesString(scope.row.goodsCategory, '优惠券')">
<div class="icon">
<CouponIcon :item="scope.row.couponInfo" />
</div>
<div class="info">
<div class="name">
{{ scope.row.pointsGoodsName }}
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="商品类型" prop="goodsCategory"></el-table-column>
<el-table-column label="消耗积分" prop="spendPoints"></el-table-column>
<el-table-column label="支付金额" prop="extraPaymentAmount"></el-table-column>
<el-table-column label="状态" prop="status" width="100">
<template v-slot="scope">
<el-tag :type="statusFilter(scope.row.status).type" disable-transitions>
{{ statusFilter(scope.row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="下单时间" prop="createTime"></el-table-column>
<el-table-column label="兑换码" prop="couponCode"></el-table-column>
<el-table-column label="核销时间" prop="checkoutTime"></el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template v-slot="scope">
<el-button link type="primary" @click="goodsRecordCheckoutHandle(scope.row)"
v-if="includesString(scope.row.status, '待核销')">核销</el-button>
<el-button link type="danger" @click="returnCostHandle(scope.row)"
v-if="includesString(scope.row.status, '退款中')">审核</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="row mt14">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[10, 30, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<el-dialog title="退款" width="600px" v-model="returnVisable">
<el-form :model="returnForm" label-width="150" label-position="left">
<el-form-item label="是否同意">
<el-radio-group v-model="returnForm.type">
<el-radio label="同意" :value="1"></el-radio>
<el-radio label="驳回" :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="驳回原因" v-if="returnForm.type === 0">
<el-input type="textarea" v-model="returnForm.reason" placeholder="请输入驳回原因"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="returnVisable = false">取消</el-button>
<el-button type="primary" @click="returnCostConfirmHandle" :loading="returnLoading">同意</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import Search from './exchangerecordsconfig/Search.vue'
import DataStatistics from './exchangerecordsconfig/DataStatistics.vue'
import Content from './exchangerecordsconfig/Content.vue'
</script>
import dayjs from 'dayjs'
import { ref, reactive, onMounted } from 'vue'
import { goodsRecordPage, goodsRecordTotal, goodsRecordCheckout, goodsRecordAgreeRefund, goodsRecordRejectRefund } from '@/api/points'
import CouponIcon from './coupon-icon.vue';
import { includesString, multiplyAndFormat } from '@/utils'
import { ElMessageBox } from 'element-plus'
const time = ref([])
const queryForm = reactive({
startTime: '',
endTime: '',
pointsGoodsName: '',
goodsCategory: '', // 商品类型 优惠券 其它商品
receiveType: '', // 领取方式 店内自取、系统发放
status: ''
})
function timeChange(e) {
if (e && e.length) {
queryForm.startTime = dayjs(e[0]).format("YYYY-MM-DD 00:00:00");
queryForm.endTime = dayjs(e[1]).format("YYYY-MM-DD 23:59:59");
} else {
queryForm.startTime = "";
queryForm.endTime = "";
}
}
const statusList = ref([
{
label: '待支付',
type: 'info'
},
{
label: '待核销',
type: 'primary'
},
{
label: '已完成',
type: 'success'
},
{
label: '退款中',
type: 'warning'
},
{
label: '已退款',
type: 'info'
},
])
function statusFilter(status) {
const obj = statusList.value.find(item => item.label == status)
if (obj) {
return obj
} else {
return {
label: status,
type: 'info'
}
}
}
const tableData = reactive({
loading: false,
page: 1,
size: 10,
total: 0,
list: []
})
const totalInfo = reactive({
count: 0,
totalAmount: 0
})
function searchHandle() {
tableData.page = 1
goodsRecordPageAjax();
goodsRecordTotalAjax()
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.size = e;
goodsRecordPageAjax();
goodsRecordTotalAjax()
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
goodsRecordPageAjax();
goodsRecordTotalAjax()
}
// 积分:积分商品:兑换统计
async function goodsRecordTotalAjax() {
try {
const res = await goodsRecordTotal({
...queryForm,
page: tableData.page,
size: tableData.size
})
totalInfo.count = res.count || 0
totalInfo.totalAmount = res.totalAmount || 0
} catch (error) {
console.log(error);
}
}
// 积分:积分商品:兑换记录
async function goodsRecordPageAjax() {
try {
tableData.loading = true
const res = await goodsRecordPage({
...queryForm,
page: tableData.page,
size: tableData.size
})
tableData.list = res.records
tableData.total = res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
// 核销
function goodsRecordCheckoutHandle(row) {
ElMessageBox.confirm('确认要核销吗?').then(async () => {
try {
await goodsRecordCheckout(row.couponCode)
ElNotification({
title: '注意',
message: '核销成功',
type: 'success'
})
goodsRecordPageAjax()
} catch (error) {
console.log(error);
}
}).catch(() => {
})
}
// 退款操作
const returnVisable = ref(false)
const returnLoading = ref(false)
const returnForm = ref({
type: 1,
recordId: '',
orderNo: '',
reason: '',
})
function returnCostHandle(row) {
returnForm.value.recordId = row.id
returnForm.value.orderNo = row.orderNo
returnVisable.value = true
}
// 退款操作
async function returnCostConfirmHandle() {
try {
returnLoading.value = true
if (returnForm.value.type == 1) {
// 同意
await goodsRecordAgreeRefund(returnForm.value)
} else {
// 驳回
await goodsRecordRejectRefund(returnForm.value)
}
returnVisable.value = false
ElNotification({
title: '注意',
message: '操作成功',
type: 'success'
})
goodsRecordPageAjax()
} catch (error) {
console.log(error);
}
setTimeout(() => {
returnLoading.value = false
}, 500);
}
onMounted(() => {
goodsRecordPageAjax()
goodsRecordTotalAjax()
})
</script>
<style scoped lang="scss">
.goods_info,
.coupon_info {
display: flex;
align-items: center;
gap: 14px;
.icon {
$size: 70px;
width: $size;
height: $size;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ececec;
border-radius: 8px;
}
.info {
display: flex;
align-items: center;
.name {
font-size: 14px;
color: #666;
}
}
}
.row {
&.mt14 {
margin-top: 14px;
}
}
.column {
display: flex;
flex-direction: column;
.t1 {
font-size: 14px;
color: #333;
}
.t2 {
font-size: 14px;
color: #666;
}
}
.total_info {
display: flex;
gap: 14px;
.item {
display: flex;
align-items: center;
gap: 10px;
padding: 14px;
background-color: #fff;
border: 1px solid #ececec;
border-radius: 8px;
.icon {
width: 44px;
height: 44px;
}
.info {
display: flex;
gap: 4px;
flex-direction: column;
.t1 {
font-size: 14px;
color: #666;
}
.t2 {
color: #333;
font-size: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<!-- 兑换记录模块 -->
<div style="padding: 15px;">
<!-- 搜索 -->
<Search></Search>
<!-- 数据统计 -->
<DataStatistics></DataStatistics>
<!-- 表格 -->
<Content></Content>
</div>
</template>
<script setup>
import Search from './exchangerecordsconfig/Search.vue'
import DataStatistics from './exchangerecordsconfig/DataStatistics.vue'
import Content from './exchangerecordsconfig/Content.vue'
</script>

View File

@@ -360,7 +360,7 @@
</div>
</div>
</div>
<div class="item item6">
<!-- <div class="item item6">
<div class="row_wrap">
<div class="row">
<div class="title">净利润</div>
@@ -375,7 +375,7 @@
</div>
</div>
</div>
</div>
</div> -->
</div>
</div>
</div>
@@ -447,8 +447,15 @@
<div class="item">
<div class="header">
<div class="rate_title">
<span class="t1">毛利率/净利率</span>
<span class="t2">今天 {{ initInterestRateTime }} 更新</span>
<div class="column">
<span class="t1">毛利率</span>
<span class="t2">今天 {{ initInterestRateTime }} 更新</span>
</div>
<!-- <span class="t1">毛利率/净利率</span> -->
<el-radio-group v-model="interestRateDay" @change="profitRateBarChart">
<el-radio-button value="7">近7天</el-radio-button>
<el-radio-button value="30">30</el-radio-button>
</el-radio-group>
</div>
</div>
<div ref="initInterestRate" v-loading="initInterestRateLoading" class="chart" style="height: 350px" />
@@ -457,8 +464,14 @@
<div class="item">
<div class="header">
<div class="rate_title">
<span class="t1">成本</span>
<span class="t2">今天 {{ costUpdateTime }} 更新</span>
<div class="column">
<span class="t1">成本</span>
<span class="t2">今天 {{ costUpdateTime }} 更新</span>
</div>
<el-radio-group v-model="costDay" @change="costLineChart">
<el-radio-button value="7">近7天</el-radio-button>
<el-radio-button value="30">30</el-radio-button>
</el-radio-group>
</div>
</div>
<div ref="costRef" v-loading="costLoading" class="chart" style="height: 350px" />
@@ -615,7 +628,9 @@ export default {
initInterestRateTime: '',
costLoading: true,
costRef: null,
costUpdateTime: ''
costUpdateTime: '',
interestRateDay: '7',
costDay: '7'
};
},
computed: {
@@ -686,14 +701,21 @@ export default {
* 获取分店列表
*/
async geiShopList() {
let res = await ShopApi.getBranchList()
this.branchList = res;
this.shopId = res[0].shopId
try {
let res = await ShopApi.getBranchList()
this.branchList = res;
this.shopId = res[0].shopId
} catch (error) {
console.log('获取分店列表===', error);
}
},
shopChange() {
this.summarytrade();
this.lineChartTypeChange(this.lineChartType)
this.dateProduct()
this.profitRateBarChart()
this.costLineChart()
},
// 切换时间
timeChange(e, index = 0) {
@@ -1071,7 +1093,6 @@ export default {
],
});
},
// 初始化毛利率/净利率图表
initInterestRateChart(time, data) {
// 销毁旧实例,避免重复创建
if (this.initInterestRate) {
@@ -1100,7 +1121,6 @@ export default {
return `
<div style="margin-bottom:4px;">${xName}</div>
<div><span style="display:inline-block;width:8px;height:8px;background:${color1};margin-right:6px;border-radius:2px;"></span>毛利率:${profitRate}%</div>
<div><span style="display:inline-block;width:8px;height:8px;background:${color2};margin-right:6px;border-radius:2px;"></span>净利率:${netProfitRate}%</div>
`;
},
// 保留你原有的提示框样式
@@ -1146,17 +1166,96 @@ export default {
barGap: "0%", // 保持柱子紧贴
barWidth: time.length <= 7 ? "50%" : "30%",
data: profitRateData, // 预处理后的毛利率数据
},
{
name: '净利率',
type: "bar",
barGap: "0%",
barWidth: time.length <= 7 ? "50%" : "30%",
data: netProfitRateData, // 预处理后的净利率数据
},
}
],
});
},
// 初始化毛利率/净利率图表
// initInterestRateChart(time, data) {
// // 销毁旧实例,避免重复创建
// if (this.initInterestRate) {
// this.initInterestRate.dispose();
// }
// this.initInterestRate = echarts.init(this.$refs.initInterestRate);
// // 预处理数据:将毛利率和净利率按索引对应(关键,用于手动匹配)
// const profitRateData = data.map(item => item.profitRate || 0);
// const netProfitRateData = data.map(item => item.netProfitRate || 0);
// this.initInterestRate.setOption({
// tooltip: {
// trigger: "item", // 保留 item 模式(确保能触发,之前已验证有效)
// // 核心:手动查找当前 X 轴索引对应的另一个系列数据,拼接显示
// formatter: (params) => {
// const index = params.dataIndex; // 获取当前数据的索引X轴位置
// const xName = time[index]; // 当前 X 轴分类名称(如时间)
// const color1 = "#165DFF"; // 毛利率颜色(与 series 一致)
// const color2 = "#14C9C9"; // 净利率颜色(与 series 一致)
// // 当前系列数据 + 另一个系列数据(按索引匹配)
// const profitRate = profitRateData[index].toFixed(2);
// const netProfitRate = netProfitRateData[index].toFixed(2);
// // 拼接提示框内容(彩色方块 + 两个指标)
// return `
// <div style="margin-bottom:4px;">${xName}</div>
// <div><span style="display:inline-block;width:8px;height:8px;background:${color1};margin-right:6px;border-radius:2px;"></span>毛利率:${profitRate}%</div>
// <div><span style="display:inline-block;width:8px;height:8px;background:${color2};margin-right:6px;border-radius:2px;"></span>净利率:${netProfitRate}%</div>
// `;
// },
// // 保留你原有的提示框样式
// padding: 10,
// textStyle: { fontSize: 11 },
// backgroundColor: "#fff",
// borderColor: "#eee",
// borderWidth: 1,
// boxShadow: "0 2px 8px rgba(0,0,0,0.08)"
// },
// xAxis: [
// {
// type: "category",
// data: time,
// axisTick: { alignWithLabel: true },
// axisLine: { lineStyle: { color: "#999" } },
// axisLabel: {
// rotate: time.length <= 7 ? 0 : 45,
// interval: 0,
// fontSize: "9",
// },
// },
// ],
// color: ["#165DFF", "#14C9C9", "#F98B26"], // 系列颜色不变
// yAxis: [
// {
// type: "value",
// axisLine: { lineStyle: { color: "#999" } },
// splitLine: { lineStyle: { type: "dashed", color: "#ececec" } },
// axisLabel: { formatter: "{value}%" }, // Y轴加百分号
// },
// ],
// grid: {
// top: '5%',
// right: '5%',
// bottom: '8%',
// left: '8%',
// },
// series: [
// {
// name: "毛利率",
// type: "bar",
// barGap: "0%", // 保持柱子紧贴
// barWidth: time.length <= 7 ? "50%" : "30%",
// data: profitRateData, // 预处理后的毛利率数据
// },
// {
// name: '净利率',
// type: "bar",
// barGap: "0%",
// barWidth: time.length <= 7 ? "50%" : "30%",
// data: netProfitRateData, // 预处理后的净利率数据
// },
// ],
// });
// },
// 初始化销售额图表
initPayChart(data) {
this.payChart = echarts.init(this.$refs.payChart);
@@ -1230,7 +1329,7 @@ export default {
async profitRateBarChart() {
try {
this.initInterestRateLoading = true;
const res = await dataSummaryApi.profitRateBarChart({ day: this.saleActive, shopId: this.shopId });
const res = await dataSummaryApi.profitRateBarChart({ day: this.interestRateDay, shopId: this.shopId });
this.initInterestRateTime = dayjs().format('HH:mm')
@@ -1254,7 +1353,7 @@ export default {
async costLineChart() {
try {
this.costLoading = true;
const res = await dataSummaryApi.costLineChart({ day: this.saleActive, shopId: this.shopId });
const res = await dataSummaryApi.costLineChart({ day: this.costDay, shopId: this.shopId });
this.costUpdateTime = dayjs().format('HH:mm')
@@ -1903,6 +2002,11 @@ export default {
align-items: center;
justify-content: space-between;
.column {
display: flex;
flex-direction: column;
}
.t1 {
font-size: 16px;
color: #111;

View File

@@ -330,6 +330,7 @@ const handleImageError = (item) => {
.name {
font-size: 14px;
font-weight: bold;
}
.intro {

View File

@@ -236,7 +236,8 @@ export default {
tableData: [],
selectItem: {},
imageUrl: "",
imgList: []
imgList: [],
shopName: ''
};
},
mounted() {
@@ -261,6 +262,9 @@ export default {
},
// 多图上传成功
async MultiOnSuccess(response) {
console.log(response);
console.log(this.imgList);
if (!response && this.imgList.length > 0) {
console.log(this.imgList);
await nextTick()
@@ -301,6 +305,8 @@ export default {
this.selectItem = { autoKey, id, name, value };
if (this.isJsonArrayString(value)) {
this.imgList = JSON.parse(value)
} else {
this.imgList = []
}
console.log(this.selectItem);
},

View File

@@ -29,10 +29,10 @@
<el-form-item label="数量">
<el-input-number v-model="form.num" controls-position="right" :min="1"></el-input-number>
</el-form-item>
<el-form-item label="选择优惠" v-if="form.isGiftCoupon == 1">
<el-form-item label="选择优惠" v-if="form.isGiftCoupon == 1">
<div>
<el-button type="primary" icon="el-icon-plus" @click="$refs.coupon.show([...productIds])">
添加优惠
添加优惠
</el-button>
</div>
<div class="shop_list">
@@ -145,7 +145,7 @@ export default {
if (this.form.isGiftCoupon == 1) {
if (this.productIds.length == 0) {
this.$message({
message: "请选择优惠",
message: "请选择优惠",
type: "warning",
});
return false;
@@ -190,7 +190,7 @@ export default {
this.dialogVisible = true;
if (obj && obj.id) {
this.form = { ...obj };
// 留着以后说不定多个优惠
// 留着以后说不定多个优惠
// let res = await activate(obj.id)
// this.productIds = res
if (obj.couponId) {