yingsa/admin/src/views/ladder/index.vue
ethanfly 02937ca33c feat(天梯): 新增选手定位功能并调整挑战赛权重
- 在小程序天梯排名页添加“定位我”按钮,点击可滚动到当前用户所在位置
- 新增获取用户排名接口 `/ladder/my-rank` 用于定位计算
- 调整挑战赛权重从 1.5 降至 1.0,与日常畅打保持一致
- 新增数据库脚本 `setChallengeMatchWeightTo1.js` 用于更新历史数据
- 在管理员界面创建天梯用户时,根据所选等级自动填充默认战力值
- 修复管理员更新比赛时挑战赛权重强制设置为 1.0 的问题
- 新增天梯汇总大屏页面及相关路由
- 添加大屏比赛列表接口 `/match/display-list` 用于展示进行中和近期比赛
- 优化用户详情页的胜负场和胜率显示逻辑
- 修复小程序用户注册时的性别选择逻辑
2026-02-02 03:22:36 +08:00

347 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="ladder-page">
<div class="page-card">
<div class="page-header">
<h2>天梯用户管理</h2>
<div class="header-actions">
<el-button type="success" @click="handleGenerate">
<el-icon><Connection /></el-icon>
批量生成账号
</el-button>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增天梯用户
</el-button>
</div>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" class="search-form">
<el-form-item v-if="userStore.isSuperAdmin">
<el-select v-model="searchForm.store_id" placeholder="选择门店" clearable style="width: 180px">
<el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="searchForm.keyword" placeholder="姓名/手机号" clearable style="width: 160px" />
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.level" placeholder="等级" clearable style="width: 130px">
<el-option label="Lv1 新锐" :value="1" />
<el-option label="Lv2 精锐" :value="2" />
<el-option label="Lv3 高手" :value="3" />
<el-option label="Lv4 大师" :value="4" />
<el-option label="Lv5 宗师" :value="5" />
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.gender" placeholder="性别" clearable style="width: 100px">
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="tableData" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户" min-width="180">
<template #default="{ row }">
<div class="user-cell">
<el-avatar :size="40" :src="row.avatar">{{ row.realName?.[0] }}</el-avatar>
<div class="user-info">
<span class="name">{{ row.realName }}</span>
<span class="phone">{{ row.phone }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column v-if="userStore.isSuperAdmin" prop="storeName" label="门店" width="140" />
<el-table-column prop="gender" label="性别" width="80">
<template #default="{ row }">
<span :class="['gender-tag', row.gender === 1 ? 'male' : 'female']">
{{ row.gender === 1 ? '男' : '女' }}
</span>
</template>
</el-table-column>
<el-table-column prop="level" label="等级" width="100">
<template #default="{ row }">
<span :class="['level-tag', 'lv' + row.level]">
Lv{{ row.level }} {{ row.levelName }}
</span>
</template>
</el-table-column>
<el-table-column prop="powerScore" label="战力值" width="100" sortable />
<el-table-column label="胜率" width="100">
<template #default="{ row }">
{{ row.matchCount ? Math.round(row.winCount / row.matchCount * 100) : 0 }}%
</template>
</el-table-column>
<el-table-column prop="matchCount" label="比赛" width="80" />
<el-table-column prop="monthlyMatchCount" label="本月" width="80" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</div>
<!-- 编辑弹窗 -->
<el-dialog
v-model="showEditDialog"
:title="editForm.id ? '编辑天梯用户' : '新增天梯用户'"
width="500px"
>
<el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="100px">
<el-form-item v-if="userStore.isSuperAdmin && !editForm.id" label="门店" prop="store_id">
<el-select v-model="editForm.store_id" placeholder="选择门店">
<el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="phone" v-if="!editForm.id">
<el-input v-model="editForm.phone" placeholder="请输入用户手机号" />
</el-form-item>
<el-form-item label="真实姓名" prop="real_name">
<el-input v-model="editForm.real_name" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="editForm.gender">
<el-radio :value="1">男</el-radio>
<el-radio :value="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="等级" prop="level">
<el-select v-model="editForm.level" placeholder="选择等级" @change="handleLevelChange">
<el-option label="Lv1 新锐" :value="1" />
<el-option label="Lv2 精锐" :value="2" />
<el-option label="Lv3 高手" :value="3" />
<el-option label="Lv4 大师" :value="4" />
<el-option label="Lv5 宗师" :value="5" />
</el-select>
</el-form-item>
<el-form-item label="战力值" prop="power_score">
<el-input-number v-model="editForm.power_score" :min="0" :max="9999" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { getLadderUsers, createLadderUser, updateLadderUser, deleteLadderUser, getStores, generateLadderUsers } from '@/api/admin'
const userStore = useUserStore()
const LEVEL_POWER_DEFAULTS = {
1: 1000,
2: 1200,
3: 1500,
4: 1800,
5: 2200
}
const loading = ref(false)
const tableData = ref([])
const stores = ref([])
const searchForm = ref({ keyword: '', store_id: '', level: '', gender: '' })
const pagination = ref({ page: 1, pageSize: 20, total: 0 })
const showEditDialog = ref(false)
const editFormRef = ref()
const editForm = ref({
id: null,
store_id: '',
phone: '',
real_name: '',
gender: 1,
level: 1,
power_score: 1000
})
const editRules = {
store_id: [{ required: true, message: '请选择门店', trigger: 'change' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
real_name: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
level: [{ required: true, message: '请选择等级', trigger: 'change' }],
power_score: [{ required: true, message: '请输入战力值', trigger: 'blur' }]
}
const fetchStores = async () => {
if (userStore.isSuperAdmin) {
const res = await getStores({ pageSize: 100 })
stores.value = res.data.list
}
}
const fetchData = async () => {
loading.value = true
try {
const res = await getLadderUsers({
...searchForm.value,
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
tableData.value = res.data.list
pagination.value.total = res.data.pagination.total
} finally {
loading.value = false
}
}
const resetSearch = () => {
searchForm.value = { keyword: '', store_id: '', level: '', gender: '' }
pagination.value.page = 1
fetchData()
}
const handleAdd = () => {
editForm.value = {
id: null,
store_id: userStore.userInfo?.storeId || '',
phone: '',
real_name: '',
gender: 1,
level: 1,
power_score: 1000
}
showEditDialog.value = true
}
const handleEdit = (row) => {
editForm.value = {
id: row.id,
store_id: row.storeId,
phone: row.phone,
real_name: row.realName,
gender: row.gender,
level: row.level,
power_score: row.powerScore
}
showEditDialog.value = true
}
const handleLevelChange = (level) => {
if (editForm.value.id) return
const next = LEVEL_POWER_DEFAULTS[level]
if (next !== undefined) {
editForm.value.power_score = next
}
}
const handleSave = async () => {
await editFormRef.value?.validate()
if (editForm.value.id) {
await updateLadderUser(editForm.value.id, editForm.value)
ElMessage.success('更新成功')
} else {
await createLadderUser(editForm.value)
ElMessage.success('创建成功')
}
showEditDialog.value = false
fetchData()
}
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除该天梯用户吗?', '提示', {
type: 'warning'
}).then(async () => {
await deleteLadderUser(row.id)
ElMessage.success('删除成功')
fetchData()
})
}
const handleGenerate = () => {
ElMessageBox.confirm(
'确定要为所有未关联账号的天梯用户生成用户账号吗?\n生成的账号将使用手机号作为标识并在用户首次微信登录时自动合并。',
'批量生成账号',
{
confirmButtonText: '确定生成',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
loading.value = true
try {
const res = await generateLadderUsers({
store_id: searchForm.value.store_id || undefined
})
ElMessage.success(res.message)
fetchData()
} finally {
loading.value = false
}
})
}
onMounted(() => {
fetchStores()
fetchData()
})
</script>
<style lang="scss" scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.header-actions {
display: flex;
gap: 12px;
}
}
.user-cell {
display: flex;
align-items: center;
gap: 12px;
.user-info {
display: flex;
flex-direction: column;
.name {
font-weight: 500;
}
.phone {
font-size: 12px;
color: var(--text-secondary);
}
}
}
</style>