mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 添加签到功能及相关设置
- 更新 .gitignore,添加 SpecStory 说明文件 - 在 App.vue 中引入 NGlobalStyle 组件 - 更新 api-models.ts,添加签到相关数据模型 - 在 CheckInSettings.vue 中实现签到功能的配置界面 - 添加签到排行榜功能,允许用户查看签到情况 - 更新 PointHistoryCard.vue,增加签到记录显示 - 在 PointSettings.vue 中添加签到相关设置项 - 更新路由,添加签到排行页面
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
Person48Filled,
|
||||
VideoAdd20Filled,
|
||||
WindowWrench20Filled,
|
||||
CheckmarkCircle24Filled,
|
||||
} from '@vicons/fluent';
|
||||
import { BrowsersOutline, Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5';
|
||||
import { useElementSize, useStorage } from '@vueuse/core';
|
||||
@@ -117,6 +118,11 @@
|
||||
key: 'user-goods', icon: renderIcon(BookCoins20Filled),
|
||||
show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.Point)
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: { name: 'user-checkin' } }, { default: () => '签到排行' }),
|
||||
key: 'user-checkin', icon: renderIcon(CheckmarkCircle24Filled),
|
||||
show: userInfo.value?.extra?.allowCheckInRanking
|
||||
},
|
||||
].filter(option => option.show !== false) as MenuOption[]; // 过滤掉 show 为 false 的菜单项
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,24 @@ const message = useMessage()
|
||||
|
||||
// 默认积分设置
|
||||
const defaultSettingPoint: Setting_Point = {
|
||||
allowType: [EventDataTypes.Guard], // 默认只允许舰长积分
|
||||
jianzhangPoint: 10, // 舰长积分
|
||||
tiduPoint: 100, // 提督积分
|
||||
zongduPoint: 1000, // 总督积分
|
||||
giftPercentMap: {}, // 礼物积分映射表
|
||||
scPointPercent: 0.1, // SC积分比例 (10%)
|
||||
giftPointPercent: 0.1, // 礼物积分比例 (10%)
|
||||
allowType: [EventDataTypes.Guard], // 默认只允许舰长积分
|
||||
jianzhangPoint: 10, // 舰长积分
|
||||
tiduPoint: 100, // 提督积分
|
||||
zongduPoint: 1000, // 总督积分
|
||||
giftPercentMap: {}, // 礼物积分映射表
|
||||
scPointPercent: 0.1, // SC积分比例 (10%)
|
||||
giftPointPercent: 0.1, // 礼物积分比例 (10%)
|
||||
giftAllowType: SettingPointGiftAllowType.All, // 默认允许所有礼物
|
||||
enableCheckIn: false,
|
||||
checkInKeyword: '签到',
|
||||
givePointsForCheckIn: false,
|
||||
baseCheckInPoints: 10,
|
||||
enableConsecutiveBonus: false,
|
||||
bonusPointsPerDay: 2,
|
||||
maxBonusPoints: 0,
|
||||
allowSelfCheckIn: false,
|
||||
requireAuth: false,
|
||||
allowCheckInRanking: false
|
||||
}
|
||||
|
||||
// 响应式设置对象
|
||||
|
||||
@@ -87,7 +87,7 @@ async function getPointHistory() {
|
||||
// 根据用户认证状态使用不同的请求参数
|
||||
const params = props.user.info.id > 0
|
||||
? { authId: props.user.info.id }
|
||||
: { id: props.user.info.userId ?? props.user.info.openId }
|
||||
: { id: props.user.info.userId > 0 ? props.user.info.userId : props.user.info.openId }
|
||||
|
||||
const data = await QueryGetAPI<ResponsePointHisrotyModel[]>(
|
||||
POINT_API_URL + 'get-user-histories',
|
||||
|
||||
468
src/views/view/CheckInRankingView.vue
Normal file
468
src/views/view/CheckInRankingView.vue
Normal file
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div class="checkin-ranking-view">
|
||||
<NSpace vertical>
|
||||
<NCard title="签到排行榜">
|
||||
<template #header-extra>
|
||||
<NSpace>
|
||||
<NSelect
|
||||
v-model:value="timeRange"
|
||||
style="width: 180px"
|
||||
:options="timeRangeOptions"
|
||||
@update:value="loadCheckInRanking"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="userFilter"
|
||||
placeholder="搜索用户"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="isLoading"
|
||||
@click="loadCheckInRanking"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<NSpin :show="isLoading">
|
||||
<div
|
||||
v-if="rankingData.length === 0 && !isLoading"
|
||||
class="empty-data"
|
||||
>
|
||||
<NEmpty description="暂无签到数据" />
|
||||
</div>
|
||||
|
||||
<!-- 自定义排行榜表格 -->
|
||||
<div
|
||||
v-else
|
||||
class="custom-ranking-table"
|
||||
>
|
||||
<!-- 排行榜头部 -->
|
||||
<div class="ranking-header">
|
||||
<div class="ranking-row">
|
||||
<div class="col-rank">
|
||||
排名
|
||||
</div>
|
||||
<div class="col-user">
|
||||
用户
|
||||
</div>
|
||||
<div class="col-days">
|
||||
连续签到
|
||||
</div>
|
||||
<div class="col-monthly">
|
||||
本月签到
|
||||
</div>
|
||||
<div class="col-total">
|
||||
总签到
|
||||
</div>
|
||||
<div class="col-time">
|
||||
最近签到时间
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜内容 -->
|
||||
<div class="ranking-body">
|
||||
<div
|
||||
v-for="(item, index) in pagedData"
|
||||
:key="index"
|
||||
class="ranking-row"
|
||||
:class="{'top-three': index < 3}"
|
||||
>
|
||||
<!-- 排名列 -->
|
||||
<div class="col-rank">
|
||||
<div
|
||||
class="rank-number"
|
||||
:class="{
|
||||
'rank-1': index === 0,
|
||||
'rank-2': index === 1,
|
||||
'rank-3': index === 2
|
||||
}"
|
||||
>
|
||||
{{ index + 1 + (pagination.page - 1) * pagination.pageSize }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列 -->
|
||||
<div class="col-user">
|
||||
<div class="user-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.isAuthed"
|
||||
class="user-authed"
|
||||
>
|
||||
已认证
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连续签到列 -->
|
||||
<div class="col-days">
|
||||
<div class="days-count">
|
||||
{{ item.consecutiveDays }}
|
||||
</div>
|
||||
<div class="days-text">
|
||||
天
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本月签到列 -->
|
||||
<div class="col-monthly">
|
||||
<div class="count-value">
|
||||
{{ item.monthlyCheckInCount || 0 }}
|
||||
</div>
|
||||
<div class="count-text">
|
||||
次
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 总签到列 -->
|
||||
<div class="col-total">
|
||||
<div class="count-value">
|
||||
{{ item.totalCheckInCount || 0 }}
|
||||
</div>
|
||||
<div class="count-text">
|
||||
次
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 签到时间列 -->
|
||||
<div class="col-time">
|
||||
{{ formatDate(item.lastCheckInTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<div class="ranking-footer">
|
||||
<NPagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="filteredRankingData.length"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
show-size-picker
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
|
||||
<div class="ranking-info">
|
||||
<NAlert type="info">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Info24Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
签到可获得积分,连续签到有额外奖励。排行榜每日更新,发送"{{ checkInKeyword }}"即可参与签到。
|
||||
</NAlert>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckInRankingInfo, UserInfo } from '@/api/api-models';
|
||||
import { QueryGetAPI } from '@/api/query';
|
||||
import { CHECKIN_API_URL } from '@/data/constants';
|
||||
import { Info24Filled } from '@vicons/fluent';
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NEmpty,
|
||||
NIcon,
|
||||
NInput,
|
||||
NPagination,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NSpin,
|
||||
} from 'naive-ui';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
biliInfo: any | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
template?: string | undefined
|
||||
}>()
|
||||
|
||||
// 状态变量
|
||||
const isLoading = ref(false);
|
||||
const rankingData = ref<CheckInRankingInfo[]>([]);
|
||||
const timeRange = ref<string>('all');
|
||||
const userFilter = ref<string>('');
|
||||
const checkInKeyword = ref('签到'); // 默认签到关键词
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// 时间段选项
|
||||
const timeRangeOptions = [
|
||||
{ label: '全部时间', value: 'all' },
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' },
|
||||
];
|
||||
|
||||
// 过滤后的排行榜数据
|
||||
const filteredRankingData = computed(() => {
|
||||
let filtered = rankingData.value;
|
||||
|
||||
// 按时间范围筛选
|
||||
if (timeRange.value !== 'all') {
|
||||
const now = new Date();
|
||||
let startTime: Date;
|
||||
|
||||
if (timeRange.value === 'today') {
|
||||
// 今天凌晨
|
||||
startTime = new Date(now);
|
||||
startTime.setHours(0, 0, 0, 0);
|
||||
} else if (timeRange.value === 'week') {
|
||||
// 本周一
|
||||
const dayOfWeek = now.getDay() || 7; // 把周日作为7处理
|
||||
startTime = new Date(now);
|
||||
startTime.setDate(now.getDate() - (dayOfWeek - 1));
|
||||
startTime.setHours(0, 0, 0, 0);
|
||||
} else if (timeRange.value === 'month') {
|
||||
// 本月1号
|
||||
startTime = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
}
|
||||
|
||||
filtered = filtered.filter(user => {
|
||||
const checkInTime = new Date(user.lastCheckInTime);
|
||||
return checkInTime >= startTime;
|
||||
});
|
||||
}
|
||||
|
||||
// 按用户名筛选
|
||||
if (userFilter.value) {
|
||||
const keyword = userFilter.value.toLowerCase();
|
||||
filtered = filtered.filter(user =>
|
||||
user.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 处理分页后的数据
|
||||
const pagedData = computed(() => {
|
||||
const { page, pageSize } = pagination.value;
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return filteredRankingData.value.slice(startIndex, endIndex);
|
||||
});
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${month}月${day}日 ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 加载签到排行榜数据
|
||||
async function loadCheckInRanking() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// 使用用户视角的签到排行API
|
||||
const response = await QueryGetAPI<CheckInRankingInfo[]>(`${CHECKIN_API_URL}ranking`, {
|
||||
vId: props.userInfo?.id,
|
||||
count: 100
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
rankingData.value = response.data;
|
||||
pagination.value.page = 1; // 重置为第一页
|
||||
} else {
|
||||
rankingData.value = [];
|
||||
window.$message?.error?.(`获取签到排行榜失败: ${response.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载签到排行榜失败:', error);
|
||||
rankingData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取签到关键词
|
||||
async function fetchCheckInKeyword() {
|
||||
if (!props.userInfo?.id) return;
|
||||
|
||||
try {
|
||||
// 获取主播的签到关键词设置
|
||||
const response = await QueryGetAPI<{
|
||||
keyword: string
|
||||
isEnabled: boolean
|
||||
requireAuth: boolean
|
||||
}>(`${CHECKIN_API_URL}keyword`, {
|
||||
vId: props.userInfo?.id
|
||||
});
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
checkInKeyword.value = response.data.keyword;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取签到关键词失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取签到排行和关键词
|
||||
onMounted(() => {
|
||||
fetchCheckInKeyword();
|
||||
loadCheckInRanking();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkin-ranking-view {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ranking-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 自定义表格样式 */
|
||||
.custom-ranking-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
/* 使用官方阴影变量 */
|
||||
box-shadow: var(--n-boxShadow1, 0 1px 2px -2px rgba(0, 0, 0, .24), 0 3px 6px 0 rgba(0, 0, 0, .18), 0 5px 12px 4px rgba(0, 0, 0, .12));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ranking-header {
|
||||
/* 使用官方背景色变量 */
|
||||
background-color: var(--n-tableHeaderColor, rgba(255, 255, 255, 0.06));
|
||||
font-weight: var(--n-fontWeightStrong, 500);
|
||||
color: var(--n-textColor2, rgba(255, 255, 255, 0.82));
|
||||
}
|
||||
|
||||
.ranking-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
/* 使用官方分割线变量 */
|
||||
border-bottom: 1px solid var(--n-dividerColor, rgba(255, 255, 255, 0.09));
|
||||
transition: background-color 0.3s var(--n-cubicBezierEaseInOut, cubic-bezier(.4, 0, .2, 1));
|
||||
}
|
||||
|
||||
.ranking-body .ranking-row:hover {
|
||||
/* 使用官方悬停背景色变量 */
|
||||
background-color: var(--n-hoverColor, rgba(255, 255, 255, 0.09));
|
||||
}
|
||||
|
||||
.ranking-body .ranking-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.top-three {
|
||||
/* 使用官方条纹背景色变量 */
|
||||
background-color: var(--n-tableColorStriped, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.col-rank {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-user {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-days,
|
||||
.col-monthly,
|
||||
.col-total {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-time {
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--n-fontWeightStrong, 500);
|
||||
/* 使用官方文本和背景色变量 */
|
||||
color: var(--n-textColor2, rgba(255, 255, 255, 0.82));
|
||||
background-color: var(--n-actionColor, rgba(255, 255, 255, 0.06));
|
||||
}
|
||||
|
||||
/* 保持奖牌颜色在暗色模式下也清晰可见 */
|
||||
.rank-1 {
|
||||
background: linear-gradient(135deg, #ffe259, #ffa751);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.rank-2 {
|
||||
background: linear-gradient(135deg, #d3d3d3, #a9a9a9);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.rank-3 {
|
||||
background: linear-gradient(135deg, #c79364, #a77347);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: var(--n-fontWeightStrong, 500);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.user-authed {
|
||||
background-color: var(--n-successColor, #63e2b7);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: var(--n-fontSizeTiny, 12px);
|
||||
}
|
||||
|
||||
.days-count,
|
||||
.count-value {
|
||||
font-weight: var(--n-fontWeightStrong, 500);
|
||||
font-size: var(--n-fontSizeLarge, 15px);
|
||||
color: var(--n-infoColor, #70c0e8);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.days-text,
|
||||
.count-text {
|
||||
color: var(--n-textColor3, rgba(255, 255, 255, 0.52));
|
||||
font-size: var(--n-fontSizeTiny, 12px);
|
||||
}
|
||||
|
||||
.ranking-footer {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* 使用官方背景色变量 */
|
||||
background-color: var(--n-tableHeaderColor, rgba(255, 255, 255, 0.06));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user