update:退款&banner&用户订单
This commit is contained in:
parent
6186c7723c
commit
47ed9526b7
2
.env
2
.env
@ -8,4 +8,4 @@ VITE_OPEN = true
|
||||
VITE_OPEN_CDN = false
|
||||
|
||||
# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
|
||||
VITE_PUBLIC_PATH = 'https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_web/202603261000/'
|
||||
VITE_PUBLIC_PATH = 'https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_web/202604011515/'
|
||||
138
src/api/match/banner.js
Normal file
138
src/api/match/banner.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { BaseApi } from '../baseApi';
|
||||
import { http } from '../../utils/request';
|
||||
|
||||
// ==================== 轮播图服务 ====================
|
||||
|
||||
/**
|
||||
* 获取轮播图配置模式
|
||||
* @returns {Promise<{
|
||||
* mode: number
|
||||
* }>}
|
||||
*/
|
||||
export const bannerConfigGet = () => {
|
||||
return http.get('/banner/config');
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新轮播图配置模式
|
||||
* @param {Object} data - 配置参数
|
||||
* @param {number} data.mode - 配置模式 (0: 默认模式, 1: 自定义模式等)
|
||||
*/
|
||||
export const bannerConfigPut = (data) => {
|
||||
return http.put('/banner/config', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取轮播图列表(前台展示用)
|
||||
* @returns {Promise<Array<{
|
||||
* id: number,
|
||||
* title: string,
|
||||
* imageUrl: string,
|
||||
* linkType: string, // 链接类型 (Event=赛事链接, Url=外部链接)
|
||||
* linkUrl: string, // 跳转链接
|
||||
* eventId: number, // 关联赛事ID (linkType为Event时使用)
|
||||
* sort: number, // 排序号
|
||||
* status: string, // 状态 (Enable=启用, Disable=禁用)
|
||||
* remark: string, // 备注
|
||||
* createdTime: string
|
||||
* }>>}
|
||||
*/
|
||||
export const bannerListGet = () => {
|
||||
return http.get('/banner/list');
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页查询(后台管理用)
|
||||
* @param {Object} data - 查询参数
|
||||
* @param {number} [data.pageNo] - 当前页码
|
||||
* @param {number} [data.pageSize] - 每页条数
|
||||
* @param {string} [data.title] - 标题 (模糊查询)
|
||||
* @returns {Promise<{
|
||||
* pageNo: number,
|
||||
* pageSize: number,
|
||||
* pages: number,
|
||||
* total: number,
|
||||
* rows: Array<{
|
||||
* id: number,
|
||||
* title: string,
|
||||
* imageUrl: string,
|
||||
* linkType: string,
|
||||
* linkUrl: string,
|
||||
* eventId: number,
|
||||
* sort: number,
|
||||
* status: string,
|
||||
* remark: string,
|
||||
* createdTime: string
|
||||
* }>
|
||||
* }>}
|
||||
*/
|
||||
export const bannerPagePost = (data) => {
|
||||
return http.post('/banner/page', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加轮播图
|
||||
* @param {Object} data - 轮播图信息
|
||||
* @param {string} data.title - 标题
|
||||
* @param {string} data.imageUrl - 图片URL
|
||||
* @param {string} data.linkType - 链接类型 (Event=赛事链接, Url=外部链接)
|
||||
* @param {string} [data.linkUrl] - 跳转链接 (linkType为Url时必填)
|
||||
* @param {number} data.eventId - 关联赛事ID,字段名 eventId (linkType 为 Event 时必传)
|
||||
* @param {number} [data.sort] - 排序号
|
||||
* @param {string} [data.remark] - 备注
|
||||
*/
|
||||
export const bannerAddPost = (data) => {
|
||||
return http.post('/banner', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新轮播图
|
||||
* @param {Object} data - 轮播图信息
|
||||
* @param {number} data.id - 轮播图ID
|
||||
* @param {string} data.title - 标题
|
||||
* @param {string} data.imageUrl - 图片URL
|
||||
* @param {string} data.linkType - 链接类型
|
||||
* @param {string} [data.linkUrl] - 跳转链接
|
||||
* @param {number} data.eventId - 关联赛事ID,字段名 eventId (linkType 为 Event 时必传)
|
||||
* @param {number} [data.sort] - 排序号
|
||||
* @param {string} [data.remark] - 备注
|
||||
* @param {string} [data.status] - 状态 (Enable, Disable)
|
||||
*/
|
||||
export const bannerEditPut = (data) => {
|
||||
return http.put('/banner', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除轮播图
|
||||
* @param {Array<number>} data - 轮播图ID数组
|
||||
*/
|
||||
export const bannerDelete = (data) => {
|
||||
return http.delete('/banner', { data });
|
||||
};
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* @param {Array<number>} data - 轮播图ID数组 (按顺序排列)
|
||||
*/
|
||||
export const bannerSortPost = (data) => {
|
||||
return http.post('/banner/sort', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除单个轮播图
|
||||
* @param {Object} data - 删除参数
|
||||
* @param {number} data.id - 轮播图ID
|
||||
*/
|
||||
export const bannerDeleteDelete = (data) => {
|
||||
return http.delete('/banner/delete', { data });
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改轮播图状态
|
||||
* @param {Object} data - 状态参数
|
||||
* @param {number} data.id - 轮播图ID
|
||||
* @param {string} data.status - 状态 (Enable=启用, Disable=禁用)
|
||||
*/
|
||||
export const bannerSetstatusPatch = (data) => {
|
||||
return http.patch('/banner/setstatus', data);
|
||||
};
|
||||
99
src/api/match/refund.js
Normal file
99
src/api/match/refund.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { BaseApi } from '../baseApi';
|
||||
import { http } from '../../utils/request';
|
||||
|
||||
// ==================== 退款配置服务 ====================
|
||||
|
||||
/**
|
||||
* 获取所有退款配置
|
||||
* @returns {Promise<Array<{
|
||||
* id: number,
|
||||
* dayStart: number,
|
||||
* dayEnd: number,
|
||||
* deductPercentage: number,
|
||||
* remark: string,
|
||||
* status: string
|
||||
* }>>}
|
||||
*/
|
||||
export const refundconfigListGet = () => {
|
||||
return http.get('/refundconfig/list');
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页查询退款配置
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} [params.Status] - 状态 (Enable=启用, Disable=禁用)
|
||||
* @param {number} [params.PageNo] - 当前页码
|
||||
* @param {number} [params.PageSize] - 每页条数
|
||||
* @returns {Promise<{
|
||||
* pageNo: number,
|
||||
* pageSize: number,
|
||||
* pages: number,
|
||||
* total: number,
|
||||
* rows: Array<{
|
||||
* id: number,
|
||||
* dayStart: number,
|
||||
* dayEnd: number,
|
||||
* deductPercentage: number,
|
||||
* remark: string,
|
||||
* status: string
|
||||
* }>
|
||||
* }>}
|
||||
*/
|
||||
export const refundconfigPageGet = (params) => {
|
||||
return http.get('/refundconfig/page', { params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加退款配置
|
||||
* @param {Object} data - 退款配置信息
|
||||
* @param {number} [data.id] - 配置ID (添加时可不传)
|
||||
* @param {number} data.dayStart - 开始天数 (如比赛开始前多少天)
|
||||
* @param {number} data.dayEnd - 结束天数
|
||||
* @param {number} data.deductPercentage - 扣除百分比 (如20表示扣除20%)
|
||||
* @param {string} [data.remark] - 备注
|
||||
* @param {string} [data.status] - 状态 (Enable=启用, Disable=禁用,默认Enable)
|
||||
*/
|
||||
export const refundconfigAddPost = (data) => {
|
||||
return http.post('/refundconfig/add', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新退款配置
|
||||
* @param {Object} data - 退款配置信息
|
||||
* @param {number} data.id - 配置ID
|
||||
* @param {number} data.dayStart - 开始天数
|
||||
* @param {number} data.dayEnd - 结束天数
|
||||
* @param {number} data.deductPercentage - 扣除百分比
|
||||
* @param {string} [data.remark] - 备注
|
||||
* @param {string} [data.status] - 状态 (Enable, Disable)
|
||||
*/
|
||||
export const refundconfigUpdatePut = (data) => {
|
||||
return http.put('/refundconfig/update', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除退款配置
|
||||
* @param {Array<number>} data - 配置ID数组
|
||||
*/
|
||||
export const refundconfigDeletePost = (data) => {
|
||||
return http.post('/refundconfig/delete', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除单个退款配置
|
||||
* @param {Object} data - 删除参数
|
||||
* @param {number} data.id - 配置ID
|
||||
*/
|
||||
export const refundconfigDeleteDelete = (data) => {
|
||||
return http.delete('/refundconfig/delete', { data });
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改退款配置状态
|
||||
* @param {Object} data - 状态参数
|
||||
* @param {number} data.id - 配置ID
|
||||
* @param {string} data.status - 状态 (Enable=启用, Disable=禁用)
|
||||
*/
|
||||
export const refundconfigSetstatusPatch = (data) => {
|
||||
return http.patch('/refundconfig/setstatus', data);
|
||||
};
|
||||
@ -416,6 +416,52 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
|
||||
icon: 'iconfont icon-shouye',
|
||||
},
|
||||
},
|
||||
//订单管理
|
||||
{
|
||||
path: '/user/order',
|
||||
name: 'userorder',
|
||||
component: () => import('/@/views/marchant/order/index.vue'),
|
||||
meta: {
|
||||
title: '订单管理',
|
||||
isLink: '',
|
||||
isHide: false,
|
||||
isKeepAlive: false,
|
||||
isAffix: false,
|
||||
isIframe: false,
|
||||
roles: ['admin', 'common'],
|
||||
icon: 'iconfont icon-shouye',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/banner',
|
||||
name: 'banner',
|
||||
component: () => import('/@/views/match/banner/index.vue'),
|
||||
meta: {
|
||||
title: '轮播图管理',
|
||||
isLink: '',
|
||||
isHide: false,
|
||||
isKeepAlive: false,
|
||||
isAffix: false,
|
||||
isIframe: false,
|
||||
roles: ['admin', 'common'],
|
||||
icon: 'iconfont icon-shouye',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/refund',
|
||||
name: 'refund',
|
||||
component: () => import('/@/views/match/refund/index.vue'),
|
||||
meta: {
|
||||
title: '退款配置',
|
||||
isLink: '',
|
||||
isHide: false,
|
||||
isKeepAlive: false,
|
||||
isAffix: false,
|
||||
isIframe: false,
|
||||
roles: ['admin', 'common'],
|
||||
icon: 'iconfont icon-shouye',
|
||||
},
|
||||
},
|
||||
//人员管理
|
||||
{
|
||||
path: '/user/personnel',
|
||||
|
||||
161
src/views/marchant/order/index.vue
Normal file
161
src/views/marchant/order/index.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="system-menu-container layout-padding">
|
||||
<ProTable ref="tableRef" :request-api="orderPageGet" :columns="columns" :pagination="true"
|
||||
:tool-button="true">
|
||||
<!-- <template #tools>
|
||||
<el-button v-auth="'order:export'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 导出 </el-button>
|
||||
</template> -->
|
||||
<template #index="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
<template #paymentMethod="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
</template>
|
||||
<template #action="scope">
|
||||
<el-button size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
|
||||
查看详情 </el-button>
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<el-dialog title="订单详情" v-model="isShowDialog" width="1100px">
|
||||
<el-skeleton :rows="6" animated v-if="detailLoading" />
|
||||
<el-empty v-else-if="!detailTableData.length" description="暂无订单明细数据" />
|
||||
<el-table v-else :data="detailTableData" border>
|
||||
<el-table-column prop="projectName" label="项目" align="center" />
|
||||
<el-table-column prop="groupName" label="组别" align="center" />
|
||||
<el-table-column prop="participantNames" label="参赛队员" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="fee" label="费用(元)" align="center" width="120" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="isShowDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="isShowDialog = false">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup >
|
||||
import { ref, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import ProTable from '/@/components/ProTable/index.vue';
|
||||
import { orderPageGet , orderDetailGet} from '/@/api/match/order';
|
||||
|
||||
// 定义表单引用类型
|
||||
const formRef = ref();
|
||||
|
||||
const formData = ref({
|
||||
orderNo: '',
|
||||
eventId: 0,
|
||||
eventName: '',
|
||||
registrationMode: 0,
|
||||
teamName: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
totalAmount: 0,
|
||||
discountAmount: 0,
|
||||
payableAmount: 0,
|
||||
status: 0,
|
||||
createdTime: '',
|
||||
paidTime: '',
|
||||
items:[],
|
||||
persons:[]
|
||||
})
|
||||
|
||||
|
||||
|
||||
const columns = ref([
|
||||
{ prop: 'orderNo', label: '订单编号', align: 'center', search: { el: 'input', }, fixed: 'left', width: 150 },
|
||||
{ prop: 'eventName', label: '所属赛事', search: { el: 'input', }, },
|
||||
{ prop: 'paymentMethod', label: '支付方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '线上', value: 1 }, { label: '线下', value: 2 },], },
|
||||
{ prop: 'actualAmount', label: '实付金额' },
|
||||
{ prop: 'createdTime', label: '支付时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
|
||||
{ prop: 'orderStatus', label: '订单状态' },
|
||||
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, },
|
||||
]);
|
||||
|
||||
const isShowDialog = ref(false);
|
||||
const dialogTitle = ref('新增');
|
||||
const detailLoading = ref(false);
|
||||
|
||||
const normalizeNames = (value) => {
|
||||
if (Array.isArray(value)) return value.filter(Boolean).join('、');
|
||||
if (typeof value === 'string') return value;
|
||||
return '';
|
||||
};
|
||||
|
||||
const detailTableData = computed(() => {
|
||||
const items = formData.value?.items ?? [];
|
||||
const persons = formData.value?.persons ?? [];
|
||||
return items.map((item) => {
|
||||
const personNamesByItem = persons
|
||||
.filter((p) => p?.itemId === item?.id || p?.projectId === item?.projectId)
|
||||
.map((p) => p?.name)
|
||||
.filter(Boolean);
|
||||
const participantNames = normalizeNames(
|
||||
item?.participantNames ?? item?.personNames ?? item?.memberNames ?? personNamesByItem
|
||||
) || '-';
|
||||
const fee = item?.subtotal ?? ((Number(item?.unitPrice) || 0) * (Number(item?.personCount) || 0));
|
||||
return {
|
||||
projectName: item?.projectName ?? '-',
|
||||
groupName: item?.groupName ?? item?.projectTypeName ?? '-',
|
||||
participantNames,
|
||||
fee,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const onOpenMenu = async(id) => {
|
||||
if (!id) return;
|
||||
isShowDialog.value = true;
|
||||
detailLoading.value = true;
|
||||
const { succeeded, data } = await orderDetailGet({ id });
|
||||
if (!succeeded) {
|
||||
ElMessage.error('订单详情加载失败');
|
||||
formData.value.items = [];
|
||||
formData.value.persons = [];
|
||||
detailLoading.value = false;
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
...(data ?? {}),
|
||||
items: data?.items ?? [],
|
||||
persons: data?.persons ?? [],
|
||||
};
|
||||
detailLoading.value = false;
|
||||
isShowDialog.value = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.dialog-footer {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
/* 时长组合框样式 */
|
||||
.duration-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.duration-row .el-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.duration-separator {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.duration-label {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
627
src/views/match/banner/index.vue
Normal file
627
src/views/match/banner/index.vue
Normal file
@ -0,0 +1,627 @@
|
||||
<template>
|
||||
<div class="banner-config layout-padding system-menu-container">
|
||||
<el-card shadow="hover" class="banner-card">
|
||||
<div class="mode-bar">
|
||||
<el-radio-group v-model="configMode" size="large">
|
||||
<el-radio-button :label="MODE_MANUAL">手动配置</el-radio-button>
|
||||
<el-radio-button :label="MODE_AUTO">自动配置</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 自动 -->
|
||||
<div v-if="configMode === MODE_AUTO" class="auto-panel" v-loading="loading">
|
||||
<p class="auto-desc">默认跳转最新的 4 场赛事</p>
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" :loading="saving" @click="onSave">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动 -->
|
||||
<div v-else class="manual-layout" v-loading="loading">
|
||||
<div class="manual-left">
|
||||
<div class="left-head">
|
||||
<h3 class="left-title">轮播图配置</h3>
|
||||
<p class="left-tip">最多添加四张轮播图,拖动可调整轮播顺序</p>
|
||||
</div>
|
||||
<ul ref="slotListRef" class="slot-list">
|
||||
<li
|
||||
v-for="(slot, index) in slots"
|
||||
:key="slot._key"
|
||||
class="slot-item"
|
||||
:class="{ 'is-active': selectedIndex === index }"
|
||||
@click="selectedIndex = index"
|
||||
>
|
||||
<span class="drag-handle" title="拖动排序">⋮⋮</span>
|
||||
<div class="slot-thumb">
|
||||
<img v-if="slot.imageUrl" :src="slot.imageUrl" alt="" />
|
||||
<el-icon v-else :size="32" class="slot-placeholder-icon"><Picture /></el-icon>
|
||||
</div>
|
||||
<span class="slot-idx">{{ index + 1 }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="manual-right">
|
||||
<el-radio-group v-model="configTab" class="sub-tabs">
|
||||
<el-radio-button label="image">配置图片</el-radio-button>
|
||||
<el-radio-button label="event">配置赛事</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="sub-panel">
|
||||
<div v-show="configTab === 'image'" class="upload-wrap">
|
||||
<UploadImg
|
||||
:image-url="currentSlot.imageUrl"
|
||||
width="100%"
|
||||
height="200px"
|
||||
border-radius="8px"
|
||||
@update:image-url="onImageUrlUpdate"
|
||||
>
|
||||
<template #empty>
|
||||
<el-icon :size="28"><Plus /></el-icon>
|
||||
<span>上传</span>
|
||||
</template>
|
||||
</UploadImg>
|
||||
</div>
|
||||
<div v-show="configTab === 'event'" class="event-wrap">
|
||||
<el-select
|
||||
:model-value="eventSelectValue"
|
||||
class="event-select"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="remoteSearchEvent"
|
||||
:loading="eventLoading"
|
||||
placeholder="搜索并选择关联赛事"
|
||||
@update:model-value="onEventSelectModel"
|
||||
@change="onEventChange"
|
||||
@visible-change="onEventSelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in eventOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<p class="event-hint">前台点击该轮播将跳转至所选赛事详情</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" :loading="saving" @click="onSave">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="matchBanner">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Picture, Plus } from '@element-plus/icons-vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import UploadImg from '/@/components/Upload/Img.vue';
|
||||
import {
|
||||
bannerConfigGet,
|
||||
bannerConfigPut,
|
||||
bannerPagePost,
|
||||
bannerAddPost,
|
||||
bannerEditPut,
|
||||
bannerDelete,
|
||||
bannerSortPost,
|
||||
} from '/@/api/match/banner';
|
||||
import { eventPagePost, eventDetailGet } from '/@/api/match/event';
|
||||
|
||||
const MODE_AUTO = 0;
|
||||
const MODE_MANUAL = 1;
|
||||
|
||||
let slotKeySeed = 0;
|
||||
const nextSlotKey = () => ++slotKeySeed;
|
||||
|
||||
const createEmptySlot = () => ({
|
||||
_key: nextSlotKey(),
|
||||
id: undefined,
|
||||
imageUrl: '',
|
||||
eventId: null,
|
||||
eventName: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
const configMode = ref(MODE_MANUAL);
|
||||
const configTab = ref('image');
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const selectedIndex = ref(0);
|
||||
const slots = ref([createEmptySlot(), createEmptySlot(), createEmptySlot(), createEmptySlot()]);
|
||||
const initialBannerIds = ref([]);
|
||||
|
||||
const slotListRef = ref(null);
|
||||
let sortableInstance = null;
|
||||
|
||||
const eventLoading = ref(false);
|
||||
const eventOptions = ref([]);
|
||||
|
||||
const currentSlot = computed(() => slots.value[selectedIndex.value] ?? slots.value[0]);
|
||||
|
||||
/** 与接口、el-option 的 value 统一为 number,避免 123 与 "123" 导致无法回显 */
|
||||
function toEventId(v) {
|
||||
if (v === null || v === undefined || v === '') return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/** el-select 绑定值(必须为与 options 中 value 同类型) */
|
||||
const eventSelectValue = computed(() => toEventId(currentSlot.value?.eventId));
|
||||
|
||||
function onEventSelectModel(v) {
|
||||
const s = slots.value[selectedIndex.value];
|
||||
if (!s) return;
|
||||
s.eventId = toEventId(v);
|
||||
}
|
||||
|
||||
/** 用于下拉展示:优先用已拉取的赛事名,其次从标题「轮播:xxx」推断 */
|
||||
function labelForSlotEvent(s) {
|
||||
const n = (s.eventName || '').trim();
|
||||
if (n) return n;
|
||||
const t = (s.title || '').trim();
|
||||
if (t.startsWith('轮播:')) return t.slice(3).trim() || t;
|
||||
return t || `赛事 #${s.eventId}`;
|
||||
}
|
||||
|
||||
/** 把当前四个槽位里已选的赛事都放进 options,保证 el-select 能回显 */
|
||||
function syncEventOptionsFromSlots() {
|
||||
const map = new Map();
|
||||
for (const s of slots.value) {
|
||||
const id = toEventId(s.eventId);
|
||||
if (id !== s.eventId) s.eventId = id;
|
||||
if (id == null) continue;
|
||||
map.set(id, { value: id, label: labelForSlotEvent(s) });
|
||||
}
|
||||
eventOptions.value = Array.from(map.values());
|
||||
}
|
||||
|
||||
/** 按 eventId 拉取赛事名称,便于后台编辑时与列表一致 */
|
||||
async function hydrateSlotEventNames() {
|
||||
await Promise.all(
|
||||
slots.value.map(async (s) => {
|
||||
const id = toEventId(s.eventId);
|
||||
if (id == null) return;
|
||||
try {
|
||||
const res = await eventDetailGet(id);
|
||||
const name = res?.data?.eventName;
|
||||
if (name) s.eventName = name;
|
||||
} catch {
|
||||
// 保留标题/占位文案
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function updateSelectedIndexAfterDrag(oldIndex, newIndex, sel) {
|
||||
if (sel === oldIndex) return newIndex;
|
||||
if (oldIndex < newIndex) {
|
||||
if (sel > oldIndex && sel <= newIndex) return sel - 1;
|
||||
} else if (oldIndex > newIndex) {
|
||||
if (sel >= newIndex && sel < oldIndex) return sel + 1;
|
||||
}
|
||||
return sel;
|
||||
}
|
||||
|
||||
function initSortable() {
|
||||
const el = slotListRef.value;
|
||||
if (!el) return;
|
||||
sortableInstance?.destroy();
|
||||
sortableInstance = Sortable.create(el, {
|
||||
animation: 180,
|
||||
handle: '.drag-handle',
|
||||
onEnd(evt) {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return;
|
||||
const list = slots.value;
|
||||
const moved = list.splice(oldIndex, 1)[0];
|
||||
list.splice(newIndex, 0, moved);
|
||||
selectedIndex.value = updateSelectedIndexAfterDrag(oldIndex, newIndex, selectedIndex.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function remoteSearchEvent(query) {
|
||||
eventLoading.value = true;
|
||||
try {
|
||||
const res = await eventPagePost({
|
||||
pageNo: 1,
|
||||
pageSize: 40,
|
||||
...(query ? { eventName: query } : {}),
|
||||
});
|
||||
const rows = res?.data?.rows ?? [];
|
||||
const fromApi = rows
|
||||
.map((r) => {
|
||||
const id = toEventId(r.id ?? r.eventId);
|
||||
if (id == null) return null;
|
||||
const label = r.eventName ?? r.EventName ?? '';
|
||||
return { label, value: id };
|
||||
})
|
||||
.filter(Boolean);
|
||||
const map = new Map(fromApi.map((o) => [o.value, o]));
|
||||
for (const s of slots.value) {
|
||||
const sid = toEventId(s.eventId);
|
||||
if (sid == null) continue;
|
||||
if (!map.has(sid)) {
|
||||
map.set(sid, { value: sid, label: labelForSlotEvent(s) });
|
||||
}
|
||||
}
|
||||
eventOptions.value = Array.from(map.values());
|
||||
} catch {
|
||||
syncEventOptionsFromSlots();
|
||||
} finally {
|
||||
eventLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onEventSelectVisible(visible) {
|
||||
if (visible) remoteSearchEvent('');
|
||||
}
|
||||
|
||||
function onEventChange(id) {
|
||||
const nid = toEventId(id);
|
||||
const row = eventOptions.value.find((o) => toEventId(o.value) === nid);
|
||||
const s = slots.value[selectedIndex.value];
|
||||
if (!s) return;
|
||||
s.eventId = nid;
|
||||
s.eventName = row?.label ?? '';
|
||||
if (nid) s.title = row?.label ? `轮播:${row.label}` : `轮播图${selectedIndex.value + 1}`;
|
||||
else s.title = '';
|
||||
syncEventOptionsFromSlots();
|
||||
}
|
||||
|
||||
function onImageUrlUpdate(url) {
|
||||
const s = slots.value[selectedIndex.value];
|
||||
if (s) s.imageUrl = url || '';
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await bannerConfigGet();
|
||||
if (res?.succeeded && res.data && typeof res.data.mode === 'number') {
|
||||
configMode.value = res.data.mode;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBanners() {
|
||||
const res = await bannerPagePost({ pageNo: 1, pageSize: 20 });
|
||||
const rows = res?.data?.rows ?? [];
|
||||
const sorted = [...rows].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||
initialBannerIds.value = sorted.map((r) => r.id).filter(Boolean);
|
||||
|
||||
const next = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const r = sorted[i];
|
||||
if (r) {
|
||||
const rawEv = r.eventId ?? r.EventId;
|
||||
next.push({
|
||||
_key: nextSlotKey(),
|
||||
id: r.id,
|
||||
imageUrl: r.imageUrl || '',
|
||||
eventId: toEventId(rawEv),
|
||||
eventName: r.title || '',
|
||||
title: r.title || `轮播图${i + 1}`,
|
||||
});
|
||||
} else {
|
||||
next.push(createEmptySlot());
|
||||
}
|
||||
}
|
||||
slots.value = next;
|
||||
selectedIndex.value = 0;
|
||||
await hydrateSlotEventNames();
|
||||
syncEventOptionsFromSlots();
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadConfig();
|
||||
await loadBanners();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateManual() {
|
||||
const list = slots.value;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const s = list[i];
|
||||
const partial = !!(s.imageUrl || s.eventId);
|
||||
if (partial && (!s.imageUrl || !s.eventId)) {
|
||||
ElMessage.warning(`第 ${i + 1} 张需同时上传图片并选择关联赛事`);
|
||||
selectedIndex.value = i;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const filled = list.filter((s) => s.imageUrl && s.eventId);
|
||||
if (!filled.length) {
|
||||
ElMessage.warning('请至少完整配置一张轮播图(图片 + 赛事)');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
saving.value = true;
|
||||
try {
|
||||
if (configMode.value === MODE_AUTO) {
|
||||
const res = await bannerConfigPut({ mode: MODE_AUTO });
|
||||
if (res?.succeeded === false) return;
|
||||
ElMessage.success('保存成功');
|
||||
await loadConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateManual()) return;
|
||||
|
||||
const resMode = await bannerConfigPut({ mode: MODE_MANUAL });
|
||||
if (resMode?.succeeded === false) return;
|
||||
|
||||
const filledSlots = slots.value.filter((s) => s.imageUrl && s.eventId);
|
||||
const keptIds = new Set();
|
||||
|
||||
for (let i = 0; i < filledSlots.length; i++) {
|
||||
const s = filledSlots[i];
|
||||
const sort = i + 1;
|
||||
// 接口要求关联赛事使用 eventId 字段(数字)
|
||||
const eventId = Number(s.eventId);
|
||||
if (!Number.isFinite(eventId) || eventId <= 0) {
|
||||
ElMessage.warning(`第 ${sort} 条轮播的关联赛事无效,请重新选择`);
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
title: s.title || s.eventName || `轮播图${sort}`,
|
||||
imageUrl: s.imageUrl,
|
||||
linkType: 'Event',
|
||||
eventId,
|
||||
sort,
|
||||
status: 'Enable',
|
||||
};
|
||||
if (s.id) {
|
||||
const r = await bannerEditPut({ id: s.id, ...payload });
|
||||
if (r?.succeeded === false) return;
|
||||
keptIds.add(s.id);
|
||||
} else {
|
||||
const r = await bannerAddPost(payload);
|
||||
if (r?.succeeded === false) return;
|
||||
const raw = r?.data;
|
||||
const newId = typeof raw === 'number' ? raw : raw?.id;
|
||||
if (typeof newId === 'number') keptIds.add(newId);
|
||||
}
|
||||
}
|
||||
|
||||
const toRemove = initialBannerIds.value.filter((id) => !keptIds.has(id));
|
||||
if (toRemove.length) {
|
||||
const r = await bannerDelete(toRemove);
|
||||
if (r?.succeeded === false) return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadBanners();
|
||||
const orderedIds = slots.value.map((s) => s.id).filter((id) => id != null);
|
||||
if (orderedIds.length) {
|
||||
const rSort = await bannerSortPost(orderedIds);
|
||||
if (rSort?.succeeded === false) return;
|
||||
await loadBanners();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
ElMessage.success('保存成功');
|
||||
} catch {
|
||||
ElMessage.error('保存失败,请稍后重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedIndex.value,
|
||||
() => {
|
||||
syncEventOptionsFromSlots();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => configTab.value,
|
||||
(tab) => {
|
||||
if (tab === 'event' && configMode.value === MODE_MANUAL) {
|
||||
syncEventOptionsFromSlots();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => configMode.value,
|
||||
async (m) => {
|
||||
if (m === MODE_MANUAL) {
|
||||
await nextTick();
|
||||
initSortable();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAll();
|
||||
await nextTick();
|
||||
if (configMode.value === MODE_MANUAL) initSortable();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sortableInstance?.destroy();
|
||||
sortableInstance = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.banner-config {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.banner-card {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.mode-bar {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auto-panel {
|
||||
padding: 24px 8px 8px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.auto-desc {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
margin: 0 0 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.manual-layout {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manual-left {
|
||||
flex: 0 0 260px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.left-head {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.left-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.left-tip {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.slot-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.slot-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: var(--el-fill-color-blank);
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
letter-spacing: -2px;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.slot-thumb {
|
||||
width: 72px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.slot-placeholder-icon {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.slot-idx {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.manual-right {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sub-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-panel {
|
||||
min-height: 220px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.upload-wrap {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.event-select {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.event-hint {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
@ -8,6 +8,15 @@
|
||||
<template #index="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
<template #paymentMethod="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
</template>
|
||||
<template #orderType="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
</template>
|
||||
<template #registrationMode="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
</template>
|
||||
<template #action="scope">
|
||||
<el-button size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
|
||||
查看详情 </el-button>
|
||||
@ -65,20 +74,20 @@ const formData = ref({
|
||||
|
||||
const columns = ref([
|
||||
{ prop: 'orderNo', label: '订单编号', align: 'center', search: { el: 'input', }, fixed: 'left', width: 150 },
|
||||
{ prop: 'unitName', label: '单位名称', align: 'center', },
|
||||
{ prop: 'eventName', label: '所属赛事', search: { el: 'input', }, },
|
||||
{ prop: 'orderType', label: '订单类型', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '收款', value: 1 }, { label: '退款', value: 2 },], },
|
||||
{ prop: 'relatedOrderNo', label: '关联订单' },
|
||||
{ prop: 'paymentMethod', label: '支付方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '线上', value: 1 }, { label: '线下', value: 2 },], },
|
||||
{ prop: 'paymentProofUrl', label: '付款凭证' },
|
||||
{ prop: 'registrationMode', label: '报名方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '个人', value: 1 }, { label: '团队', value: 2 },], },
|
||||
{ prop: 'contactName', label: '报名用户' },
|
||||
{ prop: 'contactPhone', label: '报名账号', search: { el: 'input' } },
|
||||
{ prop: 'originalAmount', label: '原金额' },
|
||||
{ prop: 'discountAmount', label: '优惠金额' },
|
||||
{ prop: 'actualAmount', label: '实付金额' },
|
||||
{ prop: 'orderStatus', label: '订单状态' },
|
||||
{ prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
|
||||
{ width: 180, prop: 'unitName', label: '单位名称', align: 'center', },
|
||||
{ width: 120, prop: 'eventName', label: '所属赛事', search: { el: 'input', }, },
|
||||
{ width: 120, prop: 'orderType', label: '订单类型', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '收款', value: 1 }, { label: '退款', value: 2 },], },
|
||||
{ width: 120, prop: 'relatedOrderNo', label: '关联订单' },
|
||||
{ width: 120, prop: 'paymentMethod', label: '支付方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '线上', value: 1 }, { label: '线下', value: 2 },], },
|
||||
{ width: 120, prop: 'paymentProofUrl', label: '付款凭证' },
|
||||
{ width: 120, prop: 'registrationMode', label: '报名方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '个人', value: 1 }, { label: '团队', value: 2 },], },
|
||||
{ width: 120, prop: 'contactName', label: '报名用户' },
|
||||
{ width: 120, prop: 'contactPhone', label: '报名账号', search: { el: 'input' } },
|
||||
{ width: 120, prop: 'originalAmount', label: '原金额' },
|
||||
{ width: 120, prop: 'discountAmount', label: '优惠金额' },
|
||||
{ width: 120, prop: 'actualAmount', label: '实付金额' },
|
||||
{ width: 120, prop: 'orderStatus', label: '订单状态' },
|
||||
{ width: 120, prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
|
||||
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, },
|
||||
]);
|
||||
|
||||
|
||||
200
src/views/match/refund/index.vue
Normal file
200
src/views/match/refund/index.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="refund-config layout-padding system-menu-container">
|
||||
<el-card shadow="hover" class="refund-card" v-loading="loading">
|
||||
<h3 class="page-title">退款配置</h3>
|
||||
<p class="page-desc">按距开赛天数区间设置退款时扣除比例(百分比)</p>
|
||||
|
||||
<el-form label-position="right" label-width="200px" class="refund-form">
|
||||
<el-form-item v-for="row in rows" :key="row.key" :label="row.label">
|
||||
<div class="pct-row">
|
||||
<el-input
|
||||
v-model="row.deductPercentage"
|
||||
class="pct-input"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="onPctInput(row)"
|
||||
/>
|
||||
<span class="pct-suffix">%</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="matchRefund">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
refundconfigListGet,
|
||||
refundconfigAddPost,
|
||||
refundconfigUpdatePut,
|
||||
} from '/@/api/match/refund';
|
||||
|
||||
/** 固定三条区间:开赛前天数 [dayStart, dayEnd](含) */
|
||||
const ROW_SPECS = [
|
||||
{ key: 'before7', label: '开赛前7天扣除金额', dayStart: 7, dayEnd: 999 },
|
||||
{ key: 'd3to7', label: '3-7天扣除金额', dayStart: 3, dayEnd: 7 },
|
||||
{ key: 'within3', label: '3天内扣除金额', dayStart: 0, dayEnd: 3 },
|
||||
];
|
||||
|
||||
function normalizeItem(raw) {
|
||||
const id = raw?.id ?? raw?.Id;
|
||||
const dayStart = Number(raw?.dayStart ?? raw?.DayStart);
|
||||
const dayEnd = Number(raw?.dayEnd ?? raw?.DayEnd);
|
||||
const deductPercentage = raw?.deductPercentage ?? raw?.DeductPercentage;
|
||||
return {
|
||||
id: id != null ? Number(id) : undefined,
|
||||
dayStart,
|
||||
dayEnd,
|
||||
deductPercentage: deductPercentage == null || deductPercentage === '' ? '' : String(deductPercentage),
|
||||
};
|
||||
}
|
||||
|
||||
function extractList(res) {
|
||||
const d = res?.data;
|
||||
if (Array.isArray(d)) return d;
|
||||
if (Array.isArray(d?.rows)) return d.rows;
|
||||
return [];
|
||||
}
|
||||
|
||||
function findByRange(list, dayStart, dayEnd) {
|
||||
return list.find((item) => {
|
||||
const n = normalizeItem(item);
|
||||
return n.dayStart === dayStart && n.dayEnd === dayEnd;
|
||||
});
|
||||
}
|
||||
|
||||
const rows = ref(
|
||||
ROW_SPECS.map((s) => ({
|
||||
...s,
|
||||
id: undefined,
|
||||
deductPercentage: '',
|
||||
}))
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
function onPctInput(row) {
|
||||
let v = String(row.deductPercentage ?? '').replace(/[^\d.]/g, '');
|
||||
const parts = v.split('.');
|
||||
if (parts.length > 2) v = `${parts[0]}.${parts.slice(1).join('')}`;
|
||||
if (parts[0]?.length > 1 && parts[0].startsWith('0') && !v.includes('.')) {
|
||||
v = String(Number(parts[0]));
|
||||
}
|
||||
row.deductPercentage = v;
|
||||
}
|
||||
|
||||
function parsePct(row) {
|
||||
const n = Number(row.deductPercentage);
|
||||
if (row.deductPercentage === '' || row.deductPercentage == null || Number.isNaN(n)) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await refundconfigListGet();
|
||||
const list = extractList(res).map((x) => normalizeItem(x));
|
||||
for (const row of rows.value) {
|
||||
const hit = findByRange(list, row.dayStart, row.dayEnd);
|
||||
if (hit && hit.id != null) {
|
||||
row.id = hit.id;
|
||||
row.deductPercentage =
|
||||
hit.deductPercentage === '' || hit.deductPercentage == null ? '' : String(hit.deductPercentage);
|
||||
} else {
|
||||
row.id = undefined;
|
||||
row.deductPercentage = '';
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
for (const row of rows.value) {
|
||||
const n = parsePct(row);
|
||||
if (n === null) {
|
||||
ElMessage.warning(`请填写「${row.label}」的扣除比例`);
|
||||
return;
|
||||
}
|
||||
if (n < 0 || n > 100) {
|
||||
ElMessage.warning(`「${row.label}」扣除比例需在 0~100 之间`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const tasks = rows.value.map(async (row) => {
|
||||
const n = parsePct(row);
|
||||
const payload = {
|
||||
dayStart: row.dayStart,
|
||||
dayEnd: row.dayEnd,
|
||||
deductPercentage: n,
|
||||
status: 'Enable',
|
||||
remark: row.label,
|
||||
};
|
||||
if (row.id != null) {
|
||||
return refundconfigUpdatePut({ ...payload, id: row.id });
|
||||
}
|
||||
return refundconfigAddPost(payload);
|
||||
});
|
||||
const results = await Promise.all(tasks);
|
||||
const bad = results.find((r) => r && r.succeeded === false);
|
||||
if (bad) {
|
||||
ElMessage.error(bad.errors || '保存失败');
|
||||
return;
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
await load();
|
||||
} catch {
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-config {
|
||||
.refund-card {
|
||||
max-width: 640px;
|
||||
}
|
||||
.page-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.page-desc {
|
||||
margin: 0 0 24px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.refund-form {
|
||||
.pct-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pct-input {
|
||||
width: 220px;
|
||||
}
|
||||
.pct-suffix {
|
||||
color: var(--el-text-color-regular);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user