676 lines
17 KiB
Vue
676 lines
17 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="team-pk-page">
|
||
<div class="container">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<h1 class="page-title">
|
||
<Icon name="fire" size="xl" color="#EF4444" style="margin-right: 12px;" />
|
||
团队 PK
|
||
</h1>
|
||
<p class="page-desc">部门间的荣誉之战,为团队荣耀而战!</p>
|
||
</div>
|
||
<div class="header-right" v-if="isAdmin">
|
||
<a-button type="primary" @click="showCreateModal">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
发起 PK
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进行中的 PK -->
|
||
<div class="section">
|
||
<h2 class="section-title">
|
||
<Icon name="bolt" size="lg" color="#F59E0B" style="margin-right: 8px;" />
|
||
正在进行
|
||
</h2>
|
||
<div v-if="loading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
<div v-else-if="activePKs.length === 0" class="empty-state">
|
||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||
<p>暂无进行中的 PK</p>
|
||
</div>
|
||
<div v-else class="pk-list">
|
||
<div v-for="pk in activePKs" :key="pk.id" class="pk-card">
|
||
<div class="pk-header">
|
||
<h3 class="pk-name">{{ pk.name }}</h3>
|
||
<div class="pk-status active">进行中</div>
|
||
</div>
|
||
|
||
<div class="pk-battle">
|
||
<!-- 甲方 -->
|
||
<div :class="['team-side', { 'winning': pk.teamAScore > pk.teamBScore }]">
|
||
<div class="team-name">{{ pk.teamAName }}</div>
|
||
<div class="team-score">{{ pk.teamAScore }}</div>
|
||
<div class="team-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-label">任务完成</span>
|
||
<span class="stat-value">{{ pk.teamATaskCount }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">五星好评</span>
|
||
<span class="stat-value">{{ pk.teamAFiveStarCount }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">准时率</span>
|
||
<span class="stat-value">{{ pk.teamAOnTimeRate }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VS -->
|
||
<div class="vs-badge">
|
||
<span class="vs-text">VS</span>
|
||
<div class="vs-time">
|
||
剩余 {{ getRemainingDays(pk.endTime) }} 天
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 乙方 -->
|
||
<div :class="['team-side', { 'winning': pk.teamBScore > pk.teamAScore }]">
|
||
<div class="team-name">{{ pk.teamBName }}</div>
|
||
<div class="team-score">{{ pk.teamBScore }}</div>
|
||
<div class="team-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-label">任务完成</span>
|
||
<span class="stat-value">{{ pk.teamBTaskCount }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">五星好评</span>
|
||
<span class="stat-value">{{ pk.teamBFiveStarCount }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">准时率</span>
|
||
<span class="stat-value">{{ pk.teamBOnTimeRate }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pk-footer">
|
||
<div class="pk-time">
|
||
<Icon name="clock" size="sm" color="#6B6B6B" />
|
||
{{ formatDate(pk.startTime) }} - {{ formatDate(pk.endTime) }}
|
||
</div>
|
||
<a-button size="small" @click="refreshPK(pk.id)">
|
||
<Icon name="arrowPath" size="sm" />
|
||
刷新数据
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 历史战绩 -->
|
||
<div class="section" v-if="endedPKs.length > 0">
|
||
<h2 class="section-title">
|
||
<Icon name="archive" size="lg" color="#6B6B6B" style="margin-right: 8px;" />
|
||
历史战绩
|
||
</h2>
|
||
<div class="history-list">
|
||
<div v-for="pk in endedPKs" :key="pk.id" class="history-card">
|
||
<div class="history-header">
|
||
<span class="history-name">{{ pk.name }}</span>
|
||
<a-tag :color="getWinnerColor(pk.winner)">
|
||
{{ getWinnerText(pk) }}
|
||
</a-tag>
|
||
</div>
|
||
<div class="history-battle">
|
||
<div class="history-team">
|
||
<span :class="{ 'winner': pk.winner === 'A' }">{{ pk.teamAName }}</span>
|
||
<span class="history-score">{{ pk.teamAScore }}</span>
|
||
</div>
|
||
<span class="history-vs">VS</span>
|
||
<div class="history-team">
|
||
<span class="history-score">{{ pk.teamBScore }}</span>
|
||
<span :class="{ 'winner': pk.winner === 'B' }">{{ pk.teamBName }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="history-time">
|
||
{{ formatDate(pk.endTime) }} 结束
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建 PK 弹窗 -->
|
||
<a-modal
|
||
v-model:open="createModalVisible"
|
||
title="发起团队 PK"
|
||
width="700px"
|
||
@ok="handleCreatePK"
|
||
:confirmLoading="creating"
|
||
>
|
||
<a-form :model="pkForm" layout="vertical">
|
||
<a-form-item label="PK 名称" required>
|
||
<a-input v-model:value="pkForm.name" placeholder="例如:12月部门对抗赛" />
|
||
</a-form-item>
|
||
<a-form-item label="PK 描述">
|
||
<a-textarea v-model:value="pkForm.description" :rows="2" placeholder="描述本次PK的主题" />
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="甲方部门" required>
|
||
<a-select v-model:value="pkForm.teamADeptId" @change="handleTeamAChange" placeholder="选择甲方部门">
|
||
<a-select-option v-for="dept in departments" :key="dept.id" :value="dept.id">
|
||
{{ dept.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="乙方部门" required>
|
||
<a-select v-model:value="pkForm.teamBDeptId" @change="handleTeamBChange" placeholder="选择乙方部门">
|
||
<a-select-option
|
||
v-for="dept in departments"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
:disabled="dept.id === pkForm.teamADeptId"
|
||
>
|
||
{{ dept.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="开始时间" required>
|
||
<a-date-picker
|
||
v-model:value="pkForm.startTime"
|
||
show-time
|
||
style="width: 100%"
|
||
placeholder="选择开始时间"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="结束时间" required>
|
||
<a-date-picker
|
||
v-model:value="pkForm.endTime"
|
||
show-time
|
||
style="width: 100%"
|
||
placeholder="选择结束时间"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-divider>奖励设置</a-divider>
|
||
<a-row :gutter="16">
|
||
<a-col :span="8">
|
||
<a-form-item label="胜方奖励积分">
|
||
<a-input-number v-model:value="pkForm.winnerReward" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="败方参与积分">
|
||
<a-input-number v-model:value="pkForm.loserReward" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="MVP 额外奖励">
|
||
<a-input-number v-model:value="pkForm.mvpReward" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</a-form>
|
||
</a-modal>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import dayjs, { Dayjs } from 'dayjs'
|
||
import {
|
||
getTeamPKList,
|
||
createTeamPK,
|
||
refreshTeamPK,
|
||
type TeamPK
|
||
} from '@/api/teamPK'
|
||
import { getDepartmentTree } from '@/api/department'
|
||
|
||
const authStore = useAuthStore()
|
||
const isAdmin = computed(() => authStore.userInfo?.roles?.some(r => r.roleCode === 'admin' || r.roleCode === 'super_admin'))
|
||
|
||
const loading = ref(false)
|
||
const pks = ref<TeamPK[]>([])
|
||
const departments = ref<any[]>([])
|
||
|
||
const activePKs = computed(() => pks.value.filter(pk => pk.status === 'active'))
|
||
const endedPKs = computed(() => pks.value.filter(pk => pk.status === 'ended'))
|
||
|
||
const createModalVisible = ref(false)
|
||
const creating = ref(false)
|
||
const pkForm = ref({
|
||
name: '',
|
||
description: '',
|
||
teamADeptId: null as number | null,
|
||
teamAName: '',
|
||
teamBDeptId: null as number | null,
|
||
teamBName: '',
|
||
startTime: null as Dayjs | null,
|
||
endTime: null as Dayjs | null,
|
||
winnerReward: 200,
|
||
loserReward: 100,
|
||
mvpReward: 300
|
||
})
|
||
|
||
const loadData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const result = await getTeamPKList({ page: 1, pageSize: 50 })
|
||
pks.value = result.records || []
|
||
} catch (error) {
|
||
console.error('加载PK列表失败:', error)
|
||
message.error('加载数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadDepartments = async () => {
|
||
try {
|
||
const result = await getDepartmentTree()
|
||
// 扁平化部门树
|
||
const flattenDepts = (depts: any[], result: any[] = []): any[] => {
|
||
for (const dept of depts) {
|
||
result.push({ id: dept.id, name: dept.name })
|
||
if (dept.children && dept.children.length > 0) {
|
||
flattenDepts(dept.children, result)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
departments.value = flattenDepts(result)
|
||
} catch (error) {
|
||
console.error('加载部门失败:', error)
|
||
}
|
||
}
|
||
|
||
const showCreateModal = () => {
|
||
pkForm.value = {
|
||
name: '',
|
||
description: '',
|
||
teamADeptId: null,
|
||
teamAName: '',
|
||
teamBDeptId: null,
|
||
teamBName: '',
|
||
startTime: null,
|
||
endTime: null,
|
||
winnerReward: 200,
|
||
loserReward: 100,
|
||
mvpReward: 300
|
||
}
|
||
createModalVisible.value = true
|
||
}
|
||
|
||
const handleTeamAChange = (deptId: number) => {
|
||
const dept = departments.value.find(d => d.id === deptId)
|
||
if (dept) {
|
||
pkForm.value.teamAName = dept.name
|
||
}
|
||
}
|
||
|
||
const handleTeamBChange = (deptId: number) => {
|
||
const dept = departments.value.find(d => d.id === deptId)
|
||
if (dept) {
|
||
pkForm.value.teamBName = dept.name
|
||
}
|
||
}
|
||
|
||
const handleCreatePK = async () => {
|
||
if (!pkForm.value.name || !pkForm.value.teamADeptId || !pkForm.value.teamBDeptId) {
|
||
message.warning('请填写必填项')
|
||
return
|
||
}
|
||
if (!pkForm.value.startTime || !pkForm.value.endTime) {
|
||
message.warning('请选择PK时间')
|
||
return
|
||
}
|
||
|
||
creating.value = true
|
||
try {
|
||
const defaultDimensions = {
|
||
taskCount: { weight: 30 },
|
||
fiveStarCount: { weight: 25 },
|
||
onTimeRate: { weight: 25 },
|
||
avgRating: { weight: 20 }
|
||
}
|
||
|
||
await createTeamPK({
|
||
name: pkForm.value.name,
|
||
description: pkForm.value.description,
|
||
teamADeptId: pkForm.value.teamADeptId,
|
||
teamAName: pkForm.value.teamAName,
|
||
teamBDeptId: pkForm.value.teamBDeptId,
|
||
teamBName: pkForm.value.teamBName,
|
||
startTime: pkForm.value.startTime!.format('YYYY-MM-DD HH:mm:ss'),
|
||
endTime: pkForm.value.endTime!.format('YYYY-MM-DD HH:mm:ss'),
|
||
dimensions: JSON.stringify(defaultDimensions),
|
||
rewards: JSON.stringify({
|
||
winnerReward: pkForm.value.winnerReward,
|
||
loserReward: pkForm.value.loserReward,
|
||
mvpReward: pkForm.value.mvpReward
|
||
})
|
||
})
|
||
|
||
message.success('PK创建成功!')
|
||
createModalVisible.value = false
|
||
loadData()
|
||
} catch (error: any) {
|
||
console.error('创建PK失败:', error)
|
||
message.error(error.message || '创建失败')
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
const refreshPK = async (id: number) => {
|
||
try {
|
||
await refreshTeamPK(id)
|
||
message.success('数据已刷新')
|
||
loadData()
|
||
} catch (error) {
|
||
message.error('刷新失败')
|
||
}
|
||
}
|
||
|
||
const getRemainingDays = (endTime: string) => {
|
||
const end = dayjs(endTime)
|
||
const now = dayjs()
|
||
return Math.max(0, end.diff(now, 'day'))
|
||
}
|
||
|
||
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD')
|
||
|
||
const getWinnerColor = (winner: string | undefined) => {
|
||
if (winner === 'A' || winner === 'B') return 'green'
|
||
if (winner === 'draw') return 'blue'
|
||
return 'default'
|
||
}
|
||
|
||
const getWinnerText = (pk: TeamPK) => {
|
||
if (pk.winner === 'A') return `${pk.teamAName} 获胜`
|
||
if (pk.winner === 'B') return `${pk.teamBName} 获胜`
|
||
if (pk.winner === 'draw') return '平局'
|
||
return '未结算'
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
loadDepartments()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.team-pk-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-desc {
|
||
color: #AFAFAF;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.pk-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.pk-card {
|
||
background: #2A2A2A;
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
border-color: #EF4444;
|
||
}
|
||
}
|
||
|
||
.pk-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.pk-name {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.pk-status {
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
|
||
&.active {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: #EF4444;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
|
||
.pk-battle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.team-side {
|
||
flex: 1;
|
||
background: #333;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
|
||
&.winning {
|
||
background: rgba(49, 240, 139, 0.1);
|
||
border: 1px solid rgba(49, 240, 139, 0.3);
|
||
}
|
||
}
|
||
|
||
.team-name {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.team-score {
|
||
font-size: 48px;
|
||
font-weight: 700;
|
||
color: #31f08b;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.team-stats {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #6B6B6B;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.vs-badge {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.vs-text {
|
||
font-size: 32px;
|
||
font-weight: 800;
|
||
color: #EF4444;
|
||
text-shadow: 0 0 20px rgba(239, 68, 68, 0.5);
|
||
}
|
||
|
||
.vs-time {
|
||
font-size: 12px;
|
||
color: #AFAFAF;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.pk-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #3A3A3A;
|
||
}
|
||
|
||
.pk-time {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #6B6B6B;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.history-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.history-card {
|
||
background: #2A2A2A;
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.history-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.history-name {
|
||
font-weight: 500;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.history-battle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.history-team {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #AFAFAF;
|
||
|
||
.winner {
|
||
color: #31f08b;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.history-score {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.history-vs {
|
||
color: #6B6B6B;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.history-time {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #6B6B6B;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 48px;
|
||
color: #6B6B6B;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 48px;
|
||
}
|
||
</style>
|