676 lines
17 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>
<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>