add: 增加新功能

This commit is contained in:
gyq
2025-10-17 11:28:41 +08:00
parent 0405a0fb99
commit 44482d9dc9
30 changed files with 4695 additions and 507 deletions

15
.vscode/settings.json vendored
View File

@@ -2,14 +2,14 @@
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"editor.codeActionsOnSave": {
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
@@ -67,7 +67,9 @@
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": false,
"i18n-ally.pathMatcher": "{namespaces}/{locale}.{ext}",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": [
@@ -93,5 +95,8 @@
"files.associations": {
"*.ttml": "xml",
"*.ttss": "css"
},
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
}

View File

@@ -4,7 +4,7 @@ const baseURL = Market_BaseUrl + "/admin/coupon";
const API = {
getList(params: getListRequest) {
return request<any>({
url: `${baseURL}`,
url: `${baseURL}/page`,
method: "get",
params
});

View File

@@ -2,7 +2,8 @@ import request from "@/utils/request";
import {
Account_BaseUrl,
Product_BaseUrl,
Market_BaseUrl
Market_BaseUrl,
System_BaseUrl
} from "@/api/config";
// 获取分店列表
@@ -13,6 +14,14 @@ export function getBranchPage() {
});
}
// 店铺分店列表(下拉展示主店和分店使用,默认第一个是主店,其余是分店)
export function getBranchList() {
return request({
url: `${Account_BaseUrl + "/admin/shopInfo/branchList"}`,
method: "get",
});
}
// 获取商品-列表
export function getProductList() {
return request({
@@ -161,4 +170,266 @@ export function getShopUserList(params) {
method: 'get',
params
});
}
}
// 消费返现 配置信息获取
export function consumeCashback() {
return request({
url: `${Market_BaseUrl + "/admin/consumeCashback"}`,
method: 'get'
});
}
// 消费返现 配置信息修改
export function consumeCashbackPost(data) {
return request({
url: `${Market_BaseUrl + "/admin/consumeCashback"}`,
method: 'post',
data
});
}
// 消费返现 记录获取
export function consumeCashbackRecord(params) {
return request({
url: `${Market_BaseUrl + "/admin/consumeCashback/record"}`,
method: 'get',
params
});
}
// 满减活动 配置信息获取
export function discountActivityPage(params) {
return request({
url: `${Market_BaseUrl + "/admin/discountActivity/page"}`,
method: 'get',
params
});
}
// 满减活动 新增
export function discountActivity(data, method = 'post') {
return request({
url: `${Market_BaseUrl + "/admin/discountActivity"}`,
method: method,
data
});
}
// 满减活动 删除
export function discountActivityDelete(id) {
return request({
url: `${Market_BaseUrl + "/admin/discountActivity"}?id=${id}`,
method: 'DELETE'
});
}
// 店铺详情
export function shopInfoGet() {
return request({
url: `${Account_BaseUrl + "/admin/shopInfo/detail"}`,
method: 'get'
});
}
// 店铺编辑
export function shopInfoPut(data) {
return request({
url: `${Account_BaseUrl + "/admin/shopInfo"}`,
method: 'put',
data
});
}
// 私域引流 配置信息获取
export function drainageConfigGet() {
return request({
url: `${Market_BaseUrl + "/admin/drainageConfig"}`,
method: 'get'
});
}
// 私域引流 配置信息修改
export function drainageConfigPost(data) {
return request({
url: `${Market_BaseUrl + "/admin/drainageConfig"}`,
method: 'post',
data
});
}
// 短信模板 新增
export function smsTemplate(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsTemplate"}`,
method: 'post',
data
});
}
// 短信模板 重新提交
export function smsTemplateResubmit(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsTemplate/resubmit"}`,
method: 'post',
data
});
}
// 短信模板 列表
export function smsTemplateGet() {
return request({
url: `${Market_BaseUrl + "/admin/smsTemplate"}`,
method: 'get',
});
}
// 短信模板 列表 管理员专用
export function smsTemplatePage(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsTemplate/query"}`,
method: 'post',
data
});
}
// 获取短信发送用户
export function getPushEventUser(data) {
return request({
url: `${Account_BaseUrl + "/admin/shopUser/getPushEventUser"}`,
method: 'post',
data
});
}
// 短信推送任务 新增
export function pushEventPost(data, method = 'post') {
return request({
url: `${Market_BaseUrl + "/admin/pushEvent"}`,
method: method,
data
});
}
// 短信推送任务 列表
export function pushEventGet(params) {
return request({
url: `${Market_BaseUrl + "/admin/pushEvent"}`,
method: 'get',
params
});
}
// 短信推送任务 删除任务
export function pushEventDel(id) {
return request({
url: `${Market_BaseUrl}/admin/pushEvent/${id}`,
method: 'DELETE'
});
}
// 获取店铺短信余额明细
export function smsMoneyDetail(params) {
return request({
url: `${Market_BaseUrl + "/admin/smsMoneyDetail"}`,
method: 'get',
params
});
}
// 霸王餐 配置信息获取
export function freeDingGet(params) {
return request({
url: `${Account_BaseUrl + "/admin/freeDing"}`,
method: 'get',
params
});
}
// 霸王餐 修改霸王餐配置信息
export function freeDingPut(data) {
return request({
url: `${Account_BaseUrl + "/admin/freeDing"}`,
method: 'put',
data
});
}
// 平台 模板状态/删除
export function shopUseDelStatus(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsTemplate/shopUse"}`,
method: 'post',
data
});
}
// 配置信息获取
export function adminSmsMoneyPage(params) {
return request({
url: `${Market_BaseUrl + "/admin/smsMoney/page"}`,
method: 'get',
params
});
}
// 变更店铺短信余额
export function smsMoneyChange(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsMoney/change"}`,
method: 'post',
data
});
}
// 平台 获取店铺短信余额明细
export function smsMoneyDetailQuery(data) {
return request({
url: `${Market_BaseUrl + "/admin/smsMoneyDetail/query"}`,
method: 'post',
data
});
}
// 获取店铺短信余额
export function smsMoneyGet() {
return request({
url: `${Market_BaseUrl + "/admin/smsMoney"}`,
method: 'get'
});
}
// 获取发送短信单价
export function smsMoneyGetFee() {
return request({
url: `${System_BaseUrl + "/admin/sysParams/code/sms_fee"}`,
method: 'get'
});
}

View File

@@ -1,10 +1,11 @@
import { BigNumber } from "bignumber.js";
/**
* Check if an element has a class
* @param {HTMLElement} ele
* @param {string} cls
* @returns {boolean}
*/
export function hasClass(ele : HTMLElement, cls : string) {
export function hasClass(ele: HTMLElement, cls: string) {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
}
@@ -13,7 +14,7 @@ export function hasClass(ele : HTMLElement, cls : string) {
* @param {HTMLElement} ele
* @param {string} cls
*/
export function addClass(ele : HTMLElement, cls : string) {
export function addClass(ele: HTMLElement, cls: string) {
if (!hasClass(ele, cls)) ele.className += " " + cls;
}
@@ -22,7 +23,7 @@ export function addClass(ele : HTMLElement, cls : string) {
* @param {HTMLElement} ele
* @param {string} cls
*/
export function removeClass(ele : HTMLElement, cls : string) {
export function removeClass(ele: HTMLElement, cls: string) {
if (hasClass(ele, cls)) {
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
ele.className = ele.className.replace(reg, " ");
@@ -35,7 +36,7 @@ export function removeClass(ele : HTMLElement, cls : string) {
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path : string) {
export function isExternal(path: string) {
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
return isExternal;
}
@@ -46,7 +47,7 @@ export function isExternal(path : string) {
* @param growthRate
* @returns
*/
export function formatGrowthRate(growthRate : number) {
export function formatGrowthRate(growthRate: number) {
if (growthRate === 0) {
return "-";
}
@@ -62,7 +63,7 @@ export function formatGrowthRate(growthRate : number) {
* @param {string} cFormat
* @returns {string}
*/
export function parseTime(time : string | number | Date | null, cFormat : string | undefined) {
export function parseTime(time: string | number | Date | null, cFormat: string | undefined) {
if (arguments.length === 0) {
return null;
}
@@ -90,7 +91,7 @@ export function parseTime(time : string | number | Date | null, cFormat : string
s: date.getSeconds(),
a: date.getDay()
};
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key : keyof typeof formatObj) : string => {
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key: keyof typeof formatObj): string => {
let value = formatObj[key];
// Note: getDay() returns 0 on Sunday
if (key === "a") {
@@ -105,7 +106,7 @@ export function parseTime(time : string | number | Date | null, cFormat : string
}
// 下载文件
export function downloadFile(obj : BlobPart, name : string, suffix : string, useUnix = true) {
export function downloadFile(obj: BlobPart, name: string, suffix: string, useUnix = true) {
const url = window.URL.createObjectURL(new Blob([obj]));
const link = document.createElement("a");
link.style.display = "none";
@@ -133,9 +134,9 @@ export function isSyncStatus() {
/**
* 判断是否有某权限
*/
export function hasPermission(params : any) {
export function hasPermission(params: any) {
let $PermissionObj = JSON.parse(localStorage.getItem("permission") || '[]')
const obj = $PermissionObj.find((v : any) => v == params || v == params)
const obj = $PermissionObj.find((v: any) => v == params || v == params)
if (obj) {
return obj
}
@@ -232,4 +233,30 @@ export function convertTimeToDate(timeStr, options = {}) {
// 5. 按指定格式返回日期字符串
return dateObj.format(format);
}
}
/**
* 乘法计算并格式化结果
* @param {string|number} num1 - 第一个乘数
* @param {string|number} num2 - 第二个乘数
* @returns {string} 保留两位小数的结果(不四舍五入,补零)
*/
export const multiplyAndFormat = (num1: any, num2: any = 1): string => {
try {
// 转换为BigNumber使用字符串构造避免精度问题
const bigNum1 = new BigNumber(num1.toString());
const bigNum2 = new BigNumber(num2.toString());
// 1. 乘法计算
const product = bigNum1.multipliedBy(bigNum2);
// 2. 截断到两位小数(不四舍五入)
const truncated = product.decimalPlaces(2, BigNumber.ROUND_DOWN);
// 3. 格式化保留两位小数(补零)
return truncated.toFixed(2);
} catch (error) {
console.error('计算错误:', error);
return '0.00'; // 出错时返回默认值
}
};

View File

@@ -1,13 +1,7 @@
<template>
<div class="login" :style="'background-image:url(' + Background + ');'">
<el-form
ref="loginForm"
:model="state.loginForm"
:rules="state.loginRules"
label-position="left"
label-width="0px"
class="login-form"
>
<el-form ref="loginForm" :model="state.loginForm" :rules="state.loginRules" label-position="left" label-width="0px"
class="login-form">
<h3 class="title">银收客后台管理</h3>
<el-form-item>
<el-radio-group v-model="state.loginForm.loginType">
@@ -16,39 +10,19 @@
</el-radio-group>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="state.loginForm.username"
type="text"
auto-complete="off"
placeholder="商户号"
></el-input>
<el-input v-model="state.loginForm.username" type="text" auto-complete="off" placeholder="商户号"></el-input>
</el-form-item>
<el-form-item prop="staffUserName" v-if="state.loginForm.loginType == 1">
<el-input
v-model="state.loginForm.staffUserName"
type="text"
auto-complete="off"
placeholder="账号"
></el-input>
<el-input v-model="state.loginForm.staffUserName" type="text" auto-complete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="state.loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
></el-input>
<el-input v-model="state.loginForm.password" type="password" auto-complete="off" placeholder="密码"
@keyup.enter="handleLogin"></el-input>
</el-form-item>
<el-form-item prop="code">
<div class="code_wrap">
<el-input
v-model="state.loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
></el-input>
<el-input v-model="state.loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleLogin"></el-input>
<div class="login-code">
<img :src="state.codeUrl" @click="getCode" />
</div>
@@ -56,13 +30,8 @@
</el-form-item>
<el-form-item style="width: 100%">
<el-button
:loading="state.loading"
size="default"
type="primary"
style="width: 100%"
@click.prevent="handleLogin"
>
<el-button :loading="state.loading" size="default" type="primary" style="width: 100%"
@click.prevent="handleLogin">
<span v-if="!state.loading"> </span>
<span v-else> 中...</span>
</el-button>
@@ -94,9 +63,9 @@ const state = reactive({
cookiePass: "",
loginForm: {
username: "",
password: "",
password: process.env.NODE_ENV === "development" ? `czg${new Date().getHours().toString().padStart(2, '0')}${new Date().getMinutes().toString().padStart(2, '0')}` : "",
// rememberMe: false,
code: "",
code: process.env.NODE_ENV === "development" ? "666666" : "",
uuid: "",
staffUserName: "",
loginType: 0,

View File

@@ -6,15 +6,15 @@
<div style="padding-top: 14px;">
<el-tabs v-model="tabsValue">
<el-tab-pane label="基础明细" :name="1">
<el-form :model="form" :rules="rules" label-position="left" label-width="100px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="left" label-width="100px">
<el-form-item label="可用门店">
<el-radio-group v-model="form.useType">
<el-radio label="全部门店" value="all"></el-radio>
<el-radio label="指定门店可用" value="part"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择门店" v-if="form.shopType == 'part'">
<selectBranchs ref="selectBranchsRef" @success="successShop" />
<el-form-item label="选择门店" prop="shopIdList" v-if="form.useType == 'part'">
<selectBranchs v-model="form.shopIdList" />
</el-form-item>
<el-form-item label="适用用户">
<el-radio-group v-model="form.applicableUser">
@@ -29,26 +29,28 @@
<el-radio label="固定金额" value="fix"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="阶梯设置" prop="">
<el-form-item label="阶梯设置" prop="stepValidate">
<div class="row">
<div class="item border" v-for="(item, index) in callbackList" :key="index">
<div class="item border" v-for="(item, index) in form.cashbackStepList" :key="index">
<div class="row_title">
<span>{{ `${index + 1}` }}</span>
<el-icon @click="callbackList.splice(index, 1)">
<el-icon @click="form.cashbackStepList.splice(index, 1)">
<Delete />
</el-icon>
</div>
<div class="ipt">
<div class="ipt_row">
<span>返现门槛</span>
<el-input placeholder="请输入返现门槛" v-model="item.amount">
<el-input placeholder="请输入返现门槛" :maxlength="8" v-model="item.amount"
@input="e => item.amount = filterNumberInput(e)">
<template #append></template>
</el-input>
</div>
<div class="ipt_row">
<span>{{ form.cashbackType == 'percentage' ? '返现比例' : '返现金额' }}</span>
<el-input :placeholder="form.cashbackType == 'percentage' ? '请输入返现比例' : '请输入返现金额'"
v-model="item.cashbackAmount">
<el-input :maxlength="8"
:placeholder="form.cashbackType == 'percentage' ? '请输入返现比例' : '请输入返现金额'"
v-model="item.cashbackAmount" @input="e => item.cashbackAmount = filterNumberInput(e)">
<template #append>
{{ form.cashbackType == 'percentage' ? '%' : '元' }}
</template>
@@ -56,19 +58,19 @@
</div>
</div>
</div>
<div class="item">
<div class="item" v-if="shopInfo.isHeadShop == 1 && shopInfo.shopType != 'only'">
<el-button type="primary" @click="addCashBackItem">添加阶梯</el-button>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary">保存</el-button>
<el-button>取消</el-button>
<el-form-item style="margin-top: 50px;" v-if="shopInfo.isHeadShop == 1 && shopInfo.shopType != 'only'">
<el-button type="primary" @click="submitHandle">保存</el-button>
<el-button @click="router.back()">取消</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="返现明细" :name="2">
<div>312321</div>
<record />
</el-tab-pane>
</el-tabs>
</div>
@@ -78,12 +80,17 @@
<script setup>
import HeaderCard from "../components/headerCard.vue";
import record from "./record.vue";
import selectBranchs from "../components/selectBranchs.vue";
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from "vue-router";
import { consumeCashback, consumeCashbackPost } from '@/api/coupon/index.js'
import { filterNumberInput } from '@/utils'
const selectBranchsRef = ref(null)
const router = useRouter();
const tabsValue = ref(1)
const formRef = ref(null)
const shopInfo = ref(JSON.parse(localStorage.getItem('userInfo')))
const form = ref({
useType: 'all', // all 全部可用 part部分门店可用
shopIdList: [], // 门店列表
@@ -94,23 +101,88 @@ const form = ref({
})
const rules = ref({
shopIdList: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.shopIdList.length <= 0) {
callback(new Error('请选择门店'))
} else {
callback()
}
},
trigger: 'change'
}
],
stepValidate: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.cashbackStepList.length <= 0) {
callback(new Error('请添加阶梯'))
return
}
let flag = true;
form.value.cashbackStepList.map(item => {
if (!item.amount || !item.cashbackAmount || item.cashbackAmount > item.amount) {
flag = false
}
})
if (!flag) {
callback(new Error('输入有误,请检查返现金额是不是大于返现门槛'))
} else {
callback()
}
},
trigger: 'change'
}
]
})
// 已选泽的店铺
function successShop(e) {
console.log('successShop===', e);
form.value.shops = e
}
// 添加返现阶梯
const callbackList = ref([])
function addCashBackItem() {
callbackList.value.push({
form.value.cashbackStepList.push({
amount: '',
cashbackAmount: ''
})
}
// 提交保存
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
await consumeCashbackPost(form.value);
ElNotification({
title: "注意",
message: "保存成功",
type: "success",
});
}
} catch (err) {
console.log(err);
}
});
}
// 配置信息获取
async function consumeCashbackAjax() {
try {
const res = await consumeCashback()
if (res.cashbackStepList == null) {
res.cashbackStepList = []
}
form.value = res
} catch (error) {
console.log(error);
}
}
onMounted(() => {
consumeCashbackAjax()
})
</script>
<style scoped lang="scss">
@@ -126,9 +198,12 @@ function addCashBackItem() {
.row {
.item {
margin-bottom: 24px;
position: relative;
&:not(:first-child) {
margin-top: 16px;
}
.row_title {
position: absolute;
top: 0;

View File

@@ -0,0 +1,183 @@
<template>
<div>
<el-form :model="queryForm" inline>
<el-form-item>
<selectBranchs :multiple="false" v-model="queryForm.shopId" />
</el-form-item>
<el-form-item>
<el-date-picker style="width: 300px" v-model="times" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
@change="selectTimeChange"></el-date-picker>
</el-form-item>
<el-form-item label="" prop="">
<el-input placeholder="请输入关键词" v-model="queryForm.key">
<template #prepend>名称</template>
</el-input>
</el-form-item>
<el-form-item label="" prop="">
<el-button type="primary" @click="searchHandle">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="data_show">
<div class="data_list">
<div class="item">
<div class="title">
返现总金额
</div>
<div class="num">
{{ tableData.totalAmount }}
</div>
</div>
</div>
</div>
<div class="table" style="width: 100%;">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading" height="500px">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="关联订单号" prop="orderNo"></el-table-column>
<el-table-column label="消费门店" prop="shopName"></el-table-column>
<el-table-column label="用户信息" prop="nickName">
<template #default="scope">
<el-link type="primary" @click="router.push({
path: '/user/index',
query: {
phone: scope.row.phone
}
})">
<div class="column">
<span>{{ scope.row.nickName }}</span>
<span>{{ scope.row.phone }}</span>
</div>
</el-link>
</template>
</el-table-column>
<el-table-column label="支付金额(元)" prop="amount"></el-table-column>
<el-table-column label="返现金额(元)" prop="cashbackAmount"></el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
</el-table>
</div>
<div class="row" style="margin-top: 14px;">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[100, 200, 300, 400]" background layout="total, sizes, prev, pager, next, jumper"
:page-count="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
</template>
<script setup>
import dayjs from 'dayjs'
import { ref, onMounted } from 'vue'
import selectBranchs from '../components/selectBranchs.vue'
import { consumeCashbackRecord } from '@/api/coupon/index.js'
import { useRouter } from "vue-router";
const router = useRouter();
const times = ref([])
const queryForm = ref({
shopId: '',
key: '',
startTime: '',
endTime: ''
})
const tableData = reactive({
totalAmount: 0,
loading: false,
page: 1,
size: 10,
list: []
})
// 搜索
function searchHandle() {
tableData.page = 1;
consumeCashbackRecordAjax()
}
// 重置搜索
function resetSearch() {
queryForm.value.shopId = ''
queryForm.value.key = ''
queryForm.value.startTime = ''
queryForm.value.endTime = ''
times.value = []
searchHandle()
}
// 选择日期
function selectTimeChange(e) {
console.log(e);
queryForm.value.startTime = dayjs(e[0]).format('YYYY-MM-DD 00:00:00')
queryForm.value.endTime = dayjs(e[1]).format('YYYY-MM-DD 23:59:59')
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
consumeCashbackRecordAjax();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
consumeCashbackRecordAjax();
}
// 获取消费返现记录
async function consumeCashbackRecordAjax(params) {
try {
tableData.loading = true
const res = await consumeCashbackRecord({
...queryForm.value,
page: 1,
size: 10,
total: 0
})
tableData.totalAmount = res.totalAmount
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
tableData.loading = false
}
onMounted(() => {
consumeCashbackRecordAjax()
})
</script>
<style scoped lang="scss">
.data_show {
padding: 0 0 14px;
.data_list {
display: flex;
.item {
width: 200px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 14px;
.title {
color: #666;
}
.num {
font-size: 24px;
color: #333;
font-weight: bold;
}
}
}
}
.column {
display: flex;
flex-direction: column;
line-height: 16px;
}
</style>

View File

@@ -1,49 +1,22 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="titleOptions.title"
width="80%"
top="4vh"
@closed="closedReset"
>
<el-dialog v-model="dialogVisible" :title="titleOptions.title" width="80%" top="4vh" @closed="closedReset">
<div class="scroll" ref="scrollRef">
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="160px"
class="dialog-form"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="160px" class="dialog-form">
<el-form-item :label="titleOptions.name" prop="title">
<el-input
v-model="form.title"
:maxlength="20"
placeholder="请输入优惠券名称"
style="width: 300px"
/>
<el-input v-model="form.title" :maxlength="20" placeholder="请输入优惠券名称" style="width: 300px" />
</el-form-item>
<div v-if="form.couponType == 1">
<el-form-item label="使用门槛" prop="fullAmount">
<div class="center">
<el-input
v-model="form.fullAmount"
placeholder="请输入使用门槛"
style="width: 240px"
input-style="text-align: center;"
:maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))"
>
<el-input v-model="form.fullAmount" placeholder="请输入使用门槛" style="width: 240px"
input-style="text-align: center;" :maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))">
<template #prepend></template>
<template #append></template>
</el-input>
<el-input
v-model="form.discountAmount"
placeholder="请输入满减金额"
style="width: 240px"
input-style="text-align: center;"
:maxlength="8"
@input="(e) => (form.discountAmount = filterNumberInput(e))"
>
<el-input v-model="form.discountAmount" placeholder="请输入满减金额" style="width: 240px"
input-style="text-align: center;" :maxlength="8"
@input="(e) => (form.discountAmount = filterNumberInput(e))">
<template #prepend></template>
<template #append></template>
</el-input>
@@ -53,14 +26,9 @@
<div v-if="form.couponType == 2">
<el-form-item label="使用门槛" prop="fullAmount2">
<div class="center">
<el-input
v-model="form.fullAmount"
placeholder="请输入使用门槛"
style="width: 200px"
input-style="text-align: center;"
:maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))"
>
<el-input v-model="form.fullAmount" placeholder="请输入使用门槛" style="width: 200px"
input-style="text-align: center;" :maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))">
<template #prepend></template>
<template #append>可用</template>
</el-input>
@@ -73,17 +41,9 @@
</el-radio-group>
</el-form-item>
<el-form-item prop="goodsType" v-if="goodsType == 2">
<el-cascader
v-model="goodsTypeCascaderValue"
:options="goodsList"
:props="cascaderProps"
:show-all-levels="false"
:max-collapse-tags="3"
collapse-tags
clearable
style="width: 300px"
@change="selectFoodsConfirm"
></el-cascader>
<el-cascader v-model="goodsTypeCascaderValue" :options="goodsList" :props="cascaderProps"
:show-all-levels="false" :max-collapse-tags="3" collapse-tags clearable style="width: 300px"
@change="selectFoodsConfirm"></el-cascader>
</el-form-item>
<el-form-item label="使用规则">
<el-radio-group v-model="form.useRule">
@@ -92,33 +52,20 @@
</el-radio-group>
</el-form-item>
<el-form-item label="可抵扣商品件数" prop="discountNum">
<el-input
v-model="form.discountNum"
placeholder="请输入可抵扣商品件数"
style="width: 200px"
@input="discountNumInput"
/>
<el-input v-model="form.discountNum" placeholder="请输入可抵扣商品件数" style="width: 200px"
@input="discountNumInput" />
</el-form-item>
</div>
<div v-if="form.couponType == 3">
<el-form-item label="折扣" prop="discountRate">
<el-input
v-model="form.discountRate"
placeholder="输入折扣(%"
style="width: 200px"
@input="discountRateInput"
/>
<el-input v-model="form.discountRate" placeholder="输入折扣(%" style="width: 200px"
@input="discountRateInput" />
</el-form-item>
<el-form-item label="使用门槛" prop="fullAmount2">
<div class="center">
<el-input
v-model="form.fullAmount"
placeholder="请输入使用门槛"
style="width: 300px"
input-style="text-align: center;"
:maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))"
>
<el-input v-model="form.fullAmount" placeholder="请输入使用门槛" style="width: 300px"
input-style="text-align: center;" :maxlength="8"
@input="(e) => (form.fullAmount = filterNumberInput(e))">
<template #prepend></template>
<template #append>可用</template>
</el-input>
@@ -126,13 +73,8 @@
</el-form-item>
<el-form-item label="可抵扣最大金额" prop="maxDiscountAmount">
<div class="center">
<el-input
v-model="form.maxDiscountAmount"
placeholder="请输入金额"
style="width: 200px"
:maxlength="8"
@input="(e) => (form.maxDiscountAmount = filterNumberInput(e))"
>
<el-input v-model="form.maxDiscountAmount" placeholder="请输入金额" style="width: 200px" :maxlength="8"
@input="(e) => (form.maxDiscountAmount = filterNumberInput(e))">
<template #append>可用</template>
</el-input>
</div>
@@ -146,17 +88,9 @@
</el-radio-group>
</el-form-item>
<el-form-item prop="goodsType" v-if="goodsType == 2">
<el-cascader
v-model="goodsTypeCascaderValue"
:options="goodsList"
:props="cascaderProps"
:show-all-levels="false"
:max-collapse-tags="3"
collapse-tags
clearable
style="width: 300px"
@change="selectFoodsConfirm"
></el-cascader>
<el-cascader v-model="goodsTypeCascaderValue" :options="goodsList" :props="cascaderProps"
:show-all-levels="false" :max-collapse-tags="3" collapse-tags clearable style="width: 300px"
@change="selectFoodsConfirm"></el-cascader>
</el-form-item>
<el-form-item label="使用规则">
<el-radio-group v-model="form.useRule">
@@ -167,11 +101,7 @@
</div>
<div v-if="form.couponType == 6"></div>
<div class="title">指定设置</div>
<el-form-item
label="选择门店"
prop="useShopType"
v-if="shopInfo.isHeadShop && shopInfo.shopType != 'only'"
>
<el-form-item label="选择门店" prop="useShopType" v-if="shopInfo.isHeadShop && shopInfo.shopType != 'only'">
<el-radio-group v-model="form.useShopType">
<el-radio label="仅本店可用" value="only"></el-radio>
<el-radio label="全部门店" value="all"></el-radio>
@@ -179,20 +109,8 @@
</el-radio-group>
</el-form-item>
<el-form-item label="选择门店" v-if="form.useShopType == 'custom'">
<el-select
v-model="shops"
multiple
clearable
placeholder="请选择门店"
@change="shopsChange"
style="width: 300px"
>
<el-option
:label="item.shopName"
:value="item.id"
v-for="item in branchList"
:key="item.id"
></el-option>
<el-select v-model="shops" multiple clearable placeholder="请选择门店" @change="shopsChange" style="width: 300px">
<el-option :label="item.shopName" :value="item.id" v-for="item in branchList" :key="item.id"></el-option>
</el-select>
</el-form-item>
<div v-if="form.couponType != 2 && form.couponType != 4 && form.couponType != 6">
@@ -203,25 +121,17 @@
</el-radio-group>
</el-form-item>
<el-form-item v-if="goodsType == 2">
<el-cascader
v-model="goodsTypeCascaderValue"
:options="goodsList"
:props="cascaderProps"
:show-all-levels="false"
:max-collapse-tags="3"
collapse-tags
clearable
style="width: 300px"
@change="selectFoodsConfirm"
></el-cascader>
<el-cascader v-model="goodsTypeCascaderValue" :options="goodsList" :props="cascaderProps"
:show-all-levels="false" :max-collapse-tags="3" collapse-tags clearable style="width: 300px"
@change="selectFoodsConfirm"></el-cascader>
</el-form-item>
</div>
<el-form-item label="可使用类型" prop="useType">
<el-checkbox-group v-model="form.useType">
<el-checkbox value="dine" label="堂食" />
<el-checkbox value="pickup" label="自取" />
<el-checkbox value="deliv" label="配送" />
<el-checkbox value="express" label="快递" />
<el-checkbox value="dine-in" label="堂食" />
<el-checkbox value="take-out" label="外带" />
<el-checkbox value="take-away" label="外卖" />
<el-checkbox value="post" label="配送" />
</el-checkbox-group>
</el-form-item>
<div class="title">时效设置</div>
@@ -232,52 +142,24 @@
</el-radio-group>
</el-form-item>
<el-form-item label="有效期" prop="validDays">
<el-input
v-model="form.validDays"
placeholder="请输入有效期"
style="width: 200px"
:maxlength="5"
v-if="form.validType == 'fixed'"
input-style="text-align: center;"
@input="validDaysInput"
>
<el-input v-model="form.validDays" placeholder="请输入有效期" style="width: 200px" :maxlength="5"
v-if="form.validType == 'fixed'" input-style="text-align: center;" @input="validDaysInput">
<template #append></template>
</el-input>
<div style="width: 200px">
<el-date-picker
v-model="validityScope"
type="daterange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
v-if="form.validType == 'custom'"
@change="validityScopeChange"
/>
<el-date-picker v-model="validityScope" type="daterange" range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" v-if="form.validType == 'custom'" @change="validityScopeChange" />
</div>
</el-form-item>
<el-form-item
label="隔天生效"
prop="daysToTakeEffect"
v-if="form.validType == 'fixed'"
>
<el-form-item label="隔天生效" prop="daysToTakeEffect" v-if="form.validType == 'fixed'">
<div class="center">
<el-input
v-model="form.daysToTakeEffect"
placeholder="请输入隔天生效日期"
style="width: 300px"
input-style="text-align: center;"
:maxlength="5"
@input="daysToTakeEffectInput"
>
<el-input v-model="form.daysToTakeEffect" placeholder="请输入隔天生效日期" style="width: 300px"
input-style="text-align: center;" :maxlength="5" @input="daysToTakeEffectInput">
<template #prepend></template>
<template #append>天生效</template>
</el-input>
<el-tooltip
class="box-item"
effect="dark"
:content="`领取后${form.daysToTakeEffect}天后的0点0分生效`"
placement="top-start"
>
<el-tooltip class="box-item" effect="dark" :content="`领取后${form.daysToTakeEffect}天后的0点0分生效`"
placement="top-start">
<el-icon size="18">
<QuestionFilled />
</el-icon>
@@ -303,14 +185,8 @@
</el-form-item>
<el-form-item v-if="form.useTimeType == 'custom'" prop="useTimeType">
<div style="width: 200px">
<el-time-picker
v-model="useTimeScope"
is-range
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
@change="useTimeScopeChange"
/>
<el-time-picker v-model="useTimeScope" is-range range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" @change="useTimeScopeChange" />
</div>
</el-form-item>
<div class="title">发放设置</div>
@@ -334,14 +210,8 @@
<span>关闭则为无限制</span>
</div>
<div v-if="infiniteGiveNum" style="margin-top: 10px">
<el-input
v-model="form.giveNum"
placeholder="请输入总发放数量"
style="width: 200px"
input-style="text-align: center;"
:maxlength="5"
@input="giveNumInput"
>
<el-input v-model="form.giveNum" placeholder="请输入总发放数量" style="width: 200px"
input-style="text-align: center;" :maxlength="5" @input="giveNumInput">
<template #append></template>
</el-input>
</div>
@@ -356,24 +226,15 @@
</el-radio-group>
</el-form-item>
<el-form-item label="每人限领量" prop="getLimit">
<el-input
v-model="form.getLimit"
placeholder="请输入每人限领量"
style="width: 200px"
input-style="text-align: center;"
:maxlength="5"
@input="getLimitInput"
>
<el-input v-model="form.getLimit" placeholder="请输入每人限领量" style="width: 200px"
input-style="text-align: center;" :maxlength="5" @input="getLimitInput">
<template #append></template>
</el-input>
</el-form-item>
<el-form-item label="每人每日使用限量">
<div class="column">
<div class="center">
<el-radio-group
v-model="infiniteUseLimit"
@change="infiniteUseLimitChange"
>
<el-radio-group v-model="infiniteUseLimit" @change="infiniteUseLimitChange">
<el-radio label="无限制" :value="true"></el-radio>
<el-radio label="每日限用" :value="false"></el-radio>
</el-radio-group>
@@ -381,14 +242,8 @@
</div>
</el-form-item>
<el-form-item label="限用数量" v-if="!infiniteUseLimit" prop="useLimit">
<el-input
v-model="form.useLimit"
placeholder="需小于或等于每人限领量"
style="width: 255px"
input-style="text-align: center;"
:maxlength="5"
@input="useLimitInput"
>
<el-input v-model="form.useLimit" placeholder="需小于或等于每人限领量" style="width: 255px"
input-style="text-align: center;" :maxlength="5" @input="useLimitInput">
<template #append>/每人1天</template>
</el-input>
</el-form-item>
@@ -396,11 +251,7 @@
<el-form-item label="与限时折扣同享">
<div class="column">
<div class="center">
<el-switch
v-model="form.discountShare"
:active-value="1"
:inactive-value="0"
/>
<el-switch v-model="form.discountShare" :active-value="1" :inactive-value="0" />
<span>开启后计算门槛时将会计入已折扣的商品</span>
</div>
</div>
@@ -408,11 +259,7 @@
<el-form-item label="与会员价/会员折扣同享">
<div class="column">
<div class="center">
<el-switch
v-model="form.vipPriceShare"
:active-value="1"
:inactive-value="0"
/>
<el-switch v-model="form.vipPriceShare" :active-value="1" :inactive-value="0" />
<span>开启后计算门槛时将会计入已享受会员价/会员折扣的商品</span>
</div>
</div>
@@ -420,11 +267,7 @@
<el-form-item label="与其他优惠券同享" v-if="form.couponType == 2">
<div class="column">
<div class="center">
<el-switch
v-model="form.otherCouponShare"
:active-value="1"
:inactive-value="0"
/>
<el-switch v-model="form.otherCouponShare" :active-value="1" :inactive-value="0" />
<span>开启后可与其他优惠券同时使用</span>
</div>
</div>
@@ -433,13 +276,8 @@
<el-form-item label-width="0">
<div class="column">
<div class="item">
<el-input
type="textarea"
:rows="4"
maxlength="250"
v-model="form.ruleDetails"
placeholder="填写内容"
></el-input>
<el-input type="textarea" :rows="4" maxlength="250" v-model="form.ruleDetails"
placeholder="填写内容"></el-input>
</div>
<div class="item textarea-num">
{{ form.ruleDetails.length }}/250字内单文本
@@ -449,10 +287,7 @@
</el-form>
</div>
<template #footer>
<div
class="dialog-footer"
v-if="(shopInfo.isHeadShop && shopInfo.shopType != 'only') || !form.syncId"
>
<div class="dialog-footer" v-if="(shopInfo.isHeadShop && shopInfo.shopType != 'only') || !form.syncId">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="submitHandle"> </el-button>
</div>
@@ -594,7 +429,7 @@ const form = ref({
useShops: "", // id字符串拼接
goodsType: 1, // 1=全部商品参与计算门槛 2=部分商品参与计算门槛
foods: "", // 指定门槛商品 id拼接
useType: ["dine"], // 可使用类型dine堂食/pickup自取/deliv配送/express快递
useType: ["dine-in"], // 堂食 dine-in 外带 take-out 外卖 take-away 配送 post
validType: "fixed", // 有效期类型fixed固定时间custom自定义时间
validDays: "", // 有效期(天)
validStartTime: "", // 有效期开始时间
@@ -1040,22 +875,27 @@ onMounted(() => {
flex: 1;
display: flex;
flex-direction: column;
.item {
flex: 1;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.scroll {
height: 76vh;
padding-bottom: 60px;
overflow-y: auto;
}
.dialog-footer {
position: relative;
&::after {
content: "";
width: 100%;
@@ -1067,6 +907,7 @@ onMounted(() => {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
}
}
.textarea-num {
color: #999;
display: flex;

View File

@@ -1,28 +1,41 @@
<!-- 选择门店组件 -->
<template>
<el-select v-model="shops" multiple clearable placeholder="请选择门店" @change="shopsChange" style="width: 300px">
<el-select :model-value="modelValue" :multiple="multiple" clearable placeholder="请选择门店"
@change="$emit('update:modelValue', $event)" style="width: 300px">
<el-option :label="item.shopName" :value="item.id" v-for="item in branchList" :key="item.id"></el-option>
</el-select>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getBranchPage } from "@/api/coupon/index.js";
import { getBranchPage, getBranchList } from "@/api/coupon/index.js";
const shops = ref([])
const emit = defineEmits(['success'])
// 选择分店
function shopsChange(e) {
emit('success', e.join(","))
}
const props = defineProps({
multiple: {
type: Boolean,
default: true
},
modelValue: {
type: [Array, String],
default: []
}
})
// 获取分店列表
const branchList = ref([]);
async function getBranchPageAjax() {
try {
const res = await getBranchPage();
branchList.value = res.records;
let res = null
if (props.multiple) {
res = await getBranchPage();
branchList.value = res.records;
} else {
res = await getBranchList()
res.map(item => {
item.id = item.shopId
})
branchList.value = res;
}
} catch (err) {
console.log(err);
}

View File

@@ -0,0 +1,502 @@
<template>
<el-dialog v-model="dialogVisible" :title="titleOptions.title" width="80%" top="4vh" @closed="closedReset">
<div class="scroll" ref="scrollRef">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="160px" class="dialog-form">
<el-form-item label="满减优惠" prop="thresholds">
<div class="thresholds_list">
<div class="item" v-for="(item, index) in form.thresholds" :key="index">
<div class="ipt">
<el-input v-model.trim="item.fullAmount" input-style="text-align: center;">
<template #prepend></template>
<template #append></template>
</el-input>
</div>
<div class="ipt">
<el-input v-model.trim="item.discountAmount" input-style="text-align: center;">
<template #prepend></template>
<template #append></template>
</el-input>
</div>
<el-button link type="danger" v-if="index > 0" @click="form.thresholds.splice(index, 1)">删除</el-button>
</div>
<div class="item">
<el-button type="primary" icon="CirclePlus" @click="addThresholdsObjHandle">添加</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="活动日期" prop="validDays">
<div style="width: 300px;">
<el-date-picker v-model="validityScope" type="daterange" range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" @change="validityScopeChange" />
</div>
</el-form-item>
<el-form-item label="可用周期" prop="useDays">
<el-checkbox-group v-model="form.useDays">
<el-checkbox value="周一" label="周一" />
<el-checkbox value="周二" label="周二" />
<el-checkbox value="周三" label="周三" />
<el-checkbox value="周四" label="周四" />
<el-checkbox value="周五" label="周五" />
<el-checkbox value="周六" label="周六" />
<el-checkbox value="周七" label="周日" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="指定时间段">
<el-radio-group v-model="form.useTimeType">
<el-radio label="全时段可用" value="all"></el-radio>
<el-radio label="指定时间段可用" value="custom"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.useTimeType == 'custom'" prop="useTimeType">
<div style="width: 200px">
<el-time-picker v-model="useTimeScope" is-range range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" @change="useTimeScopeChange" />
</div>
</el-form-item>
<el-form-item label="可使用类型" prop="useType">
<el-checkbox-group v-model="form.useType">
<el-checkbox value="dine-in" label="堂食" />
<el-checkbox value="take-out" label="外带" />
<el-checkbox value="take-away" label="外卖" />
<el-checkbox value="post" label="配送" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="排序">
<div class="column">
<div>
<el-input v-model="form.sort" placeholder="默认值0" @input="e => form.sort = filterNumberInput(e, true)"
style="width: 200px;"></el-input>
</div>
<div style="color: #FF2F2F;font-size: 12px;">数值越大排序越靠前重复时段下按照排序值最高的活动减免</div>
</div>
</el-form-item>
<el-form-item label="与优惠券同享">
<div class="column">
<div class="center">
<el-switch disabled v-model="form.couponShare" :active-value="1" :inactive-value="0" />
<span>不能和优惠券同时使用</span>
</div>
</div>
</el-form-item>
<el-form-item label="与限时折扣同享">
<div class="column">
<div class="center">
<el-switch v-model="form.discountShare" :active-value="1" :inactive-value="0" />
<span>开启后计算门槛时将会按照折扣价计算</span>
</div>
</div>
</el-form-item>
<el-form-item label="与会员价/会员折扣同享">
<div class="column">
<div class="center">
<el-switch v-model="form.vipPriceShare" :active-value="1" :inactive-value="0" />
<span>开启后计算门槛时将会按照会员价/会员折扣价计算</span>
</div>
</div>
</el-form-item>
<el-form-item label="与积分抵扣同享">
<div class="column">
<div class="center">
<el-switch v-model="form.pointsShare" :active-value="1" :inactive-value="0" />
<span>开启后可和积分抵扣同时使用</span>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer" v-if="(shopInfo.isHeadShop && shopInfo.shopType != 'only') || !form.syncId">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="submitHandle"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import _ from "lodash";
import { dayjs } from "element-plus";
import { ref, reactive, onMounted } from "vue";
import { filterNumberInput } from "@/utils";
import { discountActivity } from "@/api/coupon/index.js";
const shopInfo = ref("");
const dialogVisible = ref(false);
const titleOptions = reactive({
title: "添加优惠券"
});
// 有效期时间
const validityScope = ref([]);
function validityScopeChange(e) {
console.log("validityScopeChange===", e);
if (e && e.length) {
form.value.validStartTime = dayjs(e[0]).format("YYYY-MM-DD");
form.value.validEndTime = dayjs(e[1]).format("YYYY-MM-DD");
} else {
form.value.validStartTime = "";
form.value.validEndTime = "";
}
}
// 可用时间
const useTimeScope = ref([]);
function useTimeScopeChange(e) {
console.log("useTimeScopeChange===", e);
if (e && e.length) {
form.value.useStartTime = dayjs(e[0]).format("HH:mm");
form.value.useEndTime = dayjs(e[1]).format("HH:mm");
} else {
form.value.useStartTime = "";
form.value.useEndTime = "";
}
}
const formRef = ref(null);
const addThresholdsObj = ref({
fullAmount: '', // 满多少金额
discountAmount: '' // 减多少金额
})
const form = ref({
id: '',
shopId: '',
validStartTime: '',
validEndTime: '',
useDays: ['周一', '周二', '周三', '周四', '周五', '周六', '周七'],
useTimeType: 'all',
useStartTime: '',
useEndTime: '',
useType: ['dine-in', 'take-out'],
sort: '',
couponShare: '',
discountShare: 1,
vipPriceShare: 1,
pointsShare: 1,
status: 1, // 状态1未开始2进行中3已结束
thresholds: [
{ ...addThresholdsObj.value }
]
});
// 添加
function addThresholdsObjHandle() {
form.value.thresholds.push({ ...addThresholdsObj.value })
}
// 初始化
const resetForm = ref(null);
function reset() {
validityScope.value = [];
useTimeScope.value = [];
form.value = { ...resetForm.value };
}
// 自定义校验固定有效期范围内可用
const useTimeTypeValidate = (rule, value, callback) => {
if (!form.value.useStartTime) {
callback(new Error("请选择指定可用时间段"));
} else {
callback();
}
};
// 验证满减规则数组的函数
function validateDiscountRules(rules) {
console.log(rules);
// 检查是否为空数组
if (!rules || rules.length === 0) {
return { valid: false, message: '请添加满减规则' };
}
// 遍历每个规则进行验证
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
const index = i + 1; // 显示给用户的序号从1开始
// 检查fullAmount是否填写
if (!rule.fullAmount) {
return {
valid: false,
message: `${index}条规则:请填写满减金额`
};
}
// 检查discountAmount是否填写
if (!rule.discountAmount) {
return {
valid: false,
message: `${index}条规则:请填写减免金额`
};
}
// 转换为数字进行比较
const full = Number(rule.fullAmount);
const discount = Number(rule.discountAmount);
// 检查是否为有效数字
if (isNaN(full) || full <= 0) {
return {
valid: false,
message: `${index}条规则:满减金额必须是有效的正数`
};
}
if (isNaN(discount) || discount <= 0) {
return {
valid: false,
message: `${index}条规则:减免金额必须是有效的正数`
};
}
// 检查减免金额是否大于满减金额
if (discount > full) {
return {
valid: false,
message: `${index}条规则:减免金额不能大于满减金额`
};
}
}
// 所有规则都通过验证
return { valid: true };
}
const formRules = reactive({
thresholds: [{
required: true,
validator: (rule, value, callback) => {
let result = validateDiscountRules(form.value.thresholds)
if (!result.valid) {
callback(result.message)
} else {
callback()
}
},
trigger: 'change'
}],
validDays: [
{
required: true,
validator: (rule, value, callback) => {
if (validityScope.value.length == 0) {
callback(new Error("请选择日期范围"));
} else {
callback();
}
},
trigger: "change",
},
],
useDays: [{ required: true, message: "请选择可用周期", trigger: "change" }],
useTimeType: [
{
validator: useTimeTypeValidate,
trigger: "change",
},
],
});
// 开始提交
const emits = defineEmits(["success"]);
const loading = ref(false);
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true;
let data = { ...form.value }
data.useDays = data.useDays.join(',')
data.useType = data.useType.join(',')
await discountActivity(data, data.id ? 'put' : 'post');
emits("success");
dialogVisible.value = false;
}
} catch (err) {
console.log(err);
}
loading.value = false;
});
}
// 从本地获取商户信息
function getLocalShopInfo() {
shopInfo.value = JSON.parse(localStorage.getItem("userInfo"));
}
// 显示弹窗
const scrollRef = ref(null);
function show(obj = null) {
nextTick(() => {
if (scrollRef.value) {
setTimeout(() => {
scrollRef.value.scrollTop = 0;
}, 50);
}
});
if (obj && obj.id) {
titleOptions.title = `编辑满减活动`;
form.value = { ...obj };
form.value.useDays = form.value.useDays.split(',');
form.value.useType = form.value.useType.split(',');
if (form.value.useTimeType == "custom") {
useTimeScope.value = [
convertTimeToDate(`${form.value.useStartTime}:00`),
convertTimeToDate(`${form.value.useEndTime}:00`),
];
console.log(useTimeScope.value);
}
} else {
reset();
titleOptions.title = `添加满减活动`;
}
dialogVisible.value = true;
}
// 关闭后重置表单验证
function closedReset() {
formRef.value.resetFields();
}
/**
* 将时分秒字符串转换为完整日期格式
* @param {string} timeStr - 时分秒字符串,格式需为 HH:mm:ss如 '00:53:00'
* @param {Object} options - 可选配置项
* @param {string} [options.customDate] - 自定义日期,格式为 YYYY-MM-DD默认使用当前日期
* @param {string} [options.format='YYYY-MM-DD HH:mm:ss'] - 输出的日期格式
* @returns {string|null} 转换后的日期字符串,失败时返回 null
*/
function convertTimeToDate(timeStr, options = {}) {
// 解构配置项,设置默认值
const { customDate, format = "YYYY-MM-DD HH:mm:ss" } = options;
// 1. 校验时分秒格式(必须为 HH:mm:ss允许数字1-2位
const timeRegex = /^\d{1,2}:\d{1,2}:\d{1,2}$/;
if (!timeRegex.test(timeStr)) {
console.error("时分秒格式错误,请使用 HH:mm:ss 格式(如 00:53:00");
return null;
}
// 2. 确定日期部分(自定义日期或当前日期)
let datePart;
if (customDate) {
// 校验自定义日期格式
if (!dayjs(customDate, "YYYY-MM-DD", true).isValid()) {
console.error("自定义日期格式错误,请使用 YYYY-MM-DD 格式(如 2024-05-20");
return null;
}
datePart = customDate;
} else {
// 使用当前日期格式YYYY-MM-DD
datePart = dayjs().format("YYYY-MM-DD");
}
// 3. 组合日期和时分秒,生成完整日期对象
const fullDateTime = `${datePart} ${timeStr}`;
const dateObj = dayjs(fullDateTime);
// 4. 校验完整日期是否有效(如避免 2024-02-30 这种无效日期)
if (!dateObj.isValid()) {
console.error("生成的日期无效,请检查日期或时分秒是否合理");
return null;
}
// 5. 按指定格式返回日期字符串
return dateObj.format(format);
}
// input过滤
const time = 500;
const discountNumInput = _.debounce(function (value) {
form.value.discountNum = filterNumberInput(value, true);
if (form.value.discountNum == "") {
form.value.discountNum = 1;
}
}, time);
defineExpose({
show,
});
onMounted(() => {
resetForm.value = { ...form.value };
getLocalShopInfo();
});
</script>
<style scoped lang="scss">
.title {
color: #000;
padding: 14px;
background-color: #f8f8f8;
margin-bottom: 14px;
font-size: 16px;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
.item {
flex: 1;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.scroll {
height: 76vh;
padding-bottom: 60px;
overflow-y: auto;
}
.dialog-footer {
position: relative;
&::after {
content: "";
width: 100%;
height: 60px;
position: absolute;
left: 0;
bottom: calc(100% + var(--el-dialog-padding-primary));
z-index: 9;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
}
}
.textarea-num {
color: #999;
display: flex;
justify-content: flex-end;
}
.thresholds_list {
.item {
display: flex;
gap: 14px;
&:not(:first-child) {
margin-top: 14px;
}
.ipt {
width: 200px;
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="gyq_container">
<div class="gyq_content">
<headerCard icon="mjhd" name="满减活动" intro="达到指定支付金额享受减价" showSwitch v-model:isOpen="form.isEnableDiscount" />
<div class="row">
<el-form :model="form" inline>
<el-form-item>
<el-button type="primary" icon="CirclePlus" @click="addDialogRef.show()">添加</el-button>
</el-form-item>
<el-form-item>
<el-date-picker style="width: 300px" v-model="times" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
@change="selectTimeChange"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="resetSearch">搜索</el-button>
<el-button icon="Refresh" @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="row">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column label="ID" prop="id" width="80"></el-table-column>
<el-table-column label="活动日期" prop="orderNo">
<template #default="scope">
<div v-if="scope.row.useTimeType == 'all'">全时段</div>
<div v-else>
{{ scope.row.validStartTime }} - {{ scope.row.validEndTime }}
</div>
</template>
</el-table-column>
<el-table-column label="生效周期" prop="useDays"></el-table-column>
<el-table-column label="生效时段" prop="validStartTime">
<template #default="scope">
<div v-if="scope.row.useTimeType == 'all'">全时段</div>
<div v-else>
{{ scope.row.useStartTime }} - {{ scope.row.useEndTime }}
</div>
</template>
</el-table-column>
<el-table-column label="活动内容" prop="thresholds">
<template #default="scope">
<div class="column">
<div class="item" v-for="(item, index) in scope.row.thresholds" :key="index">
{{ item.fullAmount }}{{ item.discountAmount }}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort"></el-table-column>
<el-table-column label="状态" prop="status">
<template #default="scope">
<el-tag disable-transitions type="primary" v-if="scope.row.status == 1">未开始</el-tag>
<el-tag disable-transitions type="success" v-if="scope.row.status == 2">进行中</el-tag>
<el-tag disable-transitions type="info" v-if="scope.row.status == 3">已结束</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="orderNo" width="120" fixed="right">
<template #default="scope">
<div class="center">
<el-button link type="primary" @click="addDialogRef.show(scope.row)">编辑</el-button>
<el-popconfirm title="确认要删除吗?" @confirm="deleteHandle(scope.row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="row" style="margin-top: 14px;">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[10, 20, 50, 100, 500]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<addDialog ref="addDialogRef" @success="searchHandle" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import addDialog from './components/addDialog.vue'
import { discountActivityPage, discountActivityDelete, shopInfoPut, shopInfoGet } from '@/api/coupon/index'
const addDialogRef = ref(null)
const form = ref({
isEnableDiscount: false
})
watch(() => form.value.isEnableDiscount, (newVal, oldVal) => {
console.log('值改变了', form.value);
if (form.value.id) {
shopInfoPutAjax()
}
})
// 更新店铺信息
async function shopInfoPutAjax() {
try {
await shopInfoPut(form.value)
} catch (error) {
console.log(error);
}
}
const times = ref([])
const queryForm = ref({
startTime: '',
endTime: ''
})
const tableData = reactive({
loading: false,
page: 1,
size: 10,
list: [],
total: 0
})
// 搜索
function searchHandle() {
tableData.page = 1;
getTableData()
}
// 重置搜索
function resetSearch() {
queryForm.value.startTime = ''
queryForm.value.endTime = ''
times.value = []
searchHandle()
}
// 选择日期
function selectTimeChange(e) {
console.log(e);
queryForm.value.startTime = dayjs(e[0]).format('YYYY-MM-DD 00:00:00')
queryForm.value.endTime = dayjs(e[1]).format('YYYY-MM-DD 23:59:59')
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.size = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
// 删除
async function deleteHandle(row) {
try {
tableData.loading = true;
await discountActivityDelete(row.id);
ElNotification({
title: '注意',
message: '已删除',
type: 'success'
})
getTableData();
} catch (err) {
console.log(err);
}
}
// 获取表格数据
async function getTableData() {
try {
tableData.loading = true
const res = await discountActivityPage({
// ...queryForm.value,
page: tableData.page,
size: tableData.size
})
tableData.list = res.records
tableData.total = +res.totalRow
console.log(tableData);
} catch (error) {
console.log(error);
}
tableData.loading = false
}
// 获取店铺信息
async function shopInfoGetAjax() {
try {
const res = await shopInfoGet()
form.value = res
} catch (error) {
console.log(error);
}
}
onMounted(() => {
getTableData()
shopInfoGetAjax()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.item {
flex: 1;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.tips {
color: #666;
}
.footer {
display: flex;
justify-content: center;
}
.textarea-num {
color: #999;
display: flex;
justify-content: flex-end;
}
.row {
padding-top: 14px;
}
</style>

View File

@@ -1,11 +1,7 @@
<template>
<div class="app_main">
<div class="card">
<headerCard
icon="xfzq"
name="满减券"
intro="用户满足指定金额后可使用优惠券立减相应金额设置满100-50券符合要求的订单满100元后立减50元。"
/>
<headerCard icon="xfzq" name="满减券" intro="用户满足指定金额后可使用优惠券立减相应金额设置满100-50券符合要求的订单满100元后立减50元。" />
<div class="tab_wrap">
<div class="row">
<el-button type="primary" @click="CouponDialogRef.show(couponType)">
@@ -42,10 +38,7 @@
<div class="center">
<el-text>{{ scope.row.giftNum }}</el-text>
<el-text>|</el-text>
<el-link
type="primary"
@click="GetDetailDialogRef.show(scope.row)"
>
<el-link type="primary" @click="GetDetailDialogRef.show(scope.row)">
详情
</el-link>
</div>
@@ -67,47 +60,25 @@
</el-table-column>
<el-table-column prop="status" label="启用状态" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
:disabled="!!scope.row.syncId"
@change="statusChange($event, scope.row)"
/>
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"
:disabled="!!scope.row.syncId" @change="statusChange($event, scope.row)" />
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column
prop="actions"
label="操作"
align="center"
width="140"
fixed="right"
>
<el-table-column prop="actions" label="操作" align="center" width="140" fixed="right">
<template #default="scope">
<template v-if="!scope.row.syncId">
<el-button
type="primary"
link
@click="CouponDialogRef.show(couponType, scope.row)"
>
<el-button type="primary" link @click="CouponDialogRef.show(couponType, scope.row)">
编辑
</el-button>
<el-popconfirm
title="确认要删除吗?"
@confirm="deleteHandle(scope.row)"
>
<el-popconfirm title="确认要删除吗?" @confirm="deleteHandle(scope.row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</template>
<template v-else>
<el-button
type="primary"
link
@click="CouponDialogRef.show(couponType, scope.row)"
>
<el-button type="primary" link @click="CouponDialogRef.show(couponType, scope.row)">
详情
</el-button>
<el-button type="danger" disabled link>删除</el-button>
@@ -117,16 +88,9 @@
</el-table>
</div>
<div class="row">
<el-pagination
v-model:current-page="tableData.page"
v-model:page-size="tableData.pageSize"
:page-sizes="[100, 200, 300, 400]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.pageSize"
:page-sizes="[100, 200, 300, 400]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
</div>
@@ -241,6 +205,7 @@ onMounted(() => {
}
}
}
.center {
display: flex;
align-items: center;

View File

@@ -0,0 +1,289 @@
<!-- 私域引流 -->
<template>
<div class="gyq_container">
<div class="gyq_content">
<headerCard icon="syyl" name="私域引流" intro="可设置用户下单成功后的群二维码" showSwitch v-model:isOpen="form.isEnable" />
<div class="form_content">
<div class="left">
<div class="preview">
<div class="info">
<div class="top">
<div class="title">{{ form.title || '请输入模块标题' }}</div>
<div class="content">{{ form.content || '请输入模块内容' }}</div>
</div>
<div class="btm">{{ form.note || '请输入模块提示语' }}</div>
</div>
<div class="img_wrap">
<el-image :src="form.qrCode" style="width: 100%;height: 100%;"></el-image>
</div>
</div>
</div>
<div class="form">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px">
<el-form-item label="可使用类型" prop="useType">
<el-checkbox-group v-model="form.useType">
<el-checkbox value="dine-in" label="堂食" />
<el-checkbox value="take-out" label="外带" />
<el-checkbox value="take-away" label="外卖" />
<el-checkbox value="post" label="配送" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="群二维码" prop="qrCode">
<single-image-upload style="width: 120px; height: 120px" v-model="form.qrCode"></single-image-upload>
</el-form-item>
<el-form-item label="模块标题" prop="title">
<el-input placeholder="请输入模块标题" :maxlength="20" v-model.trim="form.title"></el-input>
</el-form-item>
<el-form-item label="模块内容">
<div class="textarea">
<el-input type="textarea" :rows="4" :maxlength="50" placeholder="请输入内容"
v-model.trim="form.content"></el-input>
<span class="num">{{ form.content.length }}/50</span>
</div>
</el-form-item>
<el-form-item label="模块提示语">
<el-input placeholder="请输模块提示语" :maxlength="20" v-model.trim="form.note"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="submitHandle">保存</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElNotification } from 'element-plus'
import { drainageConfigGet, drainageConfigPost } from '@/api/coupon/index'
const defaultNote = ref('长按识别,微信内扫一扫加好友')
const form = ref({
id: '',
useType: ['dine-in'],
qrCode: '',
title: '',
content: '',
note: '长按识别,微信内扫一扫加好友',
isEnable: '',
mainShopId: '',
shopId: ''
})
const rules = reactive({
useType: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.useType.length <= 0) {
callback(new Error('请选择可使用类型'))
} else {
callback()
}
},
triiger: 'change'
}
],
qrCode: [
{
required: true,
message: '请上传群二维码',
triiger: 'change'
}
],
title: [
{
required: true,
message: '请输入模块标题',
triiger: 'change'
}
],
content: [
{
required: true,
message: '请输入内容',
triiger: 'change'
}
],
note: [
{
required: true,
message: '请输模块提示语',
triiger: 'change'
}
],
})
// 开始提交
const formRef = ref(null)
const emits = defineEmits(["success"]);
const loading = ref(false);
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true;
let data = { ...form.value }
await drainageConfigPost(data);
ElNotification({
title: '注意',
message: '保存成功',
type: 'success'
})
}
} catch (err) {
console.log(err);
}
loading.value = false;
});
}
// 获取配置信息
async function drainageConfigGetAjax() {
try {
const res = await drainageConfigGet()
if (res.useType == null) {
res.useType = []
}
form.value = res
} catch (error) {
console.log(error);
}
}
onMounted(() => {
drainageConfigGetAjax()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.item {
flex: 1;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.tips {
color: #666;
}
.footer {
display: flex;
justify-content: center;
}
.textarea-num {
color: #999;
display: flex;
justify-content: flex-end;
}
.row {
padding-top: 14px;
}
.form_content {
padding: 14px 0 0;
display: flex;
.left {
width: 374px;
height: 720px;
background-color: rgba(0, 0, 0, 0.6);
position: relative;
background: url('https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/3/ac537d7f47464ec5a8e7577db1fd4f3c.png') no-repeat center center / 100% 100%;
.preview {
width: 350px;
height: 130px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: 6px;
display: flex;
align-items: center;
padding: 14px;
.info {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5px 0;
.top {
flex: 1;
.title {
font-size: 16px;
color: #333;
font-weight: bold;
}
.content {
font-size: 12px;
color: #666;
}
}
.btm {
font-size: 14px;
color: #999;
}
}
.img_wrap {
width: 100px;
height: 100px;
.img {
width: 100%;
height: 100%;
}
}
}
}
}
.textarea {
width: 300px;
position: relative;
.num {
font-size: 12px;
color: #666;
position: absolute;
right: 10px;
bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,167 @@
<!-- 霸王餐 -->
<template>
<div class="gyq_container">
<div class="gyq_content">
<HeaderCard name="霸王餐" intro="设置充值消费的N倍当前订单立即免单" icon="xffx" showSwitch v-model:isOpen="form.enable">
</HeaderCard>
<div class="row">
<el-tabs v-model="tabsValue">
<el-tab-pane label="基础设置" :name="1">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px">
<el-form-item label="充值设置" prop="rechargeTimes">
<div class="center">
<span>用户消费结账时成功充值</span>
<el-input v-model="form.rechargeTimes" placeholder="请输入内容" style="width: 100px;"
input-style="text-align: center;"></el-input>
<span>倍的金额本单即可享受免单</span>
</div>
</el-form-item>
<el-form-item label="充值门槛" prop="rechargeThreshold">
<div class="center">
<span>订单支付金额需满</span>
<el-input v-model="form.rechargeThreshold" placeholder="请输入内容" style="width: 100px;"
input-style="text-align: center;"></el-input>
<span>元才能使用</span>
</div>
</el-form-item>
<el-form-item label="可用门店">
<el-radio-group v-model="form.useShopType">
<el-radio label="全部门店" value="all"></el-radio>
<el-radio label="指定门店可用" value="part"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择门店" prop="shopIdList" v-if="form.useShopType == 'part'">
<selectBranchs v-model="form.shopIdList" />
</el-form-item>
<!-- 可使用类型 -->
<el-form-item label="可使用类型" prop="useType">
<el-checkbox-group v-model="form.useType">
<el-checkbox label="店内" value="dine-in"></el-checkbox>
<el-checkbox label="自取" value="take-out"></el-checkbox>
<el-checkbox label="快递" value="post"></el-checkbox>
<el-checkbox label="外卖" value="takeaway"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="可与优惠券同享">
<el-switch v-model="form.withCoupon"></el-switch>
</el-form-item>
<el-form-item label="可与积分同享">
<el-switch v-model="form.withPoints"></el-switch>
</el-form-item>
<el-form-item style="margin-top: 50px;" v-if="shopInfo.isHeadShop == 1 && shopInfo.shopType != 'only'">
<el-button type="primary" @click="submitHandle">保存</el-button>
<el-button @click="router.back()">取消</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- <el-tab-pane label="充值记录" :name="2">
<record />
</el-tab-pane> -->
</el-tabs>
</div>
</div>
</div>
</template>
<script setup>
import HeaderCard from "../components/headerCard.vue";
import record from "./record.vue";
import { freeDingGet, freeDingPut } from "@/api/coupon";
import { ref, onMounted } from 'vue'
import { useRouter } from "vue-router";
const shopInfo = ref(JSON.parse(localStorage.getItem('userInfo')))
const router = useRouter();
const tabsValue = ref(1)
const form = ref({
id: '',
enable: false, //是否启用
rechargeTimes: '', //充值倍数
rechargeThreshold: '', //充值门槛
useShopType: 'all', //门店类型 all全部 part部分
shopIdList: [], // 门店列表
useType: ['dine-in'], // 可使用类型 dine-in店内 takeaway外卖 takeout自取 post快递
withCoupon: false, //是否可与优惠券同享
withPoints: true, //是否可与积分同享
})
const rules = ref({
rechargeTimes: [
{ required: true, message: '请输入充值倍数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' }
],
rechargeThreshold: [
{ required: true, message: '请输入充值门槛', trigger: 'blur' },
{ pattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/, message: '请输入正确的金额', trigger: 'blur' }
],
shopIdList: [
{ required: true, message: '请选择门店', trigger: 'change' },
],
useType: [
{ required: true, message: '请选择可使用类型', trigger: 'change' }
]
})
// 提交保存
const formRef = ref(null)
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
const data = { ...form.value }
data.enable = form.value.enable ? true : false
await freeDingPut(form.value);
ElNotification({
title: "注意",
message: "保存成功",
type: "success",
});
}
} catch (err) {
console.log(err);
}
});
}
// 获取当前店铺霸王餐配置信息列表
async function freeDingGetAjax() {
try {
const res = await freeDingGet();
form.value = { ...res }
form.value.enable = res.enable ? 1 : 0
} catch (err) {
console.log(err);
}
}
onMounted(() => {
freeDingGetAjax()
})
</script>
<style lang="scss" scoped>
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.row {
padding-top: 14px;
}
.center {
display: flex;
align-items: center;
gap: 10px;
span {
color: #333;
}
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
<div>充值记录</div>
</template>
<script setup>
</script>

View File

@@ -42,10 +42,10 @@ const menus = ref([
{
name: "霸王餐",
icon: "bwc",
pathName: "bwc",
pathName: "king_dine",
intro: "设置充值消费的N倍当前订单立即免单",
},
{ name: "邀请列表", icon: "yqlb", pathName: "invite", intro: "邀请好友领券" },
// { name: "邀请列表", icon: "yqlb", pathName: "invite", intro: "邀请好友领券" },
{
name: "积分锁客",
icon: "jfsk",
@@ -67,10 +67,10 @@ const menus = ref([
{
name: "私域引流",
icon: "syyl",
pathName: "",
pathName: "drainage",
intro: "可设置用户下单成功后的群二维码",
},
{ name: "满减活动", icon: "mjhd", pathName: "", intro: "达到指定支付金额享受减价" },
{ name: "满减活动", icon: "mjhd", pathName: "discount_activity", intro: "达到指定支付金额享受减价" },
{ name: "生日有礼", icon: "sryl", pathName: "", intro: "用户生日管理设置" },
{
name: "点餐智能推荐",
@@ -155,7 +155,7 @@ const menus = ref([
label: "推送功能",
list: [
{ name: "推送活动消息", icon: "tshdxx", pathName: "", intro: "给用户推送服务通知" },
{ name: "短信推送", icon: "dxts", pathName: "", intro: "给用户推送服务通知" },
{ name: "短信推送", icon: "dxts", pathName: "note_push", intro: "给用户推送服务通知" },
],
},
{

View File

@@ -0,0 +1,766 @@
<template>
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑推送任务' : '添加推送任务'" width="1200px" top="4vh">
<div class="scroll" ref="scrollRef">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="160px">
<div class="title">短信内容</div>
<el-form-item label-width="0">
<div class="shop_user_wrap">
<div class="item">
<el-form-item label="选择模板" prop="pushEventId">
<el-select v-model="form.pushEventId" style="width: 300px;" @change="selectTempChange">
<el-option :label="item.title" :value="item.id" v-for="item in tempList" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item v-for="(item, index) in selectTempList" :key="index" :label="item.key"
style="margin-top: 14px;" prop="selectTempList">
<el-input v-if="item.type === 'input'" v-model="item.value" :placeholder="`请输入${item.key}`"
style="width:300px"></el-input>
<el-time-picker v-else-if="item.type === 'time'" v-model="item.value" placeholder="请选择时间" format="HH:mm"
value-format="HH:mm" :picker-options="{ selectableRange: '00:00:00-23:59:59' }"
style="width: 300px;" />
</el-form-item>
<el-form-item label="赠送优惠券" style="margin-top: 14px;" prop="coupon">
<div class="center" v-for="(item, index) in selectCoupons" :key="item.id">
<el-select v-model="item.id" @change="selectCouponChnge($event, index)">
<el-option :label="val.title" :value="val.id" v-for="val in couponList" :key="val.id"></el-option>
</el-select>
<el-input v-model="item.num" input-style="text-align:center;">
<template #append>数量</template>
</el-input>
<div class="del" @click="selectCoupons.splice(index, 1)">
<el-icon size="18" color="#FF2F2F">
<Delete />
</el-icon>
</div>
</div>
<div class="center">
<el-button link type="primary" icon="CirclePlus" @click="addCoupon">新增券</el-button>
</div>
</el-form-item>
</div>
<div class="item">
<div class="temp_preview">
<div class="temp_preview_title">模板预览</div>
<div class="temp_preview_content" ref="tempPrewiewContentRef" v-if="selectTemp" v-html="previewHtml">
</div>
<div class="temp_preview_content" style="margin-top: 14px;" v-else>请选择模板</div>
</div>
</div>
</div>
</el-form-item>
<div class="title">选择目标用户</div>
<el-form-item label-width="0">
<div class="shop_user_wrap">
<div class="item">
<el-form-item label="发送对象">
<el-radio-group v-model="form.userType" @change="getPushEventUserAjax">
<el-radio label="全部绑定手机号用户" :value="1"></el-radio>
<el-radio label="自定义用户" :value="2"></el-radio>
</el-radio-group>
</el-form-item>
<div v-if="form.userType == 2">
<el-form-item label="性别">
<el-checkbox v-model="form.smsPushEventUser.sexMan" label="男" :true-value="1" :false-value="0"
@change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.sexWoman" label="女" :true-value="1" :false-value="0"
@change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.sexUnknown" label="未知" :true-value="1" :false-value="0"
@change="getPushEventUserAjax"></el-checkbox>
</el-form-item>
<el-form-item label="下单">
<el-checkbox v-model="form.smsPushEventUser.noOrder" label="从未下单" :true-value="1" :false-value="0"
@change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.oneOrder" label="下过1单" :true-value="1" :false-value="0"
@change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.fiveOrder" label="下过5单及以上" :true-value="1"
:false-value="0" @change="getPushEventUserAjax"></el-checkbox>
</el-form-item>
<el-form-item label="下单时间">
<el-checkbox v-model="form.smsPushEventUser.orderTimeToday" label="今天" :true-value="1"
:false-value="0" @change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.orderTimeYesterday" label="昨天" :true-value="1"
:false-value="0" @change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.orderTimeTwoWeeks" label="2周内" :true-value="1"
:false-value="0" @change="getPushEventUserAjax"></el-checkbox>
<el-checkbox v-model="form.smsPushEventUser.orderTimeMoreThanTwoWeeks" label="2周前" :true-value="1"
:false-value="0" @change="getPushEventUserAjax"></el-checkbox>
</el-form-item>
<el-form-item label="会员">
<el-radio-group v-model="form.smsPushEventUser.isVip" @change="getPushEventUserAjax">
<el-radio label="会员" :value="1"></el-radio>
<el-radio label="非会员" :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="充值">
<el-radio-group v-model="form.smsPushEventUser.isRecharge" @change="getPushEventUserAjax">
<el-radio label="从未充值过" :value="0"></el-radio>
<el-radio label="充值过" :value="1"></el-radio>
</el-radio-group>
</el-form-item>
</div>
</div>
<div class="item2">
<div class="user_wrap" v-loading="userTableData.loading">
<div class="user_header">
<span>预计发送</span>
<span>{{ form.estimateNum }}</span>
</div>
<div class="list">
<div class="item" v-for="item in userTableData.list" :key="item.id">
<div class="avatar">
<el-avatar :size="50" :src="item.headImg" />
</div>
<div class="info">
<div class="name">{{ item.nickName }}</div>
<div class="info_wrap">
<span>余额{{ item.amount }}</span>
<span>积分{{ item.accountPoints }}</span>
<span>手机号{{ item.phone }}</span>
</div>
</div>
</div>
</div>
<div style="display: flex;justify-content: center;padding-bottom: 14px;">
<el-pagination v-model:current-page="userTableData.page" v-model:page-size="userTableData.pageSize"
:page-sizes="[100, 200, 300, 400]" background layout="prev, pager, next"
:total="userTableData.total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</div>
</div>
</div>
</el-form-item>
<div class="title">发送设置</div>
<el-form-item label="发送时间">
<el-radio-group v-model="form.sendType">
<el-radio label="立即发送" :value="1"></el-radio>
<el-radio label="定时发送" :value="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择时间" v-if="form.sendType == 2" prop="sendTime">
<el-date-picker v-model="form.sendTime" type="datetime" placeholder="请选择发送时间" format="YYYY-MM-DD HH:mm:sss"
value-format="YYYY-MM-DD HH:mm:ss" style="width: 220px;" />
</el-form-item>
<el-form-item label="预计发送人数">
{{ form.estimateNum }}
</el-form-item>
<el-form-item label="短信单价">
{{ notePirce }}/
</el-form-item>
<el-form-item label="预计费用">
¥{{ predictPrice }}
</el-form-item>
<el-form-item label="账户余额">
¥{{ multiplyAndFormat(shopBalance.money || 0) }}
<span v-if="predictPrice > shopBalance.money"
style="color:#FF2F2F;margin-left: 14px;">余额不足请联系管理员充值后再发送</span>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :disabled="form.estimateNum <= 0 || predictPrice > shopBalance.money"
:loading="loading" @click="submitHandle">
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import _ from 'lodash'
import { multiplyAndFormat } from '@/utils'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { getPushEventUser, smsTemplateGet, couponPage, pushEventPost, smsMoneyGet, smsMoneyGetFee } from '@/api/coupon'
const dialogVisible = ref(false)
const smsPushEventUserObj = ref({
sexMan: 1,
sexWoman: 1,
sexUnknown: 1,
isRecharge: 0,
noOrder: 1,
oneOrder: 0,
fiveOrder: 0,
orderTimeToday: 0,
orderTimeYesterday: 0,
orderTimeTwoWeeks: 0,
orderTimeMoreThanTwoWeeks: 0,
isVip: 1,
isRecharge: 1,
vipLevel: 0,
vipLevelId: 0
})
const form = ref({
id: '',
shopId: localStorage.getItem('shopId') || '',
pushType: 1,
userType: 1,
pushEventId: '',
estimateNum: 0, // 预计人数
content: '',
json: '',
coupon: '',
sendType: 1,
sendTime: '',
smsPushEventUser: { ...smsPushEventUserObj.value }
})
const resetForm = ref({})
const formRules = ref({
pushEventId: [{ required: true, message: '请选择模板', trigger: 'change' }],
// 校验 selectTempList
selectTempList: [
{
validator: (rule, value, callback) => {
// value 实际不会自动传递,需要手动取 selectTempList.value
const fields = selectTempList.value;
if (!fields || fields.length === 0) {
callback();
} else {
for (let i = 0; i < fields.length; i++) {
if (fields[i].value === '' || fields[i].value == null) {
callback(new Error(`请填写${fields[i].key}`));
return;
}
}
callback();
}
},
trigger: 'change'
}
],
coupon: [
{
validator: (rule, value, callback) => {
// value 是 selectCoupons.value 的 JSON 字符串
let coupons = selectCoupons.value;
if (coupons.length === 0) {
// 没有添加优惠券,不校验
callback();
} else {
// 校验每个优惠券
for (let i = 0; i < coupons.length; i++) {
if (!coupons[i].id) {
callback(new Error('请选择优惠券'));
return;
}
if (!coupons[i].num || !Number.isInteger(Number(coupons[i].num)) || Number(coupons[i].num) <= 0) {
callback(new Error('请输入大于零的优惠券数量'));
return;
}
}
callback();
}
},
trigger: 'change'
}
],
sendTime: [
{
required: (form) => form.sendType == 2,
message: '请选择发送时间',
trigger: 'change'
}
]
})
// 开始提交
const formRef = ref(null)
const emits = defineEmits(["success"]);
const loading = ref(false);
const tempPrewiewContentRef = ref(null);
function getJsonFromFields(fields, mapping) {
const result = {};
// 先处理可编辑字段
fields.forEach(f => {
result[f.key] = f.value;
});
// 再处理 disabled 且有 default 的字段
mapping.forEach(m => {
if (m.disabled && m.default != null) {
result[m.key] = m.default;
}
});
return result;
}
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true;
let data = { ...form.value }
data.coupon = JSON.stringify(selectCoupons.value);
data.content = tempPrewiewContentRef.value ? tempPrewiewContentRef.value.innerText : '';
// 转换 json
data.json = JSON.stringify(getJsonFromFields(selectTempList.value, mapping));
await pushEventPost(data, data.id ? 'put' : 'post');
emits("success");
dialogVisible.value = false;
ElNotification({
title: '注意',
message: '保存成功',
type: 'success'
})
form.value = { ...resetForm.value }
}
} catch (err) {
console.log(err);
}
loading.value = false;
});
}
// 获取用户列表
const notePirce = ref(0); // 短信单价
// 预计费用
const predictPrice = computed(() => {
return multiplyAndFormat(form.value.estimateNum, notePirce.value);
})
const userTableData = reactive({
loading: false,
list: [],
total: 0,
page: 1,
pageSize: 5
})
// 获取推送用户
async function getPushEventUserAjax() {
try {
userTableData.loading = true
const res = await getPushEventUser({
...form.value.smsPushEventUser,
isAll: form.value.userType,
shopId: form.value.shopId,
page: userTableData.page,
size: userTableData.pageSize
})
userTableData.list = res.records
userTableData.total = res.totalRow
form.value.estimateNum = res.totalRow
console.log(form.value);
} catch (error) {
console.log(error);
}
userTableData.loading = false
}
// 分页大小发生变化
function handleSizeChange(e) {
userTableData.pageSize = e;
getPushEventUserAjax();
}
// 分页发生变化
function handleCurrentChange(e) {
userTableData.page = e;
getPushEventUserAjax();
}
// 获取商户短信余额
const shopBalance = ref('')
async function smsMoneyGetAjax() {
try {
const res = await smsMoneyGet()
shopBalance.value = res
} catch (error) {
console.log(error);
}
}
// 获取短信单价
async function smsMoneyGetFeeAjax() {
try {
const res = await smsMoneyGetFee()
console.log('获取短信单价', res);
notePirce.value = +res.paramValue
} catch (error) {
console.log(error);
}
}
// 显示弹窗
const scrollRef = ref(null);
async function show(obj, temp = null) {
dialogVisible.value = true
nextTick(() => {
if (scrollRef.value) {
setTimeout(() => {
scrollRef.value.scrollTop = 0;
}, 50);
}
});
smsMoneyGetAjax()
smsMoneyGetFeeAjax()
if (obj && obj.id) {
form.value = { ...obj }
form.value.userType = +obj.userType
if (obj.userType == 1) {
form.value.smsPushEventUser = { ...smsPushEventUserObj.value }
}
if (obj.pushEventId && tempList.value.length > 0) {
// 选择模板
selectTemp.value = tempList.value.find(item => item.id === obj.pushEventId)
// 提取可编辑字段
selectTempList.value = parseTemplate(selectTemp.value?.content)
} else {
selectTempList.value = []
}
// 解析优惠券
if (form.value.coupon) {
try {
const coupons = JSON.parse(form.value.coupon)
if (Array.isArray(coupons)) {
selectCoupons.value = coupons
}
} catch (error) {
console.log(error);
}
} else {
selectCoupons.value = []
}
// 选择模板
if (form.value.pushEventId) {
selectTempChange(form.value.pushEventId)
// 回填字段值(只回填可编辑字段)
if (form.value.json) {
try {
const jsonObj = JSON.parse(form.value.json)
selectTempList.value = selectTempList.value.map(field => {
if (jsonObj.hasOwnProperty(field.key)) {
return { ...field, value: jsonObj[field.key] }
}
return field
})
} catch (error) {
console.log(error)
}
}
} else {
selectTempList.value = []
}
} else {
form.value = { ...resetForm.value }
selectTempList.value = []
selectCoupons.value = []
// 选中模板id
if (temp && temp.id) {
form.value.pushEventId = temp.id
await getTempListAjax()
selectTempChange(temp.id)
} else {
form.value.pushEventId = ''
selectTemp.value = null
selectTempList.value = []
}
form.value.estimateNum = userTableData.total
}
}
// 从本地获取商户信息
const shopInfo = ref(JSON.parse(localStorage.getItem("userInfo")));
// 获取模板列表
const tempList = ref([])
const selectTemp = ref(null);
const selectTempList = ref([])
// 映射表(你给的样例)
const mapping = [
{ key: '用户昵称', inputType: 'input', disabled: true, default: '某某某' },
{ key: '店铺名称', inputType: 'input', disabled: true, default: shopInfo.value ? shopInfo.value.shopName : '' },
{ key: '活动名称', inputType: 'input' },
{ key: '活动时间', inputType: 'time' },
{ key: '数量', inputType: 'input' },
{ key: '金额', inputType: 'input' },
{ key: '时间', inputType: 'time' }
]
// parseTemplate: 提取占位符并映射到目标数组
function parseTemplate(templateStr, mappingList = mapping) {
if (!templateStr) return []
const regex = /\$\{\s*([^}]+?)\s*\}/g
const seen = new Set()
const result = []
let match
while ((match = regex.exec(templateStr)) !== null) {
const key = match[1].trim()
if (!key || seen.has(key)) continue
seen.add(key)
const map = mappingList.find(item => item.key === key)
// 如果映射项被标记为 disabled则不加入可编辑列表但会用于预览默认值
const type = map ? (map.inputType || map.type || 'input') : 'input'
if (map && map.disabled) {
continue
}
result.push({ key, value: '', type })
}
return result
}
// 选择模板
function selectTempChange(e) {
selectTemp.value = tempList.value.find(item => item.id == e)
form.value.content = selectTemp.value.content;
selectTempList.value = parseTemplate(selectTemp.value.content)
console.log('选择模板===', selectTempList.value);
}
// 获取短信模板列表
async function getTempListAjax() {
try {
const res = await smsTemplateGet()
tempList.value = res.filter(item => item.status === 2)
} catch (error) {
console.log(error);
}
}
// 选择优惠券开始
const couponList = ref([])
const selectCoupons = ref([]);
const couponObj = ref({ id: '', num: 1, title: '' });
// 选择优惠券添加标题
function selectCouponChnge(e, index) {
const coupon = couponList.value.find(item => item.id === e)
if (coupon) {
selectCoupons.value[index].title = coupon.title
}
}
// 新增优惠券
function addCoupon() {
selectCoupons.value.push(_.cloneDeep(couponObj.value));
}
// 获取优惠券列表
async function couponPageAjax() {
try {
const res = await couponPage({
shopId: form.value.shopId,
page: 1,
size: 500
})
couponList.value = res.records
} catch (error) {
console.log(error);
}
}
// 选择优惠券结束
// HTML escape
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
// 渲染预览 HTML
function renderPreviewHtml(template, fields = []) {
if (!template) return ''
const map = {}
fields.forEach(f => { map[f.key] = f.value != null && f.value !== '' ? escapeHtml(String(f.value)) : null })
// 把 mapping 中 disabled 且有 default 的加入 map 作为默认值
mapping.forEach(m => {
if (m.disabled && m.default != null) {
if (!map.hasOwnProperty(m.key) || map[m.key] === null) {
map[m.key] = escapeHtml(String(m.default))
}
}
})
const replaced = template.replace(/\$\{\s*([^}]+?)\s*\}/g, (m, p1) => {
const key = p1.trim();
if (map.hasOwnProperty(key)) {
return map[key] != null ? map[key] : '[未填写]'
}
return '[未填写]'
})
// 保留换行为 <br/>
return (replaced || '').replace(/\n/g, '<br/>')
}
// 预览 HTML
const previewHtml = computed(() => {
if (!selectTemp.value) return ''
// 合并 title + content
const tpl = `${selectTemp.value.title || ''}${selectTemp.value.content || ''}`
return renderPreviewHtml(tpl, selectTempList.value)
})
defineExpose({
show
})
onMounted(() => {
resetForm.value = { ...form.value }
getPushEventUserAjax()
getTempListAjax()
couponPageAjax()
})
</script>
<style scoped lang="scss">
.title {
color: #000;
padding: 14px;
background-color: #f8f8f8;
margin-bottom: 14px;
font-size: 16px;
}
.scroll {
height: 76vh;
padding-bottom: 60px;
overflow-y: auto;
}
.center {
display: flex;
align-items: center;
gap: 14px;
&:not(:first-child) {
margin-top: 14px;
}
.del {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
}
.shop_user_wrap {
width: 100%;
display: flex;
gap: 14px;
.item {
flex: 1;
.temp_preview {
border: 1px solid #D9D9D9;
border-radius: 6px;
overflow: hidden;
background-color: #F8F8F8;
padding: 14px;
.temp_preview_title {
font-size: 16px;
font-weight: bold;
line-height: 16px;
}
.temp_preview_content {
font-size: 14px;
color: #666;
line-height: 16px;
}
}
}
.item2 {
width: 400px;
margin-right: 50px;
.user_wrap {
border: 1px solid #ddd;
border-radius: 6px;
overflow: hidden;
.user_header {
padding: 20px 14px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #F8F8F8;
span {
font-size: 16px;
color: #333;
font-weight: bold;
}
}
.list {
--count: 5;
--itemHeight: 70px;
height: calc(var(--count) * var(--itemHeight) + 30px);
.item {
height: var(--itemHeight);
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
padding: 0 14px;
.avatar {
display: flex;
align-items: center;
}
.info {
flex: 1;
margin-left: 10px;
.name {
font-size: 14px;
color: #333;
font-weight: bold;
}
.info_wrap {
margin-top: 4px;
font-size: 12px;
color: #999;
display: flex;
span {
&:nth-child(1) {
flex: 1;
}
&:nth-child(2) {
flex: 1;
}
&:nth-child(3) {
flex: 1.5;
}
}
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<el-dialog v-model="dialogVisible" title="申请短信模板" width="800px">
<!-- <div class="title">填写模板信息并提交审核审核通过后可用于短信推送</div> -->
<div class="gyq_flex">
<div class="item">
<el-form ref="formRef" :model="form" :rules="rules" label-position="left" label-width="120px">
<el-form-item label="模板名称" prop="title">
<el-input placeholder="请输入内容" v-model="form.title" :maxlength="30" style="width: 300px;"></el-input>
</el-form-item>
<el-form-item label="模版内容" prop="title">
<div style="width: 300px;">
<editorDiv ref="editorDivRef" v-model="form.content" />
</div>
</el-form-item>
<el-form-item label="使用场景">
<div class="ipt_wrap">
<el-input type="textarea" :rows="8" :maxlength="500"
placeholder="为了提高您短信模板审核的通过率,请您详细描述您的短信模板的使用场景(例如:说明该短信在何种情境下发送,以及短信接收对象等)如果短信中涉及到变量,也请详细说明变量的内容。此外,您也可以提供一份完整的、已填入变量内容的短停样例以供参考"
v-model="form.sceneDetail" style="width: 300px;"></el-input>
<span class="n">{{ form.sceneDetail.length }}/500</span>
</div>
</el-form-item>
</el-form>
</div>
<div class="item">
<div class="preview_wrap">
<div class="preview_title">模板示例</div>
<div class="preview">
尊敬的{姓名}您的会员卡将于{到期日期}到期请及时续费享受会员权益
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="submitHandle"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
import editorDiv from './editorDiv.vue'
import { smsTemplate, smsTemplateResubmit } from '@/api/coupon/index'
import { ElNotification } from 'element-plus'
const editorDivRef = ref(null)
const form = ref({
id: '',
shopId: '',
title: '',
content: '',
sceneDetail: '',
})
const resetForm = ref('')
const rules = {
title: [
{ required: true, message: '请输入模板名称', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入模版内容', trigger: 'blur' },
{ min: 1, max: 500, message: '长度在 1 到 500 个字符', trigger: 'blur' }
]
}
// 开始提交
const formRef = ref(null)
const emits = defineEmits(["success"]);
const loading = ref(false);
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true;
let data = { ...form.value }
data.content = `${data.content}拒收请回复R`
if (data.id) {
await smsTemplateResubmit(data);
} else {
await smsTemplate(data);
}
emits("success");
dialogVisible.value = false;
ElNotification({
title: '注意',
message: '保存成功',
type: 'success'
})
form.value = { ...resetForm.value }
}
} catch (err) {
console.log(err);
}
loading.value = false;
});
}
// 从本地获取商户信息
const shopInfo = ref("");
function getLocalShopInfo() {
shopInfo.value = JSON.parse(localStorage.getItem("userInfo"));
}
const dialogVisible = ref(false)
function show(obj = null) {
if (obj && obj.id) {
form.value = { ...obj }
} else {
nextTick(() => {
editorDivRef.value?.reset()
})
form.value = { ...resetForm.value }
}
dialogVisible.value = true;
}
defineExpose({
show,
});
onMounted(() => {
resetForm.value = { ...form.value }
getLocalShopInfo()
form.value.shopId = shopInfo.value.shopId
})
</script>
<style scoped lang="scss">
.editor {
min-height: 40px;
border: 1px solid #ccc;
padding: 8px;
margin-top: 8px;
}
.title {
color: #000;
padding: 14px;
background-color: #f8f8f8;
margin-bottom: 14px;
font-size: 16px;
}
.gyq_flex {
display: flex;
gap: 14px;
.item {
flex: 1;
flex-shrink: 0;
}
}
.column {
width: 100%;
display: flex;
flex-direction: column;
.column_item {
flex: 1;
display: flex;
}
}
.ipt_wrap {
position: relative;
.n {
position: absolute;
right: 10px;
bottom: 0;
color: #999;
font-size: 12px;
}
}
.preview_wrap {
border-radius: 4px;
border: 1px solid #D9D9D9;
background-color: #F8F8F8;
padding: 14px;
.preview_title {
font-size: 16px;
color: #333;
font-weight: bold;
margin-bottom: 10px;
}
.preview {
color: #666;
}
}
</style>

View File

@@ -0,0 +1,424 @@
<template>
<div class="wrap">
<div class="editor_div" ref="editorRef" contenteditable></div>
<div class="tag_list">
<el-tag v-for="tag in dynamicTags" :key="tag" :closable="tag.type == 'custom'" disable-transitions
@close="handleClose(tag)" @mousedown.prevent="saveRange" @click="selectTag(tag)" style="cursor: pointer;"
size="large">
{{ tag.label }}
</el-tag>
<div v-if="inputVisible" style="width: 92px;"><el-input ref="InputRef" v-model="inputValue" placeholder="请输入"
@keyup.enter="handleInputConfirm" @blur="handleInputConfirm" /></div>
<el-button v-else class="button-new-tag" @click="showInput">
+标签
</el-button>
</div>
<div class="tips">变量限制不支持QQ号微信号网址信息</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onBeforeUnmount, defineProps, defineEmits } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const editorRef = ref(null)
let lastRange = null // 记录光标位置
let lastInsertTime = 0 // 防止快速连续插入
let needsInput = false // 插入后必须输入内容才能再次插入
const InputRef = ref(null)
const inputValue = ref('')
const inputVisible = ref(false)
const dynamicTags = ref([
{
label: '用户昵称',
value: '${用户昵称}',
type: 'fix'
},
{
label: '店铺名称',
value: '${店铺名称}',
type: 'fix'
},
{
label: '活动名称',
value: '${活动名称}',
type: 'fix'
},
{
label: '活动时间',
value: '${活动时间}',
type: 'fix'
},
{
label: '数量',
value: '${数量}',
type: 'fix'
},
{
label: '金额',
value: '${金额}',
type: 'fix'
},
{
label: '时间',
value: '${时间}',
type: 'fix'
}
])
function handleClose(tag) {
const index = dynamicTags.value.indexOf(tag);
dynamicTags.value.splice(index, 1);
}
function showInput() {
inputVisible.value = true;
nextTick(() => {
InputRef.value.$el.querySelector('input').focus();
});
}
function handleInputConfirm() {
if (inputValue.value && dynamicTags.value.indexOf(inputValue.value) === -1
) {
dynamicTags.value.push({ label: inputValue.value, value: '${' + inputValue.value + '}', type: 'custom' });
}
inputVisible.value = false;
inputValue.value = '';
}
// 记录 selection range提升到模块作用域便于模板调用
function saveRange() {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
// 只在光标在 editorRef 内部时才记录
if (editorRef.value && editorRef.value.contains(range.startContainer)) {
lastRange = range.cloneRange();
}
}
}
// 选择标签
function selectTag(tag) {
// 防抖300ms 内再次触发插入判定为重复,忽略
const now = Date.now();
if (now - lastInsertTime < 300) {
return;
}
// 如果上次插入后还没输入任何可见字符,则禁止再次插入
if (needsInput) {
ElMessage.warning('请输入内容');
return;
}
// 新需求:同一种标签只能存在一个,如果已存在则提示并阻止插入
if (editorRef.value) {
const existing = Array.from(editorRef.value.querySelectorAll('.tag-chip'))
.some(el => el.dataset && el.dataset.value === tag.value);
if (existing) {
ElMessage.warning('已存在该标签');
return;
}
}
if (editorRef.value) {
// 如果光标前紧邻一个变量标签且两者之间没有其他可见字符,则禁止连续插入
function isCaretAfterTag(range) {
if (!range) return false;
const sc = range.startContainer;
const offset = range.startOffset;
// 辅助:去除零宽空格并判断文本是否为空
const textEmpty = (txt) => !txt || txt.replace(/\u200B/g, '').trim().length === 0;
// 如果在文本节点内
if (sc.nodeType === Node.TEXT_NODE) {
const before = sc.textContent.slice(0, offset);
if (!textEmpty(before)) return false;
// 向前查找非空白节点
let p = sc.previousSibling;
while (p) {
if (p.nodeType === Node.TEXT_NODE) {
if (!textEmpty(p.textContent)) return false;
p = p.previousSibling; continue;
}
if (p.nodeType === Node.ELEMENT_NODE) {
return !!(p.dataset && p.dataset.value);
}
p = p.previousSibling;
}
return false;
}
// 如果在元素节点内
if (sc.nodeType === Node.ELEMENT_NODE) {
if (offset === 0) {
// 在元素开始位置,检查元素的前一个兄弟节点
let p = sc.previousSibling;
while (p) {
if (p.nodeType === Node.TEXT_NODE) {
if (!textEmpty(p.textContent)) return false;
p = p.previousSibling; continue;
}
if (p.nodeType === Node.ELEMENT_NODE) {
return !!(p.dataset && p.dataset.value);
}
p = p.previousSibling;
}
return false;
} else {
const nodeBefore = sc.childNodes[offset - 1];
if (!nodeBefore) return false;
if (nodeBefore.nodeType === Node.ELEMENT_NODE) {
return !!(nodeBefore.dataset && nodeBefore.dataset.value);
}
if (nodeBefore.nodeType === Node.TEXT_NODE) {
if (!textEmpty(nodeBefore.textContent)) return false;
// 再向前找
let p = nodeBefore.previousSibling;
while (p) {
if (p.nodeType === Node.TEXT_NODE) {
if (!textEmpty(p.textContent)) return false;
p = p.previousSibling; continue;
}
if (p.nodeType === Node.ELEMENT_NODE) {
return !!(p.dataset && p.dataset.value);
}
p = p.previousSibling;
}
}
return false;
}
}
return false;
}
// 检查当前保存的 lastRange 或当前 selection
const checkRange = lastRange || (window.getSelection() && window.getSelection().rangeCount ? window.getSelection().getRangeAt(0) : null);
// 进一步:如果光标前最近的可见节点就是相同变量标签,禁止插入(防止首次双插入)
function getNodeBeforeCaret(range) {
if (!range) return null;
const sc = range.startContainer;
const offset = range.startOffset;
// 如果在文本节点内,从文本节点切片前部判断
if (sc.nodeType === Node.TEXT_NODE) {
// 若文本前有非空白字符则返回 null
const beforeText = sc.textContent.slice(0, offset).replace(/\u200B/g, '');
if (beforeText.trim().length > 0) return null;
// 检查前一个兄弟
let p = sc.previousSibling;
while (p) {
if (p.nodeType === Node.TEXT_NODE) {
if (p.textContent.replace(/\u200B/g, '').trim().length > 0) return null;
p = p.previousSibling; continue;
}
return p;
}
return sc.parentNode && sc.parentNode.previousSibling ? sc.parentNode.previousSibling : null;
}
// 如果在元素节点
if (sc.nodeType === Node.ELEMENT_NODE) {
if (offset === 0) {
return sc.previousSibling;
}
return sc.childNodes[offset - 1] || null;
}
return null;
}
const nodeBefore = getNodeBeforeCaret(checkRange);
if (nodeBefore && nodeBefore.nodeType === Node.ELEMENT_NODE) {
const el = nodeBefore;
if (el.dataset && el.dataset.value && el.dataset.value === tag.value) {
ElMessage.warning('请输入内容');
return;
}
}
if (checkRange && isCaretAfterTag(checkRange)) {
ElMessage.warning('请输入内容');
return;
}
// 先恢复光标位置(不要先 focusfocus 可能改变 selection
// 创建一个span标签用于插入
const span = document.createElement('span');
span.textContent = tag.label;
span.contentEditable = "false";
span.style.display = "inline-block";
span.style.background = "#f0f0f0";
span.style.borderRadius = "4px";
span.style.padding = "0 4px";
span.style.fontSize = "12px";
span.style.margin = "0 2px";
span.style.cursor = "pointer";
span.style.border = "1px solid #dcdcdc";
span.style.userSelect = "none";
// 关键:保存变量值到 dataset
span.dataset.value = tag.value;
span.dataset.type = tag.type || 'custom';
span.className = 'tag-chip';
// 添加删除按钮
const closeBtn = document.createElement('span');
closeBtn.textContent = ' ×';
closeBtn.style.color = "#999";
closeBtn.style.cursor = "pointer";
closeBtn.className = 'tag-close';
closeBtn.onclick = function (e) {
e.stopPropagation();
span.remove();
emitValue();
};
span.appendChild(closeBtn);
// 恢复光标位置
let sel = window.getSelection();
let inserted = false;
if (lastRange) {
sel.removeAllRanges();
sel.addRange(lastRange);
// 插入到光标处
if (sel && sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(span);
// 光标移到span后
range.setStartAfter(span);
range.setEndAfter(span);
sel.removeAllRanges();
sel.addRange(range);
// 更新 lastRange
lastRange = range.cloneRange();
inserted = true;
}
}
// 插入完成后确保 editor 聚焦(在插入之后调用 focus 更稳妥)
if (editorRef.value) {
try { editorRef.value.focus(); } catch (e) { }
}
// 如果没有光标,则插入到内容尾部
if (!inserted) {
editorRef.value.appendChild(span);
}
// 插入后 emit 内容
emitValue();
lastInsertTime = Date.now();
// 设置需要输入标志,直到用户输入可见字符
needsInput = true;
}
}
// 获取序列化内容(如:尊共的${username}
function getSerializedContent() {
if (!editorRef.value) return '';
function walk(node) {
let out = '';
node.childNodes.forEach(n => {
if (n.nodeType === Node.TEXT_NODE) {
out += n.textContent;
} else if (n.nodeType === Node.ELEMENT_NODE) {
// 如果是我们插入的 tag 节点并且有 data-value则用 data-value
const el = n;
if (el.dataset && el.dataset.value) {
out += el.dataset.value;
} else {
out += walk(el);
}
}
});
return out;
}
return walk(editorRef.value);
}
// emit 内容给父组件
function emitValue() {
emit('update:modelValue', getSerializedContent());
}
// 重置编辑器内容和内部状态
function reset() {
if (editorRef.value) {
// 清空内容
editorRef.value.innerHTML = '';
}
// 重置状态
lastRange = null;
lastInsertTime = 0;
needsInput = false;
inputValue.value = '';
inputVisible.value = false;
// 通知父组件内容清空
emit('update:modelValue', '');
}
onMounted(() => {
const events = ['input', 'keyup', 'mouseup', 'blur', 'focus'];
events.forEach((ev) => editorRef.value && editorRef.value.addEventListener(ev, saveRange));
// input 事件实时 emit 内容
editorRef.value && editorRef.value.addEventListener('input', emitValue);
// 当用户输入可见字符时,允许再次插入
const inputHandler = (e) => {
const text = editorRef.value ? editorRef.value.innerText.replace(/\u200B/g, '').trim() : '';
if (text.length > 0) {
needsInput = false;
}
}
editorRef.value && editorRef.value.addEventListener('input', inputHandler);
onBeforeUnmount(() => {
events.forEach((ev) => editorRef.value && editorRef.value.removeEventListener(ev, saveRange));
editorRef.value && editorRef.value.removeEventListener('input', emitValue);
editorRef.value && editorRef.value.removeEventListener('input', inputHandler);
});
})
// 暴露方法给父组件
defineExpose({
reset
});
</script>
<style scoped lang="scss">
.wrap {
width: 100%;
}
.tag_list {
padding: 10px 0;
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.tips {
color: #999;
font-size: 12px;
}
.editor_div {
flex: 1;
border: 1px solid #ddd;
border-radius: 4px;
min-height: 100px;
padding: 10px 10px;
transition: all .1s ease-in-out;
&:focus {
border-color: var(--el-color-primary);
}
/* 核心placeholder样式 */
&:empty:not(:focus):before {
content: '请输入内容';
/* placeholder文本 */
color: rgb(168, 168, 168);
/* 占位符颜色 */
pointer-events: none;
font-size: 14px;
/* 点击时穿透到容器 */
}
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="gyq_container">
<div class="gyq_content">
<headerCard icon="dxts" name="短信推送" intro="给用户推送服务通知" />
<div class="row">
<el-tabs v-model="activeKey">
<el-tab-pane label="群发短信" :name="1">
<noteList ref="noteListRef" />
</el-tab-pane>
<el-tab-pane label="用量记录" :name="2">
<record />
</el-tab-pane>
<el-tab-pane label="模版管理" :name="3">
<templateManage @useTemplate="useTemp" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import noteList from './noteList.vue'
import record from './record.vue'
import templateManage from './templateManage.vue'
const activeKey = ref(1)
const noteListRef = ref(null)
function useTemp(item) {
activeKey.value = 1
noteListRef.value?.useTemplate(item)
}
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
</style>

View File

@@ -0,0 +1,115 @@
<!-- 群发短信 -->
<template>
<div>
<div class="row">
<el-button type="primary" @click="addTaskRef?.show()">添加任务</el-button>
</div>
<div class="row">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="短信内容" prop="content" width="300"></el-table-column>
<el-table-column label="优惠券信息" prop="coupon"></el-table-column>
<el-table-column label="发送对象" prop="userType"></el-table-column>
<el-table-column label="发送人数「预计」" prop="estimateNum" width="150"></el-table-column>
<el-table-column label="发送时间" prop="sendTime"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="操作" fixed="right" width="120">
<template #default="scope">
<el-button link type="primary" v-if="scope.row.status != 2"
@click="addTaskRef.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">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[100, 200, 300, 400]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<addTask ref="addTaskRef" @success="getTableData" />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import addTask from './components/addTask.vue';
import { pushEventGet, pushEventDel } from '@/api/coupon'
const addTaskRef = ref(null)
const tableData = reactive({
loading: false,
page: 1,
size: 10,
list: []
})
// 删除
async function deleteHandle(row) {
try {
tableData.loading = true;
await pushEventDel(row.id);
ElNotification({
title: '注意',
message: '已删除',
type: 'success'
})
getTableData();
} catch (err) {
console.log(err);
}
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
// 获取列表
const getTableData = async () => {
try {
tableData.loading = true;
const res = await pushEventGet({
page: tableData.page,
size: tableData.size
});
tableData.list = res.records;
tableData.total = res.totalRow;
} catch (error) {
console.log(error);
}
tableData.loading = false;
}
// 添加模板,并且默认选中模板
function useTemplate(item) {
addTaskRef.value?.show(null, item)
}
defineExpose({
useTemplate
})
onMounted(() => {
getTableData();
});
</script>
<style scoped lang="scss">
.row {
padding-top: 14px;
}
</style>

View File

@@ -0,0 +1,120 @@
<!-- 用量记录 -->
<template>
<div>
<div class="data_show">
<div class="data_list">
<div class="item">
<div class="title">
返现总金额
</div>
<div class="num">
{{ tableData.sendTotal }}/{{ multiplyAndFormat(tableData.sendAmountTotal, 1) }}
</div>
</div>
</div>
</div>
<div class="table" style="width: 100%;">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading" height="500px">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="记录类型" prop="type">
<template #default="scope">
<div class="column">
<div v-if="scope.row.type === 1">增加</div>
<div v-else-if="scope.row.type === 2">减少</div>
</div>
</template>
</el-table-column>
<el-table-column label="变动原因" prop="reason"></el-table-column>
<el-table-column label="变动前余额" prop="balance"></el-table-column>
<el-table-column label="变动金额" prop="expense"></el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
</el-table>
</div>
<div class="row" style="margin-top: 14px;">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[100, 200, 300, 400]" background layout="total, sizes, prev, pager, next, jumper"
:page-count="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { smsMoneyDetail } from '@/api/coupon/index.js'
import { multiplyAndFormat } from '@/utils'
const tableData = reactive({
sendAmountTotal: 0,
sendTotal: 0,
loading: false,
page: 1,
size: 10,
list: []
})
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
// 获取店铺短信余额明细
async function getTableData(params) {
try {
tableData.loading = true
const res = await smsMoneyDetail({
page: 1,
size: 10
})
tableData.totalAmount = res.totalAmount
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
tableData.loading = false
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.data_show {
padding: 0 0 14px;
.data_list {
display: flex;
.item {
width: 300px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 14px;
.title {
color: #666;
}
.num {
font-size: 24px;
color: #333;
font-weight: bold;
}
}
}
}
.column {
display: flex;
flex-direction: column;
line-height: 16px;
}
</style>

View File

@@ -0,0 +1,155 @@
<!-- 模板管理 -->
<template>
<div>
<div class="row">
<el-button type="primary" @click="addTemplateRef.show()">申请新模板</el-button>
</div>
<div class="row">
<div class="title">可用模板</div>
<div class="card_wrap" :class="[`card_wrap${useData.length}`]">
<div class="card" v-for="item in useData" :key="item.id">
<div class="card_title">
<span>{{ item.title }}</span>
<el-button type="primary" @click="toUse(item)">去使用</el-button>
</div>
<div class="card_content">
{{ item.content }}
</div>
</div>
<el-empty description="没有数据" v-if="useData.length == 0" />
</div>
<div class="title">我申请的模板</div>
<div class="card_wrap" :class="[`card_wrap${applyData.length}`]">
<div class="card" v-for="item in applyData" :key="item.id">
<div class="card_title">
<span>{{ item.title }}</span>
<el-button :type="statusOptions?.find(val => val.value == item.status).type" plain
@click="toApplayHandle(item)">
{{statusOptions?.find(val => val.value == item.status).label}}
</el-button>
</div>
<div class="card_content">
{{ item.content }}
</div>
</div>
<el-empty description="没有数据" v-if="applyData.length == 0" />
</div>
</div>
<addTemplate ref="addTemplateRef" @success="smsTemplateGetAjax" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import addTemplate from './components/addTemplate.vue';
import { smsTemplateGet } from '@/api/coupon/index'
const addTemplateRef = ref(null)
const statusOptions = ref([
{
label: '待审核',
value: 0,
type: 'info'
},
{
label: '审核中',
value: 1,
type: 'warning'
},
{
label: '已通过',
value: 2,
type: 'success'
},
{
label: '未通过',
value: -1,
type: 'danger'
},
{
label: '待审核',
value: -2,
type: 'info'
}
])
function toApplayHandle(item) {
if (item.status === -1) {
// 已通过 去使用
addTemplateRef.value.show(item)
}
}
const useData = ref([]) // 可用模板
const applyData = ref([]) // 申请模板
// 获取模板列表
async function smsTemplateGetAjax() {
try {
const res = await smsTemplateGet()
useData.value = res.filter(item => item.status === 2)
applyData.value = res.filter(item => item.status !== 2)
} catch (error) {
console.log('error', error);
}
}
// 跳转去使用模板
const emit = defineEmits(['useTemplate'])
function toUse(item) {
emit('useTemplate', item)
}
onMounted(() => {
smsTemplateGetAjax()
})
</script>
<style scoped lang="scss">
.row {
padding-top: 14px;
}
.title {
color: #333;
padding-bottom: 14px;
font-size: 16px;
font-weight: bold;
}
.card_wrap {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto;
grid-column-gap: 14px;
grid-row-gap: 14px;
margin-bottom: 24px;
&.card_wrap0 {
grid-template-columns: repeat(1, 1fr);
}
.card {
padding: 24px 14px;
border-radius: 6px;
border: 1px solid #ddd;
.card_title {
display: flex;
align-items: center;
justify-content: space-between;
span {
font-size: 16px;
color: #333;
}
}
.card_content {
color: #666;
padding-top: 14px;
}
}
}
</style>

View File

@@ -1,23 +1,11 @@
<template>
<div class="gyq_container">
<div class="gyq_content">
<HeaderCard
name="智慧充值"
intro="允许客户充值并使用余额支付"
icon="zhcz"
showSwitch
v-model:isOpen="form.isEnable"
></HeaderCard>
<HeaderCard name="智慧充值" intro="允许客户充值并使用余额支付" icon="zhcz" showSwitch v-model:isOpen="form.isEnable"></HeaderCard>
<div style="padding-top: 14px">
<el-tabs v-model="tabsValue">
<el-tab-pane label="基础设置" :name="1">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120"
label-position="right"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120" label-position="right">
<el-form-item label="充值面额" required>
<el-button type="primary" @click="AddDialogRef.open()">
添加面额
@@ -26,18 +14,9 @@
<el-form-item prop="rechargeDetailList">
<el-table :data="form.rechargeDetailList" border stripe>
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column
label="充值金额(元)"
prop="amount"
></el-table-column>
<el-table-column
label="赠送金额"
prop="rewardAmount"
></el-table-column>
<el-table-column
label="赠送积分"
prop="rewardPoints"
></el-table-column>
<el-table-column label="充值金额(元)" prop="amount"></el-table-column>
<el-table-column label="赠送金额" prop="rewardAmount"></el-table-column>
<el-table-column label="赠送积分" prop="rewardPoints"></el-table-column>
<el-table-column label="赠送优惠券" prop="couponInfoList">
<template #default="scope">
<div class="column">
@@ -53,60 +32,34 @@
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="AddDialogRef.open(scope.row, scope.$index)"
>
<el-button type="primary" link @click="AddDialogRef.open(scope.row, scope.$index)">
编辑
</el-button>
<el-button
type="danger"
link
@click="
form.rechargeDetailList.splice(scope.$index, 1)
"
>
<el-button type="danger" link @click="
form.rechargeDetailList.splice(scope.$index, 1)
">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item
label="选择门店"
prop="useType"
v-if="shopInfo.isHeadShop"
>
<el-form-item label="选择门店" prop="useType" v-if="shopInfo.isHeadShop">
<el-radio-group v-model="form.useType">
<el-radio label="全部门店" value="all"></el-radio>
<el-radio label="指定门店可用" value="part"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择门店" v-if="form.useType == 'part'">
<el-select
v-model="form.shopIdList"
multiple
clearable
placeholder="请选择门店"
style="width: 300px"
>
<el-option
:label="item.shopName"
:value="item.id"
v-for="item in branchList"
:key="item.id"
></el-option>
<el-select v-model="form.shopIdList" multiple clearable placeholder="请选择门店" style="width: 300px">
<el-option :label="item.shopName" :value="item.id" v-for="item in branchList"
:key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="自定义金额">
<div class="column">
<div class="center">
<el-switch
v-model="form.isCustom"
:active-value="1"
:inactive-value="0"
/>
<el-switch v-model="form.isCustom" :active-value="1" :inactive-value="0" />
<span class="tips">自定义金额不参与赠送优惠</span>
</div>
</div>
@@ -114,11 +67,7 @@
<el-form-item label="充值并下单">
<div class="column">
<div class="center">
<el-switch
v-model="form.isOrder"
:active-value="1"
:inactive-value="0"
/>
<el-switch v-model="form.isOrder" :active-value="1" :inactive-value="0" />
<span class="tips">开启后订单支付页面显示充值选项</span>
</div>
</div>
@@ -126,13 +75,8 @@
<el-form-item label="充值说明" prop="remark">
<div class="column">
<div class="item">
<el-input
type="textarea"
:rows="4"
:maxlength="250"
v-model="form.remark"
placeholder="填写内容"
></el-input>
<el-input type="textarea" :rows="4" :maxlength="250" v-model="form.remark"
placeholder="填写内容"></el-input>
</div>
<div class="item textarea-num">
{{ form.remark.length }}/250字内单文本
@@ -141,12 +85,7 @@
</el-form-item>
</el-form>
<div class="footer">
<el-button
type="primary"
size="large"
@click="submitHandle"
v-if="shopInfo.isHeadShop"
>
<el-button type="primary" size="large" @click="submitHandle" v-if="shopInfo.isHeadShop">
保存
</el-button>
<el-button size="large" @click="back">取消</el-button>

View File

@@ -0,0 +1,110 @@
<!-- 增加或者减少余额弹窗 -->
<template>
<el-dialog :title="`余额${form.type == 1 ? '增加' : '扣减'}`" width="400px" v-model="visible" @closed="onClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px" label-position="left">
<el-form-item label="店铺名称">
<el-input v-model="row.shopName" readonly style="width: 300px;"></el-input>
</el-form-item>
<el-form-item :label="`${form.type == 1 ? '充值' : '扣减'}金额`" prop="expense">
<el-input v-model="form.expense" placeholder="请输入金额" style="width: 300px;"
@input="e => form.expense = filterNumberInput(e)">
<template #append></template>
</el-input>
<div>当前余额<span style="color: red;">{{ multiplyAndFormat(row.money) }}</span></div>
</el-form-item>
<el-form-item label="扣减原因" v-if="form.type == 2" prop="reason">
<el-input type="textarea" :rows="5" v-model="form.reason" placeholder="请输入扣减原因,必填 "></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="submitHandle" :loading="loading"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
import { smsMoneyChange } from '@/api/coupon'
import { ElNotification } from 'element-plus'
import { multiplyAndFormat, filterNumberInput } from '@/utils'
const visible = ref(false)
const loading = ref(false)
const formRef = ref(null)
const row = ref({
shopName: '',
money: 0
})
const form = ref({
expense: '',
type: 1,
reason: ''
})
const rules = ref({
expense: [
{
required: true,
message: '请输入充值金额',
triiger: 'blur'
}
],
reason: [
{
required: true,
message: '请输入扣减原因',
triiger: 'blur'
}
]
})
function onClose() {
form.value = {
expense: '',
type: 1,
reason: ''
}
formRef.value.resetFields()
}
const emit = defineEmits(['success'])
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true
await smsMoneyChange({
shopId: row.value.shopId,
...form.value
})
emit('success')
visible.value = false
ElNotification({
title: '注意',
message: `${form.value.type == 1 ? '增加' : '扣减'}成功`,
type: 'success'
})
}
} catch (error) {
console.log(error);
}
loading.value = false
})
}
// 显示
function show(obj = {}, type = 1) {
visible.value = true
row.value = { ...obj }
form.value.type = type
}
defineExpose({
show
})
</script>

View File

@@ -0,0 +1,139 @@
<!-- 充值记录 -->
<template>
<el-dialog :title="`记录(${tableRow.shopName}`" width="1200px" v-model="visible">
<el-form label-width="0" inline>
<el-form-item>
<el-date-picker v-model="dateRange" type="datetimerange" range-separator="至" start-placeholder="开始日期时间"
end-placeholder="结束日期时间" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
@change="timeChange" />
</el-form-item>
<el-form-item>
<el-select v-model="queryForm.type" placeholder="类型" style="width: 200px;" @change="getTableData">
<el-option :label="item.label" :value="item.value" v-for="item in statusList" :key="item.value"></el-option>
</el-select>
</el-form-item>
</el-form>
<div class="row">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading" height="500px">
<el-table-column label="变动金额" prop="expense">
<template #default="scope">
{{ multiplyAndFormat(scope.row.money || 0) }}
</template>
</el-table-column>
<el-table-column label="变动后余额" prop="balance">
<template #default="scope">
{{ multiplyAndFormat(scope.row.balance || 0) }}
</template>
</el-table-column>
<el-table-column label="变动时间" prop="createTime"></el-table-column>
<el-table-column label="变动原因" prop="reason"></el-table-column>
</el-table>
</div>
<div class="row">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
:page-sizes="[10, 20, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { smsMoneyDetailQuery } from '@/api/coupon'
import { multiplyAndFormat } from '@/utils'
const visible = ref(false)
const statusList = ref([
{
value: '',
label: '全部'
},
{
value: 1,
label: '增加'
},
{
value: 2,
label: '减少'
},
])
const dateRange = ref([])
const resetQueryForm = ref({
startTime: '',
endTime: '',
type: '',
})
const queryForm = ref({ ...resetQueryForm.value })
const tableData = reactive({
loading: false,
list: [],
total: 0,
page: 1,
size: 10
})
// 选择时间
function timeChange(e) {
if (e == null) {
queryForm.value.startTime = ''
queryForm.value.endTime = ''
} else {
queryForm.value.startTime = e[0]
queryForm.value.endTime = e[1]
}
tableData.page = 1
getTableData()
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
async function getTableData() {
try {
tableData.loading = true
const res = await smsMoneyDetailQuery({
shopId: tableRow.value.shopId,
page: tableData.page,
size: tableData.size,
...queryForm.value
})
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
const tableRow = ref('')
function show(row) {
dateRange.value = []
queryForm.value = { ...resetQueryForm.value }
tableRow.value = { ...row }
visible.value = true
getTableData()
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.row {
padding-top: 14px;
}
</style>

View File

@@ -0,0 +1,274 @@
<!-- 店铺配置管理 -->
<template>
<div class="gyq_container">
<div class="gyq_content">
<div class="row">
<div class="center">
<el-select v-model="status" placeholder="状态" style="width: 200px;">
<el-option label="全部" value=""></el-option>
<el-option label="启用中" value="1"></el-option>
<el-option label="已禁用" value="0"></el-option>
</el-select>
<el-button type="primary" @click="searchHandle">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="primary" plain @click="recordRef.show()">商家申请记录</el-button>
</div>
</div>
<div class="row mt14">
<el-button type="primary" @click="visible = true">添加模板</el-button>
</div>
<div class="row mt14">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column prop="id" label="ID"></el-table-column>
<el-table-column prop="title" label="模版名称" />
<el-table-column prop="content" label="短信模版内容" width="500" />
<el-table-column prop="shopUse" label="启用状态">
<template #default="scope">
<el-switch v-model="scope.row.shopUse" :active-value="1" :inactive-value="0"
@change="statusChange($event, scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button type="primary" link @click="editorHandle(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, 20, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<el-dialog :title="`${selectRow.id ? '编辑' : '添加'}模板`" width="500px" v-model="visible" @closed="onClose">
<el-form :model="addForm" label-width="80px" :rules="addFormRules" ref="addFormRef">
<el-form-item label="模板名称" prop="title">
<el-input v-model="addForm.title" :maxlength="20" placeholder="请输入模板名称" />
</el-form-item>
<el-form-item label="模板内容" prop="content">
<div class="ipt">
<el-input type="textarea" :maxlength="maxLength" :rows="4" v-model="addForm.content"
placeholder="请输入模板内容" />
<span>{{ addForm.content.length }}/{{ maxLength }}</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel"> </el-button>
<el-button type="primary" @click="handleOk" :loading="confirmLoading"> </el-button>
</div>
</template>
</el-dialog>
<record ref="recordRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import record from './record.vue'
import { smsTemplatePage, smsTemplate, smsTemplateResubmit, shopUseDelStatus } from '@/api/coupon/index.js'
const recordRef = ref(null)
const status = ref('')
// 搜索
function searchHandle() {
tableData.page = 1
getTableData()
}
// 重置搜索
function resetSearch() {
status.value = ''
tableData.page = 1
getTableData()
}
const tableData = reactive({
loading: false,
list: [],
total: 0,
page: 1,
size: 10
})
// 添加模板 statr
const selectRow = ref({ id: '' })
const visible = ref(false)
const confirmLoading = ref(false)
const maxLength = ref(500)
const addForm = ref({
title: '',
content: ''
})
const addFormRules = {
title: [
{ required: true, message: '请输入模板名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入模板内容', trigger: 'blur' },
{ min: 1, max: maxLength.value, message: `长度在 1 到 ${maxLength.value} 个字符`, trigger: 'blur' }
]
}
const addFormRef = ref(null)
function onClose() {
selectRow.value = { id: '' }
visible.value = false
addForm.value = {
title: '',
content: ''
}
if (addFormRef.value) {
addFormRef.value.clearValidate()
}
}
// 取消提交
function handleCancel() {
onClose()
}
// 显示编辑
function editorHandle(row) {
selectRow.value = { ...row }
addForm.value = { ...row }
visible.value = true
}
// 开始提交
function handleOk() {
if (!addFormRef.value) return
addFormRef.value.validate(async (valid) => {
try {
if (valid) {
confirmLoading.value = true
if (selectRow.value.id) {
await smsTemplateResubmit(addForm.value)
} else {
await smsTemplate({
...addForm.value,
shopId: localStorage.getItem('shopId'),
})
}
onClose()
getTableData()
}
} catch (error) {
console.log(error);
}
confirmLoading.value = false
})
}
// 添加模板 end
// 状态改变
async function statusChange(e, row) {
try {
if (tableData.loading) return
await shopUseDelStatus({
id: row.id,
shopUse: row.shopUse
})
getTableData()
} catch (error) {
console.log(error);
}
}
// 删除
async function deleteHandle(row) {
try {
await shopUseDelStatus({
id: row.id,
isDel: 1,
});
getTableData();
} catch (err) {
console.log(err);
}
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
async function getTableData() {
try {
tableData.loading = true
const res = await smsTemplatePage({
page: tableData.page,
size: tableData.size,
shopUse: status.value,
shopId: localStorage.getItem('shopId')
})
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.row {
&.mt14 {
margin-top: 14px;
}
}
.ipt {
width: 100%;
position: relative;
span {
position: absolute;
right: 10px;
bottom: 0;
color: #999;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<!-- 商家申请记录 -->
<template>
<el-dialog title="商家申请记录" width="1200" top="10vh" v-model="visible" @close="onClose">
<div class="row">
<el-form inline label-width="0">
<el-form-item>
<el-date-picker v-model="dateRange" type="datetimerange" range-separator="至" start-placeholder="开始日期时间"
end-placeholder="结束日期时间" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
@change="timeChange" />
</el-form-item>
<el-form-item>
<el-select v-model="queryForm.status" placeholder="状态" style="width: 200px;" @change="getTableData">
<el-option label="全部" value=""></el-option>
<el-option :label="item.label" :value="item.value" v-for="item in statusList" :key="item.value"></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="row mt14">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading" height="500px">
<el-table-column label="申请商家" prop="shopName"></el-table-column>
<el-table-column label="模版名称" prop="title"></el-table-column>
<el-table-column label="模版内容" prop="content"></el-table-column>
<el-table-column label="使用场景" prop="sceneDetail"></el-table-column>
<el-table-column label="提交时间" prop="createTime"></el-table-column>
<el-table-column label="审核状态" prop="status" width="100">
<template #default="scope">
{{statusList.find(item => item.value == scope.row.status).label}}
</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, 20, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { smsTemplatePage } from '@/api/coupon/index.js'
const visible = ref(false)
// 0 待申请 1 审核中 2 成功 -1失败 -2 重新申请中
const statusList = reactive([
{
value: 0,
label: '待申请'
},
{
value: 1,
label: '审核中'
},
{
value: 2,
label: '成功'
},
{
value: -1,
label: '1失败'
},
{
value: -2,
label: '重新申请中'
}
])
const dateRange = ref([])
const queryForm = ref({
startTime: '',
endTime: '',
status: ''
})
function onClose() {
visible.value = false
}
function show() {
visible.value = true
getTableData()
}
// 选择时间
function timeChange(e) {
if (e == null) {
queryForm.value.startTime = ''
queryForm.value.endTime = ''
} else {
queryForm.value.startTime = e[0]
queryForm.value.endTime = e[1]
}
console.log(queryForm.value);
tableData.page = 1
getTableData()
}
// 分页大小发生变化
function handleSizeChange(e) {
tableData.size = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
const tableData = reactive({
loading: false,
page: 1,
size: 10,
list: []
})
// 获取数据
async function getTableData() {
try {
tableData.loading = true
const res = await smsTemplatePage({
page: tableData.page,
size: tableData.size,
...queryForm.value
})
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.row {
&.mt14 {
margin-top: 14px;
}
}
</style>

View File

@@ -0,0 +1,196 @@
<!-- 店铺配置管理 -->
<template>
<div class="gyq_container">
<div class="gyq_content">
<div class="row">
<div class="center">
<el-input v-model="searchValue" style="width: 300px;" placeholder="请输入内容" clearable @clear="getTableData">
<template #prepend>名称</template>
</el-input>
<el-button type="primary" @click="searchHandle">搜索</el-button>
</div>
</div>
<div class="row mt14">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column prop="name" label="店铺名称" width="300">
<template #default="scope">
<div class="shop_info">
<el-avatar :src="scope.row.coverImg" shape="square" :size="50"></el-avatar>
<div class="info">
<div class="name">
{{ scope.row.shopName }}
</div>
<div class="tag">
<el-tag effect="dark" type="success" disable-transitions
v-if="scope.row.profiles == 'release'">正式</el-tag>
<el-tag effect="dark" type="warning" disable-transitions
v-if="scope.row.profiles == 'trial'">试用版</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="店铺类型">
<template #default="scope">
<div class="column">
<div>{{shopTypeList.find(item => item.value == scope.row.shopType).label}}</div>
<div v-if="scope.row.shopType == 'join'">{{ scope.row.mainShopName }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="money" label="可用余额">
<template #default="scope">
{{ multiplyAndFormat(scope.row.money || 0) }}
</template>
</el-table-column>
<el-table-column prop="amountTotal" label="累计用量">
<template #default="scope">
{{ multiplyAndFormat(scope.row.amountTotal || 0) }}
</template>
</el-table-column>
<el-table-column prop="monthSendTotal" label="本月用量">
<template #default="scope">
{{ multiplyAndFormat(scope.row.monthSendTotal || 0) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="addBlanceRef.show(scope.row, 1)">充值</el-button>
<el-button link type="primary" @click="addBlanceRef.show(scope.row, 2)">扣减</el-button>
<el-button link type="primary" @click="recordRef.show(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, 20, 50, 100]" background layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<addBlance ref="addBlanceRef" @success="getTableData" />
<record ref="recordRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { adminSmsMoneyPage } from '@/api/coupon'
import addBlance from '../components/addBlance.vue'
import record from '../components/record.vue'
import { multiplyAndFormat } from '@/utils'
const addBlanceRef = ref(null)
const recordRef = ref(null)
const searchValue = ref('')
function searchHandle() {
tableData.page = 1
getTableData()
}
const shopTypeList = ref([
{
value: 'only',
label: '单店'
},
{
value: 'chain',
label: '连锁店'
},
{
value: 'join',
label: '加盟店'
},
])
const tableData = reactive({
loading: false,
list: [],
total: 0,
page: 1,
size: 10
})
// 分页大小发生变化
function handleSizeChange(e) {
tableData.pageSize = e;
getTableData();
}
// 分页发生变化
function handleCurrentChange(e) {
tableData.page = e;
getTableData();
}
// 平台:短信店铺配置 列表
async function getTableData() {
try {
tableData.loading = true
const res = await adminSmsMoneyPage({
page: tableData.page,
size: tableData.size,
name: searchValue.value
})
tableData.list = res.records
tableData.total = +res.totalRow
} catch (error) {
console.log(error);
}
setTimeout(() => {
tableData.loading = false
}, 500);
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
.row {
&.mt14 {
margin-top: 14px;
}
}
.shop_info {
display: flex;
align-items: center;
.info {
padding-left: 10px;
display: flex;
flex-direction: column;
gap: 8px;
.name {
font-size: 16px;
color: #333;
}
}
}
.column {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -2,12 +2,8 @@
<div class="app-container">
<!-- 列表 -->
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="searchQueryClick"
@reset-click="handleResetClick"
/>
<page-search ref="searchRef" :search-config="searchConfig" @query-click="searchQueryClick"
@reset-click="handleResetClick" />
<div class="head-container">
<div class="card">
<!-- <div class="title">统计数据</div> -->
@@ -32,17 +28,9 @@
</div>
</div>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
@edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
@toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -55,21 +43,18 @@
{{ scope.row[scope.prop] ? "是" : "否" }}
</template>
<template #gender="scope">
<el-tag
:type="
scope.row[scope.prop] == null
? 'info'
: scope.row[scope.prop] == 1
? 'success'
: 'warning'
"
>
<el-tag :type="scope.row[scope.prop] == null
? 'info'
: scope.row[scope.prop] == 1
? 'success'
: 'warning'
">
{{
scope.row[scope.prop] === null
? "未知"
: scope.row[scope.prop] == 1
? "男"
: "女"
? "男"
: "女"
}}
</el-tag>
</template>
@@ -86,11 +71,7 @@
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
<copy-button v-if="scope.row[scope.prop]" :text="scope.row[scope.prop]" style="margin-left: 2px" />
</template>
<template #coupon="scope">
@@ -104,25 +85,13 @@
</page-content>
<!-- 新增 -->
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
></page-modal>
<page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick"></page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
></page-modal>
<page-modal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick"></page-modal>
<!-- 用户余额修改 -->
<page-modal
ref="editMoneyModalRef"
:modal-config="editMoneyModalConfig"
@formDataChange="formDataChange"
@submit-click="handleSubmitClick"
></page-modal>
<page-modal ref="editMoneyModalRef" :modal-config="editMoneyModalConfig" @formDataChange="formDataChange"
@submit-click="handleSubmitClick"></page-modal>
<!-- 用户优惠券详情 -->
<UserCouponDialog ref="userCouponDialogRef"></UserCouponDialog>
@@ -142,6 +111,7 @@ import editMoneyModalConfig, { addOptions, reduceOptions } from "./config/edit-m
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
import shopUserApi from "@/api/account/shopUser";
import { useRoute } from 'vue-router'
const editMoneyModalRef = ref(null);
const userCouponDialogRef = ref(null);
const GiveCouponRef = ref(null);
@@ -165,6 +135,9 @@ const {
} = usePage();
function searchQueryClick(e) {
console.log(e);
handleQueryClick(e);
// 获取统计数据
getSummary();
@@ -224,7 +197,7 @@ function handleToolbarClick(name) {
}
// 赠送券
function toGiveCoupon() {}
function toGiveCoupon() { }
// 其他操作列
async function handleOperatClick(data) {
const row = data.row;
@@ -250,7 +223,14 @@ async function handleOperatClick(data) {
}
}
const route = useRoute()
onMounted(() => {
if (route.query.phone) {
searchQueryClick({
key: route.query.phone
})
}
getSummary(searchRef.value.getQueryParams());
});
</script>
@@ -258,6 +238,7 @@ onMounted(() => {
.align-center {
align-items: center;
}
.card {
background-color: #f5f5f5;
padding: 0 14px;