This commit is contained in:
yziiy
2026-01-29 14:53:16 +08:00
parent 733f246414
commit 17e971d548
16 changed files with 1716 additions and 0 deletions

77
src/api/modules/game.ts Normal file
View File

@@ -0,0 +1,77 @@
import { http } from "@/utils/http";
type Result = {
code: string;
data: any;
msg: string;
};
// 游戏列表
export const getMonsterList = params => {
return http.request<Result>("get", "/adminapi/Monster/get_monster_list", {
params
});
};
// 游戏详情
export const getMonsterDetail = params => {
return http.request<Result>("get", "/adminapi/Monster/get_monster_info", {
params
});
};
// 编辑游戏
export const editMonster = data => {
return http.request<Result>("post", "/adminapi/Monster/edit_monster", {
data
});
};
// 获取礼物列表
export const getGiftList = params => {
return http.request<Result>("get", "/adminapi/Monster/get_gift_list", {
params
});
};
// 获取期数列表
export const getMonsterLog = params => {
return http.request<Result>("get", "/adminapi/Monster/get_monster_log", {
params
});
};
// 获取投入记录
export const getUserMonsterLog = params => {
return http.request<Result>("get", "/adminapi/Monster/get_user_monster_log", {
params
});
};
// 获取中奖人员
export const getUserMonsterWinLog = params => {
return http.request<Result>("get", "/adminapi/Monster/get_user_monster_win_log", {
params
});
};
// 获取倍数列表
export const getMonsterMultipleList = params => {
return http.request<Result>("get", "/adminapi/Monster/get_monster_multiple_list", {
params
});
};
// 获取倍数详情
export const getMonsterMultipleDetail = params => {
return http.request<Result>("get", "/adminapi/Monster/get_monster_multiple_info", {
params
});
};
// 编辑倍数
export const editMonsterMultiple = data => {
return http.request<Result>("post", "/adminapi/Monster/edit_monster_multiple", {
data
});
};

View File

@@ -66,3 +66,9 @@ export const realTimeByluckyList = params => {
params
});
};
// 幸运币抽奖统计
export const getLotteryPoolFlow = params => {
return http.request<Result>("get", "/adminapi/Lottery/pool_flow_list", {
params
});
};

View File

@@ -0,0 +1,200 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getMonsterDetail } from "@/api/modules/game";
const props = defineProps(["id"]);
const loading = ref(true);
const detailData = ref<any>({});
const basicInfo = ref([
{ label: "游戏名称", prop: "type_name" },
{ label: "礼物名称", prop: "gift_name" },
{ label: "礼物数量", prop: "num" },
{ label: "倍数", prop: "multiple" },
{ label: "创建时间", prop: "createtime" },
{ label: "更新时间", prop: "updatetime" }
]);
const fetchDetail = async () => {
loading.value = true;
try {
const { data, code } = await getMonsterDetail({ id: props.id });
if (code) {
detailData.value = data;
}
} catch (error) {
console.error("获取详情失败:", error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchDetail();
});
</script>
<template>
<div v-loading="loading" class="detail-container">
<div v-if="!loading && detailData.id" class="detail-content">
<!-- 头部信息 -->
<div class="detail-header">
<div class="header-left">
<el-image v-if="detailData.gift_icon" :src="detailData.gift_icon" fit="cover"
style="width: 120px; height: 120px; border-radius: 8px" />
<div v-else class="no-image">
<el-icon :size="60">
<Picture />
</el-icon>
</div>
</div>
<div class="header-info">
<h2>{{ detailData.type_name || "未命名" }}</h2>
</div>
</div>
<el-divider />
<!-- 基本信息 -->
<div class="info-section">
<h3>基本信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item v-for="item in basicInfo" :key="item.prop" :label="item.label">
{{ detailData[item.prop] || "-" }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 礼物信息 -->
<div v-if="detailData.gift_name" class="info-section">
<h3>礼物信息</h3>
<el-card shadow="never">
<div class="gift-info">
<el-image v-if="detailData.gift_icon" :src="detailData.gift_icon" fit="cover"
style="width: 80px; height: 80px; border-radius: 4px" />
<div class="gift-details">
<div class="gift-name">{{ detailData.gift_name }}</div>
<div class="gift-count">数量: {{ detailData.num }}</div>
<div v-if="detailData.gift_price" class="gift-price">
价格: {{ detailData.gift_price }} 金币
</div>
</div>
</div>
</el-card>
</div>
<!-- 统计信息 -->
<div v-if="detailData.total_count || detailData.total_amount" class="info-section">
<h3>统计信息</h3>
<el-row :gutter="20">
<el-col v-if="detailData.total_count" :span="12">
<el-statistic title="总参与次数" :value="detailData.total_count" group-separator="," />
</el-col>
<el-col v-if="detailData.total_amount" :span="12">
<el-statistic title="总金额" :value="detailData.total_amount" prefix="¥" :precision="2" group-separator="," />
</el-col>
</el-row>
</div>
<!-- 其他信息 -->
<div v-if="detailData.remark || detailData.desc" class="info-section">
<h3>其他信息</h3>
<el-card shadow="never">
<div class="remark-content">
{{ detailData.remark || detailData.desc || "暂无备注" }}
</div>
</el-card>
</div>
</div>
<el-empty v-else-if="!loading" description="暂无数据" />
</div>
</template>
<style scoped lang="scss">
.detail-container {
padding: 20px;
min-height: 400px;
.detail-content {
.detail-header {
display: flex;
align-items: flex-start;
gap: 20px;
margin-bottom: 20px;
.header-left {
.no-image {
width: 120px;
height: 120px;
border-radius: 8px;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
color: #c0c4cc;
}
}
.header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.info-tags {
display: flex;
gap: 10px;
align-items: center;
}
}
}
.info-section {
margin-top: 30px;
h3 {
margin-bottom: 15px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.gift-info {
display: flex;
gap: 20px;
align-items: center;
.gift-details {
flex: 1;
.gift-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
}
.gift-count,
.gift-price {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
}
}
}
.remark-content {
line-height: 1.8;
color: #606266;
white-space: pre-wrap;
}
}
}
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getGiftList } from "@/api/modules/game";
defineProps(["formInline"]);
const ruleFormRef = ref();
const giftList = ref([]);
const loading = ref(false);
const getRef = () => {
return ruleFormRef.value;
};
defineExpose({ getRef });
const rules = {
gid: [{ required: true, message: "请选择礼物", trigger: "change" }],
num: [{ required: true, message: "请输入数量", trigger: "blur" }],
type_name: [{ required: true, message: "请输入游戏名称", trigger: "blur" }],
edit_monster_multiple: [
{ required: true, message: "请输入倍率", trigger: "blur" }
]
};
// 获取礼物列表
const fetchGiftList = async () => {
loading.value = true;
try {
const { data, code } = await getGiftList({});
if (code) {
giftList.value = data.lists || data.data || [];
}
} catch (error) {
console.error("获取礼物列表失败:", error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchGiftList();
});
</script>
<template>
<el-form ref="ruleFormRef" :model="formInline" :rules="rules" label-width="100px">
<el-form-item label="活动ID" prop="id">
<el-input v-model="formInline.id" disabled />
</el-form-item>
<el-form-item label="礼物" prop="gid">
<el-select v-model="formInline.gid" placeholder="请选择礼物" clearable filterable :loading="loading"
style="width: 100%">
<el-option v-for="gift in giftList" :key="gift.id" :label="gift.name || gift.gift_name" :value="gift.gid">
<div style="display: flex; align-items: center; gap: 10px">
<!-- <el-image v-if="gift.icon || gift.gift_icon" :src="gift.icon || gift.gift_icon"
style="width: 30px; height: 30px" fit="cover" /> -->
<span>{{ gift.name || gift.gift_name }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数量" prop="num">
<el-input-number v-model="formInline.num" :min="1" :max="99999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="游戏名称" prop="type_name">
<el-input v-model="formInline.type_name" placeholder="请输入游戏名称" clearable />
</el-form-item>
<el-form-item label="倍率" prop="edit_monster_multiple">
<el-input-number v-model="formInline.edit_monster_multiple" :min="1" :max="10000" :step="0.1" :precision="1"
controls-position="right" style="width: 100%" />
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.el-form {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,173 @@
import { ref, h } from "vue";
import { getMonsterList, editMonster } from "@/api/modules/game";
import { message } from "@/utils/message";
import { addDialog } from "@/components/ReDialog";
import detailView from "./detail.vue";
import editFormView from "./form.vue";
export function useData() {
const formRef = ref();
const loading = ref(true);
const tableList = ref([]);
const isShow = ref(false);
const pagination = ref({
total: 0,
pageSize: 10,
pageSizes: [10, 20, 50, 100, 500, 1000, 2000],
currentPage: 1,
background: true
});
const searchForm = ref({
order: "id",
sort: "desc"
});
const tableLabel = ref([
{
label: "ID",
prop: "id"
},
{
label: "游戏名称",
prop: "type_name"
},
{
label: "礼物",
prop: "gift_name"
},
{
label: "礼物图标",
prop: "icon",
cellRenderer: ({ row }) => (
<el-image
fit="cover"
preview-teleported={true}
src={row.base_image}
preview-src-list={Array.of(row.base_image)}
class="w-[50px] h-[50px] align-middle"
/>
)
},
{
label: "礼物价格",
prop: "gift_price"
},
{
label: "数量",
prop: "num"
},
{
label: "比率",
prop: "rate"
},
{
label: "创建时间",
prop: "createtime"
},
{
label: "操作",
fixed: "right",
width: 200,
slot: "operation"
}
]);
const onSearch = async formData => {
loading.value = true;
searchForm.value = { ...formData };
const { data, code } = await getMonsterList({
...formData,
page: pagination.value.currentPage,
limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.data || [];
pagination.value.total = data.count || data.total || 0;
pagination.value.currentPage = data.page || pagination.value.currentPage;
}
loading.value = false;
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
onSearch(searchForm.value);
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
onSearch(searchForm.value);
};
// 查看详情
const viewDetail = rowData => {
addDialog({
title: `游戏详情`,
props: {
id: rowData.id
},
width: "70%",
closeOnClickModal: false,
hideFooter: true,
contentRenderer: () => h(detailView)
});
};
// 编辑游戏
const openEditDialog = rowData => {
addDialog({
title: `编辑游戏`,
props: {
formInline: {
id: rowData.id,
gid: rowData.gid || "",
num: rowData.num || 1,
type_name: rowData.type_name || rowData.name || "",
edit_monster_multiple: rowData.multiple || 1
}
},
width: "40%",
closeOnClickModal: false,
contentRenderer: () =>
h(editFormView, { ref: formRef, formInline: null }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline;
FormRef.validate(async valid => {
if (!valid) return;
const { code, msg } = await editMonster({
id: curData.id,
gid: curData.gid,
num: curData.num,
type_name: curData.type_name,
edit_monster_multiple: curData.edit_monster_multiple
});
if (code) {
message("编辑成功", { type: "success" });
onSearch(searchForm.value);
done();
} else {
message(msg || "编辑失败", { type: "error" });
}
});
}
});
};
return {
searchForm,
onSearch,
isShow,
tableList,
tableLabel,
pagination,
handleSizeChange,
handleCurrentChange,
loading,
viewDetail,
openEditDialog
};
}

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { onBeforeMount } from "vue";
import { useData } from "./hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { deviceDetection } from "@pureadmin/utils";
const {
searchForm,
onSearch,
isShow,
tableList,
pagination,
tableLabel,
handleSizeChange,
handleCurrentChange,
loading,
viewDetail,
openEditDialog
} = useData();
onBeforeMount(() => {
onSearch(searchForm.value);
});
defineOptions({
name: "gameList"
});
</script>
<template>
<div class="main">
<div ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
<PureTableBar v-if="!loading" title="游戏列表" :class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
:columns="tableLabel" @refresh="onSearch">
<template v-slot="{ size, dynamicColumns }">
<pure-table ref="tableRef" align-whole="center" showOverflowTooltip table-layout="auto" default-expand-all
:loading="loading" :size="size" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }"
:data="tableList" :columns="dynamicColumns" :pagination="{ ...pagination, size }" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange">
<template #operation="{ row }">
<el-button type="primary" size="small" link @click="viewDetail(row)">
详情
</el-button>
<el-button type="primary" size="small" link @click="openEditDialog(row)">
编辑
</el-button>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>

View File

@@ -0,0 +1,188 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getMonsterMultipleDetail } from "@/api/modules/game";
const props = defineProps(["id"]);
const loading = ref(true);
const detailData = ref<any>({});
const basicInfo = ref([
{ label: "ID", prop: "id" },
{ label: "类型名称", prop: "type_name" },
{ label: "倍率", prop: "multiple" },
{ label: "礼物ID", prop: "gift_id" },
{ label: "礼物名称", prop: "gift_name" },
{ label: "礼物价格", prop: "gift_price" },
{ label: "数量", prop: "num" },
{ label: "中奖概率", prop: "probability" },
{ label: "创建时间", prop: "createtime" },
{ label: "更新时间", prop: "updatetime" }
]);
const fetchDetail = async () => {
loading.value = true;
try {
const { data, code } = await getMonsterMultipleDetail({ id: props.id });
if (code) {
detailData.value = data;
}
} catch (error) {
console.error("获取详情失败:", error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchDetail();
});
</script>
<template>
<div v-loading="loading" class="detail-container">
<div v-if="!loading && detailData.id" class="detail-content">
<!-- 头部信息 -->
<div class="detail-header">
<div class="header-left">
<el-image v-if="detailData.gift_icon" :src="detailData.gift_icon" fit="cover"
style="width: 120px; height: 120px; border-radius: 8px" />
<div v-else class="no-image">
<el-icon :size="60">
<Picture />
</el-icon>
</div>
</div>
<div class="header-info">
<h2>{{ detailData.type_name || "未命名" }}</h2>
<div class="info-tags">
<el-tag type="primary" size="large">
倍率: {{ detailData.multiple }}x
</el-tag>
<el-tag type="success" size="large">
概率: {{ detailData.probability }}%
</el-tag>
</div>
</div>
</div>
<el-divider />
<!-- 基本信息 -->
<div class="info-section">
<h3>基本信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item v-for="item in basicInfo" :key="item.prop" :label="item.label">
<template v-if="item.prop === 'probability'">
{{ detailData[item.prop] }}%
</template>
<template v-else>
{{ detailData[item.prop] || "-" }}
</template>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 礼物信息 -->
<div v-if="detailData.gift_name" class="info-section">
<h3>礼物信息</h3>
<el-card shadow="never">
<div class="gift-info">
<el-image v-if="detailData.gift_icon" :src="detailData.gift_icon" fit="cover"
style="width: 80px; height: 80px; border-radius: 4px" />
<div class="gift-details">
<div class="gift-name">{{ detailData.gift_name }}</div>
<div class="gift-count">数量: {{ detailData.num }}</div>
<div v-if="detailData.gift_price" class="gift-price">
价格: {{ detailData.gift_price }} 金币
</div>
</div>
</div>
</el-card>
</div>
</div>
<el-empty v-else-if="!loading" description="暂无数据" />
</div>
</template>
<style scoped lang="scss">
.detail-container {
padding: 20px;
min-height: 400px;
.detail-content {
.detail-header {
display: flex;
align-items: flex-start;
gap: 20px;
margin-bottom: 20px;
.header-left {
.no-image {
width: 120px;
height: 120px;
border-radius: 8px;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
color: #c0c4cc;
}
}
.header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.info-tags {
display: flex;
gap: 10px;
align-items: center;
}
}
}
.info-section {
margin-top: 30px;
h3 {
margin-bottom: 15px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.gift-info {
display: flex;
gap: 20px;
align-items: center;
.gift-details {
flex: 1;
.gift-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
}
.gift-count,
.gift-price {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { ref } from "vue";
defineProps(["formInline"]);
const ruleFormRef = ref();
const getRef = () => {
return ruleFormRef.value;
};
defineExpose({ getRef });
const rules = {
multiple: [{ required: true, message: "请输入倍率", trigger: "blur" }]
};
</script>
<template>
<el-form ref="ruleFormRef" :model="formInline" :rules="rules" label-width="100px">
<el-form-item label="ID" prop="id">
<el-input v-model="formInline.id" disabled />
</el-form-item>
<el-form-item label="倍率" prop="multiple">
<el-input-number v-model="formInline.multiple" :min="1" :max="10000" :step="0.1" :precision="1"
controls-position="right" style="width: 100%" />
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.el-form {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,138 @@
import { ref, h } from "vue";
import {
getMonsterMultipleList,
editMonsterMultiple
} from "@/api/modules/game";
import { message } from "@/utils/message";
import { addDialog } from "@/components/ReDialog";
import detailView from "./detail.vue";
import editFormView from "./form.vue";
export function useData() {
const formRef = ref();
const loading = ref(true);
const tableList = ref([]);
const isShow = ref(false);
const pagination = ref({
total: 0,
pageSize: 10,
pageSizes: [10, 20, 50, 100, 500, 1000, 2000],
currentPage: 1,
background: true
});
const searchForm = ref({});
const tableLabel = ref([
{
label: "ID",
prop: "id"
},
{
label: "倍率",
prop: "multiple"
},
{
label: "更新时间",
prop: "updatetime"
},
{
label: "操作",
fixed: "right",
width: 200,
slot: "operation"
}
]);
const onSearch = async formData => {
loading.value = true;
searchForm.value = { ...formData };
const { data, code } = await getMonsterMultipleList({
...formData,
page: pagination.value.currentPage,
limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.lists || data.data || [];
pagination.value.total = data.count || data.total || 0;
pagination.value.currentPage = data.page || pagination.value.currentPage;
}
loading.value = false;
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
onSearch(searchForm.value);
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
onSearch(searchForm.value);
};
// 查看详情
const viewDetail = rowData => {
addDialog({
title: `倍数详情`,
props: {
id: rowData.id
},
width: "60%",
closeOnClickModal: false,
hideFooter: true,
contentRenderer: () => h(detailView)
});
};
// 编辑倍数
const openEditDialog = rowData => {
addDialog({
title: `编辑倍数`,
props: {
formInline: {
id: rowData.id,
multiple: rowData.multiple || 1
}
},
width: "40%",
closeOnClickModal: false,
contentRenderer: () =>
h(editFormView, { ref: formRef, formInline: null }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline;
FormRef.validate(async valid => {
if (!valid) return;
const { code, msg } = await editMonsterMultiple({
id: curData.id,
multiple: curData.multiple
});
if (code) {
message("编辑成功", { type: "success" });
onSearch(searchForm.value);
done();
} else {
message(msg || "编辑失败", { type: "error" });
}
});
}
});
};
return {
searchForm,
onSearch,
isShow,
tableList,
tableLabel,
pagination,
handleSizeChange,
handleCurrentChange,
loading,
viewDetail,
openEditDialog
};
}

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { onBeforeMount } from "vue";
import { useData } from "./hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { deviceDetection } from "@pureadmin/utils";
const {
searchForm,
onSearch,
isShow,
tableList,
pagination,
tableLabel,
handleSizeChange,
handleCurrentChange,
loading,
viewDetail,
openEditDialog
} = useData();
onBeforeMount(() => {
onSearch(searchForm.value);
});
defineOptions({
name: "multipleList"
});
</script>
<template>
<div class="main">
<div ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
<PureTableBar v-if="!loading" title="倍数列表" :class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
:columns="tableLabel" @refresh="onSearch">
<template v-slot="{ size, dynamicColumns }">
<pure-table ref="tableRef" align-whole="center" showOverflowTooltip table-layout="auto" default-expand-all
:loading="loading" :size="size" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }"
:data="tableList" :columns="dynamicColumns" :pagination="{ ...pagination, size }" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange">
<template #operation="{ row }">
<!-- <el-button type="primary" size="small" link @click="viewDetail(row)">
详情
</el-button> -->
<el-button type="primary" size="small" link @click="openEditDialog(row)">
编辑
</el-button>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>
<!-- <style scoped lang="scss">
.main {
padding: 20px;
}
</style> -->

View File

@@ -0,0 +1,132 @@
import { ref, h } from "vue";
import { getMonsterLog } from "@/api/modules/game";
import { message } from "@/utils/message";
import { addDialog } from "@/components/ReDialog";
import investRecordView from "./investRecord.vue";
import winnerListView from "./winnerList.vue";
export function useData() {
const loading = ref(true);
const tableList = ref([]);
const isShow = ref(false);
const pagination = ref({
total: 0,
pageSize: 10,
pageSizes: [10, 20, 50, 100, 500, 1000, 2000],
currentPage: 1,
background: true
});
const searchForm = ref({});
const tableLabel = ref([
{
label: "期数ID",
prop: "id"
},
{
label: "游戏名称",
prop: "type_name"
},
{
label: "礼物名称",
prop: "gift_name"
},
{
label: "礼物价值",
prop: "gift_price"
},
{
label: "投入总数",
prop: "out_amount"
},
{
label: "支出总数",
prop: "in_amount"
},
{
label: "开始时间",
prop: "createtime"
},
{
label: "结束时间",
prop: "end_time"
},
{
label: "操作",
fixed: "right",
width: 220,
slot: "operation"
}
]);
const onSearch = async formData => {
loading.value = true;
searchForm.value = { ...formData };
const { data, code } = await getMonsterLog({
...formData,
page: pagination.value.currentPage,
limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.lists || data.data || [];
pagination.value.total = data.count || data.total || 0;
pagination.value.currentPage = data.page || pagination.value.currentPage;
}
loading.value = false;
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
onSearch(searchForm.value);
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
onSearch(searchForm.value);
};
// 查看投入记录
const viewInvestRecord = rowData => {
addDialog({
title: `投入记录 - 期数${rowData.period_no || rowData.id}`,
props: {
periodId: rowData.id,
periodNo: rowData.period_no
},
width: "70%",
closeOnClickModal: false,
hideFooter: true,
contentRenderer: () => h(investRecordView)
});
};
// 查看中奖人员
const viewWinnerList = rowData => {
addDialog({
title: `中奖人员 - 期数${rowData.period_no || rowData.id}`,
props: {
periodId: rowData.id,
periodNo: rowData.period_no
},
width: "70%",
closeOnClickModal: false,
hideFooter: true,
contentRenderer: () => h(winnerListView)
});
};
return {
searchForm,
onSearch,
isShow,
tableList,
tableLabel,
pagination,
handleSizeChange,
handleCurrentChange,
loading,
viewInvestRecord,
viewWinnerList
};
}

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { onBeforeMount } from "vue";
import { useData } from "./hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { deviceDetection } from "@pureadmin/utils";
const {
searchForm,
onSearch,
isShow,
tableList,
pagination,
tableLabel,
handleSizeChange,
handleCurrentChange,
loading,
viewInvestRecord,
viewWinnerList
} = useData();
onBeforeMount(() => {
onSearch(searchForm.value);
});
defineOptions({
name: "periodsList"
});
</script>
<template>
<div class="main">
<div ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
<PureTableBar v-if="!loading" title="期数列表" :class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
:columns="tableLabel" @refresh="onSearch">
<template v-slot="{ size, dynamicColumns }">
<pure-table ref="tableRef" align-whole="center" showOverflowTooltip table-layout="auto" default-expand-all
:loading="loading" :size="size" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }"
:data="tableList" :columns="dynamicColumns" :pagination="{ ...pagination, size }" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange">
<template #operation="{ row }">
<el-button type="primary" size="small" link @click="viewInvestRecord(row)">
投入记录
</el-button>
<el-button type="success" size="small" link @click="viewWinnerList(row)">
中奖人员
</el-button>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>
<!--
<style scoped lang="scss">
.main {
padding: 20px;
}
</style> -->

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getUserMonsterLog } from "@/api/modules/game";
const props = defineProps(["periodId", "periodNo"]);
const loading = ref(true);
const tableList = ref([]);
const pagination = ref({
total: 0,
pageSize: 10,
currentPage: 1,
background: true
});
const tableColumns = ref([
{
label: "底数ID",
prop: "id"
},
{
label: "用户昵称",
prop: "nickname"
},
{
label: "头像",
prop: "avatar",
slot: "avatar"
},
{
label: "投入类型",
prop: "type_name"
},
{
label: "总投入金额",
prop: "price"
},
{
label: "创建时间",
prop: "createtime"
}
]);
// 获取投入记录
const fetchInvestRecord = async () => {
loading.value = true;
try {
const { data, code } = await getUserMonsterLog({
mid: props.periodId,
page: pagination.value.currentPage,
limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.lists || data.data || [];
pagination.value.total = data.count || data.total || 0;
}
} catch (error) {
console.error("获取投入记录失败:", error);
} finally {
loading.value = false;
}
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
fetchInvestRecord();
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
fetchInvestRecord();
};
onMounted(() => {
fetchInvestRecord();
});
</script>
<template>
<div class="invest-record-container">
<div class="info-bar">
<el-alert :title="`期数:${periodNo || periodId}`" type="info" :closable="false" />
</div>
<pure-table ref="tableRef" class="mt-5" align-whole="center" showOverflowTooltip table-layout="auto"
:loading="loading" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }" :data="tableList"
:columns="tableColumns" :pagination="pagination" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange">
<template #avatar="{ row }">
<el-image v-if="row.avatar" :src="row.avatar" fit="cover" preview-teleported :preview-src-list="[row.avatar]"
style="width: 40px; height: 40px; border-radius: 50%" />
<span v-else>-</span>
</template>
</pure-table>
</div>
</template>
<style scoped lang="scss">
.invest-record-container {
padding: 20px;
.info-bar {
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getUserMonsterWinLog } from "@/api/modules/game";
const props = defineProps(["periodId", "periodNo"]);
const loading = ref(true);
const tableList = ref([]);
const pagination = ref({
total: 0,
pageSize: 10,
currentPage: 1,
background: true
});
const tableColumns = ref([
{
label: "期数ID",
prop: "id"
},
{
label: "用户昵称",
prop: "nickname"
},
{
label: "头像",
prop: "avatar",
slot: "avatar"
},
{
label: "投入类型",
prop: "nickname"
},
{
label: "中奖礼物ID",
prop: "win_gid"
},
{
label: "中奖礼物名称",
prop: "gift_name"
},
{
label: "总投入金额",
prop: "price"
},
{
label: "获取礼物数量",
prop: "num"
},
{
label: "获取礼物价值",
prop: "gift_price"
},
{
label: "创建时间",
prop: "createtime"
}
]);
// 获取中奖人员
const fetchWinnerList = async () => {
loading.value = true;
try {
const { data, code } = await getUserMonsterWinLog({
mid: props.periodId,
page: pagination.value.currentPage,
limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.lists || data.data || [];
pagination.value.total = data.count || data.total || 0;
}
} catch (error) {
console.error("获取中奖人员失败:", error);
} finally {
loading.value = false;
}
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
fetchWinnerList();
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
fetchWinnerList();
};
onMounted(() => {
fetchWinnerList();
});
</script>
<template>
<div class="winner-list-container">
<div class="info-bar">
<el-alert :title="`期数:${periodNo || periodId}`" type="success" :closable="false" />
</div>
<pure-table ref="tableRef" class="mt-5" align-whole="center" showOverflowTooltip table-layout="auto"
:loading="loading" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }" :data="tableList"
:columns="tableColumns" :pagination="pagination" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange">
<template #avatar="{ row }">
<el-image v-if="row.avatar" :src="row.avatar" fit="cover" preview-teleported :preview-src-list="[row.avatar]"
style="width: 40px; height: 40px; border-radius: 50%" />
<span v-else>-</span>
</template>
<template #giftIcon="{ row }">
<el-image v-if="row.gift_icon" :src="row.gift_icon" fit="cover" preview-teleported
:preview-src-list="[row.gift_icon]" style="width: 50px; height: 50px; border-radius: 4px" />
<span v-else>-</span>
</template>
</pure-table>
</div>
</template>
<style scoped lang="scss">
.winner-list-container {
padding: 20px;
.info-bar {
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,206 @@
import { ref, h } from "vue";
import { getLotteryPoolFlow } from "@/api/modules/statistics";
import { utils, writeFile } from "xlsx";
import ExportForm from "@/components/exportDialog/index.vue";
import { addDialog } from "@/components/ReDialog";
import { message } from "@/utils/message";
import dayjs from "dayjs";
export function useData() {
const loading = ref(true);
const tableList = ref([]);
const isShow = ref(false);
const pagination = ref({
total: 0,
pageSize: 10,
pageSizes: [10, 20, 50, 100, 500, 1000, 2000],
currentPage: 1,
background: true
});
// 获取当月第一天 00:00:00 和当前时间
const getDefaultTimeRange = () => {
const now = dayjs();
const startOfMonth = now.startOf("month").format("YYYY-MM-DD HH:mm:ss");
const currentTime = now.format("YYYY-MM-DD HH:mm:ss");
return { stime: startOfMonth, etime: currentTime };
};
const defaultTime = getDefaultTimeRange();
const searchForm = ref({
stime: defaultTime.stime,
etime: defaultTime.etime,
pool_type: 1,
user_code: ""
});
const searchLabel = ref([
{ label: "开始时间", prop: "stime", type: "date" },
{ label: "结束时间", prop: "etime", type: "date" },
{ label: "用户ID", prop: "user_code", type: "input" },
{
label: "类型",
prop: "pool_type",
type: "select",
optionList: [
{ label: "初级", value: 1 },
{ label: "中级", value: 3 },
{ label: "高级", value: 4 }
]
}
]);
const tableLabel = ref([
{
label: "ID",
prop: "id"
},
{
label: "抽奖用户(送礼用户)",
prop: "send_nickname"
},
{
label: "收礼用户",
prop: "recv_nickname"
},
{
label: "礼物信息",
prop: "gift_name"
},
{
label: "礼物价值",
prop: "gift_gold"
},
{
label: "收礼人收益",
prop: "recv_gold"
},
{
label: "划入奖池的金币",
prop: "small_pool_add"
},
{
label: "备注",
prop: "remark"
},
{
label: "时间",
prop: "createtime"
}
]);
const onSearch = async formData => {
loading.value = true;
searchForm.value = { ...formData };
const { data, code } = await getLotteryPoolFlow({
...formData,
page: pagination.value.currentPage,
page_limit: pagination.value.pageSize
});
if (code) {
tableList.value = data.lists.map(ele => {
return {
...ele,
receive_income: parseFloat(ele.receive_income || 0),
pool_amount: parseFloat(ele.pool_amount || 0)
};
});
pagination.value.total = data.count;
pagination.value.currentPage = parseFloat(data.page);
}
loading.value = false;
};
const handleSizeChange = (val: number) => {
pagination.value.pageSize = val;
onSearch(searchForm.value);
};
const handleCurrentChange = (val: number) => {
pagination.value.currentPage = val;
onSearch(searchForm.value);
};
const exportFormRef = ref(null);
const exportExcel = () => {
let exportTableList = [];
addDialog({
title: `导出数据`,
props: {
formInline: {
time: ""
}
},
width: "40%",
closeOnClickModal: false,
contentRenderer: () =>
h(ExportForm, { ref: exportFormRef, formInline: null }),
beforeSure: (done, { options }) => {
const FormRef = exportFormRef.value.getRef();
const curData = options.props.formInline;
const exportData = async formData => {
const { data, code } = await getLotteryPoolFlow({
...formData,
page: 1,
page_limit: 20000
});
if (code) {
exportTableList = data.lists;
const res = exportTableList.map(item => {
const arr = [];
tableLabel.value.forEach(column => {
arr.push(item[column.prop as string]);
});
return arr;
});
const titleList = [];
tableLabel.value.forEach(column => {
titleList.push(column.label);
});
res.unshift(titleList);
const workSheet = utils.aoa_to_sheet(res);
const workBook = utils.book_new();
utils.book_append_sheet(workBook, workSheet, "数据报表");
writeFile(
workBook,
`幸运币抽奖统计${formData.start_time} - ${formData.end_time}.xlsx`
);
message("导出成功", {
type: "success"
});
done();
} else {
message("获取数据失败,请重试!", {
type: "error"
});
}
};
FormRef.validate(valid => {
if (valid) {
if (curData.time && curData.time.length) {
exportData({
start_time: curData.time[0] || "",
end_time: curData.time[1] || ""
});
}
}
});
}
});
};
return {
searchForm,
searchLabel,
onSearch,
isShow,
tableList,
tableLabel,
pagination,
exportExcel,
handleSizeChange,
handleCurrentChange,
loading
};
}

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { onBeforeMount } from "vue";
import { useData } from "./hook";
import SearchForm from "@/components/SearchForm/index.vue";
import { PureTableBar } from "@/components/RePureTableBar";
import { deviceDetection } from "@pureadmin/utils";
const {
searchLabel,
searchForm,
onSearch,
isShow,
tableList,
pagination,
tableLabel,
exportExcel,
handleSizeChange,
handleCurrentChange,
loading
} = useData();
onBeforeMount(() => {
onSearch(searchForm.value);
});
defineOptions({
name: "luckycoinLottery"
});
</script>
<template>
<div class="main">
<SearchForm class="pb-2" :LabelList="searchLabel" :formData="searchForm" @handleSearch="onSearch" />
<div ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
<PureTableBar v-if="!loading" title="幸运币抽奖统计列表" :class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
:columns="tableLabel" @refresh="onSearch">
<template #buttons>
<el-button type="primary" @click="exportExcel"> 导出 </el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table ref="tableRef" align-whole="center" showOverflowTooltip table-layout="auto" default-expand-all
:loading="loading" :size="size" row-key="id" adaptive :adaptiveConfig="{ offsetBottom: 108 }"
:data="tableList" :columns="dynamicColumns" :pagination="{ ...pagination, size }" :header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange" />
</template>
</PureTableBar>
</div>
</div>
</template>
<style scoped lang="scss">
.content-flex {
width: 100%;
display: inline-flex;
background-color: #fff;
padding: 20px;
text-align: center;
align-items: center;
justify-content: space-between;
}
</style>