Compare commits

...

10 Commits

Author SHA1 Message Date
yziiy
fd9905ffe7 更新交接文档 2026-01-31 10:23:11 +08:00
yziiy
ba1d300f4e 更新 2026-01-31 10:18:36 +08:00
yziiy
5443a6ccf2 更新正式地址 2026-01-30 14:42:52 +08:00
yziiy
6df60fcd9f 更新 2026-01-29 18:19:22 +08:00
yziiy
79aef9aee2 更新 2026-01-29 17:46:52 +08:00
yziiy
e7ce2524cd 工会增加解散时间 2026-01-29 15:08:03 +08:00
yziiy
17e971d548 更新 2026-01-29 14:53:16 +08:00
yziiy
733f246414 更新 2026-01-29 10:19:38 +08:00
yziiy
0e2d3d3097 房间详情增加幸运值流水 2026-01-28 14:40:27 +08:00
yziiy
ffcc0e77c1 更新中奖特效配置 2026-01-28 10:22:35 +08:00
29 changed files with 2933 additions and 36 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

@@ -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 }
);
};

View File

@@ -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 }
);
};

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

@@ -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";

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,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
};
}

View 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> -->

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

@@ -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,

View File

@@ -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>

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>

View 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>

View File

@@ -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>

View File

@@ -178,6 +178,10 @@ export function useData() {
</el-tag>
)
},
{
label: "禁用理由",
prop: "user_block_reason"
},
{
label: "操作",
fixed: "right",

View File

@@ -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>

View File

@@ -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>

View File

@@ -102,6 +102,10 @@ export function useData() {
label: "创建时间",
prop: "createtime"
},
{
label: "解散时间",
prop: "delete_time"
},
{
label: "操作",
fixed: "right",

View File

@@ -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
View 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**: 预留国际化接口
- **文档**: 提供国际化版本文档