Compare commits
10 Commits
275d4d46e1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd9905ffe7 | ||
|
|
ba1d300f4e | ||
|
|
5443a6ccf2 | ||
|
|
6df60fcd9f | ||
|
|
79aef9aee2 | ||
|
|
e7ce2524cd | ||
|
|
17e971d548 | ||
|
|
733f246414 | ||
|
|
0e2d3d3097 | ||
|
|
ffcc0e77c1 |
77
src/api/modules/game.ts
Normal file
77
src/api/modules/game.ts
Normal 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
|
||||
});
|
||||
};
|
||||
@@ -151,3 +151,12 @@ export const getBanDay = () => {
|
||||
"/adminapi/User/getBanDay"
|
||||
);
|
||||
};
|
||||
|
||||
// 用户CP列表
|
||||
export const userCpList = params => {
|
||||
return http.request<Result>(
|
||||
"get",
|
||||
"/adminapi/User/user_cp_list",
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -316,4 +316,12 @@ export const getRoomHostList = params => {
|
||||
"/adminapi/Room/room_host_list",
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
// 获取房间幸运值流水列表
|
||||
export const getRoomLuckList = params => {
|
||||
return http.request<Result>(
|
||||
"get",
|
||||
"/adminapi/Room/room_luck_list",
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
@@ -66,3 +66,9 @@ export const realTimeByluckyList = params => {
|
||||
params
|
||||
});
|
||||
};
|
||||
// 幸运币抽奖统计
|
||||
export const getLotteryPoolFlow = params => {
|
||||
return http.request<Result>("get", "/adminapi/Lottery/pool_flow_list", {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// export const URL = "https://yushengapi.qxyushen.top";
|
||||
export const URL = "https://yushengapi.qxyushen.top";
|
||||
// 预测版
|
||||
// export const URL = "https://vsyusheng.qxhs.xyz";
|
||||
// 测试
|
||||
export const URL = "https://test.vespa.qxyushen.top";
|
||||
// export const URL = "https://test.vespa.qxyushen.top";
|
||||
// 声网appId 在这里换
|
||||
export const appIdBySw = "02f7339ec98947deaeab173599891932";
|
||||
|
||||
200
src/views/LXlegend/gameList/detail.vue
Normal file
200
src/views/LXlegend/gameList/detail.vue
Normal 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>
|
||||
84
src/views/LXlegend/gameList/form.vue
Normal file
84
src/views/LXlegend/gameList/form.vue
Normal 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>
|
||||
173
src/views/LXlegend/gameList/hook.tsx
Normal file
173
src/views/LXlegend/gameList/hook.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
55
src/views/LXlegend/gameList/index.vue
Normal file
55
src/views/LXlegend/gameList/index.vue
Normal 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>
|
||||
188
src/views/LXlegend/multipleList/detail.vue
Normal file
188
src/views/LXlegend/multipleList/detail.vue
Normal 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>
|
||||
36
src/views/LXlegend/multipleList/form.vue
Normal file
36
src/views/LXlegend/multipleList/form.vue
Normal 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>
|
||||
138
src/views/LXlegend/multipleList/hook.tsx
Normal file
138
src/views/LXlegend/multipleList/hook.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
61
src/views/LXlegend/multipleList/index.vue
Normal file
61
src/views/LXlegend/multipleList/index.vue
Normal 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> -->
|
||||
135
src/views/LXlegend/periodsList/hook.tsx
Normal file
135
src/views/LXlegend/periodsList/hook.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
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 totalRow = ref({});
|
||||
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 || [];
|
||||
totalRow.value = data.totalRow;
|
||||
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,
|
||||
totalRow
|
||||
};
|
||||
}
|
||||
69
src/views/LXlegend/periodsList/index.vue
Normal file
69
src/views/LXlegend/periodsList/index.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<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,
|
||||
totalRow,
|
||||
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 #buttons>
|
||||
<span>当前总投入:
|
||||
<span style="color: red">{{ totalRow.out_amount || 0 }}</span>
|
||||
当前总支出:
|
||||
<span style="color: red">{{ totalRow.in_amount || 0 }}</span>
|
||||
</span>
|
||||
</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 #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> -->
|
||||
107
src/views/LXlegend/periodsList/investRecord.vue
Normal file
107
src/views/LXlegend/periodsList/investRecord.vue
Normal 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>
|
||||
129
src/views/LXlegend/periodsList/winnerList.vue
Normal file
129
src/views/LXlegend/periodsList/winnerList.vue
Normal 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>
|
||||
@@ -1,9 +1,17 @@
|
||||
import { ref, h } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { queryUserNobility } from "@/api/modules/nobility";
|
||||
export function useData() {
|
||||
const loading = ref(true);
|
||||
const tableList = ref([]);
|
||||
const isShow = ref(false);
|
||||
const searchForm = ref({
|
||||
user_code: "",
|
||||
user_nick_name: ""
|
||||
});
|
||||
const searchLabel = ref([
|
||||
{ label: "用户ID", prop: "user_code", type: "input" },
|
||||
{ label: "用户昵称", prop: "user_nick_name", type: "input" }
|
||||
]);
|
||||
const pagination = ref({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
@@ -35,7 +43,7 @@ export function useData() {
|
||||
preview-src-list={Array.of(row.user_avatar)}
|
||||
class="w-[24px] h-[24px] rounded-full align-middle"
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
label: "爵位名称",
|
||||
@@ -44,11 +52,13 @@ export function useData() {
|
||||
{
|
||||
label: "名称颜色",
|
||||
prop: "nick_name_color"
|
||||
},
|
||||
}
|
||||
]);
|
||||
const onSearch = async () => {
|
||||
const onSearch = async formData => {
|
||||
loading.value = true;
|
||||
searchForm.value = { ...formData };
|
||||
const { data, code } = await queryUserNobility({
|
||||
...formData,
|
||||
page: pagination.value.currentPage,
|
||||
page_limit: pagination.value.pageSize
|
||||
});
|
||||
@@ -61,13 +71,15 @@ export function useData() {
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
onSearch();
|
||||
onSearch(searchForm.value);
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
onSearch();
|
||||
onSearch(searchForm.value);
|
||||
};
|
||||
return {
|
||||
searchForm,
|
||||
searchLabel,
|
||||
onSearch,
|
||||
isShow,
|
||||
tableList,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } 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,
|
||||
@@ -11,19 +14,19 @@ const {
|
||||
tableLabel,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
loading,
|
||||
loading
|
||||
} = useData();
|
||||
defineOptions({
|
||||
name: "charmGrade"
|
||||
});
|
||||
onMounted(() => {
|
||||
onSearch();
|
||||
onSearch(searchForm.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<SearchForm class="pb-2" :LabelList="searchLabel" :formData="searchForm" @handleSearch="onSearch" />
|
||||
<div ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
|
||||
<PureTableBar title="用户爵位列表" :class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
|
||||
:columns="tableLabel" @refresh="onSearch">
|
||||
@@ -33,8 +36,7 @@ onMounted(() => {
|
||||
: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">
|
||||
</pure-table>
|
||||
}" @page-current-change="handleCurrentChange" @page-size-change="handleSizeChange" />
|
||||
</template>
|
||||
</PureTableBar>
|
||||
</div>
|
||||
|
||||
206
src/views/Statistical/luckycoinLottery/hook.tsx
Normal file
206
src/views/Statistical/luckycoinLottery/hook.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
63
src/views/Statistical/luckycoinLottery/index.vue
Normal file
63
src/views/Statistical/luckycoinLottery/index.vue
Normal 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>
|
||||
169
src/views/newuser/newuserList/cpList.vue
Normal file
169
src/views/newuser/newuserList/cpList.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, h } from "vue";
|
||||
import { userCpList } from "@/api/modules/newuserList";
|
||||
import { utils, writeFile } from "xlsx";
|
||||
import ExportForm from "@/components/exportDialog/index.vue";
|
||||
import { addDialog } from "@/components/ReDialog";
|
||||
import { message } from "@/utils/message";
|
||||
|
||||
const props = defineProps(["userId"]);
|
||||
const exportFormRef = ref(null);
|
||||
const loading = ref(true);
|
||||
const tableList = ref([]);
|
||||
const pagination = ref({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
background: true
|
||||
});
|
||||
|
||||
const cpColumns = ref([
|
||||
{
|
||||
label: "ID",
|
||||
prop: "id"
|
||||
},
|
||||
{
|
||||
label: "用户1",
|
||||
prop: "user1_nickname"
|
||||
},
|
||||
{
|
||||
label: "用户2",
|
||||
prop: "user2_nickname"
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
prop: "status_str"
|
||||
},
|
||||
{
|
||||
label: "建立时间",
|
||||
prop: "createtime"
|
||||
}
|
||||
]);
|
||||
|
||||
const getCpData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, code } = await userCpList({
|
||||
user_id: props.userId,
|
||||
page: pagination.value.currentPage,
|
||||
page_limit: pagination.value.pageSize
|
||||
});
|
||||
if (code) {
|
||||
tableList.value = data.lists || [];
|
||||
pagination.value.total = data.count || 0;
|
||||
pagination.value.pageSize = +data.page_limit || 10;
|
||||
pagination.value.currentPage = +data.page || 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取CP列表失败:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
getCpData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
getCpData();
|
||||
};
|
||||
|
||||
const exportTable = () => {
|
||||
if (tableList.value.length === 0) {
|
||||
message("暂无数据导出", { type: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
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 userCpList({
|
||||
user_id: props.userId,
|
||||
page: 1,
|
||||
page_limit: 10000
|
||||
});
|
||||
|
||||
if (code) {
|
||||
const exportTableList = data.lists || [];
|
||||
const res = exportTableList.map(item => {
|
||||
const arr = [];
|
||||
cpColumns.value.forEach(column => {
|
||||
arr.push(item[column.prop as string]);
|
||||
});
|
||||
return arr;
|
||||
});
|
||||
|
||||
const titleList = [];
|
||||
cpColumns.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,
|
||||
`用户CP列表统计——${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] || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getCpData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cp-list-container">
|
||||
<div class="mb-5" style="float: right">
|
||||
<el-button type="primary" @click="exportTable">导出当前表格</el-button>
|
||||
</div>
|
||||
<pure-table ref="tableRef" align-whole="center" showOverflowTooltip table-layout="auto" :loading="loading"
|
||||
:adaptiveConfig="{ offsetBottom: 108 }" :data="tableList" :columns="cpColumns" :pagination="pagination"
|
||||
:header-cell-style="{
|
||||
background: 'var(--el-fill-color-light)',
|
||||
color: 'var(--el-text-color-primary)'
|
||||
}" @page-size-change="handleSizeChange" @page-current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cp-list-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
bandFamilyUser,
|
||||
userRelationList
|
||||
} from "@/api/modules/newuserList";
|
||||
import cpListView from "./cpList.vue";
|
||||
const userData = ref({
|
||||
...props.userInfo.user_info,
|
||||
...props.userInfo.follow_num,
|
||||
@@ -216,6 +217,8 @@ const handleClick = tab => {
|
||||
getFamilyData();
|
||||
} else if (name == "5") {
|
||||
getRelationData();
|
||||
} else if (name == "6") {
|
||||
// 用户CP列表 - 由子组件自己处理
|
||||
}
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
@@ -459,6 +462,9 @@ const exportTable = async activeIndex => {
|
||||
</template>
|
||||
</pure-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="用户CP" name="6">
|
||||
<cpListView v-if="activeIndex === '6'" :userId="userData.userId" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -178,6 +178,10 @@ export function useData() {
|
||||
</el-tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: "禁用理由",
|
||||
prop: "user_block_reason"
|
||||
},
|
||||
{
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
getRoomDetail,
|
||||
getRoomWaterFlow,
|
||||
getRoomEnterByUser,
|
||||
getRoomHostList
|
||||
getRoomHostList,
|
||||
getRoomLuckList
|
||||
} from "@/api/modules/room";
|
||||
const props = defineProps(["tableData"]);
|
||||
const dataBytable = ref({ ...props.tableData });
|
||||
@@ -147,6 +148,17 @@ const getFlowData = async index => {
|
||||
pagination.value.total = data.count;
|
||||
pagination.value.currentPage = data.page;
|
||||
}
|
||||
} else if (index === 5) {
|
||||
const { data, code } = await getRoomLuckList({
|
||||
room_id: dataBytable.value.room_id,
|
||||
page: pagination.value.currentPage,
|
||||
page_limit: pagination.value.pageSize
|
||||
});
|
||||
if (code) {
|
||||
flowTableList.value = data.lists;
|
||||
pagination.value.total = data.count;
|
||||
pagination.value.currentPage = data.page;
|
||||
}
|
||||
}
|
||||
};
|
||||
const getExportData = async index => {
|
||||
@@ -231,6 +243,36 @@ const dynamicHostColumns = ref([
|
||||
slot: "avatar"
|
||||
}
|
||||
]);
|
||||
const dynamicLuckColumns = ref([
|
||||
{
|
||||
label: "ID",
|
||||
prop: "id"
|
||||
},
|
||||
{
|
||||
label: "送礼人",
|
||||
prop: "send_nickname"
|
||||
},
|
||||
{
|
||||
label: "礼物",
|
||||
prop: "gift_name"
|
||||
},
|
||||
{
|
||||
label: "数量",
|
||||
prop: "num"
|
||||
},
|
||||
{
|
||||
label: "收礼人",
|
||||
prop: "recv_nickname"
|
||||
},
|
||||
{
|
||||
label: "幸运值",
|
||||
prop: "luck_value"
|
||||
},
|
||||
{
|
||||
label: "时间",
|
||||
prop: "createtime"
|
||||
}
|
||||
]);
|
||||
const activeName = ref("1");
|
||||
const tagValue = ref(1);
|
||||
const dateSearchValue = ref(getDefaultTimeRange());
|
||||
@@ -250,9 +292,15 @@ const handleClick = tab => {
|
||||
pagination.value.currentPage = 1;
|
||||
flowTableList.value = [];
|
||||
activeIndex.value = name;
|
||||
if (["1", "2", "4"].includes(name)) {
|
||||
if (["1", "2", "4", "5"].includes(name)) {
|
||||
getFlowData(
|
||||
activeIndex.value == "1" ? 1 : activeIndex.value == "2" ? 2 : 4
|
||||
activeIndex.value == "1"
|
||||
? 1
|
||||
: activeIndex.value == "2"
|
||||
? 2
|
||||
: activeIndex.value == "4"
|
||||
? 4
|
||||
: 5
|
||||
);
|
||||
} else {
|
||||
console.log("点歌记录");
|
||||
@@ -260,11 +308,27 @@ const handleClick = tab => {
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
getFlowData(activeIndex.value == "1" ? 1 : activeIndex.value == "2" ? 2 : 4);
|
||||
getFlowData(
|
||||
activeIndex.value == "1"
|
||||
? 1
|
||||
: activeIndex.value == "2"
|
||||
? 2
|
||||
: activeIndex.value == "4"
|
||||
? 4
|
||||
: 5
|
||||
);
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
getFlowData(activeIndex.value == "1" ? 1 : activeIndex.value == "2" ? 2 : 4);
|
||||
getFlowData(
|
||||
activeIndex.value == "1"
|
||||
? 1
|
||||
: activeIndex.value == "2"
|
||||
? 2
|
||||
: activeIndex.value == "4"
|
||||
? 4
|
||||
: 5
|
||||
);
|
||||
};
|
||||
const changeTime = val => {
|
||||
// console.log(val)
|
||||
@@ -389,6 +453,14 @@ const exportExcal = async () => {
|
||||
</template>
|
||||
</pure-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="幸运值流水" name="5">
|
||||
<pure-table ref="tableRef" class="mt-5" align-whole="center" showOverflowTooltip table-layout="auto"
|
||||
default-expand-all row-key="id" :adaptiveConfig="{ offsetBottom: 108 }" :data="flowTableList"
|
||||
:columns="dynamicLuckColumns" :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" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMounted } from "vue";
|
||||
import { useData } from "./hook";
|
||||
import { deviceDetection } from "@pureadmin/utils";
|
||||
import uploadImage from '@/components/UploadImage/index.vue';
|
||||
import uploadImage from "@/components/UploadImage/index.vue";
|
||||
const {
|
||||
onSearch,
|
||||
loading,
|
||||
@@ -20,22 +20,25 @@ onMounted(() => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="main">
|
||||
<div style="margin-bottom: 20px;text-align: right;"> <el-button type="primary"
|
||||
@click="saveConfig">保存当前配置</el-button></div>
|
||||
<div ref="contentRef" v-if="!loading" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
|
||||
<el-card style="width: 100%;">
|
||||
<el-form ref="form" :model="formData" label-width="200px">
|
||||
<div style="margin-bottom: 20px; text-align: right">
|
||||
<el-button type="primary" @click="saveConfig">保存当前配置</el-button>
|
||||
</div>
|
||||
<div v-if="!loading" ref="contentRef" :class="['flex', deviceDetection() ? 'flex-wrap' : '']">
|
||||
<el-card style="width: 100%">
|
||||
<el-form ref="form" :model="formData" label-width="400px">
|
||||
<!-- {{ formLabel }} -->
|
||||
<el-form-item :label="ele.desc" v-for="ele in formLabel">
|
||||
<div style="width: 100%;display: inline-flex;">
|
||||
<div style="width: 20%;">
|
||||
<el-input v-if="![7, 8].includes(ele.id)" v-model="formData[ele.key]"></el-input>
|
||||
<el-form-item v-for="ele in formLabel" :label="ele.desc">
|
||||
<div style="width: 100%; display: inline-flex">
|
||||
<div style="width: 20%">
|
||||
<el-input v-if="![7, 8, 17].includes(ele.id)" v-model="formData[ele.key]" />
|
||||
<div v-else>
|
||||
<uploadImage :acceptType="'.mp4'" @handleSuccess="(event) => handleFileSuccess(event, ele)" :limit="1"
|
||||
:echoUrl="formData[ele.key]" />
|
||||
<uploadImage :acceptType="'.mp4'" :limit="1" :echoUrl="formData[ele.key]"
|
||||
@handleSuccess="event => handleFileSuccess(event, ele)" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 70%; margin-left: 20px;color: #666;">{{ ele.key_desc }}</div>
|
||||
<div style="width: 70%; margin-left: 20px; color: #666">
|
||||
{{ ele.key_desc }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -102,6 +102,10 @@ export function useData() {
|
||||
label: "创建时间",
|
||||
prop: "createtime"
|
||||
},
|
||||
{
|
||||
label: "解散时间",
|
||||
prop: "delete_time"
|
||||
},
|
||||
{
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
|
||||
@@ -39,14 +39,14 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
|
||||
// "Access-Control-Allow-Origin",
|
||||
// "http://adminvs.qxhs.xyz"
|
||||
// );
|
||||
// res.setHeader(
|
||||
// "Access-Control-Allow-Origin",
|
||||
// "http://yushenggliht.qxyushen.top"
|
||||
// );
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
"https://test.vespa.qxyushen.top"
|
||||
"http://yushenggliht.qxyushen.top"
|
||||
);
|
||||
// res.setHeader(
|
||||
// "Access-Control-Allow-Origin",
|
||||
// "https://test.vespa.qxyushen.top"
|
||||
// );
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS"
|
||||
|
||||
881
项目交接文档.md
Normal file
881
项目交接文档.md
Normal file
@@ -0,0 +1,881 @@
|
||||
# 项目交接文档
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
### 1.1 项目基本信息
|
||||
|
||||
- **项目名称**: pure-admin-thin(精简版后台管理系统)
|
||||
- **项目版本**: 5.9.0
|
||||
- **技术栈**: Vue 3 + TypeScript + Vite + Element Plus + Pinia
|
||||
- **开发端口**: 8848
|
||||
- **Node版本要求**: ^18.18.0 || ^20.9.0 || >=22.0.0
|
||||
- **包管理器**: pnpm >= 9
|
||||
|
||||
### 1.2 项目简介
|
||||
|
||||
本项目是基于 vue-pure-admin 精简版开发的后台管理系统,主要用于管理直播平台的各项业务功能。项目采用前后端分离架构,使用 Vue 3 组合式 API 开发,集成了完整的权限管理、路由管理、状态管理等功能。
|
||||
|
||||
### 1.3 在线地址
|
||||
|
||||
- **测试环境**: https://test.vespa.qxyushen.top
|
||||
- **API地址**: https://test.vespa.qxyushen.top/adminapi
|
||||
|
||||
## 二、技术架构
|
||||
|
||||
### 2.1 核心技术栈
|
||||
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Vue | 3.5.13 | 渐进式 JavaScript 框架 |
|
||||
| TypeScript | 5.6.3 | JavaScript 的超集 |
|
||||
| Vite | 6.0.3 | 新一代前端构建工具 |
|
||||
| Element Plus | 2.9.0 | Vue 3 UI 组件库 |
|
||||
| Pinia | 2.3.0 | Vue 状态管理库 |
|
||||
| Vue Router | 4.5.0 | Vue 官方路由管理器 |
|
||||
| Axios | 1.7.9 | HTTP 请求库 |
|
||||
| Tailwind CSS | 3.4.16 | 原子化 CSS 框架 |
|
||||
|
||||
### 2.2 主要依赖库
|
||||
|
||||
|
||||
- **@pureadmin/table**: 表格组件增强
|
||||
- **@pureadmin/utils**: 工具函数库
|
||||
- **@wangeditor/editor**: 富文本编辑器
|
||||
- **echarts**: 数据可视化图表库
|
||||
- **dayjs**: 日期处理库
|
||||
- **ali-oss**: 阿里云 OSS 上传
|
||||
- **agora-rtc-sdk-ng**: 声网音视频 SDK
|
||||
- **xlsx**: Excel 导入导出
|
||||
- **sortablejs**: 拖拽排序库
|
||||
|
||||
### 2.3 项目结构
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── .husky/ # Git hooks 配置
|
||||
├── build/ # 构建配置文件
|
||||
├── mock/ # Mock 数据
|
||||
├── public/ # 静态资源
|
||||
│ ├── favicon.ico
|
||||
│ ├── logo.svg
|
||||
│ └── platform-config.json # 平台配置文件
|
||||
├── src/ # 源代码目录
|
||||
│ ├── api/ # API 接口
|
||||
│ │ ├── modules/ # 接口模块
|
||||
│ │ ├── routes.ts # 路由接口
|
||||
│ │ └── user.ts # 用户接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── iconfont/ # 图标字体
|
||||
│ │ ├── login/ # 登录页资源
|
||||
│ │ ├── svg/ # SVG 图标
|
||||
│ │ └── user.jpg # 默认头像
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── ReAuth/ # 权限组件
|
||||
│ │ ├── ReDialog/ # 对话框组件
|
||||
│ │ ├── ReIcon/ # 图标组件
|
||||
│ │ ├── RePureTableBar/ # 表格工具栏
|
||||
│ │ ├── SearchForm/ # 搜索表单
|
||||
│ │ ├── UploadImage/ # 图片上传
|
||||
│ │ └── ...
|
||||
│ ├── config/ # 配置文件
|
||||
│ │ └── index.ts # 全局配置
|
||||
│ ├── directives/ # 自定义指令
|
||||
│ │ ├── auth/ # 权限指令
|
||||
│ │ ├── copy/ # 复制指令
|
||||
│ │ ├── longpress/ # 长按指令
|
||||
│ │ └── ...
|
||||
│ ├── layout/ # 布局组件
|
||||
│ │ ├── components/ # 布局子组件
|
||||
│ │ ├── index.vue # 主布局
|
||||
│ │ └── frame.vue # 框架布局
|
||||
│ ├── plugins/ # 插件配置
|
||||
│ │ ├── echarts.ts # ECharts 配置
|
||||
│ │ └── elementPlus.ts # Element Plus 配置
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ ├── modules/ # 路由模块
|
||||
│ │ ├── index.ts # 路由入口
|
||||
│ │ └── utils.ts # 路由工具
|
||||
│ ├── store/ # 状态管理
|
||||
│ │ ├── modules/ # Store 模块
|
||||
│ │ ├── index.ts # Store 入口
|
||||
│ │ └── types.ts # 类型定义
|
||||
│ ├── style/ # 全局样式
|
||||
│ │ ├── index.scss # 主样式
|
||||
│ │ ├── reset.scss # 重置样式
|
||||
│ │ ├── tailwind.css # Tailwind 样式
|
||||
│ │ └── ...
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── http/ # HTTP 请求封装
|
||||
│ │ ├── auth.ts # 认证工具
|
||||
│ │ ├── ali-oss.js # OSS 上传
|
||||
│ │ └── ...
|
||||
│ ├── views/ # 页面视图
|
||||
│ │ ├── login/ # 登录页
|
||||
│ │ ├── welcome/ # 欢迎页
|
||||
│ │ └── ...(详见业务模块)
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 入口文件
|
||||
├── types/ # 类型声明
|
||||
├── .env # 环境变量(开发)
|
||||
├── .env.production # 环境变量(生产)
|
||||
├── .env.staging # 环境变量(预发布)
|
||||
├── Dockerfile # Docker 配置
|
||||
├── package.json # 项目依赖
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── vite.config.ts # Vite 配置
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 三、业务模块
|
||||
|
||||
### 3.1 核心业务模块
|
||||
|
||||
|
||||
#### 1. 用户管理模块 (Admin)
|
||||
- **路径**: `src/views/Admin/`
|
||||
- **功能**:
|
||||
- 用户列表管理 (userList)
|
||||
- 用户操作日志 (userLog)
|
||||
|
||||
#### 2. 房间管理模块 (room)
|
||||
- **路径**: `src/views/room/`
|
||||
- **功能**:
|
||||
- 房间列表 (roomList)
|
||||
- 房间审核 (roomExamine)
|
||||
- 房间背景 (roomBackground)
|
||||
- 房间标签 (roomTag)
|
||||
- 房间类型 (roomType)
|
||||
- 房间规则 (roomRules)
|
||||
- 房间关系 (roomRelation)
|
||||
- 房间补贴 (roomSubsidy)
|
||||
- 房间头条 (roomHeadlines)
|
||||
- 房间日志 (roomLog)
|
||||
- 表情管理 (expression)
|
||||
- 小时榜 (hourlyChart)
|
||||
- 影院房间 (movieRoom)
|
||||
- 红包管理 (RedEnvelope)
|
||||
|
||||
#### 3. 财务管理模块 (Financial)
|
||||
- **路径**: `src/views/Financial/`
|
||||
- **功能**:
|
||||
- 充值管理 (Recharge)
|
||||
- 后台充值 (backRecharge)
|
||||
- 提现管理 (Withdrawal)
|
||||
- 兑换管理 (exchange)
|
||||
|
||||
#### 4. 礼物管理模块 (gift)
|
||||
- **路径**: `src/views/gift/`
|
||||
- **功能**:
|
||||
- 礼物列表 (giftList)
|
||||
- 礼物分类 (giftClassif)
|
||||
|
||||
#### 5. 等级管理模块 (Level)
|
||||
- **路径**: `src/views/Level/`
|
||||
- **功能**:
|
||||
- 财富等级 (wealthGrade)
|
||||
- 魅力等级 (charmGrade)
|
||||
- 歌手等级 (singerLevel)
|
||||
- CP等级 (cpLevel)
|
||||
|
||||
#### 6. 贵族管理模块 (Nobility)
|
||||
- **路径**: `src/views/Nobility/`
|
||||
- **功能**:
|
||||
- 贵族列表 (nobilityList)
|
||||
- 贵族特权 (nobilityPower)
|
||||
- 用户贵族特权 (userPowerByNobility)
|
||||
|
||||
#### 7. 活动管理模块 (Activities)
|
||||
- **路径**: `src/views/Activities/`
|
||||
- **功能**:
|
||||
- 新人活动 (newcomer)
|
||||
- 礼包活动 (giftPack)
|
||||
- 好礼活动 (goodGift)
|
||||
- 充值列表 (RechargeList)
|
||||
|
||||
#### 8. 盲盒管理模块 (BlindBox)
|
||||
- **路径**: `src/views/BlindBox/`
|
||||
- **功能**:
|
||||
- 盲盒列表 (boxList)
|
||||
- 开启记录 (openRecord)
|
||||
- 转盘管理 (turntable)
|
||||
|
||||
#### 9. 每日任务模块 (dailyTasksBox)
|
||||
- **路径**: `src/views/dailyTasksBox/`
|
||||
- **功能**:
|
||||
- 活动列表 (ActivitiesList)
|
||||
- 发放列表 (sendList)
|
||||
|
||||
#### 10. 装扮管理模块 (Decorate)
|
||||
- **路径**: `src/views/Decorate/`
|
||||
- **功能**:
|
||||
- 装扮列表 (decorateList)
|
||||
- 用户装扮 (decorateUser)
|
||||
|
||||
#### 11. 动态管理模块 (dynamics)
|
||||
- **路径**: `src/views/dynamics/`
|
||||
- **功能**:
|
||||
- 动态列表 (dynamicsList)
|
||||
- 动态话题 (dynamicsTopic)
|
||||
|
||||
#### 12. 举报管理模块 (Inform)
|
||||
- **路径**: `src/views/Inform/`
|
||||
- **功能**:
|
||||
- 举报列表 (reportLIst)
|
||||
- 举报类型 (reportType)
|
||||
- 用户反馈 (feedback)
|
||||
- 安卓日志 (androidlog)
|
||||
|
||||
#### 13. 邀请管理模块 (Invited)
|
||||
- **路径**: `src/views/Invited/`
|
||||
- **功能**:
|
||||
- 邀请列表 (inviteList)
|
||||
- 收益列表 (incomeList)
|
||||
|
||||
#### 14. 马迎新春游戏模块 (LXlegend)
|
||||
- **路径**: `src/views/LXlegend/`
|
||||
- **功能**:
|
||||
- 游戏列表 (gameList)
|
||||
- 期数列表 (periodsList)
|
||||
- 多期列表 (multipleList)
|
||||
|
||||
#### 15. 新用户管理模块 (newuser)
|
||||
- **路径**: `src/views/newuser/`
|
||||
- **功能**:
|
||||
- 新用户列表 (newuserList)
|
||||
- 用户标签 (newuserTag)
|
||||
- 背包列表 (backpackList)
|
||||
- 禁用用户 (disableUser)
|
||||
- 歌手用户 (singerUser)
|
||||
- 机器人列表 (robotList)
|
||||
|
||||
#### 16. 乐园管理模块 (paradise)
|
||||
- **路径**: `src/views/paradise/`
|
||||
- **功能**:
|
||||
- 乐园列表 (paradiseList)
|
||||
- 抽奖/锁定列表 (drawOrlockList)
|
||||
|
||||
#### 17. 统计分析模块 (Statistical)
|
||||
- **路径**: `src/views/Statistical/`
|
||||
- **功能**:
|
||||
- 礼物记录 (giftRecord)
|
||||
- 礼物排行 (giftRank)
|
||||
- 充值排行 (rechargeRank)
|
||||
- 消费排行 (consumerRank)
|
||||
- 收礼排行 (acceptGiftsRank)
|
||||
- 房间流水排行 (roomFlowRank)
|
||||
- 幸运币排行 (luckycoinRank)
|
||||
- 幸运币抽奖 (luckycoinLottery)
|
||||
- 任务分配 (taskAssignment)
|
||||
|
||||
#### 18. 系统管理模块 (system)
|
||||
- **路径**: `src/views/system/`
|
||||
- **功能**:
|
||||
- 帮助中心 (helpCenter)
|
||||
- 幸运币管理 (luckyCoin)
|
||||
- 隐私设置 (private)
|
||||
- 充值规则 (rechargeRules)
|
||||
- 二级密码 (secondPassword)
|
||||
- 单页管理 (singlePage)
|
||||
- 任务管理 (Tasks)
|
||||
- 主题管理 (themeManage)
|
||||
|
||||
#### 19. 未成年管理模块 (Underage)
|
||||
- **路径**: `src/views/Underage/`
|
||||
- **功能**:
|
||||
- 青少年列表 (adolescentList)
|
||||
- 青少年类型 (adolescentType)
|
||||
|
||||
#### 20. 公会管理模块 (union)
|
||||
- **路径**: `src/views/union/`
|
||||
- **功能**:
|
||||
- 公会列表 (unionList)
|
||||
- 公会规则 (unionRule)
|
||||
- 公会补贴 (unionSubsidy)
|
||||
|
||||
#### 21. 其他模块
|
||||
- **广告管理** (advertisement): 广告位管理
|
||||
- **消息管理** (message): 系统消息推送
|
||||
- **版本管理** (Version): APP版本控制
|
||||
- **权限管理** (permission): 角色权限配置
|
||||
|
||||
### 3.2 API 接口模块
|
||||
|
||||
所有 API 接口统一放在 `src/api/modules/` 目录下,按业务模块划分:
|
||||
|
||||
|
||||
- `activities.ts` - 活动相关接口
|
||||
- `admin.ts` - 管理员相关接口
|
||||
- `adolescent.ts` - 青少年相关接口
|
||||
- `advertisement.ts` - 广告相关接口
|
||||
- `backpack.ts` - 背包相关接口
|
||||
- `blindBox.ts` - 盲盒相关接口
|
||||
- `decorate.ts` - 装扮相关接口
|
||||
- `dynamics.ts` - 动态相关接口
|
||||
- `expression.ts` - 表情相关接口
|
||||
- `Financial.ts` - 财务相关接口
|
||||
- `game.ts` - 游戏相关接口
|
||||
- `gift.ts` - 礼物相关接口
|
||||
- `home.ts` - 首页相关接口
|
||||
- `hourlyChart.ts` - 小时榜相关接口
|
||||
- `Inform.ts` - 举报相关接口
|
||||
- `invite.ts` - 邀请相关接口
|
||||
- `level.ts` - 等级相关接口
|
||||
- `message.ts` - 消息相关接口
|
||||
- `newuserList.ts` - 新用户列表接口
|
||||
- `newuserTag.ts` - 用户标签接口
|
||||
- `nobility.ts` - 贵族相关接口
|
||||
- `permission.ts` - 权限相关接口
|
||||
- `room.ts` - 房间相关接口
|
||||
- `statistics.ts` - 统计相关接口
|
||||
- `system.ts` - 系统相关接口
|
||||
- `union.ts` - 公会相关接口
|
||||
- `Version.ts` - 版本相关接口
|
||||
|
||||
## 四、开发指南
|
||||
|
||||
### 4.1 环境准备
|
||||
|
||||
1. **安装 Node.js**
|
||||
- 版本要求: ^18.18.0 || ^20.9.0 || >=22.0.0
|
||||
- 推荐使用 nvm 管理 Node 版本
|
||||
|
||||
2. **安装 pnpm**
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
3. **克隆项目**
|
||||
```bash
|
||||
git clone [项目地址]
|
||||
cd [项目目录]
|
||||
```
|
||||
|
||||
4. **安装依赖**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 4.2 开发命令
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
|
||||
# 构建生产环境
|
||||
pnpm build
|
||||
|
||||
# 构建预发布环境
|
||||
pnpm build:staging
|
||||
|
||||
# 预览构建结果
|
||||
pnpm preview
|
||||
|
||||
# 类型检查
|
||||
pnpm typecheck
|
||||
|
||||
# 代码格式化
|
||||
pnpm lint
|
||||
|
||||
# ESLint 检查
|
||||
pnpm lint:eslint
|
||||
|
||||
# Prettier 格式化
|
||||
pnpm lint:prettier
|
||||
|
||||
# Stylelint 检查
|
||||
pnpm lint:stylelint
|
||||
|
||||
# 清理缓存
|
||||
pnpm clean:cache
|
||||
```
|
||||
|
||||
### 4.3 环境配置
|
||||
|
||||
项目支持三种环境配置:
|
||||
|
||||
1. **开发环境** (`.env`)
|
||||
- 端口: 8848
|
||||
- 自动代理到测试服务器
|
||||
|
||||
2. **预发布环境** (`.env.staging`)
|
||||
- 用于预发布测试
|
||||
|
||||
3. **生产环境** (`.env.production`)
|
||||
- 路由模式: hash
|
||||
- CDN: 关闭
|
||||
- 压缩: 关闭
|
||||
|
||||
### 4.4 代理配置
|
||||
|
||||
开发环境下,所有 `/adminapi` 开头的请求会被代理到测试服务器:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
proxy: {
|
||||
"/adminapi": {
|
||||
target: "https://test.vespa.qxyushen.top",
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 API 配置
|
||||
|
||||
后端接口地址配置在 `src/utils/http/config.ts`:
|
||||
|
||||
```typescript
|
||||
// 测试环境
|
||||
export const URL = "https://test.vespa.qxyushen.top";
|
||||
|
||||
// 声网 AppId
|
||||
export const appIdBySw = "02f7339ec98947deaeab173599891932";
|
||||
```
|
||||
|
||||
### 4.6 开发规范
|
||||
|
||||
#### 4.6.1 代码规范
|
||||
|
||||
- 使用 TypeScript 开发
|
||||
- 遵循 ESLint 规则
|
||||
- 使用 Prettier 格式化代码
|
||||
- 组件使用 Vue 3 组合式 API (setup script)
|
||||
- 使用 Pinia 进行状态管理
|
||||
|
||||
#### 4.6.2 命名规范
|
||||
|
||||
- 组件文件名: PascalCase (如 `UserList.vue`)
|
||||
- 工具函数: camelCase (如 `getUserInfo`)
|
||||
- 常量: UPPER_SNAKE_CASE (如 `API_BASE_URL`)
|
||||
- 类型/接口: PascalCase (如 `UserInfo`)
|
||||
|
||||
#### 4.6.3 目录规范
|
||||
|
||||
- 页面组件放在 `src/views/` 下,按业务模块分类
|
||||
- 公共组件放在 `src/components/` 下
|
||||
- API 接口放在 `src/api/modules/` 下
|
||||
- 工具函数放在 `src/utils/` 下
|
||||
- 类型定义放在 `types/` 或对应模块的 `types.ts` 中
|
||||
|
||||
#### 4.6.4 Git 提交规范
|
||||
|
||||
项目使用 commitlint 规范提交信息,格式如下:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
type 类型:
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复 bug
|
||||
- `docs`: 文档更新
|
||||
- `style`: 代码格式调整
|
||||
- `refactor`: 重构
|
||||
- `perf`: 性能优化
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建/工具链相关
|
||||
|
||||
示例:
|
||||
```bash
|
||||
git commit -m "feat(user): 添加用户列表导出功能"
|
||||
git commit -m "fix(room): 修复房间列表分页问题"
|
||||
```
|
||||
|
||||
## 五、核心功能说明
|
||||
|
||||
### 5.1 权限管理
|
||||
|
||||
项目采用基于角色的权限控制(RBAC):
|
||||
|
||||
1. **路由权限**
|
||||
- 在路由 meta 中配置 `roles` 字段
|
||||
- 用户登录后根据角色动态加载路由
|
||||
|
||||
2. **按钮权限**
|
||||
- 使用 `<Auth>` 组件包裹需要权限控制的按钮
|
||||
- 使用 `v-auth` 指令控制元素显示
|
||||
|
||||
3. **接口权限**
|
||||
- 请求拦截器自动添加 token
|
||||
- 响应拦截器统一处理权限错误
|
||||
|
||||
### 5.2 路由管理
|
||||
|
||||
|
||||
#### 5.2.1 路由结构
|
||||
|
||||
- **静态路由**: 不需要权限的路由(登录、404等)
|
||||
- **动态路由**: 根据用户权限动态加载的路由
|
||||
|
||||
#### 5.2.2 路由配置
|
||||
|
||||
路由配置在 `src/router/modules/` 目录下,按模块划分:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
path: "/user",
|
||||
name: "User",
|
||||
redirect: "/user/list",
|
||||
meta: {
|
||||
icon: "user",
|
||||
title: "用户管理",
|
||||
rank: 1
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/user/list",
|
||||
name: "UserList",
|
||||
component: () => import("@/views/Admin/userList/index.vue"),
|
||||
meta: {
|
||||
title: "用户列表",
|
||||
roles: ["admin"]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
#### 5.2.3 路由守卫
|
||||
|
||||
- **beforeEach**: 权限验证、登录状态检查
|
||||
- **afterEach**: 进度条关闭、页面标题设置
|
||||
|
||||
### 5.3 状态管理
|
||||
|
||||
使用 Pinia 进行状态管理,主要模块:
|
||||
|
||||
- **user**: 用户信息、登录状态
|
||||
- **permission**: 权限信息、动态路由
|
||||
- **multiTags**: 标签页管理
|
||||
- **settings**: 系统设置
|
||||
|
||||
### 5.4 HTTP 请求
|
||||
|
||||
#### 5.4.1 请求封装
|
||||
|
||||
基于 Axios 封装,位于 `src/utils/http/`:
|
||||
|
||||
- 自动添加 token
|
||||
- 统一错误处理
|
||||
- 请求/响应拦截
|
||||
- 支持取消请求
|
||||
|
||||
#### 5.4.2 使用示例
|
||||
|
||||
```typescript
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
// GET 请求
|
||||
export const getUserList = (params) => {
|
||||
return http.request("get", "/adminapi/user/list", { params });
|
||||
};
|
||||
|
||||
// POST 请求
|
||||
export const createUser = (data) => {
|
||||
return http.request("post", "/adminapi/user/create", { data });
|
||||
};
|
||||
```
|
||||
|
||||
### 5.5 表格组件
|
||||
|
||||
使用 `@pureadmin/table` 增强的 Element Plus 表格:
|
||||
|
||||
- 支持分页
|
||||
- 支持排序
|
||||
- 支持筛选
|
||||
- 支持导出
|
||||
- 支持列配置
|
||||
|
||||
### 5.6 表单组件
|
||||
|
||||
#### 5.6.1 搜索表单
|
||||
|
||||
使用 `SearchForm` 组件快速构建搜索表单:
|
||||
|
||||
```vue
|
||||
<SearchForm
|
||||
:columns="searchColumns"
|
||||
:model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
```
|
||||
|
||||
#### 5.6.2 表单验证
|
||||
|
||||
使用 Element Plus 表单验证:
|
||||
|
||||
```typescript
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" }
|
||||
],
|
||||
email: [
|
||||
{ type: "email", message: "请输入正确的邮箱", trigger: "blur" }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 5.7 文件上传
|
||||
|
||||
#### 5.7.1 图片上传
|
||||
|
||||
使用 `UploadImage` 组件:
|
||||
|
||||
```vue
|
||||
<UploadImage
|
||||
v-model="form.avatar"
|
||||
:limit="1"
|
||||
:size="2"
|
||||
/>
|
||||
```
|
||||
|
||||
#### 5.7.2 OSS 上传
|
||||
|
||||
使用阿里云 OSS 直传,配置在 `src/utils/ali-oss.js`
|
||||
|
||||
### 5.8 富文本编辑器
|
||||
|
||||
使用 wangEditor 5:
|
||||
|
||||
```vue
|
||||
<RichText v-model="form.content" />
|
||||
```
|
||||
|
||||
### 5.9 图表组件
|
||||
|
||||
使用 ECharts 5,配置在 `src/plugins/echarts.ts`
|
||||
|
||||
### 5.10 音视频功能
|
||||
|
||||
使用声网 SDK (agora-rtc-sdk-ng),AppId 配置在 `src/utils/http/config.ts`
|
||||
|
||||
## 六、部署指南
|
||||
|
||||
### 6.1 Docker 部署
|
||||
|
||||
项目包含 Dockerfile,支持 Docker 部署:
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t pure-admin-thin .
|
||||
|
||||
# 运行容器
|
||||
docker run -d -p 80:80 pure-admin-thin
|
||||
```
|
||||
|
||||
Dockerfile 说明:
|
||||
- 基础镜像: node:20-alpine
|
||||
- 构建工具: pnpm
|
||||
- Web 服务器: nginx
|
||||
- 暴露端口: 80
|
||||
|
||||
### 6.2 传统部署
|
||||
|
||||
#### 6.2.1 构建
|
||||
|
||||
```bash
|
||||
# 生产环境构建
|
||||
pnpm build
|
||||
|
||||
# 预发布环境构建
|
||||
pnpm build:staging
|
||||
```
|
||||
|
||||
#### 6.2.2 部署
|
||||
|
||||
将 `dist` 目录下的文件部署到 Web 服务器(Nginx/Apache)
|
||||
|
||||
#### 6.2.3 Nginx 配置示例
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /adminapi {
|
||||
proxy_pass https://test.vespa.qxyushen.top;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 环境变量配置
|
||||
|
||||
部署前需要根据环境修改以下配置:
|
||||
|
||||
1. **API 地址**: `src/utils/http/config.ts`
|
||||
2. **路由模式**: `.env.production` 中的 `VITE_ROUTER_HISTORY`
|
||||
3. **公共路径**: `.env.production` 中的 `VITE_PUBLIC_PATH`
|
||||
|
||||
### 6.4 性能优化
|
||||
|
||||
项目已配置以下优化:
|
||||
|
||||
- Vite 构建优化
|
||||
- 代码分割
|
||||
- 静态资源压缩(可选)
|
||||
- CDN 加速(可选)
|
||||
- 路由懒加载
|
||||
- 组件按需引入
|
||||
|
||||
## 七、常见问题
|
||||
|
||||
### 7.1 开发环境问题
|
||||
|
||||
**Q: 启动项目报错 "Cannot find module"**
|
||||
A: 删除 `node_modules` 和 `pnpm-lock.yaml`,重新执行 `pnpm install`
|
||||
|
||||
**Q: 端口被占用**
|
||||
A: 修改 `.env` 文件中的 `VITE_PORT` 配置
|
||||
|
||||
**Q: 代理不生效**
|
||||
A: 检查 `vite.config.ts` 中的 proxy 配置,确保 target 地址正确
|
||||
|
||||
### 7.2 构建问题
|
||||
|
||||
**Q: 构建内存溢出**
|
||||
A: 项目已配置 `NODE_OPTIONS=--max-old-space-size=8192`,如仍有问题可适当增加
|
||||
|
||||
**Q: 构建后白屏**
|
||||
A: 检查路由模式配置,hash 模式需要设置 `VITE_ROUTER_HISTORY = "hash"`
|
||||
|
||||
### 7.3 权限问题
|
||||
|
||||
**Q: 登录后看不到菜单**
|
||||
A: 检查用户角色配置,确保路由 meta 中的 roles 包含用户角色
|
||||
|
||||
**Q: 接口返回 401**
|
||||
A: Token 过期或无效,需要重新登录
|
||||
|
||||
### 7.4 样式问题
|
||||
|
||||
**Q: Element Plus 样式不生效**
|
||||
A: 确保在 `main.ts` 中引入了 `element-plus/dist/index.css`
|
||||
|
||||
**Q: Tailwind 样式不生效**
|
||||
A: 确保在 `main.ts` 中引入了 `./style/tailwind.css`
|
||||
|
||||
## 八、技术支持
|
||||
|
||||
### 8.1 相关文档
|
||||
|
||||
- **Vue 3 官方文档**: https://cn.vuejs.org/
|
||||
- **Vite 官方文档**: https://cn.vitejs.dev/
|
||||
- **Element Plus 文档**: https://element-plus.org/zh-CN/
|
||||
- **Pinia 文档**: https://pinia.vuejs.org/zh/
|
||||
- **vue-pure-admin 文档**: https://pure-admin.cn/
|
||||
- **@pureadmin/utils 文档**: https://pure-admin-utils.netlify.app
|
||||
|
||||
### 8.2 常用资源
|
||||
|
||||
- **图标库**:
|
||||
- Iconify: https://icon-sets.iconify.design/
|
||||
- Element Plus Icons: https://element-plus.org/zh-CN/component/icon.html
|
||||
|
||||
- **UI 设计**:
|
||||
- Element Plus 设计规范
|
||||
- Tailwind CSS 工具类
|
||||
|
||||
- **工具库**:
|
||||
- dayjs: 日期处理
|
||||
- lodash-es: 工具函数
|
||||
- @pureadmin/utils: 项目工具函数
|
||||
|
||||
### 8.3 问题反馈
|
||||
|
||||
如遇到问题,可以通过以下方式反馈:
|
||||
|
||||
1. 查看项目 README 和文档
|
||||
2. 搜索相关 issue
|
||||
3. 联系项目负责人
|
||||
4. 提交新的 issue
|
||||
|
||||
## 九、项目特色功能
|
||||
|
||||
### 9.1 声网音视频集成
|
||||
|
||||
项目集成了声网 SDK,支持音视频通话功能:
|
||||
|
||||
- **AppId**: 配置在 `src/utils/http/config.ts`
|
||||
- **SDK**: agora-rtc-sdk-ng v4.23.4
|
||||
- **功能**: 支持音视频通话、屏幕共享等
|
||||
|
||||
### 9.2 阿里云 OSS 上传
|
||||
|
||||
支持文件直传到阿里云 OSS:
|
||||
|
||||
- **配置**: `src/utils/ali-oss.js`
|
||||
- **组件**: `UploadImage` 组件封装
|
||||
- **功能**: 支持图片上传、进度显示、预览等
|
||||
|
||||
### 9.3 Excel 导入导出
|
||||
|
||||
使用 xlsx 库实现 Excel 功能:
|
||||
|
||||
- **导出**: 表格数据导出为 Excel
|
||||
- **导入**: Excel 文件解析导入
|
||||
- **组件**: `exportDialog` 组件封装
|
||||
|
||||
### 9.4 富文本编辑器
|
||||
|
||||
集成 wangEditor 5:
|
||||
|
||||
- **组件**: `RichText` 组件
|
||||
- **功能**: 支持图片上传、视频插入、表格等
|
||||
- **配置**: 可自定义工具栏
|
||||
|
||||
### 9.5 ECharts 图表
|
||||
|
||||
集成 ECharts 5 数据可视化:
|
||||
|
||||
- **配置**: `src/plugins/echarts.ts`
|
||||
- **按需引入**: 只引入使用的图表类型
|
||||
- **响应式**: 自动适配容器大小
|
||||
|
||||
### 9.6 拖拽排序
|
||||
|
||||
使用 sortablejs 实现拖拽功能:
|
||||
|
||||
- **表格行拖拽**: 调整数据顺序
|
||||
- **菜单拖拽**: 自定义菜单排序
|
||||
- **组件拖拽**: 页面布局调整
|
||||
|
||||
### 9.7 多标签页
|
||||
|
||||
支持多标签页功能:
|
||||
|
||||
- **标签管理**: 打开、关闭、刷新
|
||||
- **右键菜单**: 关闭其他、关闭所有
|
||||
- **缓存**: 支持页面缓存
|
||||
- **持久化**: 刷新后保持标签状态
|
||||
|
||||
### 9.8 主题切换
|
||||
|
||||
支持明暗主题切换:
|
||||
|
||||
- **配置**: `src/style/theme.scss`
|
||||
- **切换**: 实时切换,无需刷新
|
||||
- **持久化**: 记住用户选择
|
||||
|
||||
### 9.9 响应式布局
|
||||
|
||||
完全响应式设计:
|
||||
|
||||
- **移动端适配**: 支持手机、平板访问
|
||||
- **侧边栏**: 可折叠、自适应
|
||||
- **表格**: 响应式列显示
|
||||
|
||||
### 9.10 国际化支持
|
||||
|
||||
虽然当前是非国际化版本,但架构支持国际化:
|
||||
|
||||
- **切换版本**: 可切换到国际化分支
|
||||
- **i18n**: 预留国际化接口
|
||||
- **文档**: 提供国际化版本文档
|
||||
Reference in New Issue
Block a user